{
  "uid": "cogitave.learn.query-the-estate-with-cogitave-query.cogitave-query",
  "kind": "moduleUnit",
  "href": "/modules/query-the-estate-with-cogitave-query/cogitave-query/",
  "title": "How Cogitave Query resolves a request",
  "summary": "",
  "type": null,
  "products": [],
  "roles": [],
  "levels": [],
  "subjects": [],
  "headings": [
    "1. Lexical - BM25 over immutable segments",
    "2. Dense - HNSW vectors",
    "3. Optional - learned sparse and late interaction",
    "4. Fusion - Reciprocal Rank Fusion",
    "Same pipeline, every surface",
    "The bounded profile: query_graph"
  ],
  "source": "[Cogitave Query](../../../../core/docs/query.md) is not a single index with a ranking tweak on top - it runs two independent retrievers in parallel and fuses their output before graph structure ever enters the picture. This unit walks the pipeline stage by stage.\n\n## 1. Lexical - BM25 over immutable segments\n\nThe lexical retriever is BM25 (Robertson/Zaragoza), the same ranking function Lucene, Elasticsearch, and Tantivy default to, over an inverted index built as **immutable, mergeable segments** with an FST term dictionary. Immutable segments give snapshot reads without locking, and they align with Core's content-addressed publish model: a new content root is just a new, additively-merged segment set, not a rewrite.\n\n## 2. Dense - HNSW vectors\n\nThe dense retriever is approximate nearest-neighbor search over per-node embeddings using **HNSW** - a multi-layer proximity graph with logarithmic search complexity, and the de-facto standard in vector search. The vector index lives in the *same Rust process* as the inverted index - no network hop - keyed by the same node `uid` the lexical index uses, so both retrievers are talking about the same node.\n\n## 3. Optional - learned sparse and late interaction\n\nFor high-value corpora, the design keeps a spec-level option for **learned sparse** expansion (SPLADE) and **late-interaction** reranking (ColBERTv2, with SPLATE for CPU-friendly candidate generation), sitting behind the same fusion interface as the other two retrievers. These are explicitly not a Day 0 dependency - an upgrade path, not something you should assume is live.\n\n## 4. Fusion - Reciprocal Rank Fusion\n\nThe two (or three) ranked lists are fused with **Reciprocal Rank Fusion**: `score(d) = Σ_r 1 / (k + rank_r(d))`, with `k = 60` by default. RRF fuses on *rank position*, not raw score - which matters because BM25's score is unbounded while cosine similarity is bounded to `[-1,1]`, and reconciling those two scales is an unsolved problem. Fusing on rank sidesteps it entirely, and it means retrievers can be added or removed without recalibrating anything else in the pipeline.\n\n## Same pipeline, every surface\n\nThe fused, graph-reranked result (graph rerank is the next unit) backs every surface identically: the MCP tools `docs_search`, `code_sample_search`, `get_related`, `get_learning_path`, `resolve_xref`, and `query_graph` on the agent side, and GraphQL/REST plus a JSON content API on the human/programmatic side. There is one ranking, read from one place.\n\n## The bounded profile: `query_graph`\n\n`query_graph` is how an agent reaches the property graph directly, shaped as a GQL/openCypher-style `MATCH … RETURN` - but only a narrow, **read-only, resource-bounded** subset of it, never the full language. A graph-query tool is exactly the kind of arbitrary-code-execution surface the MCP security model says to treat with caution, so every bound below is enforced server side, not left as advisory guidance:\n\n| Bound | Rule |\n|---|---|\n| Read-only | No `CREATE`, `MERGE`, `SET`, `DELETE`, `REMOVE`, `INSERT`, procedure `CALL{…}`, or schema ops - only `MATCH`/`OPTIONAL MATCH … WHERE … RETURN` with `ORDER BY`/`SKIP`/`LIMIT`. |\n| Allowlisted labels | Relationship types must be in the closed **9**-edge vocabulary (`partOf, dependsOn, xref, appliesTo, forRole, teachesSkill, supersededBy, implementedBy, derivedFrom`); an unknown label rejects the query. |\n| Max traversal depth | Variable-length patterns are capped at `d ≤ 4`; an unbounded `*` is rejected and must be written `*1..d`. |\n| Max result rows | `limit` defaults to 100, capped at 1000; an over-cap response is truncated with `truncated: true`. |\n| Timeout | A ~250 ms wall-clock budget; exceeding it aborts the query. |\n| Parameterized only | `params` bind by name and are never string-spliced into the pattern - the pattern cannot be widened at call time. |\n\nA violation is returned as a **tool execution error** (`isError: true`), not a protocol error, so the calling model can read why and self-correct instead of the connection failing outright. The same bounded traversal underlies graph faceting and `get_related`, too - there is exactly one enforced path into the graph, not a second, unbounded one.\n",
  "partOf": "cogitave.learn.query-the-estate-with-cogitave-query",
  "durationInMinutes": 7,
  "quiz": null
}