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
Overview — Global Ctrl+K Search¶
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:
- Dialog opens — the plugin renders the search overlay with an input field and result list.
- Domain set from SSOT —
window.EidosCore.DOMAIN_REGISTRYis the single source of truth for registered domains. When scope is'project'and at least one domain is registered, the plugin iteratesObject.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. - Parallel fan-out — one
GET /search?q=…&scope=…request is sent per domain using each domain'sbaseURI. All requests share a singleAbortControllersignal 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). - 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'sbaseURI, an id returned by two different domains is kept twice (once per domain). - 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.
Backend — Wave A + B Search¶
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.