Hybrid memory retrieval

Hybrid retrieval combines exact lexical terms with similarity from externally generated embeddings. Use it when a query contains names, identifiers or technical terms but relevant memories may also express the same idea in different words. QilbeeDB retrieves evidence; your application decides how to use it.

Use POST /api/v1/memory/search/hybrid with memory_read and an exact scope grant. The existing POST /api/v1/memory/search continues to return cosine scores; it is not reinterpreted as hybrid search. This new method is experimental: functional validation does not establish a held-out relevance gain.

{
  "contract_version": 1,
  "mode": "hybrid",
  "scope": {"project_id":"project","mission_id":null,"agent_id":"agent","visibility":"shared"},
  "query": {
    "text": "ZX17 retry failure",
    "space": {"provider":"fixture","model":"fixture-embedding","revision":"v1","dimensions":3},
    "vector": [1.0, 0.0, 0.0],
    "ranking_version": "weighted_rrf_v1",
    "limit": 10,
    "min_score": 0.0,
    "scan_limit": 10000,
    "scan_bytes_limit": 8388608
  }
}

These three-dimensional vectors illustrate the contract, not language-model relevance. Generate real document and query vectors externally. No provider credential is accepted or required by QilbeeDB.

The response envelope contains contract_version, scope, mode: "hybrid", ranking_version and page. Rust applications can use RocksDbMemoryStorage::search_memory_hybrid(namespace, &HybridQuery) after performing their own authentication and namespace derivation.

Keep model generation outside the database#

Generate document and query vectors with the same external embedding service and immutable model configuration. Attach document vectors using the embedding contract. QilbeeDB does not choose a provider, load an embedding model, rewrite text with a language model, or call a reranker. Provider identity is a client declaration, not a provider attestation.

A source update invalidates its previous embedding. Until its new vector is attached, the current memory can still match by BM25. A wrong or missing model space yields no semantic candidates; lexical candidates remain available. The response exposes this through embedded_records and per-hit contributions.

Understand ranking#

Both channels read the same authorized record corpus, RocksDB snapshot and visibility timestamp. Deleted, expired and filtered memories are excluded before corpus statistics and ranking. BM25 uses primary, secondary and context text, Unicode alphanumeric tokens lowercased without stemming, k1 = 1.2, and b = 0.75. It does not search metadata or perform substring matching.

Each channel sorts its matches by descending raw score, then ascending UUID, and keeps up to 100 candidates. The server-owned weighted_rrf_v1 profile fixes both weights at 0.5 and the rank constant at 60. The separately versioned weighted_rrf_v2 uses lexical weight 0.25, semantic weight 0.75 and rank constant 2. Both remain experimental and use the same 100-candidate cap. A request must name its version explicitly; publishing v2 does not change v1 or select a new default. Weighted reciprocal rank fusion assigns:

lexical contribution = lexical_weight / (rank_constant + lexical_rank)
semantic contribution = semantic_weight / (rank_constant + semantic_rank)
fused score = lexical contribution + semantic contribution

Ranks start at one. An absent candidate contributes zero. Weights are not renormalized when one channel has no matches. The candidate union is deduplicated by record UUID, sorted by fused score then UUID, and truncated to limit. BM25, cosine and fused scores are ranking signals, not probabilities or calibrated confidence.

hits[].lexical and hits[].semantic each contain rank, raw score, and weighted contribution, or null if absent from that candidate list. hits[].embedding is the original revision-bound receipt when the semantic channel contributes, otherwise null. The record always includes its source revision, author, validity and payload. A source can change after the snapshot; use the returned revision for subsequent conditional writes.

Discover the server's supported profiles#

Call GET /api/v1/memory/ranking-profiles with a current memory_read credential. No scope or query body is required because this endpoint reads only server-wide implementation metadata. It returns no memory records, corpus statistics, registered embedding spaces or tenant configuration.

