Keywords Are Back For Google Shopping Campaigns!

Shopping campaigns aren’t like Search campaigns with all their precise knobs, levers, and switches. For Shopping campaigns you just give Google a feed of products and hope it can match them to the right queries. It’s a lot like pure broad match (ugh!). You can have negative keywords, but no positive ones.


Many advanced PPCers have circumvented this lack of control using strategies like this one by @bloomarty, which funnels queries into different campaigns using priority settings and negatives. This is a superb strategy and will undoubtedly offer you more control over Shopping campaigns. We’ve been using it with great success — but sometimes we need more granularity than three priority levels.


What follows here is an AdWords script from Brainlabs that allows you to choose the exact searches your Shopping campaigns appear on. The script reads a Google spreadsheet that you’ve created and matches that against your campaigns’ queries. It’s a bit like our script to reverse close variant matching, only your keywords are stored in a spreadsheet instead of in the account. If there’s a query that doesn’t match one of your keywords, a negative keyword will be added to exclude the query.


The script chops the query into bits and makes phrase match negatives wherever possible, so each negative can exclude as many unwanted searches as possible while keeping anything that matches the keywords. It’s a bit of a hack, but if you read on, you’ll see the results are worth it.


Using this script, you could run one campaign just for your key search terms. But it also means that you can run your Shopping campaigns like the best Search ones — with separate Broad and Exact campaigns. The Exact campaigns will bring in your “business as usual” traffic, and your Broad ones bring in the less predictable long-tail queries. A basic version might look something like this:


Google Shopping Campaigns


The beauty of using scripts is that you can get a lot more granular than that with no limit on scale. The Exact campaign contains ad groups, each of which has its own set of keywords that you can define in a spreadsheet.


So you can match specific keywords to bids and products. This is a really handy feature — the ability to decide the product range that you want to show for specific generic queries.


Google Shopping Campaigns

When we implemented this strategy for our cycle clothing client, Always Riding, we saw a revenue increase of 148 percent, with a 61 percent improvement in ROAS. Isn’t that marvelous? Someone should tell Google that keywords are a great way to manage online advertising campaigns.


What You Need To Do

If you’re new to scripts, check out the excellent AdWords Scripts series by Ginny Marvin before starting.


Write your exact match keywords into a Google Spreadsheet, with their campaign and ad group.


Google Shopping Campaigns


Note that there are no “close variants” here — the script will exclude searches that are as much as a letter off. Also note that the script ignores capitalization and treats all punctuation (aside from ampersands) as a space, so “Spider-man’s” is the same as “spider man s” here.


Set your Broad shopping campaign to High priority. Copy all of your keywords from your spreadsheet and add them as exact negatives. Set your Exact shopping campaign to Low priority, so it only picks up queries that can’t be taken by the Broad campaign. Then, copy the script and paste it into your account. At the top of the script, you need to change spreadsheetUrl to the URL of the Google Doc spreadsheet with the keywords in it.


Before running the script for the first time, click “Preview” — the script will run but won’t actually make changes, so you can see what negatives the script is going to add. If there are any problems, then there will be messages in the Logs.


When it looks like everything is fine, click “Run script now” so the script will run for real and actually add negatives. Then, schedule the script to run daily — that means it can keep reading your new queries and adding new negatives.


If you want to add new Exact keywords later:

  • Write the keyword, with its campaign and ad group, at the end of the spreadsheet.
  • Add the keyword as an exact match negative to the Broad campaign.
  • Look at the keyword’s ad group in the Exact campaign, and remove any negative keywords that would exclude the keyword.

Because the script checks search queries daily, it can add negatives for anything required. You don’t have to worry about changing the Exact campaign’s negatives if you’re adding negatives to the Broad campaign, and you don’t have to worry about letting in the wrong queries if you take negatives out of the Exact campaign — the script will fill in the necessary negatives once they appear in the SQR.


If you want to split up your budget, you can have multiple Exact campaigns; because you’re funneling queries with the script’s negatives, you can have multiple campaigns at the same priority.


If this version is popular, then we’ll work on another version with phrase match/broad match modifier. We’ll also add functions to automatically add the Exact keywords as negatives to the Broad campaign, and check for conflicts between new keywords and existing negatives (hat tip @RichardFergie). Let us know if you think of anything else.


