Back to Theory
Architecture7 min read · June 16, 2026

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.

F
Feather DB
Engineering

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_countstickinesseffective age (at day 90)
01.0090.0 days
51.7950.3 days
102.4037.5 days
203.0429.6 days
503.9322.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:

DomainRecommended half_lifeRationale
News / current events agent7–14 daysFacts go stale fast; last week's headlines are rarely relevant
Customer support conversation14–30 daysIssue context relevant for a month; older tickets less so
Personal assistant memory30–60 daysUser preferences stable but evolve over weeks
Research assistant90–180 daysPaper claims stay valid for months; foundational work longer
Architecture decisions180–365 daysWhy 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