← Back to Metrics

Zero-Results Searches

Also known as: Null Searches, No Results Searches, Zero Result Searches, Searches with No Results, Failed Searches

ExperienceProduct searchProduct discoverySearch qualityE-commerceRetail

The number of submitted product searches that show no matching products to the shopper during the reporting period.

Business Use Case

1️⃣ Prioritize synonyms and redirects for common searches with no matches. 2️⃣ Identify demand for products missing from the assortment. 3️⃣ Monitor search quality after search engine or product catalog changes.

Business Criticality

High

Journey / Navigation Area

Discovery

Owning Function

Merchandising

Scope

Event

Formula

Count of submitted product searches with zero matching products

Calculation Notes

1️⃣ Count each submission ID once; deliberate repeats use new IDs. Require immutable final responses and quarantine conflicts before export. 2️⃣ Require an explicit valid zero count and completed response; exclude errors, timeouts, missing counts, fractions, and invalid values. 3️⃣ Require product, query, and external labels; exclude bots, staff, tests, navigation, autocomplete requests, and refinements. 4️⃣ Use final matches after correction; recommendations are not matches. Date by submission in the configured zone and complete-day window. 5️⃣ Confirm coverage and completion lag; return unknown for incomplete coverage. Sum only separate periods or mutually exclusive segments. 6️⃣ Match Site Searches rules, periods, and segments for Zero-Results Search Rate; no eligible searches means an undefined rate. 7️⃣ PII flag covers aggregate output only. Sanitize terms before collection; restrict raw terms and linked identifiers as personal data.

Technical Details

Measure TypeCounter
Aggregation WindowDaily, Weekly, Monthly

Events

PlatformEvent
Google Analytics 4Use the recommended search event with sanitized search_term. Custom parameters: search_submission_id, search_submitted_at, search_results_count, search_type, submission_kind, traffic_type, response_status. Emit the final response for a deliberate submission; retries retain its identifier and values. Configure collection to serialize text parameters as strings. Store search_results_count in exactly one GA4 value slot: integer, double, float, or a decimal string. Decimal strings representing zero must contain only an optional sign, zeros, and an optional decimal point followed by zeros. Parameter keys must occur once. Submission timestamps use RFC 3339 UTC strings. The SQL checks numeric zero directly without rounding. Register custom dimensions only where reporting requires them.
AdobeConfigure a custom product-search completion event and map its submission identifier, submission timestamp, final result count, search type, submission kind, traffic classification, and response status to the XDM search fields shown. An Adobe Analytics custom counter may be populated through an explicitly configured integration; search or null searches is not an automatic native equivalent. The AEP query uses ExperienceEvent records, not Analytics report totals. Replace _tenantId with the configured tenant namespace in the mapping and SQL. The schema must define searchResultsCount as double, submittedAt as date-time, and the remaining search fields as strings. Configure ingestion for the custom search eventType. W3 and Adobe Client Data Layer examples require an integration to analytics. The synthetic ECID illustrates identityMap and is unnecessary for counting.

Data Mappings

W3 Data Layer
{
  "digitalData": {
    "event": [
      {
        "eventInfo": {
          "eventName": "search"
        },
        "attributes": {
          "searchTerm": "blue boots",
          "searchSubmissionId": "synthetic-search-001",
          "searchSubmittedAt": "2026-10-14T12:00:00Z",
          "searchResultsCount": 0,
          "searchType": "product",
          "submissionKind": "query",
          "trafficType": "external",
          "responseStatus": "completed"
        }
      }
    ]
  }
}
GA4 Data Layer
{
  "event": "search",
  "search_term": "blue boots",
  "search_submission_id": "synthetic-search-001",
  "search_submitted_at": "2026-10-14T12:00:00Z",
  "search_results_count": 0,
  "search_type": "product",
  "submission_kind": "query",
  "traffic_type": "external",
  "response_status": "completed"
}
Adobe Client Data Layer
[
  {
    "event": "search",
    "eventInfo": {
      "searchTerm": "blue boots",
      "searchSubmissionId": "synthetic-search-001",
      "searchSubmittedAt": "2026-10-14T12:00:00Z",
      "searchResultsCount": 0,
      "searchType": "product",
      "submissionKind": "query",
      "trafficType": "external",
      "responseStatus": "completed"
    }
  }
]
XDM Mapping
{
  "_id": "synthetic-response-001",
  "timestamp": "2026-10-14T12:00:01Z",
  "eventType": "search",
  "identityMap": {
    "ECID": [
      {
        "id": "synthetic-ecid-001",
        "authenticatedState": "ambiguous",
        "primary": true
      }
    ]
  },
  "_tenantId": {
    "search": {
      "searchTerm": "blue boots",
      "submissionId": "synthetic-search-001",
      "submittedAt": "2026-10-14T12:00:00Z",
      "searchResultsCount": 0,
      "searchType": "product",
      "submissionKind": "query",
      "trafficType": "external",
      "responseStatus": "completed"
    }
  }
}

