At the end of the day, ads scripts keep your campaigns running like a lean, mean, well-oiled machine.
Nils Rooijmans, Wordstream
Negative keywords prevent ads from appearing to irrelevant search queries. However, in some cases, they may block your normal keywords from showing to valuable traffic. This issue often arises when you choose a wrong match type for your negative keywords. When your negative keyword was set broader than intended, you may be blocking your valuable money keywords.
Negative Keyword Conflicts refer to checking whether your negative keywords block normal keywords. The script finds and records all such conflicts for you to take appropriate action.
Such an issue also arises when keywords are updated by more than a single person, or updated way back in the past and they are now forgotten. Whatever the reason is, we’re always here to provide you the best solution for your PPC efforts. In this post, we’ll show you how to effectively analyze conflicting keywords in your PPC campaigns.
Step 1: Problems with keyword conflicts
A keyword conflict is when you’re blocking your ads from showing on active keywords. Although you have chosen keywords to run on your campaigns, they’re not appearing on search results due to conflicts.
Problem 1: You may block traffic by accident
You blocked search patterns in the past because of bad performance, or you don’t have some products in your inventory anymore. However, your inventory is changing over time and you have a new situation. When you add new keywords to your account, they are maybe still blocked because of your old negative keywords.
As an example, you see terms like this when you analyze your search term reports:
- cheap red hats
- cheap green trousers
- green Adidas trousers
Say you’re selling premium red hats, and you added “cheap” as a negative keyword. Later, you have incoming inventory and you added a lot of new queries including “cheap.” As you didn’t update your negative keyword lists, your new keywords including cheap won’t show for relevant search queries.
Problem 2: You used negatives to “pause” bad performing ones in your large keyword inventory
This approach used to work perfectly to handle your bad performing keywords. However, the pausing does not work anymore like in the past as Google started matching a lot of close variants to those bad performing keywords. Now you should really change the keyword status to “paused.”
Problem 3: Conflicts can still get impressions
Marketeers often don’t realize when a keyword is blocked because conflicts can also get impressions. Look at the below example:
Say this is your ad group:
- Phrase match keyword: Cheap running trousers
- Negative keyword: running
Singulars, plurals, misspellings, and different stemming are still matched in phrase match. So although you have the negative keyword -running, your ads will still appear for these results.
- cheap run trousers (different stemming)
- cheap rnning trousers (misspelling)
With Google’s latest exact match changes, you’ll have tons of such variations.
Problem 4: Negative keyword conflict reports
You’ll often see alerts in your notification bar, either in Google, Bing, etc. Or, keyword conflict reports can be manually run. However, these reports are often not quite useful, especially Ads. The reports from Ads only examines ad group and campaign negatives. Ads keyword conflict report doesn’t include campaign lists. The function is also known to not always find all conflicts in your campaigns.
While many search engines, including Google, give you keyword conflict data, it’s not surprising that they miss a lot of conflicts.
The following Ads script is addressing both problems. You can have further actions by appropriately labeling actions.
Step 2: Conflicting keywords Ads script by PEMAVOR
//User reports // keyword_view : https://developers.google.com/google-ads/api/fields/v8/keyword_view // shared_criterion : https://developers.google.com/google-ads/api/fields/v7/shared_criterion //Declare label to mark keywords. var labelText = "Blocked by Negative Test" var labelsArr = {} //foundIdsOnly will keep keywordId,adGroupId -> "keywordid:adgroupid" : [11111,22222] //Will use this object to call withIds method on AdsApp.keywords() method because Google Suggests : Use IDs for filtering when possible var foundIdsOnly = {} //Will keep negative word details for keywordIds incase we want to log it. "keywordId:adGroupId" = { negativeWord: "keyword" } var foundArr = {} //Add filters if necessary. NAME IN ['NAME1','NAME2','NAME3'] //var accountFilter = "Name IN ['Brand']" var columns = [ "Action", 'Customer ID', 'Campaign ID', 'Ad Group ID', 'Keyword ID', 'Label' ]; //Main function to start script function main() { Logger.log("Starting script") //Run script as MCC. Select accounts with condition. //var accountSelector = AdsManagerApp.accounts().withCondition(accountFilter) var accountSelector = AdsManagerApp.accounts() //Iterate through found client accounts parallel accountSelector.executeInParallel("processClientAccount", "afterProcessAllClientAccounts"); } function processClientAccount() { //Select client account as current AdApp account var account = AdsApp.currentAccount(); var accObject = { accountId: account.getCustomerId(), accountName: account.getName() } Logger.log("[" + accObject.accountName + " (" + accObject.accountId + ")] Running script") //Call function to fetch report keyword_view (keyword list) var keywords = createReportKeywords(accObject); var sharedSets = getSharedSets() //Call function to fetch report shared_criterion (negative list) var count = createReportNegatives(keywords, accObject, sharedSets); //Call function to label keywords and return labeled count //Object to return from parallel function var returnDetails = { accId: account.getCustomerId(), accName: account.getName(), count: count } return JSON.stringify(returnDetails); } //All client accounts completed function afterProcessAllClientAccounts(results) { Logger.log("===== RESULTS FOR EACH ACCOUNTS =====") for (var i = 0; i < results.length; i++) { var result = JSON.parse(results[i].getReturnValue()); Logger.log("[" + result.accId + " (" + result.accName + ")] : " + result.count) } } //Function to fetch keywords report function createReportLabels() { var acc = AdsApp.currentAccount(); AdsManagerApp.select(acc); var customerId = acc.getCustomerId() var report = AdsApp.report("SELECT label.name,label.status FROM label WHERE customer.id = " + prepareCustomerId(customerId) + " AND label.name LIKE 'Blocked%'"); var rows = report.rows(); var i = 0 if (rows.hasNext()) { //Loop through reports rows while (rows.hasNext()) { row = rows.next() var text = row['label.name']; AdsApp.removeLabel(text) } } } function createReportKeywords(accObject) { var dateStart = Date.now(); Logger.log("[" + accObject.accountName + "] Fetching report for keywords") var account = AdsApp.currentAccount() var customerId = account.getCustomerId() var keywords = {} var report = AdsApp.report('SELECT Criteria, Id, AdGroupId, Labels, CampaignId ' + 'FROM KEYWORDS_PERFORMANCE_REPORT WHERE ExternalCustomerId = "' + prepareCustomerId(customerId) + '"'); var rows = report.rows(); Logger.log("[" + accObject.accountName + "] Fetched keywords from report. Preparing...") var i = 0 if (rows.hasNext()) { //Loop through reports rows while (rows.hasNext()) { var row = rows.next(); //Read columns from reports row var text = row['Criteria']; var id = row['Id']; var campaignId = row["CampaignId"] var adGroupId = row['AdGroupId']; var labels = row['Labels']; //Split keyword to words to prepare object. var keywordsArr = text.split(" "); //Loop through words in keyword for (k = 0; k < keywordsArr.length; k++) { //If key already exists in object if (keywords[keywordsArr[k]]) { //If id already exists in key' ids property if (keywords[keywordsArr[k]].ids.indexOf(id) >= 0) {} else { //Push new id to ids property keywords[keywordsArr[k]].ids.push(id) //Push new adgroup to adGroupIds property keywords[keywordsArr[k]].adGroupIds.push(adGroupId) keywords[keywordsArr[k]].labels.push(labels) keywords[keywordsArr[k]].campaigns.push(campaignId) keywords[keywordsArr[k]].keywords.push(text) } } //If key does not exist in object else { keywords[keywordsArr[k]] = { ids: [id], adGroupIds: [adGroupId], campaigns: [campaignId], labels: [labels], keywords: [text] } } } i++ } } Logger.log("[" + accObject.accountName + "] Fetching keywords completed (Took " + (Math.round((Date.now() - dateStart) / 1000)) + " seconds). Found keywords count : " + i) return keywords } //Function to fetch negatives report function createReportNegatives(keywordList, accObject, sharedSets) { var account = AdsApp.currentAccount() //Check if label exists in account var labelIterator = AdsApp.labels().withCondition("Name CONTAINS 'Blocked:'").get() if (labelIterator.hasNext()) { while (labelIterator.hasNext()) { label = labelIterator.next() label.remove() } } var upload = AdsApp.bulkUploads().newCsvUpload( columns); var dateStart = Date.now(); var count = 0 Logger.log("[" + accObject.accountName + "] Fetching report for negatives") var account = AdsApp.currentAccount() var customerId = account.getCustomerId() var report = AdsApp.report('SELECT shared_criterion.keyword.match_type, shared_set.name, shared_criterion.keyword.text ' + 'FROM shared_criterion ' + 'WHERE shared_set.status = "ENABLED" AND shared_set.type = "NEGATIVE_KEYWORDS" AND customer.id = "' + prepareCustomerId(customerId) + '"'); var rows = report.rows(); Logger.log("[" + accObject.accountName + "] Fetched negatives from report. Preparing...") var i = 0 if (rows.hasNext()) { //Loop through rows in report while (rows.hasNext()) { var row = rows.next(); var keyword = row["shared_criterion.keyword.text"]; var sharedSet = row["shared_set.name"]; var listName = "Blocked:" + row["shared_set.name"] var matchType = row["shared_criterion.keyword.match_type"] var splitted = keyword.split(" ") var found = 0 var Ids = []; var adGroupIds = []; var labels = []; var originalWords = []; //Split found negative to words (To check if 1gram or more...) for (s = 0; s < splitted.length; s++) { //If word key exists in keywordList object if (keywordList[splitted[s]]) { found++ //If first word of negative if (s == 0) { //Get ids and adgroupids by key from object Ids = keywordList[splitted[s]].ids adGroupIds = keywordList[splitted[s]].adGroupIds campaigns = keywordList[splitted[s]].campaigns labels = keywordList[splitted[s]].labels originalWords = keywordList[splitted[s]].keywords } else { //If not first word (not ngram) newAdGroups = [] newIds = [] newLabels = [] newOriginalWords = [] newCampaigns = [] //Filter previous found keys id and adgroup id to check if keyword ids that can be found in all lookups newIds = Ids.filter(function(val, index) { if (keywordList[splitted[s]].ids.indexOf(val) >= 0) { newAdGroups.push(keywordList[splitted[s]].adGroupIds[keywordList[splitted[s]].ids.indexOf(val)]) newLabels.push(keywordList[splitted[s]].labels[keywordList[splitted[s]].ids.indexOf(val)]) newOriginalWords.push(keywordList[splitted[s]].keywords[keywordList[splitted[s]].ids.indexOf(val)]) newCampaigns.push(keywordList[splitted[s]].campaigns[keywordList[splitted[s]].ids.indexOf(val)]) return true } else { return false } }) Ids = newIds; adGroupIds = newAdGroups; labels = newLabels; campaigns = newCampaigns; originalWords = newOriginalWords } } } //If key in object exists for all words in negative if (found == splitted.length) { if (Ids && Ids.length > 0) { for (x = 0; x < Ids.length; x++) { if (foundIdsOnly[Ids[x] + ":" + adGroupIds[x]]) {} else { var campId = campaigns[x] foundIdsOnly[Ids[x] + ":" + adGroupIds[x]] = [adGroupIds[x], Ids[x]] foundArr[Ids[x] + ":" + adGroupIds[x]] = { negativeWord: keyword } var labelToArr if (isJson(labels[x])) { labelToArr = JSON.parse(labels[x]) } else { labelToArr = [] } if (labelsArr[listName]) {} else { if (sharedSets[campId]) { if (sharedSets[campId].indexOf(sharedSet) >= 0) { AdsApp.createLabel(listName) labelsArr[listName] = "ENABLED" } else { } } } labelToArr.push(listName) var strLabels = labelToArr.toString() strLabels = strLabels.replace(/,/g, ";") var willAppend = false if (matchType == "BROAD") { willAppend = true } if (matchType == "PHRASE") { var re = new RegExp("(^|\\W)" + keyword + "($|\\W)", "gi") var res = originalWords[x].match(re) if (res) { willAppend = true } } if (matchType == "EXACT") { if (keyword == originalWords[x]) { willAppend = true } } if (sharedSets[campId]) { if (sharedSets[campId].indexOf(sharedSet) >= 0) { } else { willAppend = false } } else{ willAppend = false } if (willAppend) { upload.append({ 'Action': "Edit", 'Customer ID': account.getCustomerId(), 'Ad Group ID': adGroupIds[x], 'Keyword ID': Ids[x], 'Campaign ID': campId, 'Label': strLabels }); count++ } } } } } i++ } upload.forCampaignManagement(); upload.apply() //upload.preview(); Logger.log("[" + accObject.accountName + "] Fetching negatives completed (Took " + (Math.round((Date.now() - dateStart) / 1000)) + " seconds). Found negatives count : " + i) } return count } function getSharedSets() { var sharedSets = {} var account = AdsApp.currentAccount() var customerId = account.getCustomerId() var query = 'SELECT shared_set.name, shared_set.id, campaign.id FROM campaign_shared_set WHERE customer.id = ' + prepareCustomerId(customerId) var report = AdsApp.report(query); var rows = report.rows(); var count = 0 //If report has rows if (rows.hasNext()) { //Loop through rows while (rows.hasNext()) { var row = rows.next() var campId = row["campaign.id"] var setName = row["shared_set.name"] if (sharedSets[campId]) { if (sharedSets[campId].indexOf(setName) >= 0) {} else { sharedSets[campId].push(setName) } } else { sharedSets[campId] = [] sharedSets[campId].push(setName) } //Store shared set name with campaing id,so there will be no need to take action on the already linked ones. } } return sharedSets } //Function to mark negativated keywords with label function isJson(item) { item = typeof item !== "string" ? JSON.stringify(item) : item; try { item = JSON.parse(item); } catch (e) { return false; } if (typeof item === "object" && item !== null) { return true; } return false; } //Pollyfill for Object.values function objectValues(obj) { var res = []; for (var i in obj) { if (Object.prototype.hasOwnProperty.call(obj, i)) { res.push(obj[i]); } } return res; } //Remove - from customerId function prepareCustomerId(str) { return str.replace(/[-]/g, "") }
Step 3: Copy the script on Google Ads Interface
Navigate to Bulk Actions > Scripts page by using top menu on your Google Ads interface.
Add new script using “+” button.
Paste the script code you copied from Step 2 to the Script Editor.
Step 4: Run the conflicting keywords script
Click to the “Run” button at the bottom of the script area to start executing script. (or use “preview” button to preview changes before running the script).
While the script runs, you can see logs at “LOGS” tab to preview the changes to be made.
Step 5: Upload process
After the script ran successfully, navigate to “Uploads” page on the left side to apply or manage bulk uploads created by script.
Use the panel below to see the upload status. Once your upload is completed, you can download results in csv format, or undo all changes if you wish.