-- Multi-key operator classes: tsvector (DESIGN.md section 17). -- -- `tsvector_ops` indexes one row under every lexeme of its tsvector, with -- text keys. A tsquery of AND and OR over plain lexemes becomes a boolean -- tree over those keys and is answered exactly; NOT, phrase search, prefixes -- and weights need positions or ranges the index does not have, and fall back -- to scanning every indexed row with a recheck. The indexes here are built -- WITH (store_positions = false) to keep it so: positions.sql tests what a -- tsvector index stores by default. -- -- Every query is run through the index and through a forced sequential scan -- and the two results compared as multisets. \set VERBOSITY terse SET client_min_messages = warning; LOAD 'pg_lion'; /* VACUUM can only set all-visible for commits that reached disk (citext.sql). */ SET synchronous_commit = on; SET default_statistics_target = 1000; CREATE EXTENSION IF NOT EXISTS pg_lion; CREATE FUNCTION lion_tscmp(q text) RETURNS text LANGUAGE plpgsql AS $$ DECLARE ln text; node text := 'seq scan'; nrows bigint; ndiff bigint; BEGIN PERFORM set_config('pg_lion.enable_count_pushdown', 'on', true); PERFORM set_config('enable_seqscan', 'off', true); PERFORM set_config('pg_lion.enable_plain_scan', 'off', true); -- the bitmap path (§29) FOR ln IN EXECUTE 'EXPLAIN (COSTS OFF) ' || q LOOP IF ln LIKE '%Custom Scan (LionCount)%' THEN node := 'pushed down'; ELSIF ln LIKE '%Bitmap Index Scan%' THEN node := 'index scan'; END IF; END LOOP; EXECUTE format('CREATE TEMP TABLE lion_t_idx AS %s', q); PERFORM set_config('pg_lion.enable_count_pushdown', 'off', true); PERFORM set_config('enable_seqscan', 'on', true); PERFORM set_config('enable_bitmapscan', 'off', true); PERFORM set_config('enable_indexscan', 'off', true); EXECUTE format('CREATE TEMP TABLE lion_t_seq AS %s', q); PERFORM set_config('enable_bitmapscan', 'on', true); PERFORM set_config('enable_indexscan', 'on', true); PERFORM set_config('pg_lion.enable_plain_scan', 'on', true); EXECUTE 'SELECT count(*) FROM lion_t_idx' INTO nrows; EXECUTE 'SELECT (SELECT count(*) FROM (SELECT * FROM lion_t_idx EXCEPT ALL SELECT * FROM lion_t_seq) a)' ' + (SELECT count(*) FROM (SELECT * FROM lion_t_seq EXCEPT ALL SELECT * FROM lion_t_idx) b)' INTO ndiff; EXECUTE 'DROP TABLE lion_t_idx, lion_t_seq'; IF ndiff <> 0 THEN RETURN format('MISMATCH: %s rows differ', ndiff); END IF; RETURN format('%s, %s rows', node, nrows); END $$; CREATE FUNCTION lion_tsplan(q text) RETURNS SETOF text LANGUAGE plpgsql AS $$ DECLARE ln text; BEGIN PERFORM set_config('enable_seqscan', 'off', true); PERFORM set_config('pg_lion.enable_plain_scan', 'off', true); -- the bitmap path (§29) FOR ln IN EXECUTE 'EXPLAIN (COSTS OFF) ' || q LOOP RETURN NEXT ln; END LOOP; PERFORM set_config('enable_seqscan', 'on', true); PERFORM set_config('pg_lion.enable_plain_scan', 'on', true); END $$; -- ---- the table --------------------------------------------------------- -- -- A few hundred generated sentences over a vocabulary of ~60 words, so that -- the posting sets are big enough to be interesting and the answers small -- enough to compare. Row 0's text is empty, so its tsvector has no lexeme at -- all and the row lands in the reserved EMPTY entry; row -1's is NULL. CREATE TABLE lion_ts ( id int NOT NULL, grp int NOT NULL, -- 6 groups, scalar lion index body text, tsv tsvector ); INSERT INTO lion_ts (id, grp, body) SELECT i, i % 6, 'alpha w' || (i % 17) || ' w' || (i % 23) || ' w' || (i % 29) || ' bravo charlie delta w' || (i % 31) FROM generate_series(1, 600) i; INSERT INTO lion_ts (id, grp, body) VALUES (0, 0, ''); INSERT INTO lion_ts (id, grp, body) VALUES (-1, 1, NULL); UPDATE lion_ts SET tsv = to_tsvector('english', coalesce(body, '')) WHERE body IS NOT NULL; CREATE INDEX lion_ts_tsv ON lion_ts USING lion (tsv) WITH (store_positions = false); CREATE INDEX lion_ts_grp ON lion_ts USING lion (grp); VACUUM (FREEZE, ANALYZE) lion_ts; -- The key column of a tsvector_ops index is text: the lexeme. SELECT a.attname, a.atttypid::regtype FROM pg_attribute a WHERE a.attrelid = 'lion_ts_tsv'::regclass AND a.attnum > 0; attname | atttypid ---------+---------- tsv | text (1 row) SELECT lion_index_verify('lion_ts_tsv', true); lion_index_verify ------------------- (1 row) SELECT entries, null_tids, empty_tids FROM lion_index_stats('lion_ts_tsv'); entries | null_tids | empty_tids ---------+-----------+------------ 37 | 1 | 1 (1 row) -- ---- plans ------------------------------------------------------------- SELECT lion_tsplan($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 & w2')$$); lion_tsplan --------------------------------------------------------- Bitmap Heap Scan on lion_ts Recheck Cond: (tsv @@ '''w1'' & ''w2'''::tsquery) -> Bitmap Index Scan on lion_ts_tsv Index Cond: (tsv @@ '''w1'' & ''w2'''::tsquery) (4 rows) SELECT lion_tsplan($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 & w2')$$); lion_tsplan ----------------------------------------------------------------- Custom Scan (LionCount) Lion Indexes: lion_ts_tsv (tsv @@ '''w1'' & ''w2'''::tsquery) (2 rows) SELECT lion_tsplan($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 | w2')$$); lion_tsplan ----------------------------------------------------------------- Custom Scan (LionCount) Lion Indexes: lion_ts_tsv (tsv @@ '''w1'' | ''w2'''::tsquery) (2 rows) -- NOT beside a lexeme, phrase and weights are counted from the superset of -- their lexemes' rows and rechecked; a prefix is never pushed down SELECT lion_tsplan($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha & !w1')$$); lion_tsplan --------------------------------------------------------------------- Custom Scan (LionCount) Lion Indexes: lion_ts_tsv (tsv @@ '''alpha'' & !''w1'''::tsquery) (2 rows) SELECT lion_tsplan($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1:*')$$); lion_tsplan ---------------------------------------------------------- Custom Scan (LionCount) Lion Indexes: lion_ts_tsv (tsv @@ '''w1'':*'::tsquery) (2 rows) SELECT lion_tsplan($$SELECT count(*) FROM lion_ts WHERE tsv @@ phraseto_tsquery('english', 'bravo charlie')$$); lion_tsplan -------------------------------------------------------------------------- Custom Scan (LionCount) Lion Indexes: lion_ts_tsv (tsv @@ '''bravo'' <-> ''charli'''::tsquery) (2 rows) SELECT lion_tsplan($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha:A')$$); lion_tsplan ------------------------------------------------------------- Custom Scan (LionCount) Lion Indexes: lion_ts_tsv (tsv @@ '''alpha'':A'::tsquery) (2 rows) SELECT lion_tsplan($$SELECT grp, count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 & w2') GROUP BY grp$$); lion_tsplan ------------------------------------------------------------------------------------ Custom Scan (LionCount) Lion Indexes: lion_ts_grp (grp), lion_ts_tsv (tsv @@ '''w1'' & ''w2'''::tsquery) Group Key: grp (3 rows) -- ---- AND, OR, nested parentheses --------------------------------------- SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1')$$); lion_tscmp --------------------- index scan, 98 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 & w2')$$); lion_tscmp --------------------- index scan, 12 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 | w2')$$); lion_tscmp ---------------------- index scan, 184 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha & (w1 | w2)')$$); lion_tscmp ---------------------- index scan, 184 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', '(w1 | w2) & (w3 | w4)')$$); lion_tscmp --------------------- index scan, 48 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha & bravo & charlie & delta')$$); lion_tscmp ---------------------- index scan, 600 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', '((w1 | w2) & w3) | (w4 & w5)')$$); lion_tscmp --------------------- index scan, 36 rows (1 row) -- a lexeme that matches nothing, alone and inside a tree SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'nosuchword')$$); lion_tscmp -------------------- index scan, 0 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha & nosuchword')$$); lion_tscmp -------------------- index scan, 0 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha | nosuchword')$$); lion_tscmp ---------------------- index scan, 600 rows (1 row) -- ---- two index quals, and `@@ ANY (...)` -------------------------------- -- The access method answers both and ANDs their lexeme trees, as it ANDs -- two columns' (DESIGN.md §5 SCAN step 5). SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1') AND tsv @@ to_tsquery('english', 'w2')$$); lion_tscmp --------------------- index scan, 12 rows (1 row) -- `@@ ANY (array of tsqueries)` is the union of the queries' answers, which -- an array_ops index cannot be given (an array of arrays does not exist). SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ ANY (ARRAY['w1 & w2', 'w3 & w4']::tsquery[])$$); lion_tscmp --------------------- index scan, 24 rows (1 row) -- one element of the list needs the ALL fallback: the union still holds SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ ANY (ARRAY['w1 & w2', '!w3']::tsquery[])$$); lion_tscmp ---------------------- index scan, 504 rows (1 row) -- ---- the fallbacks ----------------------------------------------------- SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha & !w1')$$); lion_tscmp ---------------------- index scan, 502 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', '!w1')$$); lion_tscmp ---------------------- index scan, 503 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1:*')$$); lion_tscmp ---------------------- index scan, 527 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha & w1:*')$$); lion_tscmp ---------------------- index scan, 527 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ phraseto_tsquery('english', 'bravo charlie')$$); lion_tscmp ---------------------- index scan, 600 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'bravo <-> charlie')$$); lion_tscmp ---------------------- index scan, 600 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'bravo <2> delta')$$); lion_tscmp ---------------------- index scan, 600 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha:A')$$); lion_tscmp -------------------- index scan, 0 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha:* & bravo:B')$$); lion_tscmp -------------------- index scan, 0 rows (1 row) -- plainto_tsquery is a plain AND, so it is answered exactly SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ plainto_tsquery('english', 'bravo charlie')$$); lion_tscmp ---------------------- index scan, 600 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ plainto_tsquery('english', 'alpha w1')$$); lion_tscmp --------------------- index scan, 98 rows (1 row) -- `@@@` is `@@` SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@@ to_tsquery('english', 'w1 & w2')$$); lion_tscmp --------------------- index scan, 12 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@@ to_tsquery('english', 'alpha & !w1')$$); lion_tscmp ---------------------- index scan, 502 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@@ to_tsquery('english', 'bravo <-> charlie')$$); lion_tscmp ---------------------- index scan, 600 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@@ to_tsquery('english', 'w1:*')$$); lion_tscmp ---------------------- index scan, 527 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_ts WHERE tsv @@@ to_tsquery('english', 'w1 | w2')$$); lion_tscmp -------------------- index scan, 1 rows (1 row) -- NULL and empty tsvectors SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv IS NULL$$); lion_tscmp -------------------- index scan, 1 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv IS NOT NULL$$); lion_tscmp ---------------------- index scan, 601 rows (1 row) -- ---- the count pushdown ------------------------------------------------ SELECT lion_tscmp($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 & w2')$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 | w2')$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha & (w1 | w2)')$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha & !w1')$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 & w2') AND grp = 3$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) -- the counts themselves SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 & w2'); count ------- 12 (1 row) SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 | w2'); count ------- 184 (1 row) SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha & (w1 | w2)'); count ------- 184 (1 row) SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'nosuchword'); count ------- 0 (1 row) -- ---- GROUP BY a scalar column with a tsquery WHERE clause -------------- SELECT lion_tscmp($$SELECT grp, count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 & w2') GROUP BY grp$$); lion_tscmp --------------------- pushed down, 6 rows (1 row) SELECT lion_tscmp($$SELECT grp, count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 | w2') GROUP BY grp$$); lion_tscmp --------------------- pushed down, 6 rows (1 row) SELECT grp, count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 & w2') GROUP BY grp ORDER BY grp; grp | count -----+------- 0 | 2 1 | 2 2 | 1 3 | 2 4 | 4 5 | 1 (6 rows) -- ---- writes ------------------------------------------------------------ INSERT INTO lion_ts (id, grp, body, tsv) SELECT i, i % 6, 'echo foxtrot w' || (i % 17), to_tsvector('english', 'echo foxtrot w' || (i % 17)) FROM generate_series(601, 900) i; INSERT INTO lion_ts (id, grp, body, tsv) VALUES (1000, 2, '', to_tsvector('english', '')); INSERT INTO lion_ts (id, grp, body, tsv) VALUES (1001, 2, NULL, NULL); SELECT lion_index_verify('lion_ts_tsv', true); lion_index_verify ------------------- (1 row) SELECT entries, null_tids, empty_tids FROM lion_index_stats('lion_ts_tsv'); entries | null_tids | empty_tids ---------+-----------+------------ 39 | 2 | 2 (1 row) -- a dirty heap SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'echo & foxtrot')$$); lion_tscmp ---------------------- index scan, 300 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'echo & w3')$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) UPDATE lion_ts SET tsv = to_tsvector('english', 'golf hotel') WHERE id BETWEEN 100 AND 150; DELETE FROM lion_ts WHERE id BETWEEN 200 AND 250; SELECT lion_index_verify('lion_ts_tsv', true); lion_index_verify ------------------- (1 row) SELECT lion_tscmp($$SELECT id FROM lion_ts WHERE tsv @@ to_tsquery('english', 'golf & hotel')$$); lion_tscmp --------------------- index scan, 51 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha & bravo')$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) VACUUM lion_ts; SELECT lion_index_verify('lion_ts_tsv', true); lion_index_verify ------------------- (1 row) VACUUM (FREEZE, ANALYZE) lion_ts; -- an all-visible heap: the counts come out of the visibility map SELECT lion_tscmp($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'alpha & bravo')$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'golf | echo')$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) SELECT lion_tscmp($$SELECT grp, count(*) FROM lion_ts WHERE tsv @@ to_tsquery('english', 'w1 & w2') GROUP BY grp$$); lion_tscmp --------------------- pushed down, 6 rows (1 row) SELECT lion_index_verify('lion_ts_tsv', true); lion_index_verify ------------------- (1 row) -- ---- the EMPTY entry spilling (2026-09-20 review, finding 1) ---------- /* * A tsvector with no lexemes belongs to the reserved EMPTY entry, which must * stay the EMPTY entry when its posting set moves onto container pages - the * array case is argued in array.sql; here the rows are empty documents. */ CREATE TABLE lion_ts_empty (id int NOT NULL, tsv tsvector); CREATE INDEX lion_ts_empty_tsv ON lion_ts_empty USING lion (tsv) WITH (inline_limit = 64, store_positions = false); INSERT INTO lion_ts_empty SELECT i, CASE WHEN i % 2 = 0 THEN to_tsvector('simple', '') ELSE to_tsvector('simple', 'onlyword') END FROM generate_series(1, 6000) i; -- one EMPTY entry with 3000 rows and one lexeme entry, both spilled SELECT entries, inline_entries, null_tids, empty_tids, container_pages > 0 AS spilled FROM lion_index_stats('lion_ts_empty_tsv'); entries | inline_entries | null_tids | empty_tids | spilled ---------+----------------+-----------+------------+--------- 2 | 0 | 0 | 3000 | t (1 row) SELECT lion_index_verify('lion_ts_empty_tsv', true); lion_index_verify ------------------- (1 row) -- these rows must join the EMPTY entry, not start a second one INSERT INTO lion_ts_empty VALUES (10001, to_tsvector('simple', '')), (10002, NULL); SELECT entries, null_tids, empty_tids FROM lion_index_stats('lion_ts_empty_tsv'); entries | null_tids | empty_tids ---------+-----------+------------ 3 | 1 | 3001 (1 row) SELECT lion_index_verify('lion_ts_empty_tsv', true); lion_index_verify ------------------- (1 row) -- and once more after a VACUUM has rewritten the entries DELETE FROM lion_ts_empty WHERE id % 4 = 0; VACUUM (FREEZE) lion_ts_empty; SELECT entries, null_tids, empty_tids FROM lion_index_stats('lion_ts_empty_tsv'); entries | null_tids | empty_tids ---------+-----------+------------ 3 | 1 | 1501 (1 row) SELECT lion_index_verify('lion_ts_empty_tsv', true); lion_index_verify ------------------- (1 row) DROP TABLE lion_ts_empty; -- ---- costing the ALL-mode fallback (2026-09-21 follow-up review) ------- /* * A phrase, a prefix, a weight mask and a NOT cannot be answered from the * stored lexeme memberships, so the extractor falls back to LION_QMODE_ALL: * the scan emits the union of every entry and every candidate row is fetched * and rechecked in the heap (DESIGN.md §17). That is correct but it is a * whole-index traversal plus a whole-table recheck, and the generic index * estimate used to price it with the PREDICATE's selectivity instead - a * prefix query estimated at 192 cost units against the sequential scan's * 9156, for a plan that emitted 1.18M posting TIDs and ran 2.6x slower than * that sequential scan (68.6/62.3 ms against 31.8/23.8 ms on 200k * documents). * * The queries here aggregate sum(id) rather than count(*) so that the count * pushdown is out of the picture and the comparison is between the bitmap * plan and the sequential scan, which is where the misestimate lived. An * exact query must keep the index - a plain Index Scan for these few rows * since DESIGN.md §29 - and an ALL-mode one must lose it. */ CREATE TABLE lion_tsc (id int NOT NULL, tsv tsvector NOT NULL); INSERT INTO lion_tsc SELECT i, to_tsvector('simple', 'common w' || (i % 20) || ' w' || (i % 400) || ' rare' || i || ' endword') FROM generate_series(1, 20000) i; CREATE INDEX lion_tsc_tsv ON lion_tsc USING lion (tsv) WITH (store_positions = false); VACUUM (FREEZE, ANALYZE) lion_tsc; -- exact: one lexeme, an AND and an OR over lexemes EXPLAIN (COSTS OFF) SELECT sum(id) FROM lion_tsc WHERE tsv @@ 'rare500'::tsquery; QUERY PLAN ----------------------------------------------------- Aggregate -> Index Scan using lion_tsc_tsv on lion_tsc Index Cond: (tsv @@ '''rare500'''::tsquery) (3 rows) EXPLAIN (COSTS OFF) SELECT sum(id) FROM lion_tsc WHERE tsv @@ 'rare500 & endword'::tsquery; QUERY PLAN ------------------------------------------------------------------- Aggregate -> Index Scan using lion_tsc_tsv on lion_tsc Index Cond: (tsv @@ '''rare500'' & ''endword'''::tsquery) (3 rows) EXPLAIN (COSTS OFF) SELECT sum(id) FROM lion_tsc WHERE tsv @@ 'rare500 | rare501'::tsquery; QUERY PLAN ------------------------------------------------------------------- Aggregate -> Index Scan using lion_tsc_tsv on lion_tsc Index Cond: (tsv @@ '''rare500'' | ''rare501'''::tsquery) (3 rows) -- a superset, rechecked: a phrase, a weight mask, a NOT beside a lexeme; -- ALL mode: a prefix EXPLAIN (COSTS OFF) SELECT sum(id) FROM lion_tsc WHERE tsv @@ 'common <-> w1'::tsquery; QUERY PLAN --------------------------------------------------------------------- Aggregate -> Bitmap Heap Scan on lion_tsc Recheck Cond: (tsv @@ '''common'' <-> ''w1'''::tsquery) -> Bitmap Index Scan on lion_tsc_tsv Index Cond: (tsv @@ '''common'' <-> ''w1'''::tsquery) (5 rows) EXPLAIN (COSTS OFF) SELECT sum(id) FROM lion_tsc WHERE tsv @@ 'rare50:*'::tsquery; QUERY PLAN ------------------------------------------------------ Aggregate -> Index Scan using lion_tsc_tsv on lion_tsc Index Cond: (tsv @@ '''rare50'':*'::tsquery) (3 rows) EXPLAIN (COSTS OFF) SELECT sum(id) FROM lion_tsc WHERE tsv @@ 'rare500:A'::tsquery; QUERY PLAN ------------------------------------------------------- Aggregate -> Index Scan using lion_tsc_tsv on lion_tsc Index Cond: (tsv @@ '''rare500'':A'::tsquery) (3 rows) EXPLAIN (COSTS OFF) SELECT sum(id) FROM lion_tsc WHERE tsv @@ 'rare500 & !endword'::tsquery; QUERY PLAN -------------------------------------------------------------------- Aggregate -> Index Scan using lion_tsc_tsv on lion_tsc Index Cond: (tsv @@ '''rare500'' & !''endword'''::tsquery) (3 rows) -- an unknown value has to be priced as the expensive shape as well PREPARE lion_tsc_p(tsquery) AS SELECT sum(id) FROM lion_tsc WHERE tsv @@ $1; SET plan_cache_mode = force_generic_plan; EXPLAIN (COSTS OFF) EXECUTE lion_tsc_p('rare500'); QUERY PLAN ----------------------------- Aggregate -> Seq Scan on lion_tsc Filter: (tsv @@ $1) (3 rows) RESET plan_cache_mode; DEALLOCATE lion_tsc_p; -- and the answers are the same whichever plan runs SELECT lion_tscmp($$SELECT count(*) FROM lion_tsc WHERE tsv @@ 'rare50:*'::tsquery$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_tsc WHERE tsv @@ 'common <-> w1'::tsquery$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_tsc WHERE tsv @@ 'rare500 & endword'::tsquery$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) -- the superset's rows, fetched by the bitmap scan and rechecked in the heap SELECT lion_tscmp($$SELECT id FROM lion_tsc WHERE tsv @@ 'common <-> w1'::tsquery$$); lion_tscmp ----------------------- index scan, 1000 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_tsc WHERE tsv @@ 'w1 <-> common'::tsquery$$); lion_tscmp -------------------- index scan, 0 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_tsc WHERE tsv @@ 'rare500 <2> w0'::tsquery$$); lion_tscmp -------------------- index scan, 0 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_tsc WHERE tsv @@ 'rare500:A'::tsquery$$); lion_tscmp -------------------- index scan, 0 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_tsc WHERE tsv @@ 'rare500 & !endword'::tsquery$$); lion_tscmp -------------------- index scan, 0 rows (1 row) SELECT lion_tscmp($$SELECT id FROM lion_tsc WHERE tsv @@ 'w3 & !(w3 <-> w3)'::tsquery$$); lion_tscmp ---------------------- index scan, 950 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_tsc WHERE tsv @@ 'w3 <-> w3'::tsquery$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) SELECT lion_tscmp($$SELECT count(*) FROM lion_tsc WHERE tsv @@ 'w1 <-> w1 | rare7'::tsquery$$); lion_tscmp --------------------- pushed down, 1 rows (1 row) DROP TABLE lion_tsc; -- ---- max_entries on a lexeme index ------------------------------------- CREATE TABLE lion_ts_many (tsv tsvector); INSERT INTO lion_ts_many SELECT to_tsvector('simple', 'lex' || i || ' lex' || (i + 1)) FROM generate_series(1, 3000) i; CREATE INDEX lion_ts_many_tsv ON lion_ts_many USING lion (tsv) WITH (max_entries = 500, store_positions = false); WARNING: lion index "lion_ts_many_tsv" has more than 500 distinct keys SELECT entries > 500 FROM lion_index_stats('lion_ts_many_tsv'); ?column? ---------- t (1 row) SELECT lion_index_verify('lion_ts_many_tsv', true); lion_index_verify ------------------- (1 row) DROP TABLE lion_ts_many; DROP TABLE lion_ts; DROP FUNCTION lion_tscmp(text); DROP FUNCTION lion_tsplan(text);