Some checks failed
Documentation Lint & Validation / Markdown Linting (push) Has been cancelled
Documentation Lint & Validation / Validate mdBook Configuration (push) Has been cancelled
Documentation Lint & Validation / Content & Structure Validation (push) Has been cancelled
mdBook Build & Deploy / Build mdBook (push) Has been cancelled
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
Documentation Lint & Validation / Lint & Validation Summary (push) Has been cancelled
mdBook Build & Deploy / Documentation Quality Check (push) Has been cancelled
mdBook Build & Deploy / Deploy to GitHub Pages (push) Has been cancelled
mdBook Build & Deploy / Notification (push) Has been cancelled
on+re:
- core.ncl: 5 new Practice nodes (notification-channels,
vapora-capabilities, agent-hot-reload-stable-identity,
merkle-audit-trail, notification-channels) + 5 new edges;
knowledge-graph-execution-history updated with HNSW+BM25+RRF
- state.ncl: production-readiness blocker/catalyst updated (hot-reload
complete, BudgetManager/LLMRouter still require restart);
ontoref-integration catalyst updated (vapora-ontology/reflection
crates, api-catalog.json, nickel contracts)
ADRs (NCL):
- adr-013: KG hybrid search — HNSW+BM25+RRF, rejected in-process scan
- adr-014: capability packages — AgentDefinition→vapora-shared,
DashMap shard-before-await constraint
- adr-015: Merkle audit trail — SHA-256 hash chain, rejected HMAC
- adr-016: agent hot-reload — stable_id=role, learning_profiles survive
drain, BudgetManager excluded from reload scope
landing page:
- 2 new feature boxes: VCS-Agnostic Worktree (jj/git), Ontology Protocol
- KG box: 20→28 tests, HNSW+BM25+RRF description
- Agents box: 71→82 tests, hot-reload + stable_id
- tech stack: Rust 21→23 crates, added jj, Radicle, ontoref badges
- status badge: 620→691 tests
83 lines
5.9 KiB
Text
83 lines
5.9 KiB
Text
let d = import "adr-defaults.ncl" in
|
|
|
|
d.make_adr {
|
|
id = "adr-013",
|
|
title = "Knowledge Graph Hybrid Search — HNSW + BM25 + Reciprocal Rank Fusion",
|
|
status = 'Accepted,
|
|
date = "2026-02-26",
|
|
|
|
context = "find_similar_executions in KGPersistence discarded its embedding argument entirely and returned N most-recent successful executions ordered by timestamp — a correctness bug masquerading as similarity search. Separately, the kg_executions table was declared SCHEMAFULL but three fields (agent_role, provider, cost_cents) used by PersistedExecution were missing from the schema. SurrealDB drops undefined fields on INSERT in SCHEMAFULL tables, causing every SELECT to return records that failed serde deserialization, silently swallowed by filter_map. stratum-embeddings SurrealDbStore was evaluated but rejected: it loads all records into memory and computes cosine similarity in-process — suitable for bounded document chunks, unsuitable for unbounded KG execution history.",
|
|
|
|
decision = "Replace stub similarity functions with a hybrid retrieval pipeline: (1) HNSW (SurrealDB 3 native ANN vector index) over the embedding field for semantic proximity, (2) BM25 (SurrealDB 3 native full-text search) over task_description for exact lexical matches, (3) Reciprocal Rank Fusion (k=60) for scale-invariant score fusion. Add migration 012_kg_hybrid_search.surql: fix the SCHEMAFULL schema gap (add missing fields), define the HNSW index on embedding, define the full-text search index on task_description.",
|
|
|
|
rationale = [
|
|
{
|
|
claim = "Hybrid retrieval is required because HNSW and BM25 cover disjoint failure modes",
|
|
detail = "HNSW (semantic) misses exact keyword matches: 'cargo clippy warnings' may not find 'clippy deny warnings fix' if the embedding model compresses the phrase differently. BM25 (lexical) misses semantic proximity: a query about error handling may not match a record about exception management if terminology differs. RRF fuses both rank lists without requiring score normalization.",
|
|
},
|
|
{
|
|
claim = "The schema bug must be fixed before the index can be created",
|
|
detail = "HNSW index creation on a SCHEMAFULL table requires the indexed field to exist in the schema. The missing agent_role/provider/cost_cents fields also caused all SELECT results to fail deserialization — fixing the schema is a prerequisite for any query correctness, not just the new index.",
|
|
},
|
|
{
|
|
claim = "RRF k=60 is the standard fusion constant and requires no tuning",
|
|
detail = "k=60 was established by Cormack et al. (2009) as a robust default. Score-based fusion alternatives (linear combination, learned weights) require per-corpus calibration. RRF is rank-only and therefore insensitive to score scale differences between HNSW cosine similarity and BM25 TF-IDF.",
|
|
},
|
|
],
|
|
|
|
consequences = {
|
|
positive = [
|
|
"find_similar_executions and find_similar_rlm_tasks now use the embedding argument correctly",
|
|
"SCHEMAFULL schema gap eliminated — all PersistedExecution fields are persisted and deserialized correctly",
|
|
"Hybrid search handles both exact crate/error-code queries (BM25) and semantic task similarity (HNSW)",
|
|
"HNSW ANN search is sub-linear in the number of records — query time does not degrade with accumulation",
|
|
],
|
|
negative = [
|
|
"SurrealDB 3 native HNSW requires SurrealDB >= 3.0 at runtime; earlier versions will fail the migration",
|
|
"RRF does not expose relevance scores to callers — ranking is ordinal only",
|
|
"Embedding dimension is fixed at creation time; changing the embedding model requires dropping and rebuilding the HNSW index",
|
|
],
|
|
},
|
|
|
|
alternatives_considered = [
|
|
{
|
|
option = "stratum-embeddings SurrealDbStore in-process cosine similarity",
|
|
why_rejected = "Loads all records into memory for every query. Acceptable for bounded document chunk sets; unacceptable for KG execution history that accumulates unbounded records across all agents and tasks over time.",
|
|
},
|
|
{
|
|
option = "Pure HNSW semantic search",
|
|
why_rejected = "Misses exact keyword matches for crate names, error codes, and specific command strings that are semantically compressed by embedding models.",
|
|
},
|
|
{
|
|
option = "Pure BM25 lexical search",
|
|
why_rejected = "Misses semantic equivalence for concept-level queries where terminology varies between the query and the stored record.",
|
|
},
|
|
],
|
|
|
|
constraints = [
|
|
{
|
|
id = "hnsw-index-required-for-kg",
|
|
claim = "kg_executions must have an HNSW index on the embedding field — brute-force in-process vector search is not permitted for this table",
|
|
scope = "migrations/012_kg_hybrid_search.surql",
|
|
severity = 'Hard,
|
|
check = { tag = 'Grep, pattern = "HNSW\\|hnsw", paths = ["migrations/012_kg_hybrid_search.surql"], must_be_empty = false },
|
|
rationale = "In-process similarity search over an unbounded table is the rejected alternative. The HNSW index must exist before production queries are issued.",
|
|
},
|
|
{
|
|
id = "hybrid-rrf-fusion",
|
|
claim = "KGPersistence similarity queries must fuse HNSW and BM25 results via RRF — no single-strategy retrieval",
|
|
scope = "crates/vapora-knowledge-graph/src/persistence.rs",
|
|
severity = 'Hard,
|
|
check = { tag = 'Grep, pattern = "rrf\\|reciprocal_rank\\|BM25\\|full_text", paths = ["crates/vapora-knowledge-graph/src/persistence.rs"], must_be_empty = false },
|
|
rationale = "Reverting to single-strategy retrieval silently degrades search quality without any compile-time signal.",
|
|
},
|
|
],
|
|
|
|
related_adrs = ["adr-009"],
|
|
|
|
ontology_check = {
|
|
decision_string = "HNSW + BM25 + RRF hybrid search in KGPersistence; migration 012 fixes SCHEMAFULL gap and creates indexes; stratum-embeddings in-process scan rejected",
|
|
invariants_at_risk = [],
|
|
verdict = 'Safe,
|
|
},
|
|
}
|