{
  "contract_version": 1,
  "component_versions": {"lexical": "bm25_v1", "semantic": "cosine_exact_v1"},
  "execution_limits": {
    "scope": "server_instance",
    "max_embedding_dimensions": 32768,
    "max_scan_bytes": 67108864,
    "default_scan_bytes": 8388608,
    "max_concurrent_retrievals": 2,
    "vector_request_body_bytes": 2097152
  },
  "hybrid_profiles": [{
    "version": "weighted_rrf_v1", "method": "weighted_rrf",
    "lexical_version": "bm25_v1", "semantic_version": "cosine_exact_v1",
    "candidate_limit": 100, "lexical_weight": 0.5, "semantic_weight": 0.5,
    "rank_constant": 60, "experimental": true
  }, {
    "version": "weighted_rrf_v2", "method": "weighted_rrf",
    "lexical_version": "bm25_v1", "semantic_version": "cosine_exact_v1",
    "candidate_limit": 100, "lexical_weight": 0.25, "semantic_weight": 0.75,
    "rank_constant": 2, "experimental": true
  }]
}

Discovery and retrieval use the same profile definitions. Compare a selected profile with the version and page.ranking returned during execution; fail closed if an evaluation's pinned profile differs. Missing, expired or revoked credentials return 401; a credential without memory_read returns 403, including an operator credential that grants only credential/policy administration. Responses use Cache-Control: no-store; do not use a cached catalog as authorization evidence. Each actual search still requires its own exact scope grant.

A listed method is supported by the running implementation. Listing does not admit it under enterprise policy, qualify its relevance or select it as a default. weighted_rrf_v1 remains unchanged and experimental. The available development results do not currently justify publishing another set of weights.

Configure candidate and scan budgets#

Query field Contract
text Required, at most 4096 UTF-8 bytes, 1–64 distinct lowercase alphanumeric terms
space, vector Required external model identity and finite, nonzero float32 query vector; same validation as semantic search
limit Final result count, 1–100
ranking_version Required weighted_rrf_v1; selects an immutable server-defined method and parameters
min_score Minimum raw cosine, finite in [-1, 1], default -1; does not filter lexical matches
scan_limit At most 1–10000 source records, default 10000
scan_bytes_limit 1–268435456 serialized bytes, subject to the operator ceiling (default 67108864); request default 8388608
after Optional source UUID cursor; a continuation starts a new snapshot
episode_type, tag Optional exact filters applied to both channels before scoring

The hybrid contract always validates text and vector. Use lexical retrieval when no query embedding is available. Clients cannot override weights, the method or candidate cap in the request; unknown fields are rejected. The QilbeeDB team publishes a new ranking version when these parameters or algorithm semantics change. page.ranking returns the exact method, weights, candidate cap, rank constant, component versions (bm25_v1, cosine_exact_v1) and experimental status.

The scan and body ceilings are implementation safeguards, not a tenant quota or an enterprise admission policy. Experimental evaluation budgets must be reported separately from an organization's production limits. This release does not add tenant-specific retrieval quotas or concurrency admission.

The byte budget counts serialized source records and selected-space bindings read for visible, filtered records, including stale bindings. It excludes keys, integrity indexes, allocation overhead and one lookahead row; it is not a process memory cap. A budget too small for the first record and binding returns a validation error. No cursor is returned that silently skips an oversized row.

Interpret coverage before using results#

Page field Meaning
scanned_records, scanned_bytes Work admitted to this source corpus page
corpus_records Visible records passing filters, including those without embeddings
embedded_records Corpus records with a current binding in the selected space
lexical_matches, semantic_matches Matches in each enabled channel before candidate truncation
lexical_candidates, semantic_candidates Candidates retained by each channel
candidates_truncated At least one channel dropped matches at its candidate limit
rank_constant The implemented RRF constant, 60
ranking Exact immutable server profile and experimental status
embedding_coverage complete, partial, missing, or empty_corpus, relative to the visible filtered corpus in this scan
next_after Last scanned source UUID if another row remains; otherwise null
exhaustive This request began without a cursor and scanned the whole scope

exhaustive: true describes corpus coverage, not unlimited candidate lists. Check candidates_truncated separately. Missing embeddings do not make the source scan partial; compare embedded_records with corpus_records to inspect coverage. embedding_coverage: missing means every returned hit is lexical-only; partial means some eligible memories lack a current selected-space vector. Neither state claims complete hybrid coverage. An empty selected model space is reported explicitly this way. Neither field certifies vector origin.

A storage read, integrity or ranking validation failure fails the entire request. The implementation does not catch a failed channel and silently return another channel's ranking. Missing bindings are data coverage, not a swallowed execution failure. Lexical and dense channels are always attempted in this profile.

When exhaustive is false, rankings and BM25 statistics describe only the scanned page. Do not merge page scores as if they were a global ranking. Narrow the scope or filters, or raise the scan budget within its limits. Continuations always report exhaustive: false, including the final page. They do not preserve a snapshot across requests. This implementation is a bounded full scan, not ANN or an inverted index.

Choose settings with evidence#

The defaults are a starting point, not an assertion that hybrid search improves every workload. Evaluate BM25, dense and hybrid retrieval on the same judged queries, frozen source corpus and externally generated vectors. Keep candidate budgets and output k explicit; report missing vectors and incomplete scans. The QilbeeDB team selects new server-owned profiles on development data and reports held-out relevance and latency; callers select the immutable version.

The design uses reciprocal rank fusion (Cormack, Clarke and Buettcher, SIGIR 2009). Research on contextual retrieval also motivates combining lexical and dense evidence. Those published results do not establish QilbeeDB's relevance, and this implementation does not perform context generation or reranking. Synthetic vector tests verify contracts, not language understanding or downstream agent improvement.

Errors and compatibility#

Requests require mode: "hybrid"; a mode that does not match the endpoint returns 400. Invalid vectors, unknown ranking versions, caller-supplied weights or candidate caps, unsupported contract versions and invalid budgets also return 400. Missing credentials return 401; revoked or expired credentials return 401; missing capability or scope returns 403; oversized bodies return 413; encountered storage corruption or unsupported stored versions return 500. The JSON body limit is 2097152 bytes (2 MiB), including text, vector, scope and formatting. Dimensions range from 1 through 32768, subject to the operator ceiling; 3072 is supported without changing cosine or fusion semantics. See the platform error envelope.

The tenant is derived from the authenticated credential. Project, agent, mission and visibility must match an exact grant. Private namespaces also include the current subject. Both channels and their corpus statistics use that namespace before candidate selection. No cross-scope ranking cache is used. Revoked credentials cannot begin new authorized searches; an already-authorized request can complete against its snapshot. A subsequent request observes source updates, deletions and expiry. Expiry is fixed at each request's visibility timestamp.

The UUID cursor traverses source records, not ranked hits or a durable snapshot. Its exclusive lower bound prevents returning the same source UUID again on a forward continuation, but inserts or updates across requests can change coverage. It cannot reproduce a multi-page global ranking under concurrent mutation.

Measure retrieval time#

The response includes timing.retrieval_micros, the server wall time spent in the retrieval method. It excludes authentication, blocking-pool queueing, JSON serialization, transport and external embedding generation. Measure the client round trip separately. See the reproducible evaluation workflow for frozen corpora, graded relevance, category regressions and timing limits.

Execution capacity#

The ranking catalog exposes the current server dimension, scan-byte and concurrent retrieval limits. These are operator settings, separate from the immutable ranking profile and from tenant authorization. A byte budget above the configured ceiling returns 400 (retrieval_scan_limit); exhausted retrieval slots return 503 (retrieval_busy). Use bounded backoff and inspect coverage on each successful response. See configure retrieval capacity.

See the 0.6.0 real-embedding report for the 300-query SciFact comparison, uncertainty and losses. Both profiles remain experimental; the measured gain does not establish a universal ranking policy.