Version 3.0.0
First release integrated with WAL logging. This is a breaking on-disk
format change: indexes built under 2.x must be REINDEXed.
The focus of this release is durability: index state now lives in the index
relation's own WAL-logged pages, participating in PostgreSQL's ordinary
recovery machinery.
New Features
-
WAL-logged, crash-safe on-disk storage. Replaces the external-file
snapshot mechanism from 2.5.0 with in-relation,GenericXLog-protected page
storage covering per-character and length bitmaps, the TID array,
tombstones, the free-slot list, and per-record string caches. Index state
now survives a crash and replicates correctly. -
Pending-list write path with opportunistic draining. Steady-state
INSERT,UPDATEandDELETEappend a small delta record to a structure's
own pending chain instead of rewriting a whole snapshot. Once a structure's
pending chain passes a threshold (biscuit.delta_compaction_slots, a new
GUC), it is re-serialized into a fresh compacted blob.VACUUMalso
performs a full drain pass and tracks lifetime drain counters. -
Read-time pending-list reconciliation. Queries transparently merge
not-yet-drained pending records into the results they read, so a backend
sees a consistent view regardless of whether another backend's writes have
been drained yet. -
Cross-backend cache coherency. Each session's cached copy of the index
carries the generation it was loaded at; every scan compares that
generation against the metapage and reloads on mismatch, so a backend
always sees other backends' committed writes. -
Candidate-mask threading across scan keys. Conjunctive queries evaluate
their most selective key first and restrict later keys to the surviving
rows, instead of computing each key independently and intersecting.
Queries combining an anchored predicate with an unanchored one benefit
substantially. -
Rewritten cost model. Costs are now derived from pattern shape, column
statistics and relation size, letting the planner weigh Biscuit against
gin_trgm_opspg_trgm and atext_pattern_opsB-tree. Costing for unanchored
patterns continues to be refined; verify withEXPLAINwhere a specific plan
matters. -
Length-predicate support. Patterns consisting only of
_wildcards
('______','______%') are recognised as length predicates and answered
from the length bitmaps in a single lookup, rather than being treated as
unusable. -
New
biscuit_like_ops/biscuit_ilike_opsoperator classes. In addition
to the defaultbiscuit_ops, which builds both case-sensitive and
case-insensitive structures, a column may be indexed withbiscuit_like_ops
(LIKE and NOT LIKE only) orbiscuit_ilike_ops(ILIKE and NOT ILIKE only),
skipping the build and maintenance cost of the structure set it will never be
queried with. The mode is derived from the column's opfamily at build and load
time and is never persisted, so it cannot go stale across aREINDEXunder a
different opclass.
Internal Changes
- Removed the background preload worker; index loading is now synchronous.
- Rewrote the on-disk persistence layer to use the new directory and
blob/pending-chain storage; the 2.5.0 flat-file snapshot format is no
longer read. - Deletes and updates now remove index entries individually and durably,
replacing the previous bulk in-memory sweep.
Known Limitations
These follow from the design and should be planned for. Figures observed during
testing will vary with hardware, data and workload.
-
Write amplification. Because one indexed string touches many
per-character structures,INSERTandUPDATEagainst a live index generate
considerably more WAL than the corresponding heap writes alone. Substantial
WAL is characteristic of maintaining any secondary text-search structure, and
in testing Biscuit's WAL volume per row was comparable to apg_trgmGIN
index on the same data. WAL per row also grows as the index grows, so
measurements taken on a small index will understate a large one.DELETEis
much cheaper, recording a tombstone rather than rewriting structures.Size
pg_walaccordingly, monitor free space, and where replication slots
are in use consider settingmax_slot_wal_keep_size. Allow for the
corresponding effect on crash-recovery duration when planning restart
windows. -
Bulk-load before indexing. Creating the index after a load is
substantially faster, and generates far less WAL, than inserting the same
rows into an already-indexed table. -
Per-connection memory. Each backend holds its own copy of the index in
session-local memory for the life of the connection, loaded lazily as
patterns are queried. Memory therefore scales with concurrent connections;
biscuit_index_memory_size()reports the current session's copy. -
Cache reload on invalidation. A committed write by any backend
invalidates cached copies, which are then reloaded in full rather than
refreshed incrementally. Read latency rises for a period after each write, and
the effect is more pronounced with many concurrent readers. Incremental
refresh is planned. -
Build cost and index size. Biscuit indexes are larger and slower to build
than comparablepg_trgmor B-tree indexes on the same column.VACUUMdoes
not reduce index size; useREINDEX. Build memory scales with row count. -
Unanchored query cost grows with the square of string length, so a small
number of unusually long values can affect query cost across the table. -
No ordered, backward, index-only or unique scans, and Biscuit indexes are
not clusterable.
Upgrade Notes
This is a breaking on-disk format change. Indexes built under 2.x must be
REINDEXed after upgrading; there is no automatic migration and no dual-format
reader. Until an index is rebuilt, its first cold load under the new version
fails with an error referring to this note.
Plan a maintenance window sized for the rebuild: index build is slower than for
pg_trgm GIN on the same data.