The Google Shopping Script to Prevent Query Bleed in Priority-Segmented Campaigns

Hola! This amazing script is from the brain of Richard Fergie. He created it for ZATO, and I’m happy to share it with you. Hop onto Twitter to thank him! @RichardFergie

I first began using this script exactly a year ago (April 20, 2016!), and it’s been doing its job nicely ever since!

The History & Purpose of the Stop-Shopping-Query-Bleed Script

The basic purpose of the script, is to fit in nicely with the query filtering strategy as introduced by Martin Roettgerding, and detailed step-by-step by me here:
A Step-By-Step Guide To Query-Level Bidding In Google Shopping

As quick background, this strategy allows you to utilize priority campaign settings, bid adjustments, shared budgets, and negative keywords to send queries into the correct campaign so you can bid according to buying intent in Shopping Ads (Let’s Make Keyword Targeting Great Again, amirite?).

I’ve run this strategy in multiple accounts now for quite awhile and I have begun to notice a recurring problem. At times, queries would bleed over from my general campaign into my Brand or SKU campaigns and throw off my budget and bidding. Now, this will naturally happen if you don’t have a shared budget connecting your campaign group, or if you have your bids in your high priority, general queries campaign set too low (this was discovered by Andreas Reiffen, it can actually remove that campaign from auction so it shifts the next campaign… your higher bid brand campaign… into the generl query auction). But those were not the case for me, sometimes, regardless of my setup supernatural shopping forces would allow for query bleed-over and destroy my bidding in 1 or 2 brands in specific clients.

Thus, I approached my friend, Richard, and asked if he would build this script for me. He graciously agreed, and in his spare time, proceeded to build it. I was hoping to introduce it at Hero Conf 2016 during my Shopping presentation, but unfortunately we were still testing it at that time.

Cautions and Generally Serious Notes

The primary concern I will note, is that this script is the *nuclear option*. You do not want to throw this into your accounts willy-nilly, as it could end up excluding far more than you intended. I would recommend ensuring all other aspects of your Shopping campaign are correct.

  1. You have setup up shared budgets for that campaign grouping
  2. You have not bid the high priority, low bid general terms campaign too low (otherwise it will drop from the auction)
  3. You have been adding general campaign-level-negative keywords to *ALL* shopping campaigns within that specific grouping

If you have done these things and are still experiencing query-bleed, then give this a try.


  • Add the specific name of the campaign on Line 2 to which you want this script to apply.
  • Add the words that you want to *KEEP* in that campaign on Line 8.  Once it is run, the script will exclude all queries that *DO NOT INCLUDE* the words you have added in Line 8.  So if you are running this on a Nike Brand Shopping campaign, you would keep “Nike”.
  • You can add as many words to *KEEP* as you want.




// enter the name of the campaign
var name_of_campaign = ‘Shopping – Nike – BR – US’

// enter the words you want to *keep*
// all search queries that DO NOT contain one of these words
// will be added as negatives

var keywords = [“nike”,”nikey”,”ETC”]

// eventually, both of the above should be populated through
// sheets

// function that splits up a list of keywords into a list of
// lists of words
// For example
// tokenise_list([“hello”,”novice scripter”]) -> [[“hello”],[“novice”,”scripter”]]
// This is important when looking at negative keywords because we want to match
// the whole of a word, not just a part of it
function tokenise_list(ls) {
var accumulator = []
for (i in ls) {
var keyword = ls[i];
var tokenised_kwd = keyword.split(” “)

function arrayEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;

// If you don’t care about the order of the elements inside
// the array, you should sort both arrays here.

for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
return true;

// returns true if query matches tokenised list
// e.g. match_tokenised(“buzz buzz”,[[“buzz”,”buzz”],[“blah”]]) = true
function match_tokenised(query,tokenised_list) {
var tokenised_query = query.split(” “)
var n = 0;

for (i in tokenised_list) {
var testkwd = tokenised_list[i]
var nwords = testkwd.length

for (var i = 0; i < (tokenised_query.length-nwords+1); i++) {
var candidate = tokenised_query.slice(i,i+nwords)
if (arrayEqual(candidate,testkwd)) {
return true;


return false

function main() {
var target_list = tokenise_list(keywords)
var negatives = []

// SQ Report. YESTERDAY has no data. LAST_7_DAY works
var report =
‘SELECT Query’ +
‘ WHERE CampaignName = “‘ + name_of_campaign + ‘”‘ +
var rows = report.rows();

// for each query in the report
while (rows.hasNext()) {
row =;

// get the search query out of the row object
var q = row[“Query”]

if(!match_tokenised(q,target_list)) {


// negative keyword list is now complete
Logger.log(“Adding the following exact match negatives:”)
for (i in negatives) {

// time to add them to the campaign
var campaigns = AdWordsApp.shoppingCampaigns().withCondition(‘CampaignName = “‘+name_of_campaign+'”‘).get()
while (campaigns.hasNext()) {
Logger.log(“Adding negatives keywords”)
var campaign =;
for (i in negatives) {
var negativekwd = negatives[i]
Logger.log(“Adding negative: “+negativekwd)
// add [] to make exact match