SQL Queries

Google Analytics 4 (BigQuery)
-- Bind {{ga4_export_dataset}} to the GA4 export dataset; query finalized daily events_* tables.
-- Parameters: @report_days INT64 > 0, @completion_lag_days INT64 >= 0, @report_tz STRING IANA zone, @coverage_complete BOOL.
-- Coverage must include every final response for the selected submissions, complete exports, and upstream conflict quarantine.
-- Submission IDs are globally unique; one immutable final response per ID; identical retries retain all values.
-- Collection excludes navigation/refinement emissions; query means deliberate submission, including a submitted suggestion.
-- Final counts exclude unrelated recommendations; contradictory payloads, duplicate keys, and ambiguous value slots are quarantined upstream.
-- Completion event time is at or after submission time. Search terms are sanitized before collection; raw access is restricted.
WITH bounds AS (
  SELECT
    TIMESTAMP(DATE_SUB(DATE_SUB(CURRENT_DATE(@report_tz), INTERVAL @completion_lag_days DAY), INTERVAL @report_days DAY), @report_tz) AS start_ts,
    TIMESTAMP(DATE_SUB(CURRENT_DATE(@report_tz), INTERVAL @completion_lag_days DAY), @report_tz) AS end_ts
), extracted AS (
  SELECT
    TIMESTAMP_MICROS(event_timestamp) AS completed_at,
    (SELECT IF(COUNT(*) = 1, ANY_VALUE(value.string_value), NULL) FROM UNNEST(event_params) WHERE key = 'search_submission_id') AS submission_id,
    SAFE_CAST((SELECT IF(COUNT(*) = 1, ANY_VALUE(value.string_value), NULL) FROM UNNEST(event_params) WHERE key = 'search_submitted_at') AS TIMESTAMP) AS submitted_at,
    (SELECT IF(COUNT(*) = 1, ANY_VALUE(value.string_value), NULL) FROM UNNEST(event_params) WHERE key = 'search_type') AS search_type,
    (SELECT IF(COUNT(*) = 1, ANY_VALUE(value.string_value), NULL) FROM UNNEST(event_params) WHERE key = 'submission_kind') AS submission_kind,
    (SELECT IF(COUNT(*) = 1, ANY_VALUE(value.string_value), NULL) FROM UNNEST(event_params) WHERE key = 'traffic_type') AS traffic_type,
    (SELECT IF(COUNT(*) = 1, ANY_VALUE(value.string_value), NULL) FROM UNNEST(event_params) WHERE key = 'response_status') AS response_status,
    (SELECT COUNT(*) = 1 AND COUNTIF(
      IF(value.int_value IS NULL, 0, 1) + IF(value.double_value IS NULL, 0, 1) + IF(value.float_value IS NULL, 0, 1) + IF(value.string_value IS NULL, 0, 1) = 1
      AND (
        value.int_value = 0 OR value.double_value = 0 OR value.float_value = 0
        OR REGEXP_CONTAINS(value.string_value, r'^[+-]?0+(\.0+)?$')
      )
    ) = 1 FROM UNNEST(event_params) WHERE key = 'search_results_count') AS valid_zero
  FROM `{{ga4_export_dataset}}.events_*`
  CROSS JOIN bounds
  WHERE REGEXP_CONTAINS(_TABLE_SUFFIX, r'^\d{8}$')
    AND event_name = 'search'
    AND TIMESTAMP_MICROS(event_timestamp) >= bounds.start_ts
    AND TIMESTAMP_MICROS(event_timestamp) < CURRENT_TIMESTAMP()
), submissions AS (
  SELECT submission_id, MIN(submitted_at) AS submitted_at
  FROM extracted
  WHERE submission_id IS NOT NULL AND TRIM(submission_id) != ''
  GROUP BY submission_id
  HAVING MIN(submitted_at) = MAX(submitted_at)
    AND LOGICAL_AND(COALESCE(
      submitted_at IS NOT NULL
      AND completed_at >= submitted_at
      AND search_type = 'product'
      AND submission_kind = 'query'
      AND traffic_type = 'external'
      AND response_status = 'completed'
      AND valid_zero,
      FALSE
    ))
)
SELECT IF(@coverage_complete, COUNT(*), CAST(NULL AS INT64)) AS zero_results_searches
FROM submissions
CROSS JOIN bounds
WHERE submitted_at >= bounds.start_ts AND submitted_at < bounds.end_ts;
Adobe Experience Platform (AEP Query Service)
-- Bind {{aep_dataset}} to the ExperienceEvent dataset and {{tenant_namespace}} to the namespace represented by _tenantId in XDM.
-- Bind {{report_days}} positive integer, {{completion_lag_days}} nonnegative integer, {{report_tz}} validated IANA zone, {{coverage_complete}} boolean literal.
-- Coverage requires every final response for selected submissions, complete ingestion, and upstream conflict quarantine.
-- Submission IDs are globally unique; one immutable final response per ID; identical retries retain all values.
-- XDM searchResultsCount is double, submittedAt is date-time, and other search fields are strings; reject invalid source values before ingestion.
-- Collection excludes navigation/refinement emissions; query means deliberate submission, including a submitted suggestion.
-- Final counts exclude unrelated recommendations. Quarantine conflicting payloads; never coerce malformed or tiny nonzero values to zero.
-- Completion timestamp is at or after submission time. Search terms are sanitized before collection; raw access is restricted.
WITH settings AS (
  SELECT
    {{report_days}} AS report_days,
    {{completion_lag_days}} AS completion_lag_days,
    '{{report_tz}}' AS report_tz,
    {{coverage_complete}} AS coverage_complete
), bounds AS (
  SELECT
    to_utc_timestamp(CAST(date_sub(date_sub(to_date(from_utc_timestamp(current_timestamp(), report_tz)), completion_lag_days), report_days) AS TIMESTAMP), report_tz) AS start_ts,
    to_utc_timestamp(CAST(date_sub(to_date(from_utc_timestamp(current_timestamp(), report_tz)), completion_lag_days) AS TIMESTAMP), report_tz) AS end_ts,
    coverage_complete
  FROM settings
), extracted AS (
  SELECT
    e.timestamp AS completed_at,
    e.{{tenant_namespace}}.search.submissionId AS submission_id,
    e.{{tenant_namespace}}.search.submittedAt AS submitted_at,
    e.{{tenant_namespace}}.search.searchResultsCount AS result_count,
    e.{{tenant_namespace}}.search.searchType AS search_type,
    e.{{tenant_namespace}}.search.submissionKind AS submission_kind,
    e.{{tenant_namespace}}.search.trafficType AS traffic_type,
    e.{{tenant_namespace}}.search.responseStatus AS response_status
  FROM {{aep_dataset}} e
  CROSS JOIN bounds b
  WHERE e.eventType = 'search'
    AND e.timestamp >= b.start_ts
    AND e.timestamp < current_timestamp()
), submissions AS (
  SELECT submission_id, MIN(submitted_at) AS submitted_at
  FROM extracted
  WHERE submission_id IS NOT NULL AND TRIM(submission_id) <> ''
  GROUP BY submission_id
  HAVING MIN(submitted_at) = MAX(submitted_at)
    AND SUM(CASE WHEN
      submitted_at IS NOT NULL
      AND completed_at >= submitted_at
      AND result_count = 0
      AND search_type = 'product'
      AND submission_kind = 'query'
      AND traffic_type = 'external'
      AND response_status = 'completed'
      THEN 0 ELSE 1 END) = 0
)
SELECT CASE WHEN (SELECT coverage_complete FROM bounds)
  THEN COUNT(*) ELSE CAST(NULL AS BIGINT) END AS zero_results_searches
FROM submissions s
CROSS JOIN bounds b
WHERE s.submitted_at >= b.start_ts AND s.submitted_at < b.end_ts;

Source Data

Product search responses and digital analytics search events

Dependencies

Prerequisite: List the dependencies required for this Metric

Events:

site-search

Dimensions:

internal-search-term

Report Attributes

Internal Search Term Device category Page Type Traffic Source

Recommended Reporting Views

Views
Site Search PerformanceProduct Discovery

Segment Eligibility

Compare searches by search term, device, page type, or traffic source. Use the same selection for total searches when calculating a percentage. Add counts only across separate periods or mutually exclusive groups.

Governance

Data SensitivityInternal

Derived KPIs

Governance

Created bydevyendarm
Created on10/12/2026
Last modified bydevyendarm
Last modified on10/15/2026
StatusPUBLISHED

GitHub

View related Pull Requestcatalog/metrics/zero-results-searches.yml

Community Discussion