-- pending_delete: a row inserted after the build (so it sits in the pending -- list) and deleted before the next VACUUM must leave the index. Before -- 1.11.0, bulkdelete tombstoned only segment postings, then the cleanup-time -- flush wrote the deleted pending rows into a segment that pointed at freed -- heap slots: count(*) overcounted and a bitmap scan returned whatever row -- later reused the slot (every release). SET client_min_messages = warning; CREATE TABLE pd (id bigint, d ftsdoc) WITH (fillfactor = 100, autovacuum_enabled = off); INSERT INTO pd SELECT g, to_ftsdoc('simple', 'common w' || (g % 97)) FROM generate_series(1, 2000) g; CREATE INDEX pd_fts ON pd USING fts (d); VACUUM ANALYZE pd; -- pending inserts, some of them deleted before the VACUUM that flushes them INSERT INTO pd SELECT g, to_ftsdoc('simple', 'common new ' || g) FROM generate_series(2001, 2300) g; DELETE FROM pd WHERE id > 2000 AND id % 3 = 0; VACUUM pd; -- reuse the freed heap slots with rows that do NOT contain the term INSERT INTO pd SELECT g, to_ftsdoc('simple', 'other ' || g) FROM generate_series(3001, 3300) g; SELECT (SELECT count(*) FROM pd WHERE fts_match(d, 'common'::ftsquery)) AS truth, (SELECT count(*) FROM pd WHERE d @@@ 'common'::ftsquery) AS count_pushdown, fts_count('pd_fts', 'common'::ftsquery) AS fts_count; truth | count_pushdown | fts_count -------+----------------+----------- 2200 | 2200 | 2200 (1 row) SET enable_seqscan = off; SET enable_indexscan = off; SELECT count(*) AS bitmap_rows, count(*) FILTER (WHERE id > 3000) AS bitmap_wrong_rows FROM (SELECT id FROM pd WHERE d @@@ 'common'::ftsquery OFFSET 0) s; bitmap_rows | bitmap_wrong_rows -------------+------------------- 2200 | 0 (1 row) RESET enable_indexscan; SET enable_bitmapscan = off; SELECT count(*) AS ranked_rows, count(*) FILTER (WHERE id > 3000) AS ranked_wrong_rows FROM (SELECT id FROM pd WHERE d @@@ 'common'::ftsquery ORDER BY d <=> 'common'::ftsquery LIMIT 5000) s; ranked_rows | ranked_wrong_rows -------------+------------------- 2200 | 0 (1 row) RESET enable_bitmapscan; RESET enable_seqscan; -- and the same after a second VACUUM and a merge VACUUM pd; SELECT fts_merge('pd_fts') IS NOT NULL AS merged; merged -------- t (1 row) SELECT (SELECT count(*) FROM pd WHERE fts_match(d, 'common'::ftsquery)) AS truth, (SELECT count(*) FROM pd WHERE d @@@ 'common'::ftsquery) AS count_pushdown; truth | count_pushdown -------+---------------- 2200 | 2200 (1 row) DROP TABLE pd;