An AdWords Script To Make Exact Match, Well…Exact

Many of you will have heard about Google’s decision to terminate exact match (at the same time as telling us that it’s for our own good). It’s a clear move to grab some more advertising dollars, and the news has been met with fury by SEM experts. Most two-year-old kids know that there is a semantic difference between singular and plural […]

Chat with SearchBot

adwords-scripts-exact-match

Many of you will have heard about Google’s decision to terminate exact match (at the same time as telling us that it’s for our own good). It’s a clear move to grab some more advertising dollars, and the news has been met with fury by SEM experts.

Most two-year-old kids know that there is a semantic difference between singular and plural forms — and anyone with the slightest command of the English language will know that there is a difference between [photographer] and [photography]. While a professional photographer might want to spend money on [photographer], they probably wouldn’t want to appear for [photography] as this is more likely to be a search for photos that users can download.

Instead of signing the petition on change.org asking Google to reverse this change, we’ve just written a script to automatically make exact match, well…exact.

The AdWords script runs search term reports and adds “close variant” terms as exact negatives if they are not the exact original keyword.

You can run this script at MCC level for all your accounts, or choose individual accounts, campaigns or ad groups. Just copy the code below, log in to AdWords, go to Bulk Operations (left-hand column) > Scripts > New. Paste the code into the box and click Preview to see the changes that the script will make if you run it.  Set up a schedule to run this script daily and your keyword matching will behave similarly to how it did before. If you’ve never run a script before you can read our Introduction to AdWords Scripts.

Two caveats: we’ve seen a lot of search terms appear under “Other search terms” in Search Query Reports. These cannot be excluded as Google does not tell us what they are. Secondly, search term report data does not appear on the same day, so we’ll always be a day behind Google’s close variants.

/**
 *
 * Adds as campaign or AdGroup negatives search queries which have triggered exact keywords
 * Version: 1.0 - maintained AdWords script on brainlabsdigital.com
 * Authors: Visar Shabi & Daniel Gilbert
 * brainlabsdigital.com
 *
 **/
function main() {
  
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
  //Options
  
  //Choose whether to add your negative exact keywords at campaign or AdGroup level.
  //Set variable as "true" to add or as "false" to not add.
  var AddAdGroupNegative = true;  // true or false
  var AddCampaignNegative = true; // true of false
  
  //Parameters for filtering by campaign name and AdGroup name. The filtering is case insensitive.
  //Leave blank, i.e. "", if you want this script to run over all campaigns and AdGroups.
  var campaignNameContains = "";
  var adGroupNameContains = "";
  
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
  
  var campaigns = {};
  var adGroups = {};
   
  var exactKeywords = [];
  
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
  //Pull a list of all exact match keywords in the account
  
  var report = AdWordsApp.report(
    "SELECT AdGroupId, Id " +
    "FROM KEYWORDS_PERFORMANCE_REPORT " +
    "WHERE Impressions > 0 AND KeywordMatchType = EXACT " +
    "DURING LAST_7_DAYS");
  
  var rows = report.rows();
  while (rows.hasNext()) {
    var row = rows.next();
    var keywordId = row['Id'];
    var adGroupId = row['AdGroupId'];
    exactKeywords.push(adGroupId + "#" + keywordId);
  }
  
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
  //Pull a list of all exact (close variant) search queries
  
  var report = AdWordsApp.report(
    "SELECT Query, AdGroupId, CampaignId, KeywordId, KeywordTextMatchingQuery, Impressions, MatchType " +
    "FROM SEARCH_QUERY_PERFORMANCE_REPORT " +
    "WHERE CampaignName CONTAINS_IGNORE_CASE '" + campaignNameContains + "' " +
    "AND AdGroupName CONTAINS_IGNORE_CASE  '" + adGroupNameContains + "' " +
    "DURING LAST_7_DAYS");
  
  var rows = report.rows();
  while (rows.hasNext()) {
    var row = rows.next();
    var adGroupId = parseInt(row['AdGroupId']);
    var campaignId = parseInt(row['CampaignId']);
    var keywordId = parseInt(row['KeywordId']);
    var searchQuery = row['Query'];
    var keyword = row['KeywordTextMatchingQuery'];
    var matchType = row['MatchType'].toLowerCase();
    if(keyword !== searchQuery && matchType.indexOf("exact (close variant)") !== -1){
      
      if(!campaigns.hasOwnProperty(campaignId)){
        campaigns[campaignId] = [[], []];
      }
      
      campaigns[campaignId][0].push(searchQuery);
      campaigns[campaignId][1].push(adGroupId + "#" + keywordId);
      
      if(!adGroups.hasOwnProperty(adGroupId)){
        adGroups[adGroupId] = [[], []];
      }
      
      adGroups[adGroupId][0].push(searchQuery);
      adGroups[adGroupId][1].push(adGroupId + "#" + keywordId);
    }
  }
  
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
  //Parse data correctly
  
  var adGroupIds = [];
  var campaignIds = [];
  var adGroupNegatives = [];
  var campaignNegatives = [];
  
  for(var x in campaigns){
    campaignIds.push(parseInt(x));
    campaignNegatives.push([]);
    for(var y = 0; y < campaigns[x][0].length; y++){
      var keywordId = campaigns[x][1][y];
      var keywordText = campaigns[x][0][y];
      if(exactKeywords.indexOf(keywordId) !== -1){
        campaignNegatives[campaignIds.indexOf(parseInt(x))].push(keywordText);
      }
    }
  }
  
  for(var x in adGroups){
    adGroupIds.push(parseInt(x));
    adGroupNegatives.push([]);
    for(var y = 0; y < adGroups[x][0].length; y++){
      var keywordId = adGroups[x][1][y];
      var keywordText = adGroups[x][0][y];
      if(exactKeywords.indexOf(keywordId) !== -1){
        adGroupNegatives[adGroupIds.indexOf(parseInt(x))].push(keywordText);
      }
    }
  }
  
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
  //Create the new negative exact keywords
  
  var campaignResults = {};
  var adGroupResults = {};
  
  if(AddCampaignNegative){
    var campaignIterator = AdWordsApp.campaigns()
    .withIds(campaignIds)
    .get();
    while(campaignIterator.hasNext()){
      var campaign = campaignIterator.next();
      var campaignId = campaign.getId();
      var campaignName = campaign.getName();
      var campaignIndex = campaignIds.indexOf(campaignId);
      for(var i = 0; i < campaignNegatives[campaignIndex].length; i++){
        campaign.createNegativeKeyword("[" + campaignNegatives[campaignIndex][i] + "]")
        if(!campaignResults.hasOwnProperty(campaignName)){
          campaignResults[campaignName] = [];
        }
        campaignResults[campaignName].push(campaignNegatives[campaignIndex][i]);
      }
    }
  }
  
  if(AddAdGroupNegative){
    var adGroupIterator = AdWordsApp.adGroups()
    .withIds(adGroupIds)
    .get();
    while(adGroupIterator.hasNext()){
      var adGroup = adGroupIterator.next();
      var adGroupId = adGroup.getId();
      var adGroupName = adGroup.getName();
      var adGroupIndex = adGroupIds.indexOf(adGroupId);
      for(var i = 0; i < adGroupNegatives[adGroupIndex].length; i++){
        adGroup.createNegativeKeyword("[" + adGroupNegatives[adGroupIndex][i] + "]");
        if(!adGroupResults.hasOwnProperty(adGroupName)){
          adGroupResults[adGroupName] = [];
        }
        adGroupResults[adGroupName].push(adGroupNegatives[adGroupIndex][i]);
      }
    }
  }
  
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
  //Format the results
  
  var resultsString = "The following negative keywords have been added to the following campaigns:";

  for(var x in campaignResults){
    resultsString += "\n\n" + x + ":\n" + campaignResults[x].join("\n");
  }
  
  resultsString += "\n\n\n\nThe following negative keywords have been added to the following AdGroups:";
  
  for(var x in adGroupResults){
    resultsString += "\n\n" + x + ":\n" + adGroupResults[x].join("\n");
  }
  
  Logger.log(resultsString);
  
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
  
}

How The Script Works

For those brave enough, we’ll now break the script down into more detail.

Choosing Your Settings

