Unified Search Operations Runbook
Audience: Operations and platform teams running AdvisoryAI unified search.
This runbook covers the AdvisoryAI unified search service end to end: setup, key endpoints and scopes, monitoring, performance tuning, feature-flag rollout, troubleshooting, and backup/recovery. Unified search federates findings, VEX, policy, graph, OpsMemory, timeline, and scanner corpora behind a single query API with weighted RRF fusion and optional synthesis.
Setup
Configure
AdvisoryAI:KnowledgeSearch:ConnectionString(sectionAdvisoryAI:KnowledgeSearch, bound toKnowledgeSearchOptions).Configure
AdvisoryAI:UnifiedSearchoptions (sectionAdvisoryAI:UnifiedSearch, bound toUnifiedSearchOptions).For live compose/runtime, set
AdvisoryAI:KnowledgeSearch:FindingsAdapterBaseUrl,...:VexAdapterBaseUrl, and...:PolicyAdapterBaseUrltogether so findings, VEX, and policy ingest from live services instead of partial fallback snapshots. Each adapter also has a...:FindingsAdapterEnabled/...:VexAdapterEnabled/...:PolicyAdapterEnabledtoggle (defaulttrue). When live adapter base URLs are configured, the background refresh service auto-enables unified indexing even ifAdvisoryAI:KnowledgeSearch:UnifiedAutoIndexEnabledis false.Point each base URL at the service that actually serves the adapter’s route. A base URL that resolves but does not host the route produces a 404 on every refresh, and the adapter answers with its committed snapshot, so the corpus looks healthy while it is a fixture. The targets:
Adapter Route it requests Service that serves it Scope the caller needs Findings GET /api/v2/security/findingsfindings-web(aliasfindings.stella-ops.local)findings:readPolicy GET /api/v1/authority/effective-policies?enabledOnly=false&includeExpired=true&limit=100policy-engine(aliaspolicy-engine.stella-ops.local)policy:readVEX GET /api/v1/canonicalno service in the current estate — Concelier was decommissioned; the successor is undecided vex:readThe VEX row is accurate, not a typo: that adapter’s live path is structurally dead and it serves its snapshot. Treat a
Snapshotreading forvexas expected until the successor is chosen; treat one forfindingsas a defect.policyhas a second, benign way to readSnapshot, and the reason string is what tells them apart (SPRINT_20260722_013 AAI-17). A reason naming an HTTP status is a defect: the route was unreachable, unauthorised, or rejected the request. A reason ofupstream returned zero mappable effective policiesis not — the call succeeded and the tenant has no effective-policy bindings. Effective-policy bindings live only in thepolicy-engineprocess (EffectivePolicyServiceholds them in aConcurrentDictionary; no Policy migration creates a table for them), so an estate that has created none throughPOST /api/v1/authority/effective-policies, or one whosepolicy-enginehas restarted since, has an empty corpus by construction and the adapter serves its snapshot. Create at least one binding before readingSource=Upstreamforpolicyas achievable.Ensure the published AdvisoryAI image carries the repo-shaped local corpus under
/app:findings,vex,policysnapshots come fromAdvisoryAI:KnowledgeSearch:Unified{Findings,Vex,Policy}SnapshotPath(defaultsrc/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/{findings,vex,policy}.snapshot.json).graph,opsmemory,timeline,scannersnapshots come fromAdvisoryAI:UnifiedSearch:Ingestion:{Graph,OpsMemory,Timeline,Scanner}SnapshotPath(defaultsrc/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/{graph,opsmemory,timeline,scanner}.snapshot.json).
Ensure model artifact path exists when
AdvisoryAI:KnowledgeSearch:VectorEncoderType=onnx:- default
AdvisoryAI:KnowledgeSearch:OnnxModelPath:models/all-MiniLM-L6-v2.onnx - When
onnxis selected but the model file is missing, the encoder falls back tohashwith a warning (no hard failure).
- default
Rebuild indexes when verifying live search quality (both require
advisory-ai:adminscope):POST /v1/advisory-ai/index/rebuild(legacy AdvisoryAI Knowledge Search — docs, OpenAPI, Doctor metadata).POST /v1/search/index/rebuild(unified index across all registered ingestion adapters).
Verify query endpoint:
POST /v1/search/querywith tenant context andadvisory-ai:operatescope (operate is also satisfied byadvisory-ai:admin).
Verify the corpus is live, not substituted.
GET /v1/search/adapters(tenant context,advisory-ai:operate) reports, per adapter, whether the rows it last produced came fromUpstreamorSnapshot, the cause when it is not upstream, the target that was attempted, and — after a 404 — when the suppressed upstream call will be retried.SnapshotBackedCountabove zero for anything butvexmeans an adapter is serving a fixture. The same list rides on thePOST /v1/search/index/rebuildresponse, so an operator who triggers a rebuild sees the provenance without a second request.
Identity note: tenant, user (
sub), and scopes are resolved from gateway-signed envelope claims only. RawX-User-Id/X-Tenant-Id/X-StellaOps-Actor/X-StellaOps-Scopes/X-StellaOps-TenantId/X-Stella-Scopes/X-Scopesheaders are stripped at inbound byInboundIdentityHeaderStripMiddleware(fullHeadersToStriplist) and are not honored by the endpoints.
Key Endpoints
Unified search group (/v1/search, all under the advisory-ai rate-limit policy):
POST /v1/search/query— unified query with weighted RRF fusion and entity-grouped cards. Scope:advisory-ai:operate.POST /v1/search/suggestions/evaluate— preflights a bounded list of suggested queries against the active corpus (no analytics recorded). Scope:advisory-ai:operate.POST /v1/search/synthesize— SSE stream: deterministic synthesis first, then optional LLM synthesis. Scope:advisory-ai:operateplus the synthesis-specific scopesearch:synthesize(oradvisory-ai:admin); requests without it return403 { "error": "Missing required scope: search:synthesize" }.POST /v1/search/index/rebuild— full unified index rebuild across all ingestion adapters. Scope:advisory-ai:admin.
Analytics & history group (/v1/advisory-ai/search):
POST /v1/advisory-ai/search/analytics— batch analytics events (max 100/request, fire-and-forget). Scope:advisory-ai:operate.GET /v1/advisory-ai/search/history— up to 50 recent queries for the current user. Scope:advisory-ai:view.DELETE /v1/advisory-ai/search/history— clear the user’s history. Scope:advisory-ai:operate.DELETE /v1/advisory-ai/search/history/{historyId}— delete one history entry (GUID id). Scope:advisory-ai:operate.
Feedback & quality group (/v1/advisory-ai/search):
POST /v1/advisory-ai/search/feedback— submithelpful/not_helpfulsignal for a result. Scope:advisory-ai:view.GET /v1/advisory-ai/search/quality/metrics— aggregate quality metrics for a period (24h,7d,30d; default7d). Scope:advisory-ai:admin.GET /v1/advisory-ai/search/quality/alerts— open quality alerts (filterable bystatus,alertType). Scope:advisory-ai:admin.PATCH /v1/advisory-ai/search/quality/alerts/{alertId}— acknowledge / resolve an alert. Scope:advisory-ai:admin.
Legacy AdvisoryAI Knowledge Search (pre-unified, still mounted under /v1/advisory-ai; group default scope advisory-ai:view):
POST /v1/advisory-ai/search— legacy AKS query (docs/api/doctor). Scope:advisory-ai:operate. The webUnifiedSearchClientfalls back to it when unified search fails (mapped result carriesdiagnostics.mode = legacy-fallback). Responses carry deprecation signalling headers set byApplyLegacyKnowledgeSearchDeprecationHeaders:Deprecation: true,Sunset: 2026-04-30T00:00:00Z,Link: </v1/search/query>; rel="successor-version", and aWarning: 299advising migration to/v1/search/query.POST /v1/advisory-ai/index/rebuild— legacy AKS index rebuild (returnsdocumentCount,chunkCount,apiSpecCount,apiOperationCount,doctorProjectionCount,durationMs). Scope:advisory-ai:admin.
Scope / policy model
AdvisoryAIPolicies defines three named policies enforced via RequireAuthorization:
advisory-ai:view— read-only (history read, feedback submit).advisory-ai:operate— query/operate (implies view at the policy level;operateis granted by holdingadvisory-ai:operateoradvisory-ai:admin).advisory-ai:admin— index rebuild, quality dashboards, alert management (admin scope only).
advisory-ai:view, advisory-ai:operate, and advisory-ai:admin are in the canonical scope catalog (StellaOpsScopes). The synthesis gate scope search:synthesize is not in StellaOpsScopes — it is checked directly in UnifiedSearchEndpoints.HasSynthesisScope and must be granted to the caller’s token explicitly (or substituted by advisory-ai:admin).
Monitoring
Track per-tenant and global:
- Query throughput (
query,click,zero_result,synthesisevents) - Self-serve journey signals (
answer_frame,reformulation,rescue_action) - P50/P95/P99 latency for
/v1/search/query - Zero-result rate
- Fallback answer rate, clarify rate, insufficient-evidence rate
- Reformulation count, rescue-action count, abandoned fallback count
- Synthesis quota denials (per-day cap
Synthesis:SynthesisRequestsPerDay, default 200; concurrency capSynthesis:MaxConcurrentPerTenant, default 10) - Index size and rebuild duration (rebuild responses report
domainCount,chunkCount,durationMs) - Unified-query retrieval mode (
diagnostics.modeishybridwhen the vector lane is used, otherwisefts-only; the web fallback path reportslegacy-fallback) - Active encoder: surfaced on the legacy
/v1/advisory-ai/searchresponse asdiagnostics.activeEncoder. Possible values (fromKnowledgeSearchService.ResolveActiveEncoderName):hash(deterministic SHA-256 selected),onnx(semantic ONNX inference active),onnx-fallback(ONNX encoder constructed but inference not active),hash-fallback(VectorEncoderType=onnxrequested but a hash encoder is in use). The unified/v1/search/querydiagnostics object does not carry an encoder field — usediagnostics.usedVectorplusdiagnostics.modethere.
Performance Targets
- Instant results: P50 < 100ms, P95 < 200ms, P99 < 300ms
- Full results (federated): P50 < 200ms, P95 < 500ms, P99 < 800ms
- Deterministic synthesis: P50 < 30ms, P95 < 50ms
- LLM synthesis: TTFB P50 < 1s, total P95 < 5s
The query-path P50 < 100ms / P95 < 500ms / P99 < 800ms numbers are the only targets backed by assertions in the test suite (UnifiedSearchPerformanceEnvelopeTests). Those tests are [Fact(Skip = …)] and run only in the dedicated perf lane, not in normal CI. The deterministic-synthesis and LLM-synthesis numbers are design targets, not currently asserted; the LLM total budget aligns with AdvisoryAI:KnowledgeSearch:SynthesisTimeoutMs (default 5000ms, which triggers template fallback when exceeded).
SQL Query Tuning and EXPLAIN Evidence
Unified search read paths rely on:
- FTS query over
advisoryai.kb_chunk.body_tsv* - Trigram fuzzy fallback (
%/similarity()) - Vector nearest-neighbor (
embedding_vec <=> query_vector)
Recommended validation commands:
EXPLAIN (ANALYZE, BUFFERS)
SELECT c.chunk_id
FROM advisoryai.kb_chunk c
WHERE c.body_tsv_en @@ websearch_to_tsquery('english', @query)
ORDER BY ts_rank_cd(c.body_tsv_en, websearch_to_tsquery('english', @query), 32) DESC, c.chunk_id
LIMIT 20;
EXPLAIN (ANALYZE, BUFFERS)
SELECT c.chunk_id
FROM advisoryai.kb_chunk c
WHERE c.embedding_vec IS NOT NULL
ORDER BY c.embedding_vec <=> CAST(@query_vector AS vector), c.chunk_id
LIMIT 20;
Index expectations:
idx_kb_chunk_body_tsv_en(GIN overbody_tsv_en)idx_kb_chunk_body_trgm(GIN trigram overbody)idx_kb_chunk_embedding_vec_hnsw(HNSW overembedding_vec)
Automated EXPLAIN evidence is captured by:
UnifiedSearchLiveAdapterIntegrationTests.PostgresKnowledgeSearchStore_ExplainAnalyze_ShowsIndexedSearchPlans
Load and Capacity Envelope
Test envelope (in-process benchmark harness, UnifiedSearchPerformanceEnvelopeTests, perf-lane only — skipped in normal CI):
- 50 concurrent requests over 300 total requests (
IncludeSynthesis: false) - Asserts P50 < 100ms, P95 < 500ms, P99 < 800ms; a separate test asserts ≤10% P95 regression vs a 120ms phase-1 baseline
Sizing guidance:
- Up to 100k chunks: 2 vCPU / 4 GB RAM
- 100k-500k chunks: 4 vCPU / 8 GB RAM
500k chunks or heavy synthesis: 8 vCPU / 16 GB RAM, split synthesis workers
Feature Flags and Rollout
Global enables (defaults in parentheses):
AdvisoryAI:UnifiedSearch:Enabled(true) — master switch for the unified search service.AdvisoryAI:UnifiedSearch:Federation:Enabled(true) — federated console/graph/timeline fan-out.AdvisoryAI:UnifiedSearch:Synthesis:Enabled(true) — deterministic + LLM synthesis path.AdvisoryAI:UnifiedSearch:GravityBoost:Enabled(true) — graph-neighbor gravity boost.AdvisoryAI:KnowledgeSearch:LlmSynthesisEnabled(false) — gate LLM-grounded synthesis; also requires non-empty...:LlmAdapterBaseUrland...:LlmProviderId.
Per-tenant overrides — config path AdvisoryAI:UnifiedSearch:TenantFeatureFlags (nullable booleans; null = inherit global):
EnabledFederationEnabledSynthesisEnabled
Ranking bias toggles (AdvisoryAI:KnowledgeSearch):
RoleBasedBiasEnabled(true) — when on,DomainWeightCalculatorapplies additive domain-weight bias from the requesting token’s scopes (e.g.scanner:readlifts findings).PopularityBoostEnabled(false) — opt-in click-through popularity boost (log2(1 + clickCount) * PopularityBoostWeight, weight default0.05); off by default to preserve deterministic results.UnifiedFreshnessBoostEnabled(false) — opt-in recency boost during unified indexing.
Background indexing and retention (AdvisoryAI:KnowledgeSearch):
UnifiedAutoIndexOnStartup(true) — rebuild the unified index on service start.UnifiedAutoIndexEnabled(false) — enable the periodic refresh loop (auto-enabled regardless when live adapter base URLs are configured).UnifiedIndexRefreshIntervalSeconds(300, min 30) — refresh cadence.SearchQualityMonitorEnabled(true) /SearchQualityMonitorIntervalSeconds(300) — periodic quality-alert refresh.SearchTelemetryEnabled(true) — analytics/feedback telemetry; retrieval, suggestions, and history stay functional when false.SearchAnalyticsRetentionEnabled(true) /SearchAnalyticsRetentionDays(90) /SearchAnalyticsRetentionIntervalSeconds(3600) — pruning of analytics/feedback/history.
Per-tenant feature-flag example:
{
"AdvisoryAI": {
"UnifiedSearch": {
"TenantFeatureFlags": {
"tenant-alpha": { "Enabled": true, "FederationEnabled": true, "SynthesisEnabled": false },
"tenant-beta": { "Enabled": true, "FederationEnabled": false, "SynthesisEnabled": false }
}
}
}
}
Troubleshooting
Symptom: empty results
- Confirm the request carries a verified envelope with a tenant claim (
stellaops:tenant/tenant/tenant_id); a missing tenant yields400withadvisoryai.validation.tenant_required, not empty results. - Verify
AdvisoryAI:UnifiedSearch:Enabledand the per-tenant flagEnabled. - Run an index rebuild (
POST /v1/search/index/rebuild, admin scope) and checkchunkCountin the response. - If suggestions also fail, run both rebuild steps from Setup (legacy
/v1/advisory-ai/index/rebuildthen unified/v1/search/index/rebuild) and re-check with a known live query such asdatabase connectivity. - If only findings answer lanes work while VEX/policy/graph/OpsMemory remain corpus-unready, verify the published snapshot files exist (findings/vex/policy under the
KnowledgeSearchsnapshot paths; graph/opsmemory/timeline/scanner under theUnifiedSearch:Ingestionsnapshot paths) and confirm the VEX/policy adapter base URLs are configured in runtime env. - Read
GET /v1/search/adaptersbefore reading logs. It names the source, the cause and the attempted target per adapter, which is the difference between “the upstream is down”, “the route does not exist on that service” and “this adapter was never configured”. Counteradvisory_ai_unified_adapter_fallback_totalcarries the same cause as areasondimension plusstructural=truefor the 404 class, which is the one that never recovers on its own. - An adapter that answered 404 is deliberately not retried every cycle. The upstream call is suppressed for five minutes after the first 404, doubling to a one-hour ceiling while the route stays absent, and released as soon as it answers again. A suppressed adapter still serves its snapshot and still reports
UpstreamSuppressedUntil, so a route that is genuinely missing produces one call per window rather than one per refresh.
Symptom: poor semantic recall
- Verify
AdvisoryAI:KnowledgeSearch:VectorEncoderTypeand (on the legacy/v1/advisory-ai/searchresponse) thediagnostics.activeEncodervalue — anonnx-fallbackorhash-fallbackvalue means the ONNX path silently degraded to the hash encoder; on/v1/search/queryconfirmdiagnostics.usedVector=true/diagnostics.mode=hybrid. - Confirm the ONNX model path is accessible and valid — a missing model silently falls back to the
hashencoder. - Rebuild the index after switching encoder.
Symptom: synthesis unavailable
- Confirm the caller holds the
search:synthesizescope (oradvisory-ai:admin) in addition toadvisory-ai:operate; otherwise the endpoint returns403. - Check
Synthesis:Enabled(global) and the per-tenantSynthesisEnabledflag. - For LLM synthesis specifically, confirm
LlmSynthesisEnabled=truewith non-emptyLlmAdapterBaseUrlandLlmProviderId; otherwise only deterministic synthesis is emitted (llm_statusSSE event reportsunavailable). - Check quota counters:
SynthesisRequestsPerDay(per-day) andMaxConcurrentPerTenant(concurrency); exhaustion surfaces asllm_status: quota_exceeded.
Symptom: search feels self-serve weak
- Inspect
GET /v1/advisory-ai/search/quality/metrics?period=7d. - Watch
fallbackAnswerRate,clarifyRate,insufficientRate,reformulationCount,rescueActionCount, andabandonedFallbackCount. - Inspect
GET /v1/advisory-ai/search/quality/alertsforfallback_loopandabandoned_fallback. - Treat repeated fallback loops as ranking/context gaps; treat abandoned fallback sessions as UX/product gaps.
Symptom: high latency
- Check federated backend timeout budget.
- Review
EXPLAIN (ANALYZE)plans. - Verify index health and cardinality growth by tenant.
Backup and Recovery
- Unified index is derivable state.
- Recovery sequence:
- Restore primary domain systems (findings/vex/policy/docs sources).
- Restore AdvisoryAI DB schema.
- Trigger full index rebuild.
- Validate with quality benchmark fast subset.
Validation Commands
# Fast PR-level quality gate
dotnet test src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.csproj \
-- --filter-class StellaOps.AdvisoryAI.Tests.UnifiedSearch.UnifiedSearchQualityBenchmarkFastSubsetTests
# Full benchmark + tuning evidence
dotnet test src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.csproj \
-- --filter-class StellaOps.AdvisoryAI.Tests.UnifiedSearch.UnifiedSearchQualityBenchmarkTests
# Performance envelope
dotnet test src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.csproj \
-- --filter-class StellaOps.AdvisoryAI.Tests.UnifiedSearch.UnifiedSearchPerformanceEnvelopeTests
# Self-serve telemetry and gap surfacing slice
dotnet build src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.csproj -v minimal
src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/bin/Debug/net10.0/StellaOps.AdvisoryAI.Tests.exe \
-method "StellaOps.AdvisoryAI.Tests.Integration.UnifiedSearchSprintIntegrationTests.G10_SelfServeMetrics_IncludeFallbackReformulationAndRescueSignals" \
-method "StellaOps.AdvisoryAI.Tests.Integration.UnifiedSearchSprintIntegrationTests.G10_RecoveredFallbackSessions_DoNotCountAsAbandoned" \
-reporter verbose -noColor
Related Documentation
- Unified Search Architecture — ranking, fusion, and ingestion design
- AdvisoryAI Knowledge Search — the legacy AKS surface this service supersedes
- AdvisoryAI Architecture — service overview and module boundaries
- LLM Setup Guide — configuring the optional synthesis provider
