The Feather DB Adaptive Scoring Formula: Similarity × Recency × Importance
Feather DB's adaptive scoring formula combines cosine similarity, time-decay recency, and explicit importance — with stickiness preventing recalled memories from aging out. Here's the full breakdown with worked numerical examples.
The scoring formula
Every memory in Feather DB gets a final retrieval score that blends three signals: vector similarity, time-based recency, and an explicit importance weight. The full formula is:
stickiness = 1 + log(1 + recall_count)
effective_age = age_days / stickiness
recency = 0.5 ** (effective_age / half_life)
score = ((1 - tw) * similarity + tw * recency) * importance
Where tw is the time weight (default 0.3), controlling how much recency competes with similarity. The four components interact: similarity anchors the result to the query, recency penalizes old memories, importance amplifies the whole expression, and stickiness protects frequently recalled memories from the recency penalty.
Component 1: Similarity
Similarity is the cosine similarity between the query vector and the stored memory vector, ranging from 0.0 (orthogonal) to 1.0 (identical). Feather DB's HNSW search returns approximate nearest neighbors; the adaptive scoring then reranks within the candidate set using the full formula.
At the default tw=0.3, similarity contributes 70% of the non-importance part of the score. A highly relevant memory with similarity=0.92 will still score strongly even if it was written 60 days ago — the similarity component dominates unless recency is very low.
Component 2: Stickiness and effective age
Stickiness is the key innovation that separates Feather DB's scoring from simple time-decay. Every time a memory is recalled (returned in a search result and written back with update_recall()), its recall_count increments. Stickiness is:
stickiness = 1 + log(1 + recall_count)
Stickiness at different recall counts:
| recall_count | stickiness | effective age (at day 90) |
|---|---|---|
| 0 | 1.00 | 90.0 days |
| 5 | 1.79 | 50.3 days |
| 10 | 2.40 | 37.5 days |
| 20 | 3.04 | 29.6 days |
| 50 | 3.93 | 22.9 days |
A memory recalled 10 times at day 90 has an effective age of only 37.5 days. A memory never recalled at day 90 has an effective age of 90 days. The logarithmic form ensures stickiness grows usefully up to recall_count ≈ 50 and then flattens, preventing highly-recalled memories from becoming permanent fixtures.
Component 3: Recency
Recency is a half-life exponential decay applied to the effective age:
recency = 0.5 ** (effective_age / half_life)
At effective_age == half_life, recency = 0.5. At effective_age == 0, recency = 1.0. At effective_age == 2 * half_life, recency = 0.25. The half_life parameter is the primary tuning knob for your domain.
Worked numerical examples
Let's trace a memory with similarity=0.85, importance=1.0, tw=0.3, half_life=30 days, at three points in time:
Day 0 — freshly written, recall_count=0:
stickiness = 1 + log(1 + 0) = 1.00
effective_age = 0 / 1.00 = 0.0
recency = 0.5 ** (0.0 / 30) = 1.000
score = (0.7 * 0.85 + 0.3 * 1.000) * 1.0 = 0.895
Day 30 — recall_count=0 (never recalled):
stickiness = 1.00
effective_age = 30 / 1.00 = 30.0
recency = 0.5 ** (30 / 30) = 0.500
score = (0.7 * 0.85 + 0.3 * 0.500) * 1.0 = 0.745
Day 30 — recall_count=10 (recalled 10 times):
stickiness = 1 + log(1 + 10) = 2.398
effective_age = 30 / 2.398 = 12.5
recency = 0.5 ** (12.5 / 30) = 0.748
score = (0.7 * 0.85 + 0.3 * 0.748) * 1.0 = 0.819
Day 90 — recall_count=0:
stickiness = 1.00
effective_age = 90.0
recency = 0.5 ** (90 / 30) = 0.125
score = (0.7 * 0.85 + 0.3 * 0.125) * 1.0 = 0.633
Day 90 — recall_count=10:
stickiness = 2.398
effective_age = 37.5
recency = 0.5 ** (37.5 / 30) = 0.420
score = (0.7 * 0.85 + 0.3 * 0.420) * 1.0 = 0.721
The recalled memory scores 0.721 vs 0.633 for the never-recalled memory at day 90 — a 14% lift just from being useful enough to recall 10 times over three months.
Component 4: Importance
Importance is a multiplicative scalar (default 1.0, range typically 0.1–2.0) that amplifies or suppresses the entire score. Because it multiplies the combined similarity-recency score, importance=2.0 doubles a memory's effective rank weight across all time and similarity values.
Use importance for structural signals that aren't captured by the content: a user's explicitly stated preference is more important than an inferred one, a confirmed fact is more important than a hypothesis, a pinned memory is more important than a transient session note.
import feather_db as fdb
db = fdb.DB.open("agent.feather", dim=768)
# High importance: explicitly stated user preference
mem = db.add(vec, text="User prefers concise responses, stated directly.")
mem.meta.set_attribute("importance", 2.0)
# Normal importance: inferred preference
mem2 = db.add(vec2, text="User seemed to prefer bullet points in session 4.")
# importance defaults to 1.0
# Low importance: speculative or uncertain fact
mem3 = db.add(vec3, text="User might be in the EU based on timezone.")
mem3.meta.set_attribute("importance", 0.5)
Tuning half_life for your domain
Half_life is the most important tuning parameter. It determines how quickly an unrecalled memory loses relevance:
| Domain | Recommended half_life | Rationale |
|---|---|---|
| News / current events agent | 7–14 days | Facts go stale fast; last week's headlines are rarely relevant |
| Customer support conversation | 14–30 days | Issue context relevant for a month; older tickets less so |
| Personal assistant memory | 30–60 days | User preferences stable but evolve over weeks |
| Research assistant | 90–180 days | Paper claims stay valid for months; foundational work longer |
| Architecture decisions | 180–365 days | Why we chose PostgreSQL in 2024 is still relevant in 2025 |
# Fast-moving domain: news agent
results = db.search(query_vec, k=10, half_life=14)
# Stable domain: architecture decisions
results = db.search(query_vec, k=10, half_life=365)
# Mixed: search with domain-appropriate half_life per query
def search_with_domain(db, vec, domain):
half_lives = {"news": 14, "preferences": 60, "architecture": 365}
return db.search(vec, k=10, half_life=half_lives.get(domain, 30))
Time weight: how much recency competes with similarity
The tw parameter (time weight, default 0.3) controls the fraction of the score allocated to recency vs similarity. At tw=0.0, the formula reduces to pure similarity search — identical to a standard vector DB. At tw=1.0, only recency matters. The default 0.3 gives recency meaningful influence without letting it override strong similarity signals.
For applications where freshness is paramount (real-time news summarizer, live meeting notes), consider tw=0.5 or higher. For applications where factual relevance trumps timing (knowledge base Q&A, policy lookup), tw=0.1 or lower is appropriate.
The formula is deliberately simple and interpretable. Every component has a clear meaning, every parameter has a clear effect, and the worked examples above let you predict what any memory's score will be at any point in time — which is exactly what you want when debugging why a memory did or did not surface.
Install: pip install feather-db · GitHub: github.com/feather-store/feather