Ads Script for Negative Keyword Conflicts in PPC

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, I’ll show you how to effectively analyze conflicting keywords in your PPC campaigns.

On this page

    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 do not have some products in your inventory anymore. But 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 will 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 ['NAME1']"
    
        var columns = [
            "Action", 'Customer 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);
    
            //Call function to fetch report shared_criterion (negative list)
            var count = createReportNegatives(keywords, accObject);
            //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 ' + '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 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]].keywords.push(text)
                            }
                        }
                        //If key does not exist in object
                        else {
                            keywords[keywordsArr[k]] = {
                                ids: [id],
                                adGroupIds: [adGroupId],
                                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) {
            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 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
                                labels = keywordList[splitted[s]].labels
                                originalWords = keywordList[splitted[s]].keywords
                            } else {
                                //If not first word (not ngram)
                                newAdGroups = []
                                newIds = []
                                newLabels = []
                                newOriginalWords = []
                                    //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)])
                                        return true
                                    } else {
                                        return false
                                    }
                                })
                                Ids = newIds;
                                adGroupIds = newAdGroups;
                                labels = newLabels;
                                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 {
                                    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 {
                                        AdsApp.createLabel(listName)
                                        labelsArr[listName] = "ENABLED"
                                    }
                                    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 (willAppend) {
                                        upload.append({
                                            'Action': "Edit",
                                            'Customer ID': account.getCustomerId(),
                                            'Ad Group ID': adGroupIds[x],
                                            'Keyword ID': Ids[x],
                                            '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 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.

    Share
    Recent Posts

    Comments

    You must be logged in to post a comment.

    More similar posts

    Menu