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
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' + (domainBase || ''), so the same node appearing in multiple domain scans appears once. - 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 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.