Analytics feedback loop
How a search query becomes a SearchUsageEvent, how clicks and conversions are recorded, and how aggregation feeds the dashboard for relevance tuning.
Every public-facing search interaction emits a row in the unified
SearchUsageEvent table. A single table with a type discriminator
(search_query, zero_results, click, conversion, widget_open,
filter_applied, visit) keeps the schema small and makes aggregation cheap.
End-to-end loop
Description
The diagram shows how a search request, a result click and a conversion all land in the unified SearchUsageEvent table with a queryId correlation, then are aggregated by index × hour × type into CTR, CVR and zero-result-rate metrics. The Studio analytics dashboard surfaces those metrics, and editor actions (synonyms, curations, field weights, presets) flow back through policy-cache to the read path — closing the loop without a deploy.
flowchart LR
classDef http fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef db fill:#fef3c7,stroke:#b45309,color:#78350f
classDef agg fill:#dcfce7,stroke:#15803d,color:#14532d
classDef ui fill:#ede9fe,stroke:#6d28d9,color:#4c1d95
subgraph Stage1["1 · Query"]
SDK1["Customer SDK / Widget"] --> Search["/api/search/public/multi"]:::http
Search --> Hit["TS multi_search<br/>returns hits"]
Hit --> Resp["Response sanitized<br/>queryId attached"]
Resp --> Ev1["recordSearchUsageAsync<br/>type=search_query"]:::db
Hit --> Zero{"hits.length == 0?"}
Zero -- yes --> Ev2["type=zero_results"]:::db
end
subgraph Stage2["2 · Click"]
SDK2["Widget / SDK click handler"] --> Track1["/api/events/track<br/>type=result_click"]:::http
Track1 --> Ev3["type=click<br/>{ queryId, productId, position }"]:::db
end
subgraph Stage3["3 · Conversion"]
SDK3["Checkout / add-to-cart"] --> Track2["/api/events/track<br/>type=conversion"]:::http
Track2 --> Ev4["type=conversion<br/>{ queryId, productId, conversionType }"]:::db
end
subgraph Stage4["4 · Aggregation (async)"]
Ev1 --> Agg["analytics aggregation<br/>group by indexId × hour × type"]:::agg
Ev2 --> Agg
Ev3 --> Agg
Ev4 --> Agg
Agg --> CTR["CTR = clicks / search_query"]
Agg --> CVR["CVR = conversion / click"]
Agg --> ZRR["zero-rate = zero_results / search_query"]
end
subgraph Stage5["5 · Dashboard refresh"]
CTR --> Studio["apps/saas analytics dashboard"]:::ui
CVR --> Studio
ZRR --> Studio
Studio --> Tune["relevance tuning:<br/>synonyms, curations,<br/>field weights, presets"]
end
Tune -. feeds .-> SearchThe event types
| Type | Emitted by | Carries |
|---|---|---|
search_query | public-handler.ts (fire-and-forget) | query, filters, sort, locale, queryId, referrer, ua |
zero_results | public-handler.ts when found == 0 | same shape as search_query |
result_click → click | widget / SDK → /api/events/track | queryId, productId, position, sessionId |
widget_open | hosted widget on mount | sessionId, anonymousUserId |
filter_used → filter_applied | widget on facet toggle | filters (the chosen facet/value), queryId |
conversion | checkout integration → /api/events/track | queryId, productId, conversionType (purchase, add_to_cart, ...) |
visit | optional widget on page-view | sessionId, anonymousUserId, referrer |
click (raw) | non-search-bound click | productId, position |
All events share the row shape { indexId, organizationId, type, count, metadata, createdAt }.
The metadata JSON column is the only schema-flexible field; it caps at 4 KB.
What ties a click back to a search
The server-side search_query event mints a stable queryId and returns it on the
response. The widget echoes the queryId back on every downstream click /
conversion event. Aggregation joins on (organizationId, indexId, queryId) to
compute click-through and conversion per query, per ranking slot.
Tuning loop
The Studio analytics dashboard surfaces the three core ratios (CTR, CVR, zero-rate) plus per-query drilldowns. An editor can act on them by:
- adding a synonym or stemming rule for a high-zero-result query,
- promoting a SKU via a curation set for an under-clicked query,
- adjusting field weights or building a preset for a slow-converting query.
These edits flow into the read path through policy-cache (60 s LRU) — the loop
closes without a deploy.
Related
- Read path — where
recordSearchUsageAsyncsits. - Connector lifecycle — ingest-side counterpart that produces the documents being searched and clicked.
Connector lifecycle
The six connector operations — handshake, heartbeat, full-sync, delta-sync, delete, diagnostics — and how they map to the Connector API surface.
Relevance Studio Overview
What Relevance Studio is, the 16 admin panels organized into 5 areas (Relevance, LTR, Health & Scale, Cross-region, Analytics & Debug), and when to use Studio.