Deep dive · 13 min read

The Shopping search-term report Google won't give you

Shopping and PMax reports show the query but never the product. Rebuild the missing query × product report with one URL parameter and two GA4 dimensions.

Illustration pairing a stack of search queries with a single product, the missing link Google's reports hide.
The serious facts are real — the article covers are not.

In short: Shopping and PMax reports show the query but hide the product. Add one URL parameter, {lpurl}?utm_content={product_id}, and within a week GA4 hands you the retired query × product report by joining sessionManualAdContent with sessionGoogleAdsQuery. On a real account that view exposed 85 % of spend flowing through pairs that never converted, while a 0.25 % slice carried the whole account at 46.8× ROAS.

A client once asked me a question that should have a one-click answer. Someone searched “cordless vacuum under 200”, clicked the Shopping ad, and the account paid for it. Which of the 5,000 products in the feed did that click land on? I opened the search terms report, then the product report, and there was no way to connect them. There still isn’t. Google shows you the query, and it shows you the product, but never together.

Years ago the old AdWords API would hand you exactly this pairing. Then it quietly disappeared, and no setting, no report, no API field brings it back. For an online shop that gap is not cosmetic. Query to product is where the real optimization lives. Whether your titles match real demand. Which products attract garbage traffic. Why a product gets clicks but never converts.

Twelve years of writing against the Google Ads API taught me that when Google retires a report, it doesn’t come back. So I rebuilt it myself, and the rebuild is almost embarrassingly simple. One URL parameter. Within a week, GA4 hands you every paid click paired as query × product, with sessions, conversions and revenue attached. The build takes minutes; the rest of this article is what you do with what comes back.

78 %
of clicks matched query × product
22
campaigns updated
1
URL parameter, that's the whole stack
7 days
to verified data in GA4

The fix: one parameter, two GA4 dimensions

Add a tracking template

Set {lpurl}?utm_content={product_id} on your Shopping and PMax campaigns. The ValueTrack variable {product_id} sends the Merchant Center product ID with every click.

GA4 stores the product

The UTM lands in the Session manual ad content dimension (sessionManualAdContent).

GA4 already knows the query

Auto-tagging (gclid) fills sessionGoogleAdsQuery on the same session.

Join the two dimensions

Every paid click becomes a query × product pair, with sessions, conversion rate and revenue attached.

Auto-tagging and the manual UTM don’t fight each other. The gclid keeps handling source, medium and campaign; your UTM only carries the product ID.

Know what you’re getting (and what you’re not)

  • Clicked queries only. This dataset starts at the click. Queries where your ad showed but nobody clicked never reach GA4. Impression-level analysis stays in Google’s standard report, without products.
  • About 20 % of clicks won’t pair up. Consent-rejected sessions, PMax surfaces with no query at all (Display, YouTube, Gmail), clicks that never fired analytics.
  • Session-scoped. One session carries one product ID, the clicked one, even if the user then browses ten others.

Pulling the report out of GA4

In the GA4 interface, open Explore and build a free-form table. Dimensions are Session manual ad content and Session Google Ads query. Metrics are sessions, key events (formerly conversions) and purchase revenue. Filter on session source/medium equal to google / cpc.

For anything serious you’ll want the Data API, because this report belongs in a dashboard or a BigQuery join, not in a browser tab:

from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
    RunReportRequest, DateRange, Dimension, Metric, FilterExpression, Filter
)

request = RunReportRequest(
    property=f"properties/{GA4_PROPERTY_ID}",
    dimensions=[
        Dimension(name="sessionManualAdContent"),  # product ID
        Dimension(name="sessionGoogleAdsQuery"),    # search term
    ],
    metrics=[
        Metric(name="sessions"),
        Metric(name="keyEvents"),
        Metric(name="purchaseRevenue"),
    ],
    date_ranges=[DateRange(start_date="30daysAgo", end_date="today")],
    dimension_filter=FilterExpression(filter=Filter(
        field_name="sessionSourceMedium",
        string_filter=Filter.StringFilter(value="google / cpc"),
    )),
)

Join sessionManualAdContent against your product feed (id to title, price, category) and the report is complete. Query, product, title and economics in one table.

Verified, not theorized

We deployed this on a live account, a Czech electronics retailer with 22 enabled Shopping campaigns, and I checked GA4 seven days later:

GA4, 7 days after deployment

  • Rows of google/cpc traffic 9,753
  • Carried the product ID (utm_content) 96 %
  • Carried the search query (sessionGoogleAdsQuery) 81 %
  • Carried both, a working query × product report 78 %

Now the fun part: what the pairs tell you

1 · The title gap

Put the queries next to the title and description they triggered. The mismatch jumps out:

