fix(query): give retrieval a total order — results were nondeterministic - #180
Merged
Merged
Conversation
Running the same benchmark twice against an unchanged corpus produced different scores (Recall@3 moved 2.4pp with nothing changed). Cause: every retrieval path ordered by relevance alone, and relevance ties constantly — measured on the real corpus, 20 retrieved rows shared just 6 distinct ts_rank_cd values, with ties at the very top. `ORDER BY rank DESC` with no second key lets Postgres return tied rows in any order, and it does. This is a correctness problem before it is a measurement one. The same query could return different memories on consecutive calls; a caller paging with LIMIT could see a row twice and miss another entirely; and no improvement could be distinguished from noise, which would have made the upcoming full benchmark run unreproducible. Adds `id DESC` as the tie-break on all six ordering sites: warm-tier keyword, code, trigram and semantic, plus both shared-pool arms. Newest -first is the useful semantic — when relevance cannot separate two memories, prefer the more recent — and it is pinned by a test so a future edit cannot silently invert it. After: three consecutive evaluations returned byte-identical retrieved- id sequences, not merely equal aggregate scores. Note this changes measured numbers slightly, because the previous ones were partly luck: R@3 on the stratified 42-question sample had been oscillating between 81.0% and 83.3%, and settles at 81.0%. tests/retrieval-determinism.test.ts covers repeat-query stability, the newest-first tie order, and page/limit consistency. The fixture needs rows that tie on rank while differing in their first 100 characters, since query() collapses same-prefix results; ts_rank_cd applies no length normalisation, so an unmatched leading marker leaves rank untouched — verified before relying on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xCqQo49d3CEbn6oEvb3Ru
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while establishing a noise floor before the full benchmark run.
The same query returned different results
Two identical evaluations against an unchanged corpus produced different scores — Recall@3 moved 2.4pp with nothing changed.
Cause: every retrieval path ordered by relevance alone, and relevance ties constantly. Measured on the real corpus: 20 retrieved rows shared only 6 distinct
ts_rank_cdvalues, with ties at the very top.ORDER BY rank DESCwithout a second key lets Postgres return tied rows in any order, and it does.Why this matters beyond the benchmark
Fix
id DESCas tie-break on all six ordering sites: warm-tier keyword, code, trigram and semantic, plus both shared-pool arms. Newest-first is the useful semantic — when relevance cannot separate two memories, prefer the more recent — and it's pinned by a test so a future edit can't silently invert it.After: three consecutive evaluations returned byte-identical retrieved-id sequences, not merely equal aggregate scores.
Numbers move slightly, because the old ones were partly luck
R@3 on the stratified 42-question sample had been oscillating between 81.0% and 83.3%; it settles at 81.0%. Worth knowing that the higher figure I reported in #179 was the lucky end of that range.
Tests
tests/retrieval-determinism.test.ts— repeat-query stability, newest-first tie order, and page/limit consistency.The fixture was interesting: it needs rows that tie on rank while differing in their first 100 characters, because
query()deliberately collapses same-prefix results ("prevents similar memories filling all top-k slots"). My first attempt used identical rows and correctly failed — 25 rows deduplicated to 1.ts_rank_cdapplies no length normalisation, so an unmatched leading marker leaves rank untouched; verified (6 rows → 1 distinct rank) before relying on it.🤖 Generated with Claude Code
https://claude.ai/code/session_011xCqQo49d3CEbn6oEvb3Ru