* Exact Match For Shopping
* This script reads a list of exact match keywords for Shopping campaigns from a Google Doc,
* and then excludes any search queries from those camapigns if they do not match those keywords.
* Version: 1.0
* Google AdWords Script maintained by
function main() {
// Put your spreadsheet's URL here:
var spreadsheetUrl = "";
// Make sure the keywords are in columns A to C in the first sheet.
var dateRange = "YESTERDAY";
// By default the script just looks at yesterday's search queries.
var impressionThreshold = 0;
// The script only looks at searches with impressions higher than this threshold.
// Use this if you get a wide range of searches and only want to exclude the highest volume ones.
// Read the spreadsheet
try {
var spreadsheet = SpreadsheetApp.openByUrl(spreadsheetUrl);
} catch (e) {
Logger.log("Problem with the spreadsheet URL: '" + e + "'");
Logger.log("Make sure you have correctly copied in your own spreadsheet URL.");
var sheet = spreadsheet.getSheets()[0];
var spreadsheetData = sheet.getDataRange().getValues();
// Record each campaigns' keywords, and the words (of more than 4 characters) that are in the keywords
var keywords = {};
var words = {};
var numberOfKeywords = 0;
var numberOfadGroups = 0;
for(var i=1; i<spreadsheetData.length; i++) {
var campaignName = spreadsheetData[i][0];
var adGroupName = spreadsheetData[i][1];
if (keywords[campaignName] == undefined) {
keywords[campaignName] = [];
words[campaignName] = {};
if (keywords[campaignName][adGroupName] == undefined) {
keywords[campaignName][adGroupName] = [];
words[campaignName][adGroupName] = {};
var keyword = spreadsheetData[i][2];
keyword = keyword.toLowerCase().replace(/[^wsd&]/g," ").replace(/ +/g," ").trim();
var keywordWords = keyword.split(" ");
for (var k=0; k<keywordWords.length; k++) {
if (keywordWords[k].length > 4) {
words[campaignName][adGroupName][keywordWords[k]] = true;
var campaignNames = Object.keys(keywords);
Logger.log("Found " + numberOfKeywords + " keywords for " + numberOfadGroups + " ad groups in " + campaignNames.length + " campaign(s).");
// Get the IDs of the ad groups named in the spreadsheet
var adGroupIds = [];
var campaignIds = [];
var campaignReport =
"SELECT CampaignName, AdGroupName, CampaignId, AdGroupId " +
"WHERE Impressions > 0 " +
'AND CampaignName IN ["' + campaignNames.join('","') + '"] ' +
"DURING " + dateRange
var campaignRows = campaignReport.rows();
while (campaignRows.hasNext()) {
var row =;
if (campaignIds.indexOf(row["CampaignId"]) < 0) {
if (keywords[row["CampaignName"]][row["AdGroupName"]] != undefined) {
}//end while
if (adGroupIds.length == 0) {
Logger.log("Could not find any ad groups with impressions that matched the given names.");
Logger.log("Found " + adGroupIds.length + " ad groups in " + campaignIds.length + " campaign(s) with impressions that matched the given names.");
// Initialise the arrays for each campaign, and sorts the keywords from longest to shortest
var negativeQueries = {}; // Contains the queries
var exactNegatives = {}; // Contains any negatives to add with exact match
var phraseNegatives = {}; // Contains any negatives to add with phrase match
for (var campaignName in keywords) {
negativeQueries[campaignName] = {};
exactNegatives[campaignName] = {};
phraseNegatives[campaignName] = {};
for (var adGroupName in keywords[campaignName]) {
negativeQueries[campaignName][adGroupName] = [];
exactNegatives[campaignName][adGroupName] = [];
phraseNegatives[campaignName][adGroupName] = [];
keywords[campaignName][adGroupName].sort(function(a,b) {return b.length - a.length;});
// Get the queries that don't exactly match keywords
var report =
"SELECT Query, AdGroupId, CampaignId, CampaignName, AdGroupName, Impressions " +
"WHERE AdGroupId IN [" + adGroupIds.join(",") + "] " +
"AND Impressions > " + impressionThreshold + " " +
"DURING " + dateRange);
var rows = report.rows();
var numberQueries = 0;
while (rows.hasNext()) {
var row =;
var query = row["Query"].toLowerCase().replace(/[^wsd&]/g," ").replace(/ +/g," ").trim();
var campaignName = row["CampaignName"];
var adGroupName = row["AdGroupName"];
if (keywords[campaignName][adGroupName].indexOf(query) < 0) {
// Process queries
Logger.log("Processing " + numberQueries + " queries that do not match any keywords.");
var numberExactNegatives = 0;
var numberPotentialPhraseNegatives = 0;
for (var campaignName in negativeQueries) {
for (var adGroupName in negativeQueries[campaignName]) {
for (var i=0; i<negativeQueries[campaignName][adGroupName].length; i++) {
var query = negativeQueries[campaignName][adGroupName][i];
var queryDone = false;
// If the query is contained within a keyword, it has to be an exact match negative
if (isStringInsideKeywords(query, keywords[campaignName][adGroupName])) {
// Check each word (that's over 4 characters) in the query - if it's not in the words array
// then it isn't in the keywords, so it's fine to use as a phrase negative
var queryWords = query.split(" ");
for (var w=0; w<queryWords.length; w++) {
if (queryWords[w].length > 4) {
if (words[campaignName][adGroupName][queryWords[w]] == undefined) {
queryDone = true;
// Check if there is a keyword inside the query. If there is, see if the part of the query before
// or after the keyword could be used as a phrase negative.
for (var k=0; k<keywords[campaignName][adGroupName].length && !queryDone; k++) {
var keyword = keywords[campaignName][adGroupName][k];
if ((" " + query + " ").indexOf(" " + keyword + " ") > -1) {
var queryBits = (" " + query + " ").split(" " + keyword + " ");
queryBits[0] = queryBits[0].trim();
queryBits[1] = queryBits[1].trim();
if (queryBits[0].length > 0 && !isStringInsideKeywords(queryBits[0], keywords[campaignName][adGroupName])) {
queryDone = true;
if (queryBits[1].length > 0 && !isStringInsideKeywords(queryBits[1], keywords[campaignName][adGroupName])) {
queryDone = true;
// If nothing smaller than the full query would work, then add the full query as a negative
if (!queryDone) {
Logger.log("Found " + numberPotentialPhraseNegatives + " potential phrase match negatives and " + numberExactNegatives + " exact match negatives.");
// Remove any redundant phrase negatives
Logger.log("Checking for redundant negatives.");
var numberPhraseNegatives = 0;
for (var campaignName in negativeQueries) {
for (var adGroupName in negativeQueries[campaignName]) {
// Order the phrases from shortest to longest
phraseNegatives[campaignName][adGroupName].sort(function(a,b) {return a.length - b.length;});
for (var i=0; i<phraseNegatives[campaignName][adGroupName].length; i++) {
var shorterPhrase = " " + phraseNegatives[campaignName][adGroupName][i] + " ";
// As the array is now ordered, any phrase negatives with higher indices must be longer than shorterPhrase
for (var j=i+1; j<phraseNegatives[campaignName][adGroupName].length; j++) {
var longerPhrase = " " + phraseNegatives[campaignName][adGroupName][j] + " ";
// If the shorterPhrase is within the longerPhrase, then the longerPhrase is redundant
// so it is removed from the array. This also means duplicates are removed.
if (longerPhrase.indexOf(shorterPhrase) > -1) {
numberPhraseNegatives += phraseNegatives[campaignName][adGroupName].length;
Logger.log("Going to create " + numberPhraseNegatives + " phrase match negatives and " + numberExactNegatives + " exact match negatives");
// Iterate through the Shopping ad groups and add the negative keywords
var groupIterator = AdWordsApp.shoppingAdGroups()
while (groupIterator.hasNext()) {
var adGroup =;
var adGroupName = adGroup.getName();
var campaignName = adGroup.getCampaign().getName();
for (var i=0; i<exactNegatives[campaignName][adGroupName].length; i++) {
adGroup.createNegativeKeyword("[" + exactNegatives[campaignName][adGroupName][i] + "]");
for (var i=0; i<phraseNegatives[campaignName][adGroupName].length; i++) {
adGroup.createNegativeKeyword('"' + phraseNegatives[campaignName][adGroupName][i] + '"');
} // end main function
// Check if a word is a substring of any strings in the keywords array
function isStringInsideKeywords(word, keywords) {
for (var k=0; k<keywords.length; k++) {
var keyword = " " + keywords[k] + " ";
if (keyword.indexOf(" " + word + " ") > -1) {
return true;
return false;

Be the first to review this item!

Bookmark this

23 Sep 2015

By Daniel Gilbert