ProtectedBaseAccess the cached provider metadata from SearchEngineBase
ProtecteddimensionResolver for a scope's declared Search Context dimensions. Overridable so a host can supply additional derivation sources (e.g. an external signal) without forking the engine.
ProtectedProviderResolve the metadata provider via SearchEngineBase (which extends BaseEngine and tracks ProviderToUse).
StaticInstanceStatic accessor for the global singleton instance
ProtectedassertEnforce a lane's RequiredMetadataKeys contract (Phase E).
The rendered filter must mention every key the author declared. This is the only guard
that catches a filter which rendered partially — where an optional {% if %} clause
disappeared because its dimension was absent or discarded, leaving a non-empty filter
that passes every other check while restricting on strictly less than intended.
Optionalcollector: LaneProblemCollectorProtectedassertFail a search CLOSED when a scope field that RESTRICTS was authored but did not render
usably (see CheckRenderedTemplate).
Throwing rather than dropping the offending row is deliberate. Dropping it would empty
the row collection, and buildScopeConstraints collapses an empty collection to
undefined — which every provider reads as "unscoped", i.e. all entities / all indexes
with no filter. So the surgical-looking fix is the one that widens; failing the search
is the one that doesn't. A broken restricting template is a misconfiguration and should
be loud and actionable, never silently degraded into a wider search.
Optionalcollector: LaneProblemCollectorProtectedbuildBuild a stable cache key for a search.
The key must include EVERY input that can change the result set, or the cache will serve one caller's results to another. Two of those inputs were previously missing and both are tenancy/authorization-relevant:
SearchContext — carries PrimaryScopeRecordID (the TENANT) and the
SecondaryScopes dimensions. Omitting it meant a user with access to two
tenants could be served the other tenant's results for up to the cache TTL,
and that two searches differing only by dimension (channel, skill, …)
collided. This is the reason for the fix.ScopeIDs — determines which corpora are searched at all.Also folded in: Mode, FusionWeightsOverride and PermissionOverfetchFactor
(all change ranking or the candidate pool, so they change results) and
AIAgentID (conservative: agent identity participates in scope resolution and
per-agent overrides upstream; including it can only cost a miss, never leak).
ScopeIDs order is deliberately NOT sorted: it is behaviourally significant,
because cross-scope reranker config and budget are taken from the first scope in
the array that supplies one. Two different orderings can therefore produce
different results and must not share a key.
The whole projection is emitted through stableStringify so key-order variation
in SecondaryScopes doesn't fragment the cache. The user ID stays as a readable
prefix for debuggability.
Note this runs AFTER scope resolution in searchInternal, so nothing here is
circular. Entitlement is resolved by the CALLERS (__Scoped_Search, the
GraphQL resolvers), which deny before reaching the engine; the engine therefore
never caches across an allow/deny boundary.
Test / admin hook: clear the result cache.
Initialize the search engine by reading active SearchProvider records from SearchEngineBase (which caches them via BaseEngine) and instantiating each via ClassFactory.
Safe to call multiple times (no-ops if already configured unless forceRefresh=true).
Engine configuration options
The user context for initialization
If true, re-initializes even if already configured
Resolve the entire access chain for one or more scopes and report what a search WOULD be able to reach — without querying any provider.
This is the answer to a question the platform previously could not answer at all: "as
this user, with this skill active, for this tenant — what is in bounds?" Every input to
that decision is transient. A grant applies because a time window is open right now; a
dimension is discarded because it was caller-authored on a ServerDerived key; a lane is
skipped because its filter lost an {% if %} clause. Afterwards, none of it is visible:
a correctly-bounded result set and an accidentally-widened one look identical.
Note the distinction from PreviewSearch, which is a real search capped at a few results. This runs no search — it reports the bound, not a sample of what is inside it. A sample cannot show you an over-broad bound, because the extra documents it would newly permit are exactly the ones you did not think to look for.
Two properties make the output trustworthy:
SearchContext a real caller would send, so the
preview shows the anti-spoof discard actually happening. A dry run that only accepted
pre-sanitized input would hide the one thing worth previewing.Unlike a real search this never throws for a scope-level problem; a scope that would fail
closed comes back with Reachable: false and the reason, since "it would have failed"
is precisely the finding the caller asked for.
scopes to explain plus the hypothetical caller context and principals
the user to evaluate entitlement for
one explanation per requested scope, in the order requested
ProtectedfilterFilter search results by entity-level and row-level security permissions.
This is a safety net. Providers are expected to do per-provider permission push-down (Section 3.6 of plans/search-scopes-rag-plus.md). If this filter is removing more than a handful of results in practice, the responsible provider's push-down is incomplete and should be fixed.
Groups results by entity for efficient permission checking:
The Global Object Store is a place to store global objects that need to be shared across the application. Depending on the execution environment, this could be the window object in a browser, or the global object in a node environment, or something else in other contexts. The key here is that in some cases static variables are not truly shared because it is possible that a given class might have copies of its code in multiple paths in a deployed application. This approach ensures that no matter how many code copies might exist, there is only one instance of the object in question by using the Global Object Store.
Public hook for callers (e.g. the GraphQL resolver) to emit a Status='Forbidden' SearchExecutionLog row when they reject a request before delegating to Search. Without this, forbidden invocations never reach the analytics dashboard — exactly the signal admins need to spot users / agents trying to access scopes they shouldn't.
Quick preview search optimized for autocomplete / typeahead. Uses preview mode (no enrichment), limited to 8 results by default. Only runs providers that have SupportsPreview=true.
The search query text
Maximum number of preview results (default: 8)
The user performing the search
Search result in preview mode
ProtectedprincipalsBuild the principal set a dimension's expansion query may bind.
Exists so the real search path and the ExplainScope dry run cannot construct principals
differently. They already did once: ExplainScope passed the agent and the search path
passed nothing, so any scope deriving its bound from AgentID previewed one bound and
searched with another. A single conversion site makes that class of drift unrepresentable
rather than merely fixed.
Accepts anything carrying the two principal IDs, which both SearchParams and
ExplainScopeInput do.
Execute a multi-source search with RRF fusion and optional enrichment.
When params.ScopeIDs is provided, each scope runs independently and the results
are combined via cross-scope RRF before deduplication, re-ranking, and enrichment.
Search parameters
The user performing the search
Aggregated search result
ProtectedstableDeterministically serialize a value with object keys sorted, so that two logically-identical inputs always produce the same string.
JSON.stringify preserves insertion order, which means a caller that builds
SecondaryScopes by spreading (a common pattern) can emit the same dimensions in
different orders across calls. Left unsorted that causes avoidable cache misses;
sorted, identity is stable. Array order is PRESERVED — see buildCacheKey for why
ScopeIDs order is significant.
Streaming variant of Search. Yields events as each pipeline stage produces output so the caller can emit partials to the UI / agent before fusion + reranking complete.
Phase 2C v1 semantics: runs the same internal pipeline as Search and emits synthetic events at each transition. This preserves all existing fusion / permission / dedup / enrich behavior — important because those steps have subtle correctness rules that we don't want to re-implement in a parallel code path. Per-provider partials are reconstructed from the final SourceCounts; a future refactor (Phase 2C v2) can split provider emission to true real-time concurrent emission once we measure that the synthetic phase is the actual bottleneck.
Cancellation: the consumer can stop iterating at any point — the underlying Search() will run to completion but its result is discarded. AbortSignal-based mid-pipeline cancellation is a Phase 2C v2 concern.
Event ordering:
provider events (one per non-empty source)fused eventreranked event (when a reranker is configured)final eventerror event in place of final.Protected StaticgetReturns the singleton instance of the class. If the instance does not exist, it is created and stored in the Global Object Store. If className is provided it will be used as part of the key in the Global Object Store, otherwise the actual class name will be used. NOTE: the class name used by default is the lowest level of the object hierarchy, so if you have a class that extends another class, the lowest level class name will be used.
OptionalclassName: string
Singleton search engine that orchestrates multi-source search with RRF fusion.
Providers are discovered from the MJ: Search Providers entity. Each active provider's DriverClass is resolved via ClassFactory to create an instance, which is then initialized with the provider's config from the DB record.
Usage: