{"slug": "cosine-similarity-doesn-t-know-what-time-it-is", "title": "Cosine Similarity Doesn't Know What Time It Is", "summary": "A developer building a co-pilot for pediatric therapists found that a plain vector index ranked a four-month-old clinical note above one from 90 minutes earlier because embeddings encode semantic similarity, not recency or validity. The fix re-ranks retrieved hits by blending cosine similarity with an exponential-decay recency term, assigning each note type its own half-life — 6 hours for acute events, 72 hours for sleep logs, and 90 days for durable protocols — with an alpha of 0.6 weighting similarity against freshness.", "body_md": "Vector search answers \"what is most *similar* to my query?\" Many real systems also need \"what is most *true right now*?\" Those are different questions.\n\nWhile building a co-pilot for pediatric therapists, my plain vector index kept ranking a four-month-old note (\"tolerated musical games well\") above a note from 90 minutes earlier about an acute auditory crisis. The old note was semantically closer to the query, so it won. In a clinical setting, that's the wrong answer.\n\nEmbeddings encode meaning, not validity. A note's timestamp isn't part of the vector, so a stale fact and a fresh one compete only on wording.\n\nRe-rank the retrieved hits with an exponential-decay recency term:\n\n``` python\ndef rescore(hits, now, half_life_hours=48, alpha=0.6):\n    ranked = []\n    for h in hits:\n        age_h = (now - h[\"ts\"]) / 3600  # ts = epoch seconds\n        recency = 0.5 ** (age_h / half_life_hours)\n        score = alpha * h[\"sim\"] + (1 - alpha) * recency\n        ranked.append({**h, \"score\": score})\n    return sorted(ranked, key=lambda x: x[\"score\"], reverse=True)\n```\n\nA note loses half its recency weight every `half_life_hours`. `alpha` controls how much similarity matters versus freshness.\n\nA crisis note goes stale in hours. A note like \"weighted lap pad resolves agitation in about 4 minutes\" is a durable protocol and shouldn't fade after a week. So give each note type its own half-life:\n\n```\nHALF_LIFE = {\n    \"acute_event\": 6,        # hours\n    \"sleep_log\": 72,\n    \"protocol\": 24 * 90,\n}\n\ndef rescore(hits, now, alpha=0.6, default_half_life=48):\n    ranked = []\n    for h in hits:\n        half_life = HALF_LIFE.get(h[\"type\"], default_half_life)\n        age_h = (now - h[\"ts\"]) / 3600\n        recency = 0.5 ** (age_h / half_life)\n        score = alpha * h[\"sim\"] + (1 - alpha) * recency\n        ranked.append({**h, \"score\": score})\n    return sorted(ranked, key=lambda x: x[\"score\"], reverse=True)\n```\n\n`alpha` and the half-lives by evaluating against real queries with known-good answers.`sim` and `recency` should both sit in roughly 0 to 1, or one term will silently dominate.\n*(The examples here are illustrative, not clinical guidance, and contain no real patient data.)*", "url": "https://wpnews.pro/news/cosine-similarity-doesn-t-know-what-time-it-is", "canonical_source": "https://dev.to/devansh564/cosine-similarity-doesnt-know-what-time-it-is-5fck", "published_at": "2026-09-29 15:34:35+00:00", "updated_at": "2026-09-29 15:46:47.082046+00:00", "lang": "en", "topics": ["ai-tools", "natural-language-processing", "machine-learning", "ai-products"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/cosine-similarity-doesn-t-know-what-time-it-is", "markdown": "https://wpnews.pro/news/cosine-similarity-doesn-t-know-what-time-it-is.md", "text": "https://wpnews.pro/news/cosine-similarity-doesn-t-know-what-time-it-is.txt", "jsonld": "https://wpnews.pro/news/cosine-similarity-doesn-t-know-what-time-it-is.jsonld"}}