Guides

Recency-weighted ranking

Bias search results toward recent documents with a single recency_weight parameter — so yesterday's note about a customer outranks a vague mention from six months ago, without throwing away semantic relevance.

By default, search ranks purely by semantic similarity: a six-month-old transcript and yesterday's compete on equal footing. For agent-memory and longitudinal use cases that is almost always wrong — the recent observation is usually the one you want. recency_weight lets you blend a recency signal into the ranking, server-side, with no extra ML and no measurable latency cost.

This is a re-ranking of the candidates the search already found — it never changes which documents match your filters, only the order of the top results. To restrict results to a window instead of just nudging the order, use the time filters (since / until / last_n_days); the two compose.

Recency is keyed to when a document was created. For documents that are edited in place, the sibling freshness parameters apply the same idea to when a document was last updated — covered at the end of this page.


How the score is computed

For each candidate hit, the server blends two scores in [0, 1]:

text
distance_score = cosine similarity of the matched passage, in [0, 1]
recency_score  = exp(-age_days / half_life_days)
final_score    = (1 - recency_weight) * distance_score
               +      recency_weight  * recency_score
  • recency_weight ranges from 0.0 to 1.0. At 0.0 (the default) ranking is pure similarity — identical to not passing the parameter at all. At 1.0 ranking is pure recency. 0.3 is a sensible starting point for memory use cases.
  • recency_score decays exponentially with the document's age, measured from its created_at timestamp. The half_life_days parameter (default 30) sets how fast: at one half-life the recency contribution is halved, at two half-lives it is a quarter, and so on.

Recency score by age

How recency_score falls off for a few half-lives:

Age of documenthalf_life_days = 7half_life_days = 30half_life_days = 90
0 days (just now)1.001.001.00
7 days0.500.850.95
30 days0.050.500.79
90 days0.000.130.50
365 days0.000.000.06

A short half-life (7 days) is aggressive — only the last week or two carries weight. A long half-life (90 days) decays gently across a quarter. Pick the window that matches how quickly your domain goes stale.


Worked example

Two documents match the query "what does the customer want for billing?" equally well on semantics, but one is from yesterday and one is from five months ago. With half_life_days = 30:

Documentcreated_atdistance_scorerecency_scorefinal_score (w = 0.0)final_score (w = 0.3)
"Switch us to annual EUR billing"1 day ago0.820.980.82 (rank 1)0.87 (rank 1)
"We pay monthly in USD"150 days ago0.840.030.84 (rank 1)0.60 (rank 2)

At recency_weight = 0.0 the older, very slightly closer match wins — and your agent tells the customer something that stopped being true months ago. At recency_weight = 0.3 the fresh document is promoted to the top, while a strong-enough old match would still hold its place. Recency nudges; it does not blindly sort by date.


Using it

recency_weight and half_life_days are accepted on the search endpoints. SDK users building an agent-memory layer get the same behavior through the Memory facade's recall(recency_weight=...), which is tuned for exactly this case.

# Server-side recency blend on the REST search endpoint
curl "https://api.aetherdb.ai/v1/search?q=billing%20preferences&k=5&recency_weight=0.3&half_life_days=30" \
  -H "Authorization: Bearer $AETHER_API_KEY"

Recency re-ranks; time filters restrict

recency_weight re-orders the candidates that already matched your query and filters — it never drops a document. When you want to exclude anything older than a cutoff, combine it with a time filter: last_n_days=90 bounds the window (a hard pre-filter), and recency_weight=0.3 then biases what's left toward the most recent. The two are complementary.


Choosing a weight

GoalSuggested recency_weightNotes
Pure semantic search (default)0.0Recency ignored; identical to omitting the parameter.
Agent memory / "what's true now"0.20.4Recent observations win ties and near-ties; strong old matches survive.
Activity feeds, "latest first" with relevance0.60.8Recency dominates, but the query still filters and orders within a period.
Strict newest-firstuse list with a time filterWhen you don't want similarity in the ranking at all, list by recency instead of searching.

Out-of-range values are rejected: recency_weight must be in [0.0, 1.0] and half_life_days must be greater than 0, otherwise the request returns 400.


Freshness: boosting recently updated documents

recency_weight is keyed to created_at — when a document first entered the store. That is the right signal for append-only data such as notes, transcripts, and observations, where a document is written once and its creation time is the information's time. Living documents — runbooks, wiki pages, tickets, customer profiles — are different: they are edited in place, and a page created a year ago but revised yesterday is current, not stale. For those, blend in freshness_weight to boost by time since last update instead:

  • freshness_weight ranges from 0.0 to 1.0. At 0.0 (the default) freshness is off — identical to not passing the parameter at all.
  • freshness_half_life_days sets how fast the freshness boost decays, exactly like half_life_days does for recency. It defaults to 14 and must be greater than 0.

Freshness reads each hit's updated_at timestamp. A document that has never been updated has no updated_at, so freshness falls back to created_at — for never-updated documents, freshness and recency measure the same age.

Freshness ranking may require a Scale plan or higher.

Composing freshness with recency

The two weights share a single blend; whatever weight you don't assign stays on semantic similarity:

text
recency_score   = exp(-days_since_created / half_life_days)
freshness_score = exp(-days_since_updated / freshness_half_life_days)
final_score     = (1 - recency_weight - freshness_weight) * distance_score
                +       recency_weight   * recency_score
                +       freshness_weight * freshness_score

Either weight can be used alone; together their sum must stay within 1.0. A request with recency_weight + freshness_weight > 1.0 returns 400, and — as with recency — an out-of-range weight or a non-positive half-life is also a 400.

Worked example

Two runbook pages match the query "database failover procedure" about equally well on semantics. With freshness_weight = 0.3 (no recency weight) and the default 14-day half-life:

Documentcreated_atLast updateddistance_scorefreshness_scorefinal_score (fw = 0.0)final_score (fw = 0.3)
"Failover runbook"300 days ago2 days ago0.800.870.80 (rank 2)0.82 (rank 1)
"Failover notes"10 days agonever0.820.490.82 (rank 1)0.72 (rank 2)

The freshly revised runbook wins even though it was created long ago — the case recency alone gets backwards: recency_weight would penalize the 300-day-old page despite yesterday's edit. The never-updated notes fall back to their creation age (10 days) for the freshness score.

Using freshness

freshness_weight and freshness_half_life_days are accepted everywhere the recency pair is — search, retrieve, vector search, and each batch-search query:

# Server-side freshness blend, keyed to updated_at
curl "https://api.aetherdb.ai/v1/search?q=failover%20procedure&k=5&freshness_weight=0.3&freshness_half_life_days=14" \
  -H "Authorization: Bearer $AETHER_API_KEY"

Choosing a freshness_weight follows the same logic as choosing a recency weight: 0.20.4 promotes recently edited documents past ties and near-ties without letting a stale-but-relevant page vanish, while higher values make edit time dominate.


Next steps