Repeating a search after a committed update returns the updated text with a stale relevance score. I reproduced this in plain SQL on the same connection, without Rails or an adapter.
Environment: Lead 1.0.3 at bd95c7e51b6afce81396790852ee2f2c169570ad, PostgreSQL 18.6, Linux arm64.
Reproduction
Run this through psql with its default autocommit behavior, executing each statement separately. Do not wrap the reproduction in BEGIN or send the entire script as one implicit transaction.
CREATE EXTENSION IF NOT EXISTS tin;
CREATE TEMP TABLE lead_cache_probe (
id integer PRIMARY KEY,
body text
);
CREATE INDEX ON lead_cache_probe USING tin (body);
INSERT INTO lead_cache_probe VALUES (1, 'Blue sofa');
SELECT body, tin.full_score(ctid) AS score
FROM lead_cache_probe
WHERE body ==> 'sofa';
UPDATE lead_cache_probe SET body = 'Red sofa';
SELECT body, tin.full_score(ctid) AS score
FROM lead_cache_probe
WHERE body ==> 'sofa';
Observed:
| Search |
Returned text |
Score |
| Before update |
Blue sofa |
0.2876821 |
| After update |
Red sofa |
0 |
Both scores should be 0.2876821: the document length and frequency of the searched term are unchanged. Matching and row visibility are correct in this reproduction, but relevance scoring is stale. Sorting by score can therefore return the wrong ranking.
Suspected cause
The score cache key includes GetTopTransactionIdIfAny() and GetCurrentCommandId(false). Separate read-only autocommit transactions can have the same values for both, so the next search reuses a corpus from an earlier snapshot.
The lookup by document text then looks for Red sofa in a cached map containing Blue sofa and defaults to 0.0.
I also reproduced a related case with two connections: a reader stays in a READ COMMITTED transaction with an explicitly assigned transaction ID; another connection commits a text update; the reader repeats the query and sees the new text but receives a zero score. A savepoint around each search does not prevent it. Assigning a transaction ID alone therefore does not address freshness across statement snapshots.
Local workaround
For a local integration proof, I invalidated the corpus at the start of each scoring call:
SCORE_CACHE.with_borrow_mut(|slot| {
*slot = None;
// Existing cache construction and scoring follow.
The same SQL then returns 0.2876821 before and after the update. Regression tests for both successive autocommit searches and an external commit during a READ COMMITTED transaction pass with this change.
This intentionally trades performance for correctness and is not a proposal to discard caching permanently. The cache needs a lifetime or invalidation rule that respects the statement's visible snapshot. I have not tested production TIN for this behavior.
Repeating a search after a committed update returns the updated text with a stale relevance score. I reproduced this in plain SQL on the same connection, without Rails or an adapter.
Environment: Lead 1.0.3 at
bd95c7e51b6afce81396790852ee2f2c169570ad, PostgreSQL 18.6, Linux arm64.Reproduction
Run this through
psqlwith its default autocommit behavior, executing each statement separately. Do not wrap the reproduction inBEGINor send the entire script as one implicit transaction.Observed:
Blue sofa0.2876821Red sofa0Both scores should be
0.2876821: the document length and frequency of the searched term are unchanged. Matching and row visibility are correct in this reproduction, but relevance scoring is stale. Sorting by score can therefore return the wrong ranking.Suspected cause
The score cache key includes
GetTopTransactionIdIfAny()andGetCurrentCommandId(false). Separate read-only autocommit transactions can have the same values for both, so the next search reuses a corpus from an earlier snapshot.The lookup by document text then looks for
Red sofain a cached map containingBlue sofaand defaults to0.0.I also reproduced a related case with two connections: a reader stays in a
READ COMMITTEDtransaction with an explicitly assigned transaction ID; another connection commits a text update; the reader repeats the query and sees the new text but receives a zero score. A savepoint around each search does not prevent it. Assigning a transaction ID alone therefore does not address freshness across statement snapshots.Local workaround
For a local integration proof, I invalidated the corpus at the start of each scoring call:
The same SQL then returns
0.2876821before and after the update. Regression tests for both successive autocommit searches and an external commit during aREAD COMMITTEDtransaction pass with this change.This intentionally trades performance for correctness and is not a proposal to discard caching permanently. The cache needs a lifetime or invalidation rule that respects the statement's visible snapshot. I have not tested production TIN for this behavior.