What people actually search
cordless vacuum pet hairvacuum for dog hair allergypet vacuum cleaner quietbest vacuum animal hair 2026
What the title says
Vacuum X300 Turbo 2000W, EAN 8595…
Description: "High-quality vacuum cleaner with modern design and rich accessories."
The gap: The title sells wattage nobody searches for and misses the pet-hair use case everybody searches for. (Illustrative example.)

The feed roadmap writes itself. I’ve spent the past two years building an AI feed-enrichment tool, and the honest weakness of the whole category is proving impact. This report fixes that. Rewrite the title around demand you’ve just proved, then watch CTR and conversion rate move per query. AI feed enrichment stops being a leap of faith and becomes a measurable loop.

2 · Who else is on “your” query, and with what product

One thing you can’t do is add negative keywords per product. Google gives you no such lever in Shopping or PMax. So when a query × product pair underperforms, asking how to exclude it leads nowhere. The productive question is why it underperforms.

Take the query and scrape the live results page for it. I use DataForSEO, endpoint serp/google/organic/live/advanced. One POST with the query text and the location_code of your market returns the whole results page as structured JSON, paid ads, shopping blocks with merchant names and prices, organic below. DataForSEO bills per SERP, and one SERP covers up to 10 results. At the current base price of about $0.002 per SERP, checking 200 default-depth queries costs about $0.40, before any paid advanced parameters.

A typical finding. Your mid-range sleeping bag collects clicks on a generic query, and the same query shows three budget brands in the same category at half your price. The product is fine. The price loses that specific auction.

Now you have real options. Reprice. Push the differentiator into the title (“down fill, −15 °C comfort”). Move the product into a campaign with bidding that matches its margin reality. Or accept the query as upper-funnel and judge it on assisted metrics instead of last click.

3 · Structure and bidding decisions

Products attracting high-intent queries deserve their own asset groups and budgets. Products collecting only generic traffic belong in catch-all groups with conservative targets. This report gives shopping segmentation an evidence base, and it replaces gut feeling.

4 · A health check for PMax

PMax tells you almost nothing about search. This report is the closest thing you’ll get to an audit of what PMax actually buys for you on the search surface, per product.

Watch me walk one real export, step by step

Everything above is the why. Here is what it actually looks like, every step, what you download, what you stare at, and the real number that comes back.

The data is a second account, a mid-size Czech online shop in the bargain segment. It isn’t the electronics retailer from the GA4 box above; I’m using it because its catalogue is broad enough that every pattern shows up at full scale. I pulled its raw Shopping search-terms report through the Google Ads API into a local SQLite table and ran the aggregations below. One caveat first. The raw report gives you the ad group or product group the query was served under, not the individual product. That is precisely the gap the UTM trick closes. But even at product-group level, the intermediate outputs are loud enough to make the point.

Source for every number in this section: Google Ads Shopping search-terms report, one mid-size Czech online-shop account, ~22.6M lines, data pulled April 2026. Amounts converted from CZK at ~25 CZK/EUR.

Step 1 · Pull the raw report and measure the pile

Export the Shopping search-terms report via the Google Ads API (search_term_view) into anything you can run GROUP BY on, SQLite or BigQuery. Before you join or filter a single thing, run a plain COUNT(*) and a couple of SUMs. You need to feel how big and how noisy the raw pile is, because that one fact resets every expectation that follows:

The raw pile, one COUNT and three SUMs

  • Report lines (query × product group) 22,640,716
  • Distinct search terms 5,370,131
  • Product groups they were served under 10,393
  • Spend / revenue (blended ROAS 6.8×) €103k / €697k

22.6 million lines, 5.4 million unique queries. No human reads that. The number’s only job is to tell you the next move, which is to collapse the pile by the dimension that pays the bills, and to warn you that per-row manual triage is hopeless.

Step 2 · Throw away 96 % of it before you analyse anything

Count the lines with zero clicks, then filter them out. The Shopping search-terms report logs every query your ad showed for, and most of those queries nobody ever clicked. Impression-only lines can’t cost you and can’t convert. They’re noise that makes the table look scary.

WHERE clicks > 0 collapses the table to 738,444 lines, a size you can actually work with.

The impression flood

  • Lines with zero clicks (pure impressions) 21,902,272 (96.7 %)
  • Lines that ever cost money 738,444 (3.3 %)

96.7 % of the scary number was never anything but noise. Filter to clicked lines before you analyse anything, and every aggregation below runs on the part that actually spends money.

Step 3 · Ask the one question that reframes the account

On the clicked lines, split spend into “converted at least once” versus “never converted,” and total the cost of each. This is the number that turns “the account is fine, ROAS is 6.8” into “most of the budget does nothing.” Compute it before any optimisation idea:

The zero-conversion drag

  • Report lines that converted zero times 99.75 %
  • Share of total spend those lines ate 85.5 %
  • Spend with no conversion behind it €88k
  • Of clicked lines only, share with zero conversions 92.3 %

85 % of the budget flowed through query × product-group combinations that never once converted.

