When optimizing campaigns in Google Ads, the search query report is often included. Search queries are excluded based on performance and common sense, but often there are also keywords that you can add to your adgroup. This gives you more control over the performance of these specific search queries, because you can adjust bids as desired.

With the script below you can automate this process. Based on the performance of a keyword, it is automatically added when the performance is good enough. You set the minimum requirements for impressions, CTR and conversions yourself. This gives you control over the keywords that the script will actually add to the adgroup.

The script also checks whether the exact keyword that it wants to add is not already present elsewhere in the account. This way, the script will not affect the structure of the account.

Settings

  • IMPRESSIONS_THRESHOLD: Set the minimum number of impressions.
  • CONVERSIONS_THRESHOLD: Set the minimum number of conversions.
  • CTR_THRESHOLD: Set the minimal CTR.
  • DATE_RANGE: Set the daterange the script will collect data on. Change the number insinde last_n_days().
  • LOG_LEVEL: When anything goes wrong, report this in the logfile.

Frequency: Depending on the size of the account, we suggest you run this script only once a day.

Script
//
// Auto-optimize Search Terms
// Created by: Remko van der Zwaag & pdds
// remkovanderzwaag.nl & pdds.nl
//
// Based on a Google example script: http://goo.gl/aunUKV
// Updated by Tibbe van Asten
//
////////////////////////////////////////////////////////////////////

var config = {

  // Minimum number of impressions to consider "enough data"
  IMPRESSIONS_THRESHOLD : 100,

  // Before new keywords are eligible to be added to the ad group,
  // the search term metrics need to exceed the Conversion OR CTR threshold

  // Minimal number of conversions
  CONVERSIONS_THRESHOLD : 2,

  // Minimal keyword CTR
  CTR_THRESHOLD = 20, // Use dots for decimals, eg 0.5

  // The date range to investigate for potential keywords
  // Formatted as an AWQL DateRange, so you can use this helper,
  // one of the enumerations ('LAST_7_DAYS', 'YESTERDAY', etc),
  // or a manual range like '20140101,20140529'
  DATE_RANGE : last_n_days(90),

  // The script doesn't do much logging in the current version. Set to 'debug' to debug.
  LOG_LEVEL : 'error'

}

////////////////////////////////////////////////////////////////////
// Please don't touch below this line

function main() {
  getAllKeywords();

  var negativeKeywords = {};
  var positiveKeywords = {};
  var allAdGroupIds = {};

  var report = AdsApp.report(
    "SELECT Query,Clicks,Cost,Ctr,ConversionRate,CostPerConversion,Conversions,CampaignId,AdGroupId " +
    " FROM SEARCH_QUERY_PERFORMANCE_REPORT " +
    " WHERE Impressions >= " + config.IMPRESSIONS_THRESHOLD +
    " AND AdGroupStatus = ENABLED " +
    " AND CampaignStatus = ENABLED " +
    " DURING " + config.DATE_RANGE);
  var rows = report.rows();

  // Iterate through search query and decide whether to
  // add them as positive or negative keywords (or ignore).
  while (rows.hasNext()) {
    var row = rows.next();
    // If query exists as keyword, we don't need to process; report and move on
    if (keywordExists(row['Query'])) {
      debug([row['Query'], 'exists'].join(': '));
      continue;
    }
    debug([row['Query'], 'doesn\'t exist'].join(': '));
    // If the keyword doesn't exist, check if query meets criteria for
    // for addition as exact keyword

    // Currently, either needs to beat the CTR_THRESHOLD or
    // the CONVERSIONS_THRESHOLD
    if (parseFloat(row['Ctr']) >= config.CTR_THRESHOLD ||
    	parseInt(row['Conversions']) >= config.CONVERSIONS_THRESHOLD) {
      // Save query as a keyword to be added to this adGroup
      addToMultiMap(positiveKeywords, row['AdGroupId'], row['Query']);
      allAdGroupIds[row['AdGroupId']] = true;
    }
  } // rowIterator

  // Copy all the adGroupIds from the object into an array to allow bulkprocessing of groups
  var adGroupIdList = [];
  for (var adGroupId in allAdGroupIds) {
    adGroupIdList.push(adGroupId);
  }

  // Fetch all touched adGroups and process relevant keywords
  var adGroups = AdsApp.adGroups().withIds(adGroupIdList).get();
  while (adGroups.hasNext()) {
    var adGroup = adGroups.next();
    // Add negative keywords that were saved to be added to the adGroup
    // This version of the script doesn't mark keywords as negative,
    // but the plumbing is there if you want to give it a try
    if (negativeKeywords[adGroup.getId()]) {
      for (var i = 0; i < negativeKeywords[adGroup.getId()].length; i++) {
        adGroup.createNegativeKeyword('[' + negativeKeywords[adGroup.getId()][i] + ']');
      }
    }
    // Add positive keywords that were saved to be added to the adGroup
    if (positiveKeywords[adGroup.getId()]) {
      for (var i = 0; i < positiveKeywords[adGroup.getId()].length; i++) {
        adGroup.createKeyword('[' + positiveKeywords[adGroup.getId()][i] + ']');
      }
    }
  } // adGroupIterator
} // function main()

