-- long_positions (1.15.0, ROADMAP I9): the configured analyzer kept token positions
-- past 16,383. parsetext() stores each ordinal in a 16-bit field capped at 16,383,
-- so through 1.14.3 every token after the 16,383rd shared one position and a phrase
-- there never matched (PR #12). Now long text is parsed in chunks under the cap and
-- each chunk's ordinals are offset; a text of one chunk is parsed as before.
SET client_min_messages = warning;
CREATE EXTENSION IF NOT EXISTS pg_fts;
-- the ROADMAP example
SELECT to_ftsdoc('simple', repeat('filler ', 17000) || 'alpha beta') @@@ '"alpha beta"'::ftsquery AS past_cap_phrase,
to_ftsdoc('simple', repeat('filler ', 17000) || 'alpha beta') @@@ '"beta alpha"'::ftsquery AS past_cap_reversed,
ftsdoc_length(to_ftsdoc('simple', repeat('filler ', 17000) || 'alpha beta')) AS doclen;
past_cap_phrase | past_cap_reversed | doclen
-----------------+-------------------+--------
t | f | 17002
(1 row)
-- a phrase at every offset around the first chunk boundary (16,383 bytes = 2,340
-- 'filler ' words): one of these straddles it, all must match, none reversed (this
-- is below the old cap, so 1.14.3 passes it too: it guards the chunking, not the cap)
SELECT count(*) FILTER (WHERE to_ftsdoc('simple', repeat('filler ', n) || 'alpha beta gamma ' || repeat('filler ', 50))
@@@ '"alpha beta gamma"'::ftsquery) AS phrase_ok,
count(*) FILTER (WHERE to_ftsdoc('simple', repeat('filler ', n) || 'alpha beta gamma ' || repeat('filler ', 50))
@@@ '"gamma beta"'::ftsquery) AS reversed_matches,
count(*) AS offsets
FROM generate_series(2330, 2350) n;
phrase_ok | reversed_matches | offsets
-----------+------------------+---------
21 | 0 | 21
(1 row)
-- NEAR distance across chunks, and a stemming configuration
SELECT to_ftsdoc('english', repeat('the running dogs ', 6000) || 'quick foxes jumped') @@@ '"quick fox jump"'::ftsquery AS english_past_cap,
to_ftsdoc('simple', repeat('w ', 20000) || 'alpha x x beta') @@@ 'alpha <3> beta'::ftsquery AS near3,
to_ftsdoc('simple', repeat('w ', 20000) || 'alpha x x beta') @@@ 'alpha <2> beta'::ftsquery AS near2;
english_past_cap | near3 | near2
------------------+-------+-------
t | t | f
(1 row)
-- the same positions as the unconfigured analyzer for plain words (which never capped)
SELECT to_ftsdoc('simple', repeat('filler ', 17000) || 'alpha beta')::text
= to_ftsdoc(repeat('filler ', 17000) || 'alpha beta')::text AS same_as_unconfigured;
same_as_unconfigured
----------------------
t
(1 row)
-- a text over 16 kB but under 16,383 tokens is parsed whole, as 1.14.3 did: an HTML
-- tag with spaces inside it, at every offset around byte 16,383, is a tag (not words)
-- and the value is the same at each offset as the same text without the padding shift
SELECT count(*) FILTER (WHERE to_ftsdoc('english', repeat('filler ', n) || ' word')
@@@ 'zzhref | zzclass'::ftsquery) AS tag_words_indexed,
count(DISTINCT ftsdoc_length(to_ftsdoc('english', repeat('filler ', n) || ' word')) - n) AS doclen_shapes,
count(*) AS offsets
FROM generate_series(2330, 2350) n;
tag_words_indexed | doclen_shapes | offsets
-------------------+---------------+---------
0 | 1 | 21
(1 row)
-- markup over 16,383 tokens (1.15.1, ROADMAP I9a): 1.15.0 cut the re-parse after any
-- whitespace, so a tag, comment or v' || g || ' w x y ', '') FROM generate_series(1, 5000) g;
ALTER TABLE lpm ADD COLUMN d ftsdoc;
UPDATE lpm SET d = to_ftsdoc('simple', t);
SELECT id, ftsdoc_length(d) AS doclen,
d @@@ 'href | class'::ftsquery AS tag_words,
d @@@ 'comment | spaces'::ftsquery AS comment_words
FROM lpm ORDER BY id;
id | doclen | tag_words | comment_words
----+--------+-----------+---------------
1 | 20000 | f | f
2 | 20000 | f | f
3 | 20000 | f | f
(3 rows)
-- script bodies: none of the 5,000 identifiers inside ' || repeat(' w', 9000) AS t
UNION ALL SELECT 2, repeat('w ', 9000) || '
tail' || repeat(' w', 9000);
SELECT id, ftsdoc_length(to_ftsdoc('simple', t)) AS doclen,
to_ftsdoc('simple', t) @@@ 'zz | var | img | src | data'::ftsquery AS markup_words
FROM lpl ORDER BY id;
id | doclen | markup_words
----+--------+--------------
1 | 18000 | f
2 | 18001 | f
(2 rows)
DROP TABLE lpl;
-- a phrase across every tag (positions not shifted): all 4,999
SELECT count(*) AS cross_tag_phrases FROM lpm, generate_series(1, 4999) g
WHERE id = 1 AND d @@@ ('"t w' || (g + 1) || '"')::ftsquery;
cross_tag_phrases
-------------------
4999
(1 row)
DROP TABLE lpm;
-- a short text is unchanged (one chunk): the output of 1.14.3
SELECT to_ftsdoc('english', 'The quick brown fox jumps over the lazy dog; the dog sleeps.');
to_ftsdoc
-----------------------------------------------------------------------------------
'brown':1@3 'dog':2@9,11 'fox':1@4 'jump':1@5 'lazi':1@8 'quick':1@2 'sleep':1@12
(1 row)
-- through an index with positions: a ranked phrase query past the cap
CREATE TABLE lp (id int, d ftsdoc);
INSERT INTO lp SELECT g, to_ftsdoc('simple', repeat('filler ', 17000) || CASE WHEN g % 2 = 0 THEN 'alpha beta' ELSE 'beta alpha' END)
FROM generate_series(1, 6) g;
CREATE INDEX lp_fts ON lp USING fts (d) WITH (positions = on);
SET enable_seqscan = off;
SELECT array_agg(id ORDER BY id) AS phrase_rows FROM lp WHERE d @@@ '"alpha beta"'::ftsquery;
phrase_rows
-------------
{2,4,6}
(1 row)
RESET enable_seqscan;
DROP TABLE lp;