We’ve made the script flexible so that it works for different account structures. For example, you can decide whether to add your negative exact keywords at campaign or ad group level. We’ve built in functionality to exclude certain campaigns or adgroups as the script runs.

 //Choose whether to add your negative exact keywords at campaign or AdGroup level.
  //Set variable as "true" to add or as "false" to not add.
  var AddAdGroupNegative = true;  // true or false
  var AddCampaignNegative = true; // true of false
  
  //Parameters for filtering by campaign name and AdGroup name. The filtering is case insensitive.
  //Leave blank, i.e. "", if you want this script to run over all campaigns and AdGroups.
  var campaignNameContains = "";
  var adGroupNameContains = "";

Getting Started

We start by pulling a search query report for the last seven days (or any other time period you wish). We store all this information in arrays, with an array for the ad group ids, campaign ids, keyword ids, search queries, keywords, and match types. It’s important to note that all the data associated with a search query is located in the same position in each of the arrays.

   var report = AdWordsApp.report(
 "SELECT Query, AdGroupId, CampaignId, KeywordId, KeywordTextMatchingQuery, Impressions, MatchType " +
 "FROM SEARCH_QUERY_PERFORMANCE_REPORT " +
 "WHERE CampaignName CONTAINS_IGNORE_CASE '" + campaignNameContains + "' " +
 "AND AdGroupName CONTAINS_IGNORE_CASE '" + adGroupNameContains + "' " +
 "DURING LAST_7_DAYS");
 
 var rows = report.rows();
 while (rows.hasNext()) {
 var row = rows.next();
 var adGroupId = parseInt(row['AdGroupId']);
 var campaignId = parseInt(row['CampaignId']);
 var keywordId = parseInt(row['KeywordId']);
 var searchQuery = row['Query'];
 var keyword = row['KeywordTextMatchingQuery'];
 var matchType = row['MatchType'].toLowerCase();

The Important Part

Next is the crucial step: the if statement below takes all the search terms that do not exactly match the keyword they triggered AND have keyword match type exact (close variant). This gives us exactly what we’re after: all those pesky new search terms that have been created as a result of Google’s change.

if(keyword !== searchQuery &amp;&amp; matchType.indexOf("exact (close variant)") !== -1)

We store all these search queries with the campaign id, ad group id and keyword id that they are associated with.

Finally Adding The Negatives

All that is left to do now is to add campaign (or ad group) negatives to the relevant campaigns (or ad groups). It looks complicated, but it’s actually quite straightforward.

We first take all the campaigns that we need to add negatives to, together with their id and name. Iterating through each campaign, we next create all the negative keywords associated with this campaign by matching up ids. And finally all that is left to do is to add these negative keywords to the correct campaign.

  var campaignResults = {};
  var adGroupResults = {};
  
  if(AddCampaignNegative){
    var campaignIterator = AdWordsApp.campaigns()
    .withIds(campaignIds)
    .get();
    while(campaignIterator.hasNext()){
      var campaign = campaignIterator.next();
      var campaignId = campaign.getId();
      var campaignName = campaign.getName();
      var campaignIndex = campaignIds.indexOf(campaignId);
      for(var i = 0; i < campaignNegatives[campaignIndex].length; i++){
        campaign.createNegativeKeyword("[" + campaignNegatives[campaignIndex][i] + "]")
        if(!campaignResults.hasOwnProperty(campaignName)){
          campaignResults[campaignName] = [];
        }
        campaignResults[campaignName].push(campaignNegatives[campaignIndex][i]);
      }
    }
  }

Checking The New Negative Keywords

For our final act, we log all the changes we’ve made to check that we’re happy with the new negative exact keywords.

var resultsString = "The following negative keywords have been added to the following campaigns:";

  for(var x in campaignResults){
    resultsString += "\n\n" + x + ":\n" + campaignResults[x].join("\n");
  }

And then we’re done. Sorry, Google!


Opinions expressed in this article are those of the guest author and not necessarily Search Engine Land. Staff authors are listed here.


About the author

Daniel Gilbert
Contributor
Daniel Gilbert is the CEO at Brainlabs, the best paid media agency in the world (self-declared). He has started and invested in a number of big data and technology startups since leaving Google in 2010.

Get the must-read newsletter for search marketers.