Skip to main content

Global search fan-out

The dashboard’s command palette (Cmd-K) does not issue one request per product pillar. It calls a single endpoint — POST /search/global — which runs one server-side fan-out across the pillars, merges the matches into a cohesive ranked list, and returns one response. Use this page to reason about why the fan-out exists, what each pillar searches, how ranking and isolation work, and how the query lands in session search analytics.

Why a server fan-out

A client-side fan of parallel fetches (one per pillar) costs and correlates differently than a single server fan-out:
  1. One round-trip. Five parallel browser fetches each pay TLS handshake, session resolution, and tenant resolution individually. The fan-out runs all sub-queries inside one API request on the same database connection, with the tenant context resolved once.
  2. Cohesive ranking. Separate fetches force the dashboard to merge unrelated groups in render order. The fan-out additionally returns a flat, pre-sorted results array, so a query like “main” lands its most relevant hit at the top regardless of which pillar produced it.
  3. Auditable analytics. One request produces one correlated analytics row (plus per-surface rows where they apply). Per-pillar client fetches would scatter the query across rows that cannot be joined by the Search Analytics aggregation.
The fan-out is deliberately humble: parallel reads against the existing per-pillar list indexes, with no new index materialisation and no cross-pillar vector search.

Pillar by pillar — what each entity searches

A query fans out to up to seven pillars. You can restrict it to a subset with the pillars field on the request body; an empty or omitted list means every pillar. Per-pillar matches are capped (default 5, maximum 10) to bound the response. Every pillar uses a prefix-anchored match (term%, never %term%): a leading wildcard makes the per-column btree indexes unusable, so prefix anchoring is a deliberate stopgap that trades mid-string matches for index-eligible, bounded-latency queries. Any literal % or _ characters in the query are escaped before matching. Two guardrails bound the fan-out further. Queries shorter than two characters are rejected up front (the dashboard gates the same way), because a single-character match floods every table. And call_logs — the largest per-tenant activity table — clamps its read to a hard server-side ceiling regardless of the requested per-pillar limit, so the fan-out can never turn into a large scan of the hottest voice table.

Ranking and partial failure

Matching strength is scored per row: exact match (case-insensitive) beats prefix beats substring. The flat results list is sorted by that score, with pillar order (contacts first, segments last) as the tie-break — contacts win ties because they are the most useful landing target in practice. Every pillar sub-query runs through Promise.allSettled, so one pillar’s failure never blocks the response. The payload carries a partial array naming the pillars whose sub-query failed (for example, a pillar whose table has not been provisioned for your workspace yet); the dashboard renders the successful pillars normally and shows a discreet “couldn’t search that pillar” hint for the degraded set. A failed pillar also records zero hits under counts, rather than inventing a result count. The ranking and partial-success contract are the same whether you call the endpoint from the command palette or directly — which is why the endpoint is part of the public API surface rather than an internal dashboard detail.

Tenant isolation and access

Each sub-query scopes to the tenant schema that the request’s session resolved, using a validated schema reference; no raw tenant schema name is ever interpolated into SQL, and the response cannot reach rows outside your workspace. Standard session authentication applies, and the route carries the same read-rate limits as the notifications and contacts list endpoints. Row shape is additionally validated at the database boundary before rendering, so a column drift degrades the affected pillar into partial instead of leaking a malformed row into the palette. Impersonation and role scope rules follow tenant isolation.

Analytics — how the query lands in Search Analytics

The fan-out writes the query to the same analytics table the per-surface ?q= list searches use, under the global surface. Where any pillar’s matches correspond to a searchable surface (contacts, conversations to messages, tickets to inbox, calls), the fan-out also stamps a second row tagged with that surface, so the Search Analytics surface filter (“Contacts”, “Messages”, “Inbox”) returns the query rather than only showing it under “All”. The response echoes the global row’s identifier, and the dashboard stamps a click against it when you open a result — which is what makes click-through measurable on the global surface. Recorded counts reflect what the palette actually showed: the dashboard passes the number of static destinations it matched (nav entries, catalog, Help, Docs) alongside the request, so a navigational query that resolved solely to a static page is not misrecorded as zero-result. The recording is deliberately write-safe: a de-dupe window collapses re-typed fragments, and a failed analytics write degrades to a null identifier instead of failing the search. Aggregation rules — top queries, zero results, click-through, parse failures — are covered in session search analytics.

See also