Add search queries as keywords

Add search terms as a keyword when they perform well. An easy way to expand an account.

Start Now!
Add search queries as keywords
Add search Get started!

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.

The 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
//
// Last update: 14-01-2020
//
////////////////////////////////////////////////////////////////////

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

  // Maximum cost/conversions. Leave empty to ignore
  CPA_THRESHOLD : , // 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) {
      
      // When CPA_THRESHOLD is set, Cost per conversion must be lower than the CPA_THRESHOLD
      if(config.CPA_THRESHOLD != ""){
        if(parseFloat(row['CostPerConversion'] < config.CPA_THRESHOLD){
          // Save query as a keyword to be added to this adGroup
          addToMultiMap(positiveKeywords, row['AdGroupId'], row['Query']);
          allAdGroupIds[row['AdGroupId']] = true;
        }
      } 

      if(config.CPA_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); }
Show whole script!
The Experts
Tibbe van Asten Head of PPC @ Increase
Nils Rooijmans Water Cooler Topics
Martijn Kraan Freelance PPC Specialist
Bas Baudoin Teamlead SEA @ Happy Leads
How about you? JOIN US!
Sharing Knowledge
Caring

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

Training &
Workshop
Contact us!
Adsscripts Training & Workshop