Skip to content

Search Engine

Verified against the code · 421c63ba3fab5894cdb9e707eed8ef694881d60d · 2026-10-20

956 D1b-1 (2026-10-20): 3 finding(s) fixed — Wave A/B are delivery phases and _search_v2_impl returns one _gs_score-sorted list, cross-domain dedup key includes baseURI, GLOBAL_SEARCH_V2 code default is off. Re-verified against main 421c63ba.

Layer: api

Global search opens on Ctrl+K (or the search icon in the toolbar). The frontend plugin (frontend/plugins/global-search.js) coordinates the full flow:

  1. Dialog opens — the plugin renders the search overlay with an input field and result list.
  2. Domain set from SSOT — window.EidosCore.DOMAIN_REGISTRY is the single source of truth for registered domains. When scope is 'project' and at least one domain is registered, the plugin iterates Object.values(registry.domains) to build the target list. Without a registry, the search falls back to a single sentinel entry (baseURI: undefined) so the fan-out code path stays uniform.
  3. Parallel fan-out — one GET /search?q=…&scope=… request is sent per domain using each domain's baseURI. All requests share a single AbortController signal so typing a new character cancels the previous wave. Promise.allSettled() is used — a failed domain request does not suppress results from other domains (AC7 — failing domain must not block).
  4. Deduplication — results are merged across domain responses by composite key id + '\x00' + (d.baseURI || '') (global-search.js:312). Repeats within one domain's response collapse; because the key includes the domain's baseURI, an id returned by two different domains is kept twice (once per domain).
  5. Stale-response guard — after the fan-out settles, the plugin re-checks that the current query string still matches; if a newer keystroke has arrived, the results are discarded.

GET /search in backend/server.py routes path/slug queries (_is_path_query) to _search_by_path first; otherwise it dispatches to _search_v2_impl() when the GLOBAL_SEARCH_V2 environment flag is "1". The code default is off (config.global_search_v2()); only ops/eidos-sys-dev-served/docker-compose.yml sets it.

Parameters:

Param Type Description
q str Query string
limit int Max results
scope str "project" | "subtree" | "domain"
subtreeRoot str? Node ID anchor for subtree scope
domain str? Domain base URI for domain scope
groups str? Comma-separated bucket names; restricts results to those buckets
selectedNodeId str? Boost selected node's neighbourhood
recentNodes str? Comma-separated IDs for recency boost

"Wave A" and "Wave B" are delivery phases, not result tiers: Wave A shipped the flag and the JSON shape, Wave B the scope, IAM and bucket routing. _search_v2_impl builds one candidate set (per-token substring/regex match over the corpus) and returns one list sorted by _gs_score.

IAM filtering applies before results are returned: nodes outside the caller's read scope are excluded.

Result Structure

Each result object carries domain provenance fields injected by the frontend fan-out layer:

{
  id, label, class, group, breadcrumb, breadcrumbDescriptions,
  description, tier, matchedIn, matchedValues,
  _domainBase,   // baseURI of the domain this result came from
  _domainLabel,  // human-readable domain label
  _domainIcon,   // domain icon identifier
  _domainColor,  // domain accent color
}

group is assigned by _gs_assign_group() to one of: documents, locations, properties, relations, nodes.

DOMAIN_REGISTRY SSOT

window.EidosCore.DOMAIN_REGISTRY (populated from GET /domains.json at boot) is the canonical authority for all registered domains. The search plugin reads it at query time — not at plugin-load time — so new domains registered mid-session are picked up without a page reload. Domain base URIs are never hardcoded in search logic.