Skip to content

Follow on to corpus stats cache - #60

Open
mason-sharp wants to merge 10 commits into
mainfrom
fix/issue-53-corpus-stats-follow-on
Open

Follow on to corpus stats cache#60
mason-sharp wants to merge 10 commits into
mainfrom
fix/issue-53-corpus-stats-follow-on

Conversation

@mason-sharp

Copy link
Copy Markdown
Member

Follow on to PR #59

Two bounds on how stale a cached reading may get.

Growth bound. The existing TTL bounds staleness in time, but the harm is proportional: a thousand chunks added to a million barely move the weights, while the same thousand added to two hundred change them several fold. pgedge_vectorizer.corpus_stats_cache_max_growth (percent, default 5) re-reads once uses of a cached entry reach that fraction of N, applied alongside the TTL so whichever is reached first wins. It scales itself — a million-row corpus tolerates fifty thousand uses, a two-hundred-row one ten, and re-reading a small table is cheap because it is small. Counting uses is a proxy rather than a measurement, since the worker does not insert chunk rows itself; that is stated in the code.

This matters more than a query-side cache would suggest. The worker runs the same lookup per queue item and writes the result into sparse_embedding, so a stale corpus size is persisted rather than recomputed on the next call.

Clamp. doc_freq is read fresh while N may be cached, so df can outrun it. ln((N+1)/(df+0.5)) then goes negative, and only scores above zero are kept, so the term is dropped from the stored vector rather than merely underweighted. Taking max(N, df) changes nothing whenever N >= df, which is every case a fresh reading can produce.

Test plan

  • 19/19 regression and TAP 001/002/003/005 pass. 004 fails identically on main and is unrelated.
  • Test 24 covers the growth bound in both directions; Test 25 covers the clamp — unclamped, the vector comes back empty.
  • Both commits build and pass independently.
  • 006_corpus_stats_growth.pl drives the real worker and checks the stored
    sparse_embedding. With the bound disabled, assertions 1–4 still pass and
    5–6 fail, so it discriminates on the feature rather than on setup.

A wall-clock TTL bounds staleness in time, but the harm is proportional:
a thousand chunks added to a million barely move the weights, the same
thousand added to two hundred change them several fold. The figures are
also written into stored vectors by the worker rather than only used for
a query, so a stale reading does not simply correct itself.

corpus_stats_cache_max_growth re-reads once uses of a cached entry reach
that percentage of N, alongside the existing TTL, whichever comes first.
It scales itself: a million-row corpus tolerates fifty thousand uses, a
two-hundred-row one ten, and re-reading a small table is cheap because
it is small. Counting uses is a proxy rather than a measurement, since
the worker does not insert chunk rows itself and a search does not grow
the corpus at all; the comment says so.

Test 24 covers both directions: a use inside the budget is served from
cache, the use that spends it re-reads and matches an uncached read, and
with the bound off the same sequence stays stale.
A term cannot appear in more documents than exist, but total_docs may be
a cached reading while doc_freq is read fresh, so doc_freq can outrun it.
Left alone ln((N+1)/(df+0.5)) goes negative, and bm25_compute_sparse_str
keeps only scores above zero, so the term is dropped from the vector
rather than merely underweighted.

Measured with doc_freq deliberately set past the corpus: unclamped the
query vector came back empty, clamped it carries a near-zero weight,
which is what a term appearing in every document should score.

Taking the larger of the two changes nothing whenever total_docs is at
least doc_freq, which is every case a fresh reading can produce, so the
expression stays bit-identical to the SQL it replaced for all previously
reachable inputs.
The regression tests exercise it through bm25_query_vector, whose result
is discarded at the end of the statement. The worker runs the same lookup
per queue item and writes the outcome into sparse_embedding, which is the
case the bound exists for.

Every chunk holds the same term and token_count, so avg_doc_len cannot
vary and doc_freq is fixed by hand; the corpus size is the only input
that differs, and the weights are checked against the ratio it predicts
rather than merely for being different. With the bound disabled the
first four assertions still pass and the last two fail.
@mason-sharp
mason-sharp requested a review from dpage August 12, 2026 01:57
@codacy-production