// All the exact keywords in the account
var allKeywordsMap = {};

////////////////////////////////////////////////////////////////////
// Fill the allKeywordsMap with all keywords
function getAllKeywords() {
  var options = { includeZeroImpressions : true }; // Include keywords that aren't used
  // AWQL query to find all keywords in the account
  var query = "SELECT Criteria, KeywordMatchType FROM KEYWORDS_PERFORMANCE_REPORT WHERE KeywordMatchType = EXACT DURING LAST_7_DAYS";
  var reportIter = AdsApp.report(query, options).rows();
  while(reportIter.hasNext()) {
    var row = reportIter.next();
    debug("Exact keyword: '" + row.Criteria + "'");
    // Save as key, for easy lookup
    allKeywordsMap[row.Criteria.toLowerCase()] = true;
  }
  return allKeywordsMap;
} // function getAllKeywords()

////////////////////////////////////////////////////////////////////
// Check if keyword exists, only works if getAllKeywords has been run.
function keywordExists(keyword) {
  return (allKeywordsMap[keyword.toLowerCase()] !== undefined);
} // function keywordExists()


////////////////////////////////////////////////////////////////////
function addToMultiMap(map, key, value) {
  if (!map[key]) {
    map[key] = [];
  }
  map[key].push(value);
} // function addToMultiMap()

////////////////////////////////////////////////////////////////////
// Convenience function to generate a date range based on the current date.
function last_n_days(n) {
	var	from = new Date(),
		to = new Date();
	to.setUTCDate(from.getUTCDate() - n);
	return google_date_range(from, to);
} // function last_n_days()

////////////////////////////////////////////////////////////////////
// Convenience function to generate a google formatted date range based on js Date objects
function google_date_range(from, to) {
	function google_format(date) {
		var date_array = [date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()];
		if (date_array[1] < 10) date_array[1] = '0' + date_array[1];
		if (date_array[2] < 10) date_array[2] = '0' + date_array[2];
		return date_array.join('');
	}
	var inverse = (from > to);
	from = google_format(from);
	to = google_format(to);
	var result = [from, to];
	if (inverse) {
		result = [to, from];
	}
	return result.join(',');
} // function google_date_range()

////////////////////////////////////////////////////////////////////
// Some functions to help with logging - gracefully borrowed from http://www.freeadwordsscripts.com
var LOG_LEVELS = { 'error':1, 'warn':2, 'info':3, 'debug':4 };
function error(msg) { if(LOG_LEVELS['error'] <= LOG_LEVELS[LOG_LEVEL]) { log('ERROR',msg); } }
function warn(msg)  { if(LOG_LEVELS['warn']  <= LOG_LEVELS[LOG_LEVEL]) { log('WARN' ,msg); } }
function info(msg)  { if(LOG_LEVELS['info']  <= LOG_LEVELS[LOG_LEVEL]) { log('INFO' ,msg); } }
function debug(msg) { if(LOG_LEVELS['debug'] <= LOG_LEVELS[LOG_LEVEL]) { log('DEBUG',msg); } }
function log(type,msg) { Logger.log(type + ' - ' + msg); }

Source: https://remkovanderzwaag.nl/blog/adwords-script-auto-optimalisatie-zoektermen

Sharing Knowledge

Adsscripts.com is all about sharing knowledge. In the current market, PPC specialists like to keep their knowledge and experience to themselves. We're convinced that sharing knowledge can ensure that everyone gets better at their work. We want to change this by sharing our knowledge about scripts with everyone.

Do you also want to contribute? We are open to new ideas and feedback on everything you find on Adsscripts.com.

Contact us

Nils Rooijmans
Google Ads Scripter
Water Cooler Topics
Nils Rooijmans, Google Ads Scripter
Bas Baudoin
Teamleader PPC
Happy Leads
Bas Baudoin, Teamleader PPC
Martijn Kraan
Freelance PPC Specialist
Brightstep
Martijn Kraan, Freelance PPC Specialist
Tibbe van Asten
PPC Specialist
Founder Adsscripts
Tibbe van Asten, Senior PPC Automation Specialist