Ranking¶
Every retrieved memory carries a score. This page is the whole formula.
The formula¶
score = (0.40 * similarity # cosine, query vs memory embedding
+ 0.20 * recency # exp decay, 14-day half-life
+ 0.25 * importance # supplied at write time, never decays
+ 0.15 * frequency) # log-scaled access_count
Weights are DEFAULT_WEIGHTS in venkai/api/memory_scoring.py. They are
global and not configurable per request. Since all four terms are in
[0, 1], score is too.
pie showData
title Score composition
"similarity 0.40" : 40
"importance 0.25" : 25
"recency 0.20" : 20
"frequency 0.15" : 15
Term by term¶
relevance — 0.40¶
Cosine similarity between the query embedding and the memory's. 0.0 when the
vectors are incompatible or either is empty; 1.0 when no query was supplied,
which lets a query-less call fall back cleanly to the other three terms.
This is the term most likely to under-deliver. With the default hashing
provider it measures token overlap dressed as a vector, so a paraphrase scores
near zero. See Embeddings.
recency — 0.20¶
| Age | Contribution to score |
|---|---|
| now | 0.200 |
| 1 week | 0.141 |
| 2 weeks | 0.100 |
| 1 month | 0.045 |
| 3 months | 0.005 |
After about two months, recency is effectively zero and a memory survives on
importance and similarity alone. This is why importance matters: it is
the only term that does not fade.
importance — 0.25¶
Exactly the float you sent at write time. No decay, no normalisation, no learning. It is the largest single lever you have over ranking, and it is entirely yours.
The second-largest term in the formula is one you set by hand. Set it thoughtfully or you have given up a quarter of the ranking.
frequency — 0.15¶
Log-scaled access_count. In practice this term is near zero for almost
every memory, because of the following.
Why retrieval does not count as an access¶
access_count is not incremented when a memory is retrieved. This is
deliberate and load-bearing.
flowchart LR
A["memory retrieved"] --> B["access_count++"]
B --> C["frequency score ↑"]
C --> D["ranks higher next query"]
D --> A
style A fill:#7f1d1d,color:#fff
style D fill:#7f1d1d,color:#fff
Counting retrievals as accesses creates a self-reinforcing loop: whatever surfaced first keeps surfacing, regardless of whether it was useful. The top-N locks in and never rotates.
The code documents the measurement that killed it: recall@5 fell from 0.54 to 0.12 after roughly 50 queries with the loop in place. That is the internal finding recorded in the source, on an internal fixture — it is a design rationale, not a product benchmark, and it is not a number to quote. See Evaluation.
So today access_count moves only through explicit feedback paths, which means
the frequency term contributes ~0 for most memories and the effective ranking
is closer to:
Planned — a separate
validated_access_count, incremented only on confirmed usefulness, so the
frequency term can be earned rather than assumed.
Worked example¶
Two memories, query "which database did we pick", hashing provider:
| Decision (Postgres) | Preference (commit messages) | |
|---|---|---|
| similarity | 0.135 | 0.000 |
| recency (fresh) | 1.000 | 1.000 |
| importance | 0.900 | 0.200 |
| frequency | 0.000 | 0.000 |
| score | 0.40·0.135 + 0.20·1 + 0.25·0.9 = 0.479 |
0 + 0.20 + 0.25·0.2 = 0.250 |
The right memory wins — but note it wins mostly on importance, not on
relevance. With a weak similarity signal, ranking is being carried by the
metadata you supplied. That is worth knowing before you conclude that retrieval
"works".
Tuning what you can¶
You cannot change the weights per request. You can change the inputs:
| Want | Do |
|---|---|
| Foundational rules to always surface | importance ≥ 0.9 |
| Better paraphrase matching | VENKAI_EMBEDDING_PROVIDER=semantic |
| Less noise from old chatter | Write ephemera at importance ≤ 0.2 — recency drops it out on its own |
| Deterministic subsets | Use the list endpoint with type=, not /relevant |