Skip to content

Compute token_count the same way everywhere - #72

Merged
dpage merged 2 commits into
mainfrom
token-count-consistency
Oct 1, 2026
Merged

dpage merged 2 commits into
mainfrom
token-count-consistency

Conversation

@dpage

@dpage dpage commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

The C chunking code sizes chunks with a four-characters-per-token estimate that rounds up. The three plpgsql paths that actually write the token_count column open-coded the same estimate as length(chunk_text) / 4, which truncates, so the two disagreed by a token on most chunks and stored a zero for anything shorter than four characters.

That matters because token_count is not decorative: bm25.c uses AVG(token_count) as the average document length and worker.c reads the per-chunk value, both feeding the BM25 length normalisation, so hybrid search scored chunks written by the trigger slightly differently from chunks written by the C chunker. worker.c was already clamping the stored zeroes back up to one.

  • Expose the existing C counter as pgedge_vectorizer.count_tokens(text), and call it from enable_vectorization(), vectorization_trigger() and recreate_chunks(), so there is one definition of the rule rather than two.
  • Declared STABLE, not IMMUTABLE. The estimate is defined in terms of pgedge_vectorizer.model, which does not matter whilst the counter ignores the model, but would quietly invalidate an expression index or a cached plan the moment it stops doing so.
  • Existing chunk tables are left as they are; the values are an approximation either way, and rewriting every chunk table to correct one token is not worth it on upgrade. Where a particular chunk table does need bringing into line, recreate_chunks() on it rewrites every row through the new path.
  • First change for 1.2, so sql/pgedge_vectorizer--1.1--1.2.sql carries the upgrade and the 1.1 scripts are untouched.

New count_tokens regression test covers the estimate itself (rounding, empty, NULL, multi-byte, the declared volatility) and, more to the point, asserts that nothing in a chunk table disagrees with count_tokens(content) after each of the three write paths.

Test run: 21 pg_regress tests and 69 TAP tests pass against PostgreSQL 18.4.

The count_tokens() part of this originates in #22, from @syedkazim110, which this replaces. refresh_token_counts() from that PR is not carried over: nothing writes chunk rows outside the paths above, so there is nothing left for it to back-fill.

@codacy-production

codacy-production Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity · 0 duplication

Metric Results
Complexity 0
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 Sep 9, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: pgEdge/pgedge-vectorizer/.coderabbit.yaml

Review profile: CHILL

Plan: Essentials

Run ID: 4dcbef86-4778-49e2-ac00-7f1707ab696b

📥 Commits

Reviewing files that changed from the base of the PR and between f52d4cb and 076ebbf.

📒 Files selected for processing (2)
  • docs/changelog.md
  • sql/pgedge_vectorizer--1.1--1.2.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/changelog.md

Included review availability: 2 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour.


📝 Walkthrough

Walkthrough

The extension version changes from 1.1 to 1.2. The release adds the count_tokens SQL function and uses it for consistent chunk token counts during backfill, triggers, and chunk reconstruction. The 1.2 SQL definition adds vectorization setup and disablement, trigger refresh and cleanup, queue management, chunk rebuilding, configuration reporting, and hybrid search. Tests cover token-counting behavior and integration paths. Documentation and changelog entries describe the new function and behavior.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 076eb

This release adds a shared count_tokens function so that initial backfill, triggers, and chunk rebuilds all store the same rounded-up token estimate. The upgrade script relies only on objects that already exist in version 1.1. Its redefined functions match a fresh 1.2 install, and users can realign existing rows by running recreate_chunks(). No merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. (2 skipped: 2… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main change: consistent token_count computation across all chunk-writing paths.
Description check ✅ Passed The description directly explains the token_count inconsistency, the implementation, upgrade behavior, regression tests, and reported test results.
Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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

Comment thread sql/pgedge_vectorizer--1.1--1.2.sql Outdated
The chunking code in C has always sized chunks with a four-characters-per-token
estimate that rounds up, whilst the three plpgsql paths that actually write the
token_count column open-coded the same estimate as length(chunk_text) / 4, which
truncates. The two therefore disagreed by a token on most chunks, and on
anything shorter than four characters the plpgsql paths stored a zero that the
BM25 scoring path in worker.c then had to clamp back up to one. Since
token_count feeds the BM25 document-length normalisation, by way of
AVG(token_count) in bm25.c and the per-chunk value in worker.c, hybrid search
scored chunks written by the trigger slightly differently from chunks written by
the C chunker.

Expose the existing C counter as pgedge_vectorizer.count_tokens(text) and call
it from enable_vectorization(), vectorization_trigger() and recreate_chunks(),
so that there is one definition of the rule rather than two. It is declared
STABLE rather than IMMUTABLE deliberately: the estimate is defined in terms of
pgedge_vectorizer.model, which does not matter whilst the counter ignores the
model, but would quietly invalidate an expression index or a cached plan the
moment it stops doing so.

Existing chunk tables are left alone. The stored values are an approximation
either way, and rewriting every chunk table to correct a single token is not a
trade worth making on upgrade.

This is the first change for 1.2, so the extension version moves on and
sql/pgedge_vectorizer--1.1--1.2.sql carries the upgrade; the 1.1 scripts are
untouched.
The upgrade leaves existing token_count values alone, but recreate_chunks()
rewrites a chunk table through the new path, so anyone who does want the
stored counts consistent has a supported way to get there. Note that in both
the upgrade script and the changelog, since dropping refresh_token_counts()
otherwise reads as there being no way to fix existing rows.
@dpage
dpage force-pushed the token-count-consistency branch from f52d4cb to 076ebbf Compare September 23, 2026 09:04
@dpage

dpage commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

Codacy's two high-severity Compatibility findings on this PR are both SQLint failing to parse the \echo ... \quit guard on line 7 of the two new extension scripts. That guard is the standard PostgreSQL convention for stopping an extension script being sourced directly in psql, and every script under sql/ already carries it, so I have marked both as false positives in Codacy rather than changing the scripts.

@ibrarahmad ibrarahmad 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.

Checked the upgrade path as well as a fresh install: 1.1 -> 1.2 applies, count_tokens() lands STABLE STRICT, and after the upgrade nothing in a chunk table disagrees with count_tokens(content) through the trigger path. The short-string case that used to store 0 now stores 1.

All three call sites are present in both the 1.2 and the 1.1--1.2 script, and the only length()/4 left anywhere is the one in the header comment describing the old behaviour. setup, count_tokens, chunking, queue and vectorization pass.

Nothing blocking from me.

@dpage
dpage merged commit c99f30c into main Oct 1, 2026
9 checks passed
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