Skip to content

Search Engine

Verified — minor divergences from the code — 5 finding(s) · 19d408be · 2026-08-25

The described flow (Ctrl+K dialog, DOMAIN_REGISTRY fan-out via Promise.allSettled with a shared AbortController, dedup by id+domainBase, GET /search dispatching to _search_v2_impl behind GLOBAL_SEARCH_V2, Wave A/B semantics, IAM filtering, _gs_assign_group bucket routing, and the _domainBase/_domainLabel/_domainIcon/_domainColor result fields) is all accurate and verified in code. However several canonical_operations entries reference symbols/signatures that do not literally exist as named, and the doc page's parameter table for GET /search omits a real, functional query parameter.

Divergences from the code — details
Sev Where Doc says Code does Evidence
MEDIUM canonical_operations: openGlobalSearch canonical_operations lists openGlobalSearch as the Ctrl+K handler function in frontend/plugins/global-search.js No symbol named openGlobalSearch exists in the file. The public entry point is window.GlobalSearch.open(opts), defined as function open(opts) and exported via window.GlobalSearch = { open: open, close: close }. frontend/plugins/global-search.js:926,961
MEDIUM canonical_operations: _gs_assign_group(result) canonical_operations lists _gs_assign_group(result) — a single-parameter function taking a result object The actual function signature is def _gs_assign_group(node: dict, matched_in: list) -> str, taking two separate parameters, not one combined result object. backend/server.py:3074
MEDIUM canonical_operations: _search_v2_impl(...) canonical_operations lists _search_v2_impl(q, limit, scope, subtreeRoot, domain, selectedNodeId, recentNodes) Actual signature is _search_v2_impl(q: str, limit: int, auth: AuthContext, scope: str = "project", subtree_root: str = "", domain: str = "", groups: str = "", selected_node_id: str = "", recent_nodes: str = "") — the documented list omits the required auth parameter and the groups parameter entirely. backend/server.py:3290-3299
MEDIUM Backend — Wave A + B Search: Parameters table The compiled doc page's Parameters table for GET /search lists only q, limit, scope, subtreeRoot, domain, selectedNodeId, recentNodes GET /search also accepts a groups query parameter (CSV of bucket names) which is threaded into _search_v2_impl and used as a real functional filter (if groups: wanted = {g.strip() for g in groups.split(",") if g.strip()}), restricting results to selected buckets. backend/server.py:2942,3467-3469
LOW canonical_operations: deduplicateResults(results) canonical_operations lists deduplicateResults(results) with key = id + '\x00' + (domainBase || '') as a distinct function There is no function named deduplicateResults anywhere in the file. The described dedup logic is correct but is inlined directly inside _doSearch() using a local seen Set, not a separately named/callable function. frontend/plugins/global-search.js:291-315

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 SSOTwindow.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' + (domainBase || ''), so the same node appearing in multiple domain scans appears once.
  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 dispatches to _search_v2_impl() when the GLOBAL_SEARCH_V2 environment flag is set (default in production).

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
selectedNodeId str? Boost selected node's neighbourhood
recentNodes str? Comma-separated IDs for recency boost

Wave A — high-relevance direct matches (label, class, reference). Returned first in the result list.

Wave B — extended matches (property values, breadcrumbs, descriptions). Returned after Wave A.

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.