Query Analytics
Popular queries, low-CTR queries, reformulations, and how to read them to drive relevance improvements.
The query analytics tab is where most teams spend their analytics time. It tells you what people search for and whether the results worked — and combining those two answers most search-quality questions.
What's tracked per query
Every public search call (/api/search, /api/search/multi) writes a SearchUsageEvent row with type: "search_query" and a metadata blob containing:
- The normalised query string (lowercased, trimmed).
- The result count (
found). - The latency in milliseconds.
- The filter expression (if any).
- The
sessionIdand (optional)anonymousUserIdfor grouping. - The
queryId— a server-generated id that ties subsequent click events to this search.
Click events (type: "result_click") carry the queryId back, so the analytics engine can compute CTR per query without further joins.
Popular queries (Top Queries)
The Top Queries view ranks queries by search volume for the selected period:
const top = await orpc.search.topQueries.call({
organizationId,
period: "30d",
limit: 10,
});Returns:
Array<{
query: string;
searches: number;
clicks: number;
ctr: number; // clicks / searches
resultsFound: number; // averaged across the period
zeroResults: boolean; // true if the period's average is 0
}>How to read it:
- High searches + high CTR. Working as designed. Don't tune.
- High searches + low CTR (under ~10 %). Relevance candidates. Look at the underlying Relevance tuning settings — synonyms, queryBy weights, curations.
- High searches + zero results. Content gap. See No-results.
- High searches + slow latency. Performance issue. See Operations / Performance.
Cap the dashboard view to the top 10 by default; pull more via the API.
Low-CTR queries
A separate cut of the same table, ordered by lowest CTR with at least N searches (default: 5 searches per period). These are the queries that have demonstrated demand but for which the result list isn't compelling.
const lowCtr = await orpc.search.topQueries.call({
organizationId,
period: "30d",
limit: 50,
minSearches: 5,
orderBy: "ctr_asc",
});A two-step playbook:
- Run the query in the Search Preview. Look at the first 10 results. If the right product isn't there, retrieval is failing — adjust
queryByweights or add a synonym. - If the right product is there but not at the top. Curate the pinned IDs for that query.
For high-volume / low-CTR queries that resist tuning, consider switching the surface — an AI answer might be the right UX, or a federated multi-search might surface a more useful list.
Reformulations
A reformulation is when a user runs query A, sees the results, then immediately runs query B (within the same sessionId, typically within 30 seconds). Reformulations carry signal: the user told you that A wasn't good enough and B was the intent.
The dashboard shows top reformulation pairs:
| Original query | Reformulated query | Sessions |
|---|---|---|
running shoes | nike running shoes | 142 |
tv | oled tv | 89 |
phone case | iphone 15 case | 73 |
Use reformulations as free synonym discovery: when "running shoes" → "nike running shoes" is a frequent pair, the brand is implicit in the broader query and you may want to boost brand fields in queryBy.
To pull the data:
const reform = await orpc.search.analyticsEvents.call({
organizationId,
period: "30d",
type: "search_query",
groupBySession: true,
});
// Then post-process: find consecutive query rows in the same session.Reading by locale
If your catalog supports multiple locales, always read query analytics grouped by metadata.locale. A query that's high-volume in one locale may be missing entirely in another — that's a translation or routing problem, not a relevance problem.
The same goes for metadata.indexSlug — the dashboard defaults to the org-wide view but a per-index drill-down often shows different patterns.
Query length and shape
Two quick distributions to keep an eye on:
- Median query length. A trend toward longer queries usually means users are giving up on short-keyword search and trying sentence-style queries. That's a signal to enable Semantic search hybrid mode.
- Wildcard / browse rate. Searches with
q: "*"(browsing with filters) vs full-text. Heavy*use can mean either healthy filter UX or that the search box itself isn't trusted.
Both are derivable from the raw event metadata via analyticsEvents.
Comparing periods
The dashboard always shows a single period. For period-over-period analysis, pull two windows and diff client-side:
const [a, b] = await Promise.all([
orpc.search.topQueries.call({ organizationId, period: "30d", limit: 200 }),
orpc.search.topQueries.call({ organizationId, period: "30d", offsetDays: 30, limit: 200 }),
]);
// Compute per-query delta in searches and CTR.offsetDays shifts the window backwards. Use it to detect drift after a deploy.
Top-queries cache and pagination
topQueries uses a per-org cache with a TTL of ~5 minutes. The dashboard refreshes the cache on demand via a refresh button; programmatic callers should cache identically (the underlying aggregation scans SearchUsageEvent and can be expensive on the largest tenants).
Pagination is offset-based. For exports beyond 1000 rows, prefer the Export flow.
Related pages
- Analytics overview
- No-results — the zero-result cut of the same data
- Conversions — search → conversion
- Relevance quality — using query analytics to tune ranking
- Analytics dashboard
- Relevance tuning