vchord_bm25 1.0.0: First Release Under PGSTY
vchord_bm25 1.0.0 is the first release maintained by PGSTY in pgsty/vchord_bm25 under the AGPL-3.0 license. It replaces the custom bm25vector data type and fixed bm25_catalog schema from 0.3.x with native PostgreSQL tsvector indexing, reworks the Block-WeakAnd (WAND) scan and storage engine for continuing scans and concurrent VACUUM, and enforces table privileges and Row-Level Security (RLS) across both indexed scans and scalar scoring.
- GitHub Release:
pgsty/vchord_bm25 1.0.0· Source Repository - Extension Catalog: pgext.cloud (
vchord_bm25) · Pigsty Extension Page (pigsty.io) - Binary Packages: RPM / DEB Repository Setup · SBOM & License Inventory
Changes in 1.0.0
| Area | Change |
|---|---|
Native tsvector API |
Indexes standard PostgreSQL tsvector columns with USING bm25 (... bm25_ops). to_bm25query(tsvector, regclass) builds queries, and <&> returns negative double precision BM25 scores (+0.0 for zero scores). |
| Relocatable schema | Extension objects install into the target schema (public by default) and support ALTER EXTENSION vchord_bm25 SET SCHEMA. The fixed bm25_catalog schema is no longer required. |
| Continuing WAND scans | bm25.limit and the index limit reloption (0..=65535, default 100, 0 selects 100) set the initial WAND top-K batch size rather than a hard result cap; scans continue through matching, zero-score, and NULL rows as needed. |
| Shared statistics cache | Prepared corpus statistics and growing-segment scores are cached per (index, query) within work_mem and shared between indexed scans and scalar <&> evaluations (PARALLEL RESTRICTED). NULL rows are excluded from document count, and WAND upper bounds adjust for avgdl drift. |
| Concurrent maintenance | Format 4 storage uses generation stamps, a growing segment, and one crash-resumable retired generation. VACUUM and bm25_maintain(regclass) merge growing tuples under ShareUpdateExclusiveLock using PostgreSQL’s GlobalVis horizon without taking AccessExclusiveLock or allocating an XID. ANALYZE is a no-op. |
| Standby replay | VACUUM tombstones only already-pruned heap TIDs, raising no false standby conflicts; only page recycling raises SQLSTATE 40001 (serialization_failure) if an older standby query is still reading a retired generation. |
| Corpus permissions & RLS | Both indexed scans and scalar <&> check current-role table SELECT (or column SELECT for simple single-column non-partial indexes) and reject queries when RLS policies apply (0A000), preventing corpus-statistic leaks. Scalar scoring locks the heap before the index. |
| Build & platform support | Upgraded to pgrx 0.19.3 for PostgreSQL 14–18 on Linux x86_64 and aarch64, with runtime SIMD dispatch (AVX512 / AVX2 / NEON) and responsive query cancellation outside page locks. |
Native tsvector indexing and relocatable schema
In vchord_bm25 1.0.0, documents are stored as standard PostgreSQL tsvector values with lexeme positions (1..=16383, up to 256 positions per lexeme). Each position entry counts as one occurrence of that lexeme when computing term frequency (tf) and document length (dl); lexemes without positions are ignored by BM25 scoring.
Because vchord_bm25 operates directly on tsvector, you can populate indexed columns with PostgreSQL’s built-in to_tsvector(regconfig, text) or with pg_tokenizer 1.0.0 (analyze_to_tsvector for string lexemes or tokenize_to_tsvector for model token IDs):
Key API changes in 1.0.0:
- Query constructor order:
to_bm25query(vector tsvector, index regclass)takes the querytsvectorfirst and the indexregclasssecond. Both arguments areSTRICT, so aNULLquery vector or index returnsNULL. - Negative BM25 scores:
<&>returns negativedouble precision(float8) scores so standard ascendingORDER BYscans return the best matches first. Zero scores are normalized to+0.0. - Relocatable extension objects: All types, functions, operators, and the
bm25access method live in the extension’s target schema (publicby default) and can be moved withALTER EXTENSION vchord_bm25 SET SCHEMA <schema>;.
See the BM25 indexing and BM25 querying guides for complete examples, including multilingual tokenization with pg_tokenizer.
Continuing WAND scans, batch sizing, and shared statistics
In 0.3.x, bm25_catalog.bm25_limit acted as a hard cap on how many candidates a BM25 index scan could yield. When a query combined ORDER BY ... <&> ... with a selective WHERE filter, post-filtering could discard most of the top candidates and return fewer rows than requested by LIMIT.
In 1.0.0, bm25.limit (and the per-index limit storage option) sets the initial WAND top-K batch size:
- Continuing execution: If the executor still needs rows after consuming the initial WAND batch (for example, because a
WHEREfilter rejected earlier rows or the query requests a largerLIMIT/OFFSET), the scan continues through the remaining matching documents, zero-score documents, andNULLrows. - Range and precedence: Both
bm25.limitand the indexlimitreloption accept0..=65535(default100); an explicit0selects the default batch size of100. Any non-built-in GUC setting (SET, session, user, database, orpostgresql.conf) overrides the index’s storage option, including explicit0orfalse(bm25.enable_scan/bm25.prefilter). Double-quote"bm25.limit"inSET/SHOW/RESETstatements becauselimitis a SQL keyword. - Shared statistics and growing-segment cache: Corpus statistics (
N,avgdl, and per-termdf) along with growing-segment scores are cached per(index, query)in backend memory bounded bywork_mem(with oldest-first eviction) and shared within an executor run between the index scan and scalar<&>evaluation inSELECTlists orWHEREclauses. Physical statistics include non-NULLrecords (including tombstones until compaction removes them, while result generation skips tombstones immediately). Because the cache lives in backend state,<&>is markedPARALLEL RESTRICTED. - Bounds under concurrent writes: When unsealed growing-segment rows shift the live average document length (
avgdl) relative to the sealed segment, WAND upper bounds adjust accordingly so candidates are not pruned early. - Planner and cancellation:
amcostestimateaccepts single-keyORDER BYqueries, recognizesto_bm25query(..., const regclass)in generic/prepared plans, and charges conservative index startup costs using PostgreSQL’s standard cost parameters. Search loops copy block jump metadata and release page locks before invoking filter predicates, checking for query cancellation between blocks so statement timeouts andpg_cancel_backendtake effect immediately.
Concurrent maintenance, VACUUM, and standby replay
Inserts and updates append through a tail hint into a growing segment and are searchable immediately without rewriting compressed posting lists on every transaction. vchord_bm25 1.0.0 introduces on-disk format 4 so segment merging and page recycling work alongside concurrent readers and writers, parallel VACUUM, crash recovery, and physical replication standbys:
- Automatic and manual merge thresholds: Each BM25 index exposes
maintenance_min_rows(default1000, range0..=2147483647) andmaintenance_scale_factor(default0.1, range0.0..=1.0).VACUUMmerges the growing segment and compacts deleted tuples when growing records reachmax(maintenance_min_rows, maintenance_scale_factor × sealed_records)or when tombstones reachmaintenance_scale_factor × total_records. CallingSELECT bm25_maintain('documents_doc_bm25_idx'::regclass);bypasses the size thresholds and returnstruewhen it publishes a merged segment (orfalsewhen there is no work or an older retired generation is still awaiting reclamation; requires table ownership or, on PostgreSQL 17+,MAINTAINprivilege). - No
AccessExclusiveLockor XID assignment: BothVACUUMandbm25_maintain(regclass)run under the heap’sShareUpdateExclusiveLockand use PostgreSQL’sGlobalVishorizon to retire superseded pages. Maintenance never takesAccessExclusiveLockon the index and never allocates a transaction ID, so it runs safely inside parallelVACUUM. RunningANALYZEalone returns immediately without rebuilding the index. - Crash-resumable retired generation: When a merge finishes while older snapshots may still be scanning the previous sealed generation, the index retains one retired generation until all older snapshots advance. If another merge is requested before that retired generation can be recycled, the merge is deferred cleanly without blocking readers or writers.
- Physical standby conflict resolution: Because
VACUUMonly tombstones heap TIDs that have already been pruned on the heap, replaying the earlier heap prune record on a physical standby already resolves snapshot conflicts (matching PostgreSQL’snbtreebehavior). Standby queries are only canceled with SQLSTATE40001(serialization_failure) if page recycling reclaims a retired index generation that a long-running standby query is still reading.
Corpus permissions and Row-Level Security (RLS)
BM25 scores depend on corpus-wide statistics (N, avgdl, and term document frequencies df) stored inside the index. Evaluating <&> without checking privileges on the underlying table would allow an unprivileged role—or a role restricted by Row-Level Security—to probe global term frequencies across rows they cannot read.
vchord_bm25 1.0.0 enforces privilege and RLS checks on both indexed scans and standalone scalar <&> evaluations:
- Table and column
SELECTchecks: Before reading index statistics,vchord_bm25verifies that the current role holdsSELECTon the indexed table (or column-levelSELECTon the indexed column for an ordinary non-partial single-column index; expression and partial indexes require table-levelSELECT). Checks use the current role even when scoring a supplied vector without reading table rows in SQL, and a view grant alone does not grant access to the underlying corpus. - Row-Level Security enforcement: When row-level security applies to the current role on the indexed table,
vchord_bm25rejects both scalar scoring and native scans with SQLSTATE0A000(feature_not_supported) to avoid exposing statistics about hidden rows. Owner andBYPASSRLSexemptions follow PostgreSQL rules (an ordinary owner is restricted underFORCE ROW LEVEL SECURITY), androw_security = offdoes not bypass this restriction. - Lock ordering: Scalar
<&>evaluations lock the heap relation before locking the BM25 index relation, matching PostgreSQL’s standard table-then-index lock order.
Migrating from 0.3.x and packages
Because vchord_bm25 1.0.0 changes the document data type, catalog schema, query constructor order, and on-disk index format, the 0.3.0 -> 1.0.0 upgrade script intentionally aborts ALTER EXTENSION vchord_bm25 UPDATE with an error message before modifying any database objects.
| Feature | vchord_bm25 0.3.x |
vchord_bm25 1.0.0 |
|---|---|---|
| Extension schema | Fixed bm25_catalog (relocatable = false) |
Target schema (public by default, relocatable = true) |
| Document column type | bm25_catalog.bm25vector (token_id:freq array) |
Native PostgreSQL pg_catalog.tsvector (with lexeme positions) |
| Query constructor | to_bm25query(regclass, bm25vector) |
to_bm25query(tsvector, regclass) |
| Score return type | real (float4) |
double precision (float8, +0.0 for zero scores) |
| GUCs | bm25_catalog.bm25_limit, bm25_catalog.enable_index |
"bm25.limit", bm25.enable_scan, bm25.prefilter |
| Maintenance helper | REINDEX required to seal growing segments on demand |
bm25_maintain(regclass) + automatic VACUUM thresholds |
To migrate an existing 0.3.x deployment, retain the source text columns, install the 1.0.0 packages in a separate PostgreSQL installation or container image with its own library directory, populate native tsvector columns, and build new USING bm25 (doc bm25_ops) indexes. Step-by-step instructions are in the BM25 Migration Guide.
Native RPM and DEB packages (1.0.0-1PIGSTY / 1.0.0-1PGSTY) for PostgreSQL 14–18 across 16 Linux distributions on x86_64 and aarch64 are available on the Download page. See the BM25 SQL Reference, GUC Reference, and SBOM page for complete operational details.