Retrieval Pipeline¶
engram runs an 8-stage hybrid retrieval pipeline that fuses four parallel search channels.
the pipeline¶
query
│
├── intent classification (why/when/who/how/what)
│ → dynamic signal weights per intent type
│
├── dense HNSW search (bge-small, 384-dim, hnswlib) → top 3k candidates
├── BM25 via sqlite FTS5 (content + hypothetical queries) → top 3k candidates
├── entity graph BFS (1-hop traversal, strength-weighted) → top k candidates
└── Hopfield associative (pattern completion, β=8.0) → top k candidates
│
▼
intent-weighted reciprocal rank fusion (k=60)
│
▼
temporal + importance boosting
│
▼
optional cross-encoder reranking (BGE by default; MiniLM or Voyage optional)
│
▼
confidence gate + prior coverage when cross-encoder reranking is on
│
▼
deep MLP reranker (optional, trained on access patterns)
│
▼
noise only when cross-encoder reranking is off
│
▼
final top-k results
stage 0: intent classification¶
queries are classified into 5 intent types using regex patterns:
| intent | triggers | effect |
|---|---|---|
why |
"why", "because", "reason" | boost graph (causal reasoning) |
when |
"when", "date", "timeline" | boost BM25 (date matching) |
who |
"who", "person", "built" | boost graph (entity lookup) |
how |
"how to", "steps", "fix" | boost dense (procedural) |
what |
default | balanced weights |
stage 1: parallel candidate generation¶
four channels run independently:
dense search (HNSW)¶
embeds the query with bge-small-en-v1.5, searches the HNSW index for approximate nearest neighbors. O(log n) at any scale.
BM25 (FTS5)¶
full-text search via SQLite's FTS5 extension. matches on content text and hypothetical queries (generated at ingestion time via docTTTTTquery).
entity graph BFS¶
extracts entity names from the query, finds matching entities, retrieves their memories (hop 0, score 1.0), then traverses 1-hop related entities (score 0.5 * relationship strength).
Hopfield associative¶
pattern completion via modern Hopfield network: ξ_new = X^T · softmax(β · X · ξ). retrieves memories by associative recall, not just similarity.
stage 2: RRF fusion¶
reciprocal rank fusion combines all four channels:
the k=60 constant comes from Cormack et al. 2009. intent-specific weights adjust channel importance per query type.
stage 3: temporal + importance boosting¶
- temporal boost for date-matching memories
- importance scaling:
score *= (0.8 + 0.4 * importance) - recency decay (Ebbinghaus):
exp(-0.693 * age_days / half_life)for episodic memories - access frequency boost:
1.0 + 0.1 * log(1 + access_count)
production search and the benchmark share date parsing and relative windows in
engram/temporal.py, including slash-separated dates, hyphenated dates and ISO
timestamps.
stage 4: cross-encoder reranking¶
the configured shortlist, 20 candidates by default, is scored by a cross-encoder
using each query/document pair. the default local model is
BAAI/bge-reranker-base; MiniLM and supported Voyage models are optional.
with rerank_passage_fallback: true (the default), a local reranker can retry
one excerpt per long document when every full-document sigmoid score is below
rerank_passage_floor (default 0.001), or when that memory would otherwise fail
the production confidence gate. already retried excerpts are not scored again.
each excerpt contains a matching sentence and nearby context,
is copied directly from the source, and is capped at 160 words. shorter
documents and documents without matching query terms keep their original
scores. the same semantic query scores both versions; the larger raw score
survives. matching supports conservative regular English singular/plural forms
and counts each original query term once per sentence. hosted rerankers skip
the retry. repeated unrelated neighboring boilerplate can be omitted for an
anchor covering every query term; unique and qualifying context is retained.
this adds inference work and may omit useful context. set
rerank_passage_fallback: false to disable it. retrieval internals
describe date handling and the raw-score/source-offset traces. the final
min_confidence gate independently controls returned results, with a default
of 0.6, even when the excerpt improves a result's rank. the activation floor was
chosen during development on LongMemEval; evaluation on that dataset is not
held-out accuracy, and neither threshold is a calibrated probability.
local logits pass through one sigmoid. hosted relevance scores keep their
normalized scale. temporal evidence applies in logit space before an optional
blend with reciprocal pre-rerank position. set rerank_fusion_alpha between 0
and 1 to enable that blend; 0 is the default. final scores stay between 0 and 1,
without separate lexical bonuses. these scores are not measured probabilities
that a memory is correct.
stage 5: confidence gate and prior coverage¶
cross-encoder results receive no random ranking noise. min_confidence filters
their final scores before coverage. with preserve_prior_candidate: true, a
request for at least two results retains the best eligible hybrid candidate.
if missing, that candidate moves to the last requested position while the
rerank winner stays first. coverage cannot admit a candidate rejected by the
confidence gate.
scores stay unchanged, so preserve the returned order instead of sorting again
by score. fusion weight 0 disables score blending; candidate coverage can still
adjust order. set preserve_prior_candidate: false to disable that step.
stage 6: deep MLP reranker¶
an optional learned reranker uses access patterns after candidate selection.
its two-layer MLP combines features including cosine similarity, importance,
access count, age, layer and retention score. train with train_reranker after
accumulating usage data.
stage 7: noise and cache¶
searches with reranking off retain the gaussian noise term (σ=0.02). the rerank cache includes fusion, confidence, coverage, passage fallback and its separate activation floor, so policy changes take effect on the next search.
retrieval profiles¶
the recall tool accepts a mode parameter that filters candidates by memory type before cross-encoder reranking:
| mode | types included | use case |
|---|---|---|
facts_only |
fact | statuses, states, structured answers |
facts_plus_rules |
fact + procedure | methodology, how-to queries |
full_context |
fact + procedure + narrative | exhaustive recall (default) |
filtering happens after RRF fusion (stage 2) but before the cross-encoder (stage 4). non-active memories (status != active) are also excluded at this stage.
this means facts_only mode still benefits from the full 4-channel candidate generation — it just removes narrative-typed candidates before the expensive reranking step.
tuning¶
all parameters are in config.yaml:
retrieval:
top_k: 10 # final results
rrf_k: 60 # RRF constant
min_confidence: 0.60 # threshold gate
rerank_candidates: 20 # cross-encoder shortlist
rerank_fusion_alpha: 0.0 # optional prior rank blend, from 0 to 1
preserve_prior_candidate: true # keep the best eligible hybrid candidate
rerank_passage_fallback: true # one excerpt for weak scores or rejected long memories
rerank_passage_floor: 0.001 # early activation floor; zero disables excerpt retries
dense_multiplier: 3 # dense candidates = top_k * 3
bm25_multiplier: 3 # BM25 candidates = top_k * 3
explain a result or a miss¶
--explain keeps the CLI's normal reranking choice: include --rerank to inspect
the cross-encoder and confidence gate. --debug remains an alias. the JSON object
contains results and explanation, even if every candidate is rejected.
each observed candidate has an outcome and reason: returned, below the confidence cutoff, outside the result budget, or filtered before scoring. eligible candidates include their observed signals, raw local logits or hosted normalized scores, temporal adjustments and any excerpt retry. forgotten, inactive and profile-filtered candidates never include their content.
diagnostics bypass the result cache and leave access history and dormant
evaluations unchanged. MCP recall_explain, native search_explain, and
/api/search/explain use the same report; the web search's Explain retrieval
option displays it. MCP explanation also leaves session handoffs unchanged.
an empty result can mean a relevant memory failed the confidence cutoff. inspect that reason before changing a threshold. a score is not a calibrated probability, and an explanation does not establish that the memory's claim is true.