That isn’t a rounding error you optimise later. It’s the main event. And you could only see it because you collapsed the query down to what it was sold against.

Step 4 · Check whether the waste is a few villains or the whole crowd

Sort the clicked lines by cost, take the top 1 % and top 10 %, and see how much of total spend they hold. This decides your tactic. If a handful of terms burn the budget, you pause them and you’re done. If the waste is spread thin, pausing terms is pointless and you need structural fixes:

Where the wasted spend actually sits

  • Top 1 % of clicked lines by cost 10.5 % of spend
  • Top 10 % of clicked lines by cost 29.8 % of spend

The drag is long-tail. The top 1 % of costly lines hold barely a tenth of spend, so pausing 20 bad terms changes nothing. Fix structure, which products sit in which campaign at which target, instead of chasing individual queries. And remember that Google won’t even let you add a negative per product anyway.

Step 5 · Find out why the long tail leaks: one query, many products

For each search term, count how many distinct product groups it was served under. This explains the waste mechanically. Shopping matches a query against your whole feed’s signals, not against one product’s relevance. A single query leaks across unrelated corners of your catalogue, and you pay for every miss.

A concrete case from the account. The query “anti-bark device” kept burning budget, so I ran COUNT(DISTINCT ad_group) on it. It had been served under 139 different ad and product groups, for €39 and roughly zero conversions. “lego technic” touched 300.

Query spray across the catalogue

  • Distinct terms served under more than one product group 46.7 %
  • Most product groups a single query reached 6,661
  • "anti-bark device" → groups / cost / conversions 139 / €39 / ~0

Nearly half the queries are smeared across multiple product groups, and the worst offenders reach thousands. That’s the engine of the zero-conversion drag from step 3, and the exact thing you can finally see once every click carries its product ID.

Step 6 · Eyeball the worst mismatches, because they’re absurd

Pull the highest-cost lines that never converted and read the query next to the product group it was sold against. The aggregate numbers convince your spreadsheet. These three rows convince your boss:

Highest-cost zero-conversion query × product-group pairs (translated and anonymised; illustrative of the real rows).
Search queryServed under product groupClicksCostConv.
dog training collar Handbags97€110
anti-bark device Baby products76€9.600
lego technic Lighting70€6.800

A dog-training-collar query paid for under the Handbags group. An anti-bark device sold against Baby products. The product group has nothing to do with the query. Google matched on broad feed signals, collected the click, and charged the account. With query × product you see this in one glance; with Google’s standard report you never will. (Categories translated and anonymised.)

Step 7 · Now the payoff: the 0.25 % that pays for the whole account

Invert step 3. Isolate only the lines that did convert and total their cost and revenue. This is the reason the whole exercise matters. Once you can separate the winners from the drag, you protect the winners and starve the rest.

The slice that earns its keep

  • Lines that converted (share of all lines) 57,209 (0.25 %)
  • What they cost €14.9k
  • What they returned €697k
  • ROAS on that slice 46.8×

A quarter of one percent of the lines run at 46.8× ROAS and effectively carry the account. The other 99.75 % drag the blended figure down to 6.8×. Finding that slice, protecting its budget and structuring everything else away from it is the entire job. And none of it is possible until every line carries the product it was sold against. That’s what the one-parameter UTM at the top of this article buys you.

Take it with you

Download the complete instructions for your AI

The whole method above, rewritten as a brief you can paste straight into Claude or any capable coding agent. It sets the tracking template, pulls the query × product report out of GA4 and runs the aggregations on your account. The template and its three traps are in there too. Leave your email and the file is yours.

One file, one list. I only write when there's something worth reading.

FAQ

Does this work for Performance Max?

Yes, for the search and shopping surface. Display, YouTube and Gmail clicks carry the product ID but no query, so expect those rows to have an empty query dimension.

Will the UTM break my GA4 attribution?

No. Auto-tagging (gclid) keeps handling source, medium and campaign; you only add ad content. What would break things is a custom parameter inside utm_campaign, so never put one there.

Why only 78 % coverage?

Consent mode, queryless PMax surfaces and analytics blockers eat the rest. 78 % is plenty for every use case above, because you’re reading patterns, not auditing cents.

Can I see queries my ad showed for but nobody clicked?

No. This dataset starts at the click. Impression-level analysis stays in the standard search terms report, without products.

Does the pattern work outside Google, say Bing or Sklik?

Yes. The pattern transfers to any platform with a URL template, a product macro and an analytics dimension to catch it. The specific macros differ per platform.

How long until I have usable data?

Depends on volume. Our account had a workable report in 7 days; smaller accounts should collect 30.


CTA: Which of your products earn their clicks, and which just bill you? Add the parameter today, and in a week you’ll know. Or send me your account and we’ll read the report together.

The point of all this

Want this level of visibility in your account?

One e-mail. I'll tell you honestly whether it's worth it for your setup.

Get in touch →