codacy-production Bot commented Aug 12, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 10 high

Results:
10 new issues

Category Results
Compatibility 10 high (10 false positives)

View in Codacy

🟢 Metrics 13 complexity · 0 duplication

Metric Results
Complexity 13
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds the pgedge_vectorizer.corpus_stats_cache_max_uses_pct GUC with a default of 5% and a range of 0–100. Corpus-statistics cache entries now track usage and refresh when the TTL or usage threshold is reached. Fresh reads reset usage. BM25 IDF processing rechecks corpus statistics when document frequency exceeds the cached corpus size. SQL and PostgreSQL regression tests cover cache growth and IDF behavior.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title identifies this as a follow-up to the corpus statistics cache changes and is related to the main changeset.
Description check ✅ Passed The description clearly explains the cache-use bound, corpus-size clamping, documentation updates, and regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-53-corpus-stats-follow-on

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
docs/index.md (1)

177-177: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider stating that the bound counts cache uses, not measured growth.

The implementation in src/bm25.c counts each use of a cached entry as one assumed added chunk. A read-only search workload therefore also spends the budget and triggers a re-read, even when the corpus does not grow. The current wording implies the extension measures corpus growth. One added clause would set the expectation for users who tune this value against query volume.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/index.md` at line 177, Update the description of
pgedge_vectorizer.corpus_stats_cache_max_growth to clarify that the percentage
bound is consumed per cached-entry use/read, not by measured corpus growth, so
read-only searches can exhaust the budget and trigger a re-read.
test/sql/hybrid_test.sql (1)

595-605: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: make the phase-2 starting state explicit.

Line 588 sets corpus_stats_cache_ttl = 0, so the gtruth call takes the uncached path in bm25_corpus_stats and writes no cache entry. The entry left by g3 therefore survives with uses_since_read = 0. When line 596 restores the TTL, h1 is served from that surviving entry rather than from a fresh read. The assertion h1 = h3 still holds, because the growth bound is off and all three calls hit the same entry.

The comment on line 595 reads as though h1 establishes a fresh baseline. Add a SET pgedge_vectorizer.corpus_stats_cache_max_growth = 0; before an explicit warm-up call, or note the carried-over entry, so the intent survives future edits to the ordering.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/sql/hybrid_test.sql` around lines 595 - 605, Make the phase-2 baseline
explicit around the growth_chunks sequence by adding the cache growth-bound
setting before an explicit warm-up call, or documenting that h1 uses the cache
entry carried over from g3. Preserve the assertion that h1 and h3 remain equal
with growth disabled, and anchor the change to the bm25_query_vector calls
producing h1, h2, and h3.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/t/006_corpus_stats_growth.pl`:
- Around line 115-137: Update the queue setup in the test around await_sparse so
item 2 is inserted and awaited before inserting item 3. Keep the existing
assertions and metadata unchanged, ensuring the worker claims item 2 before item
3 deterministically.

---

Nitpick comments:
In `@docs/index.md`:
- Line 177: Update the description of
pgedge_vectorizer.corpus_stats_cache_max_growth to clarify that the percentage
bound is consumed per cached-entry use/read, not by measured corpus growth, so
read-only searches can exhaust the budget and trigger a re-read.

In `@test/sql/hybrid_test.sql`:
- Around line 595-605: Make the phase-2 baseline explicit around the
growth_chunks sequence by adding the cache growth-bound setting before an
explicit warm-up call, or documenting that h1 uses the cache entry carried over
from g3. Preserve the assertion that h1 and h3 remain equal with growth
disabled, and anchor the change to the bm25_query_vector calls producing h1, h2,
and h3.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 85727514-7d1c-4d1b-828d-9171589fa673

📥 Commits

Reviewing files that changed from the base of the PR and between abc775e and 461d0e4.

⛔ Files ignored due to path filters (1)
  • test/expected/hybrid_test.out is excluded by !**/*.out
📒 Files selected for processing (6)
  • docs/index.md
  • src/bm25.c
  • src/guc.c
  • src/pgedge_vectorizer.h
  • test/sql/hybrid_test.sql
  • test/t/006_corpus_stats_growth.pl

Comment thread test/t/006_corpus_stats_growth.pl
Inserted together they shared a created_at, and the claim orders by that
column alone with no tie-breaker, so which one the worker took first was
unspecified. Taken in the other order the cached use and the re-read swap
places and both assertions invert, failing against working code.

Raised by CodeRabbit on #62.

@dpage dpage left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Both mechanisms are individually sound and the C is careful. I checked the parts that usually go wrong and found them right: the budget arithmetic cannot overflow (int64 * <= 100, and N would have to exceed 9e16), there is no division by zero (the divisor is the literal 100, bm25_corpus_stats_uncached() clamps N to at least 1, and Max(budget, 1) covers small N), the threshold is recomputed from entry->total_docs on every call so it tracks the refreshed reading, the counter resets on re-read, and the cache remains a per-backend HTAB in TopMemoryContext with no shared state to race over. PGC_USERSET with no unit flag matches the sibling corpus_stats_cache_ttl and is right for a percentage. The two bounds compose coherently in both directions: max_growth = 0 returns false and leaves the TTL alone, and ttl = 0 short-circuits to the uncached path before the growth bound is consulted, so max_growth simply has no effect there, consistent with what the docs say about ttl = 0. Regression tests 24 and 25 both genuinely discriminate; I verified that unclamped, test 25's term scores -6.12, is dropped by the score > 0.0 filter, and the vector comes back as {}/65536.

My concerns are not with the implementation but with its evidence and its labelling. There is one blocker in the new TAP test, where a fixture schema mismatch means two assertions pass for the wrong reason, and the GUC's name and documentation promise a measurement that the code deliberately does not make.


🔴 Must Fix

1. TAP 006's fixture omits updated_at, so every bm25_update_idf_stats() call fails silently and two assertions pass for the wrong reason

test/t/006_corpus_stats_growth.pl:65-67 creates chunks_idf_stats (term, doc_freq), but the extension creates that table with a third column, updated_at (sql/pgedge_vectorizer--1.1.sql:316-321), and upsert_term_idf() emits ... ON CONFLICT (term) DO UPDATE SET doc_freq = ..., updated_at = now() (src/bm25.c:870-876). Against this fixture that statement fails at parse-analysis with column "updated_at" of relation "chunks_idf_stats" does not exist, and bm25_update_idf_stats() swallows the failure wholesale in its own PG_CATCH and reduces it to a WARNING (src/bm25.c:913-924). I confirmed this on a scratch cluster: the statement errors and doc_freq stays at 5.

The consequence is that doc_freq is frozen for the whole test, which is exactly the premise stated at line 18, that "the corpus size is then the only input that differs". In a real deployment it is not: the worker increments doc_freq for every first-processed chunk (src/worker.c:1769-1772) after bm25_load_idf_stats() has already read it, so items 1, 2 and 3 see df of 5, 6 and 7 respectively. Working the arithmetic through (tf = 1 and token_count = 10 = avg_doc_len, so the tf factor cancels and the stored weight is exactly the IDF), with a schema-correct fixture:

  • line 140, is($cached, 't'), fails: w1 = 1.33977 at df 5 and w2 = 1.17272 at df 6, both at the cached N = 20.
  • line 165, is($ratio, 't'), fails: the real ratio is 2.5252 (N 220 / df 7 over N 20 / df 5) against the 2.7567 the test hardcodes from ln(221/5.5)/ln(21/5.5), which is the df-frozen figure. The tolerance is 0.01 and the error is 0.23.

So the "with the bound disabled, assertions 1-4 still pass and 5-6 fail" check in the description is true only in a configuration that cannot occur in production, and as written the test is a green light sitting on top of a per-item error.

Suggested fix: add updated_at TIMESTAMPTZ DEFAULT now() so the fixture matches what the extension builds, then remove the dependence on df being constant by giving each chunk its own single term (alpha, beta, gamma), each seeded at doc_freq = 5. The three terms then have independent df, only N differs between them, the existing weight-extraction SQL still works, and the hardcoded ratio stays correct. Failing that, drop the cross-item equality and assert each weight against the (N, df) pair actually in force at that point.


🟡 Should Fix

2. The GUC name and documentation claim to bound growth; the code bounds uses of the cache entry

src/guc.c:334-341 and docs/index.md:177 both describe "Percent the corpus may grow before cached figures are re-read", but corpus_stats_stale_by_growth() (src/bm25.c:267-279) never looks at the corpus at all: it compares uses_since_read against N * pct / 100. The code comment at lines 259-265 is admirably honest that this is a proxy, and even notes that a search does not grow the corpus, yet none of that honesty reaches the operator who has to tune the setting. A read-only search workload against a completely static corpus will re-read every N/20 calls, which is close to the opposite of what the parameter appears to promise, and this is a PGC_USERSET knob that is meant to be tuned.

Suggested fix: either carry the proxy into the GUC long description and the docs row (something along the lines of "counted as uses of the cached figures, taken as a proxy for chunks added, since the figures are read once per queue item and once per search"), or rename to something that does not overclaim, for instance corpus_stats_cache_max_uses_pct. Renaming now is cheap; after a release it is not.

3. doc_freq > N is unambiguous evidence that the cached N is stale, and the clamp hides that evidence rather than acting on it

src/bm25.c:606 substitutes Max(total_docs, doc_freq) for the affected term only. The maths is right, and I agree it prevents the term being dropped by the score > 0.0 filter at src/bm25.c:791. But the clamp leaves the stale entry in place, so every subsequent document processed within the remaining TTL keeps getting the substituted weight of log(1 + 0.5/(df+0.5)), which is effectively zero, and those weights are persisted into sparse_embedding rather than recomputed on the next call. That persistence is this PR's own stated reason for caring. Since the cache is per-backend and in-process, the fix is a couple of lines: on observing doc_freq > total_docs, force a re-read or stamp the entry stale and recompute, which repairs this document and every one after it.

A smaller point in the same area: the clamp is applied per row, so in a pathological case two terms of the same document are weighted against different effective corpus sizes, which BM25 does not contemplate. Taking the maximum df across the result set and clamping once would at least keep the model internally consistent.

4. No changelog entry, and the README GUC table is now two parameters behind

docs/changelog.md keeps a detailed [Unreleased] section, and the most recent comparable addition, pgedge_vectorizer.worker_service_quantum, got an ### Added entry (docs/changelog.md:46-51), as did a GUC context change under ### Security. Neither corpus_stats_cache_ttl (missed by #59) nor corpus_stats_cache_max_growth appears anywhere in the changelog. Separately, the "Hybrid Search GUC Parameters" table at README.md:211-215 lists enable_hybrid, bm25_k1 and bm25_b, and is now missing both cache parameters. docs/index.md was updated correctly.

Suggested fix: add an ### Added entry covering both parameters, since this PR is the natural place to catch up #59's omission, and add both rows to the README table.


🟢 Suggestions

5. Remove the dead ignored CTE column in test/t/006_corpus_stats_growth.pl:156

(sparse_embedding::text)::jsonb on a sparsevec is not valid JSON; I confirmed that ('{1234:1.34}/65536')::jsonb raises "invalid input syntax for type json". The query runs today only because w is referenced once, gets inlined, and the planner then prunes the unreferenced target entry. Anything that causes the CTE to be materialised instead, whether a second reference, a MATERIALIZED keyword or a planner change, turns a green test into a hard safe_psql failure. The column contributes nothing to the assertion, so it may as well go.

6. The first use after a refresh is free, so the effective allowance is budget + 1

src/bm25.c:398 increments only on the hit path and src/bm25.c:416 resets to zero on refresh, but the refresh itself is a use, so at N = 20 with 5% the entry serves two lookups from the read rather than one. Harmless, and test 24 is written around it, but the documented semantics would read more truthfully with the counter starting at 1 after a read.

7. Two small test tidies

test/sql/hybrid_test.sql:602 computes h2 and never asserts on it, and since the growth bound is disabled in that block it is not advancing anything meaningful either. Test 25 at test/sql/hybrid_test.sql:631 asserts only that the vector is non-empty, whilst its own comment claims "the weight below is near zero, which is what a term appearing in every document should score"; bounding that weight above by something small would test the claim rather than restate it.


ℹ️ Notes

8. The clamp's rescue has a ceiling, imposed by %f

build_sparsevec_string() formats scores with %f (src/bm25.c:741), so anything below 5e-7 prints as 0.000000, and pgvector strips zero elements on input (I confirmed '{1234:0.000000}/65536'::sparsevec equals '{}/65536'). The clamped weight is roughly 0.5/df times the tf factor, so past a df of a couple of million the term is dropped again regardless of the clamp. That is an unlikely corner, since the growth bound would normally have fired long before, and the six-decimal truncation is pre-existing rather than introduced here, but it is worth knowing the clamp is not an unconditional guarantee.

9. Read-heavy cost is self-limiting

The bound does mean a static corpus under query traffic re-reads periodically for no benefit, but because the budget scales with N the amortised cost falls as the scan gets more expensive, roughly 33ms per 15,000 queries at 300k chunks, and a 200-row scan is free. I do not think this needs changing. It is worth stating only because "the cache is now invalidated by reads as well as writes" is a behaviour change from #59 that an operator might not expect.

10. CodeRabbit's thread was legitimate and correctly resolved

It objected that items 2 and 3, inserted in one VALUES, share a created_at whilst the claim query orders by created_at alone with no unique tie-breaker (src/worker.c:1406), leaving their order unspecified. I confirmed the ordering, and ce70b6e fixes it properly by inserting and awaiting each item separately rather than papering over it with a sleep. The absence of a tie-breaker in the claim itself remains a latent fragility, but it is pre-existing and out of scope here.


Checklist

  • Correctness — the growth bound and the clamp are both arithmetically sound; no overflow, divide-by-zero or race
  • Security — nothing introduced; no new user input reaches SQL, and the GUC context matches its sibling
  • Error handling — failures are handled, though finding 1 turns an inherited swallow-and-warn path into a hidden test failure
  • Tests — the regression tests discriminate, but TAP 006 passes for the wrong reason (finding 1)
  • Documentation — docs/index.md updated, but the description overclaims (finding 2) and the changelog and README are missing entries (finding 4)

Back to you @mason-sharp. Finding 1 is the one that has to change before this goes in; finding 2 is a judgement call and the name is yours to pick, but I would rather we settled it before the parameter ships. The rest is small.

The fixture omitted updated_at, which upsert_term_idf() writes, so every
doc_freq update failed and bm25_update_idf_stats() reduced it to a
WARNING: the test passed while logging three per-item errors, and it did
so because doc_freq was frozen at its seeded value.

With the column present doc_freq moves as it does in production, which
would have broken two assertions. Each chunk now carries its own term so
the terms have independent doc_freq, restoring the stated premise that
the corpus size is the only input that differs. The weights are compared
directly, since vectors over different terms occupy different dimensions,
and a new assertion on doc_freq catches the fixture drifting again.

Raised by dpage on #60.
The parameter was called max_growth and documented as bounding how far
the corpus may grow, but nothing measures the corpus: the code compares
uses of the cached entry against a percentage of N. A search spends the
budget without adding anything, so a static corpus under query traffic
re-reads periodically, which is close to the opposite of what the name
promised.

Renamed to corpus_stats_cache_max_uses_pct, with the GUC description and
the docs row now saying that uses stand in for chunks added, that this is
a proxy rather than a measurement, and that reads spend it too. The
internal helper follows the same naming.

Raised by dpage on #60.
doc_freq above N proves the cached corpus size is stale, since doc_freq
is read fresh on every call. Clamping the weight left the stale entry in
place, so every later chunk in the same window was scored against the
same wrong corpus and had that persisted into sparse_embedding. Drop the
entry and read again, which repairs this document and the ones after it.

If the fresh reading is still below the largest doc_freq the statistics
are inconsistent rather than stale, and re-reading cannot help, so the
entry adopts that value: measured, twenty further calls then cause no
additional scans, where an unguarded recheck would rescan on every one.

The maximum doc_freq is taken across the whole result set before any
weight is computed, so one corpus size covers the whole document rather
than each term being scored against its own.

Raised by dpage on #60.
Neither cache parameter appeared in either: corpus_stats_cache_ttl was
missed when the cache landed, and max_uses_pct followed it. The changelog
entry covers both, since this is the natural place to catch up the first.

Raised by dpage on #60.
h2 was computed and never used, so the block checked only that the ends
matched; it now checks all three, which is what "no number of uses moves
the figures" actually means. Test 25 claimed in a comment that the
clamped weight is near zero and asserted only that the vector was
non-empty; it now bounds the weight.

The counter's semantics are documented rather than changed: counting the
reading call would leave no cached use at all wherever the budget works
out to one, which is every corpus below 20 rows at the default 5%.

Raised by dpage on #60.
The re-read had no automated test: it was verified by hand, which is the
same gap that let the fixture mismatch through earlier on this branch.
Test 26 checks that a doc_freq above the cached size produces the weights
of a fresh read rather than a clamped one, and that the entry stays
repaired once doc_freq drops back below it. Both assertions return false
against a clamp-only build, so they discriminate on the re-read itself.

Also note why the re-read is not skipped when the reading was already
fresh: it costs one scan, only for inconsistent statistics, and only
until max_df is adopted, after which nothing contradicts the entry.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
docs/changelog.md (1)

65-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider documenting the third re-read trigger.

The entry states that two settings bound staleness and that whichever is reached first triggers a re-read. src/bm25.c adds a third trigger in this PR: a fresh doc_freq above the cached corpus size drops the entry and reads the figures again, even when both bounds are still satisfied. An operator who sees an unexpected scan cannot map it to either documented bound.

📝 Proposed addition
       Each use of a cached reading counts as one chunk that may have been added
       since — a proxy rather than a measurement, so searches spend the budget
       too. Setting it to 0 bounds by time alone.
+
+  A cached reading is also dropped and read again when a term's document
+  frequency exceeds the cached corpus size, since a term cannot appear in more
+  documents than exist. If the fresh reading still contradicts the data, the
+  cached corpus size adopts the largest observed document frequency so that
+  weights stay non-negative.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/changelog.md` around lines 65 - 76, Update the changelog entry to
document the third corpus-stats cache re-read trigger: when a fresh doc_freq
exceeds the cached corpus size, the cache entry is invalidated and the figures
are reread even if both configured bounds remain satisfied. Clarify that this
can cause an unexpected scan independently of corpus_stats_cache_ttl and
corpus_stats_cache_max_uses_pct.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@docs/changelog.md`:
- Around line 65-76: Update the changelog entry to document the third
corpus-stats cache re-read trigger: when a fresh doc_freq exceeds the cached
corpus size, the cache entry is invalidated and the figures are reread even if
both configured bounds remain satisfied. Clarify that this can cause an
unexpected scan independently of corpus_stats_cache_ttl and
corpus_stats_cache_max_uses_pct.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3078acad-59c1-4177-ae5a-38d7e3deb6d6

📥 Commits

Reviewing files that changed from the base of the PR and between 461d0e4 and 05865b1.

⛔ Files ignored due to path filters (1)
  • test/expected/hybrid_test.out is excluded by !**/*.out
📒 Files selected for processing (8)
  • README.md
  • docs/changelog.md
  • docs/index.md
  • src/bm25.c
  • src/guc.c
  • src/pgedge_vectorizer.h
  • test/sql/hybrid_test.sql
  • test/t/006_corpus_stats_growth.pl

@mason-sharp
mason-sharp requested a review from dpage August 12, 2026 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants