Для текстового поиска в PostgreSQL используется вспомогательный «векторизованный» формат TSvector, который упрощенно описывает текст. Он хранит список отдельных лексем (единиц, совпадающих с любой грамматической формой данного слова) и к каждой лексеме — набор позиций (номеров слов от начала «текста»). С его помощью можно находить записи базы данных, в текстовых полях которых встречаются искомые:
Выглядит TSvector так:
select to_tsvector('pg_catalog.russian','Джон Донн уснул, уснуло все вокруг. Уснули стены, пол, постель, картины, уснули стол, ковры, засовы, крюк, весь гардероб, буфет, свеча, гардины.');
---------------------------------------------------
'буфет':19 'ве':17 'вокруг':6 'гардероб':18 'гардин':21 'джон':1 'дон':2 'засов':15 'картин':11 'ковр':14 'крюк':16 'пол':9 'постел':10 'свеч':20 'стен':8 'стол':13 'уснул':3,4,7,12
Для ускорения такого текстового поиска в PostgreSQL используются обратные индексы, наподобие внутренних предметных указателей. Технически, в отличие от обычных индексов, которые строятся по значению поля, эти построены по лексемам — частям поля специальной структуры (TSvector). В составе PostgreSQL такие индексы поддерживает метод GIN, а также существует его модификация — RUM, которая подключается отдельным модулем.
Если в запросе есть только слова с логическими операторами, GIN и RUM работают одинаково. Если же в запросе есть фразы, то при поиске надо учитывать не только вхождение слов, но и расстояние между ними.
Полнотекстовый поиск PostgreSQL можно использовать при поиске сразу по нескольким полнотекстовым полям. Например, название, аннотация и полный текст книги. И находить слова и фразы или только в названии, или и в названии, и в полном тексте и т.п. — как хотим.
Для этого в лексемах TSvector, кроме слова и позиции, можно (но не обязательно) сохранить четыре метки: A, B, C или D. Они не обязательно должны быть назначены именно по разным полям. Можно как угодно, по частям речи, к примеру, если вдруг мы не хотим считать одинаковыми сходные в написании глаголы и существительные.
В запросе к типу TSVector (он называется TSQuery) можно (но не обязательно) использовать метки, которым должна удовлетворять каждая лексема:
'Рим:a & перехлестом:ab'::TSQuery
Несмотря на кажущуюся широту запроса, кроме «Писем Римскому другу» Бродского, вряд ли много произведений содержат что-то относящееся к перехлесту и одновременно с Рим в названии.
Что происходит с этими метками, если построить индекс?
Упрощенно запрос к индексу (RUM) обрабатывается в два этапа:
Второй этап реализован методом consistent (функция rum_tsquery_consistent() — выдает, удовлетворяет ли найденная в индексе запись запросу). Для этого из RUM она вызывает в PostgresSql функцию TS_execute(), которая реализует рекурсивное дерево проверки логической структуре запроса. Узлы этого дерева — операторы, листья — логические результаты определения, удовлетворяет ли запись в индексе операнду, ветви — промежуточные результаты. Листья “проверяются” снова в RUM функцией проверки checkcondition_rum(), которую это дерево вызывает, когда на данной ветви уже нет операторов-узлов, а остался только операнд-лист.
Если узел — фразовый оператор, то TS_execute() вызывает другую функцию TS_phrase_execute(), которая проверяет (1) взаимное положение лексем, соединенных фразовым оператором. (2) логические операции, стоящие внутри фразового. Удивительно, что внутри фразового оператора логические операции работают другим образом, чем вне. Так запрос (a & b) <-> c не дает то же самое, что и (a <-> с) & (b <->c). В первом примере накладывается дополнительное ограничение, чтобы позиции a и b в tsvector совпадали, т.е. так как позиции в tsvector уникальны, то a и b не могут быть отдельными словами в поиске, а только разными весами одного слова. Это свойство требуется для фразового поиска в таких языках, как немецкий, где употребляется слияние многих слов в одно. Для фразового поиска оно разделяется на разные корни-лексемы, которые получают один и тот же номер позиции в tsvector. Таким образом, первый из приведенных запросов означает одно составное слово из корней a и b перед одним словом с, а второй — оба этих корня, стоящих перед словом с в тексте, возможно, в двух разных позициях.
В листах дерева (и логического, и фразового) обратно из RUM вызывается функция проверки одиночного операнда checkcondition_rum(). Она выдает единственный результат да/нет в логический оператор, а во фразовый — список позиций для лексемы, в которых она встречается в данной записи.
Между логическим деревом и функцией проверки операнда соблюдается разграничение: первое не знает ничего об операндах, кроме результатов их сравнения с записью в индексе, вторая — ничего не знает о логике, как ее результаты могут быть логически связаны с другими. В случае фразового дерева это разграничение имеет исключение в виде передачи в него правильных позиций для каждого операнда и “сборкой” фразовым оператором результата по совпадению этих позиций с “упрощенным” поведением логических операторов внутри фразового (см. выше).
Среди логических операторов в запросе может оказаться ! (NOT). Если есть однозначное соответствие наличия лексемы в индексе и в тексте, то оператор ! полностью определен как над отдельным операндом (лексема не содержится в тексте), так и над логической комбинацией операндов. Это реализуется, если в запросе нет весов (которые требуют селекции только части записей с вхождением лексемы в индекс).
Хотя в самих индексах веса отсутствуют, лексемы в запросе они вполне могут содержать выбор по ним. Поэтому запрос A & !B может быть проверен по индексу, а А & !B:d — нет. Отрицание !B:d в последнем запросе может быть положительным, если в тексте содержится, например, B:c (в индексе В в таком случае тоже содержится — без веса).
Раньше в индексе GIN при запросах с весами устанавливался флаг для перепроверки результата по таблице. Однако перепроверка может выбросить ложноположительные значения, которые вернул индекс, но ничего не может поделать с ложноотрицательными, которые уже выброшены при проверке. Так как оператор ! меняет true на false, то для B:d нет бинарного значения, которое гарантированно не сделает его в дереве операндов ложноотрицательным.
Именно такие запросы А & !B:d по индексам GIN и GiST теряли до сих пор ложнотрицательные тексты, содержащие A & B:c — они оказывались выброшенными до перепроверки.
Чтобы это исправить, надо в таких “сомнительных” случаях передавать от функции проверки операнда в дерево запроса третий результат — maybe (ни да, ни нет), который гарантированно дойдет до корня логического дерева, независимо на отрицания и инициирует перепроверку. Изменение передаваемого результата с двоичного на трехзначный решило описанную проблему в индексах GIN и GiST (после этого коммита в PostgreSQL https://github.com/postgres/postgres/commit/2f2007fbb255be178aca586780967f43885203a7 )
В индексе RUM веса есть, поэтому при запросе A:b он может выдавать:
1. в случае логического запроса только те записи, где слово A встречается хотя бы в одной позиции с весом b
2. в случае фразового запроса — номера позиций слова A с весом b
Отрицательный оператор при этом определен как двойной: !B:d = B:abc | !B
Но технически проверить такое условие в узле нельзя: для этого требуется знать о всех ветвях выше этого узла (например, что они содержит запросы с весами, веса всех этих запросов, есть ли вложенные отрицания, есть ли вложенные фразовые операторы и как логически скомбинированы результаты выше узла).
Таким образом, чтобы отрицательный оператор в индексе RUM работал корректно, есть две возможности:
1. Аналогично GIN: все операнды с весами должны выдавать неопределенный результат сравнения — maybe, который в корне дерева вызовет функцию перепроверки.
Это позволило избавиться от потери результатов как в GIN/GiST (см. выше). Но если результат запроса с весами в любом случае неопределенный и требует перепроверки, то и селекция весов в функции проверки операнда не нужна: она может ускорить поиск, избавив нас от перепроверки результатов с весами.
2. Отрицательный оператор не должен инвертировать yes на no, если в листьях выше узла есть операнды с весами.
Во этом случае можно выбирать операнды входящие с правильными весами, если ближе к корню дерева от операндов с весами не будет оператора ! (например C:d & ! (A & B) ). Совершенно корректным будет передавать от операнда до корня дерева yes, если хотя бы в одной позиции внутри лексемы вес соответствует операнду из запроса и no: если лексема отсутствует или ни одна из позиций не входит с правильным весом. Такие запросы с весами, но без отрицаний выдают точный результат, который не требуют перепроверки, что ускоряет поиск.
Оператор ! должен выдавать maybe, если хотя бы один из операндов внутри него содержат веса, а если нет — то можно обычным образом инвертировать результат. Проверка в промежуточном узле дерева, есть ли веса в операндах ближе к листьям, выглядит некоторым превышением полномочий функции логического перебора только операторов (которая по умолчанию не знает ничего об операндах). Но технически это не сложно и позволяет не замедлять перепроверками запросы, содержащими оператор !, но не содержащими весов в операндах ближе него к листьям (например, запросы вида C:d & !(A & B) )
Технически для этого функции, реализующие логическое дерево перебора должны быть перенесены из PostgreSQL в RUM, чтобы логика, измененная в них и действительная для индекса RUM (содержащего информацию о весах) не влияла на другие индексы.
Таким образом, получилось существенное ускорение в положительных запросах с весами из-за того, что они не требуют перепроверки. Все запросы без весов имеют такую же скорость, как и раньше. Запросы, в которых операнд с весом стоит “внутри” оператора ! должны быть перепроверены аналогично GIN (новому, корректному поведению, которое не теряет результаты).
(Правильное поведение RUM никак не зависит от того, внесено ли изменение в PostgreSQL https://github.com/postgres/postgres/commit/2f2007fbb255be178aca586780967f43885203a7, он будет правильно работать и с более ранними и с новыми версиями)
Тесты я делал на обыкновенном ноутбуке (i7 2300Mhz 16Mb RAM, HDD) по базе рассылок постгрес-разработчиков с 1998 по 2016 годы построив по ним три индекса: GIN, RUM до изменений (https://github.com/postgrespro/rum/commit/bc917c9f0d667432412df998a3fe6b6c935b3053) и RUM после изменений (самый свежий коммит на сегодня, совместимый с 13 версией Постгрес https://github.com/postgrespro/rum/commit/3d331aa8faaff06e48172084201f1907d6eed471 )
Импортируем базу данных
psql -d postgres -c '\i ~/Downloads/pglist-28-04-16.dump.gz'
Удалим старый(если есть), соберем новый модуль RUM и подключим его:
psql -d postgres -c 'DROP EXTENSION RUM CASCADE;'
cd ~/rum
make USE_PGXS=1 distclean
make USE_PGXS=1
make USE_PGXS=1 install
psql -d postgres -c 'CREATE EXTENSION RUM;'
Построим индексы:
CREATE INDEX pglist_fts_idx ON pglist USING gin (fts);
CREATE INDEX pglist_rum_idx ON pglist USING rum (fts rum_tsvector_ops);
SET enable_seqscan=off;
(Если мы хотим отключить все индексы кроме одного, чтобы протестировать выборку именно по нему, а не как решит планировщие Постгрес, удобно использовать такую команду для отключения индекса update pg_index set indisvalid = false where indexrelid = 'pglist_rum_idx'::regclass; )
Сделаем запрос с примерно 50% селективностью по меткам.
explain analyse SELECT * FROM pglist WHERE fts @@ to_tsquery('pg_catalog.english', 'Bartunov:bc & Oleg:c');
GIN ----------
Bitmap Heap Scan on pglist (cost=44.20..143.59 rows=25 width=1242) (actual time=2.735..90.144 rows=2567 loops=1)
Recheck Cond: (fts @@ '''bartunov'':BC & ''oleg'':C'::tsquery)
Rows Removed by Index Recheck: 2111
Heap Blocks: exact=3750
-> Bitmap Index Scan on pglist_fts_idx (cost=0.00..44.19 rows=25 width=0) (actual time=1.954..1.954 rows=4678 loops=1)
Index Cond: (fts @@ '''bartunov'':BC & ''oleg'':C'::tsquery)
Planning Time: 4.533 ms
Execution Time: 90.686 ms
RUM(orig) ----------
Index Scan using pglist_rum_idx on pglist (cost=48.00..152.44 rows=25 width=1242) (actual time=3.143..95.514 rows=2567 loops=1)
Index Cond: (fts @@ '''bartunov'':BC & ''oleg'':C'::tsquery)
Rows Removed by Index Recheck: 2111
Planning Time: 3.158 ms
Execution Time: 96.090 ms
RUM(mod) ----------
Index Scan using pglist_rum_idx on pglist (cost=48.00..152.44 rows=25 width=1242) (actual time=3.541..21.324 rows=2567 loops=1)
Index Cond: (fts @@ '''bartunov'':BC & ''oleg'':C'::tsquery)
Planning Time: 3.076 ms
Execution Time: 21.736 ms
Выигрыш в 4 раза! Видно, что время, сэкономленное на перепроверке, гораздо больше потраченного на множественное сравнение позиций в узлах дерева запроса. Но здесь число позиций в индексе по каждой лексеме (при равном количестве операндов оно примерно пропорционально общему количеству совпадений) не очень велико.
Попробуем сделать хуже: более многопозиционный по количеству меток в каждой лексеме из запроса. Селективность по результатам прежняя — около 50%.
explain analyse SELECT * FROM pglist WHERE fts @@ to_tsquery('pg_catalog.english', 'Tom:b & lane:c');
GIN -----------
Bitmap Heap Scan on pglist (cost=527.41..111288.90 rows=52569 width=1242) (actual time=84.123..1792.180 rows=103610 loops=1)
Recheck Cond: (fts @@ '''tom'':B & ''lane'':C'::tsquery)
Rows Removed by Index Recheck: 119203
Heap Blocks: exact=105992
-> Bitmap Index Scan on pglist_fts_idx (cost=0.00..514.27 rows=52569 width=0) (actual time=61.065..61.065 rows=222813 loops=1)
Index Cond: (fts @@ '''tom'':B & ''lane'':C'::tsquery)
Planning Time: 3.210 ms
Execution Time: 1808.157 ms
RUM(orig) ----------
Bitmap Heap Scan on pglist (cost=535.41..111296.90 rows=52569 width=1242) (actual time=156.136..1870.167 rows=103610 loops=1)
Recheck Cond: (fts @@ '''tom'':B & ''lane'':C'::tsquery)
Rows Removed by Index Recheck: 119203
Heap Blocks: exact=105992
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..522.27 rows=52569 width=0) (actual time=132.542..132.542 rows=222813 loops=1)
Index Cond: (fts @@ '''tom'':B & ''lane'':C'::tsquery)
Planning Time: 3.135 ms
Execution Time: 1887.881 ms
RUM(mod) ----------
Bitmap Heap Scan on pglist (cost=535.41..111296.90 rows=52569 width=1242) (actual time=151.851..486.524 rows=103610 loops=1)
Recheck Cond: (fts @@ '''tom'':B & ''lane'':C'::tsquery)
Heap Blocks: exact=71165
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..522.27 rows=52569 width=0) (actual time=137.607..137.607 rows=103610 loops=1)
Index Cond: (fts @@ '''tom'':B & ''lane'':C'::tsquery)
Planning Time: 3.203 ms
Execution Time: 498.604 ms
Выигрыш времени в 3.5 раза.
Фразовый поиск с низкой селективностью. Почти 100% выбранных в индексе данных удовлетворяют запросу.
explain analyse SELECT * FROM pglist WHERE fts @@ to_tsquery('pg_catalog.english', 'Tom <-> lane');
GIN -----------
Bitmap Heap Scan on pglist (cost=527.41..111288.90 rows=52569 width=1242) (actual time=85.355..1857.377 rows=222777 loops=1)
Recheck Cond: (fts @@ '''tom'' <-> ''lane'''::tsquery)
Rows Removed by Index Recheck: 36
Heap Blocks: exact=105992
-> Bitmap Index Scan on pglist_fts_idx (cost=0.00..514.27 rows=52569 width=0) (actual time=61.188..61.188 rows=222813 loops=1)
Index Cond: (fts @@ '''tom'' <-> ''lane'''::tsquery)
Planning Time: 3.256 ms
Execution Time: 1887.208 ms
RUM (orig) ----------
BBitmap Heap Scan on pglist (cost=535.41..111296.90 rows=52569 width=1242) (actual time=186.795..716.592 rows=222777 loops=1)
Recheck Cond: (fts @@ '''tom'' <-> ''lane'''::tsquery)
Heap Blocks: exact=105978
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..522.27 rows=52569 width=0) (actual time=163.717..163.717 rows=222777 loops=1)
Index Cond: (fts @@ '''tom'' <-> ''lane'''::tsquery)
Planning Time: 4.075 ms
Execution Time: 740.540 ms
RUM (mod) ----------
Bitmap Heap Scan on pglist (cost=535.41..111296.90 rows=52569 width=1242) (actual time=187.515..699.380 rows=222777 loops=1)
Recheck Cond: (fts @@ '''tom'' <-> ''lane'''::tsquery)
Heap Blocks: exact=105978
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..522.27 rows=52569 width=0) (actual time=162.768..162.768 rows=222777 loops=1)
Index Cond: (fts @@ '''tom'' <-> ''lane'''::tsquery)
Planning Time: 3.187 ms
Execution Time: 723.066 ms
Потеря времени в GIN идет на перепроверку. Она зависит от абсолютного числа перепроверяемых запросов, а от селективности не зависит (это видно из сравнения с предыдущим результатом). Так как этот запрос не содержит весов, он одинаково выполняется и в модифицированном, и в немодифицированном RUM.
Контрольный запрос: чисто логический без меток. C ним перепроверка не нужна ни одному из трех методов. GIN показывает немного лучший результат за счет меньшего размера индекса.
explain analyse SELECT * FROM pglist WHERE fts @@ to_tsquery('pg_catalog.english', 'Tom & lane');
GIN -----------
Bitmap Heap Scan on pglist (cost=527.41..111288.90 rows=52569 width=1242) (actual time=88.311..615.489 rows=222813 loops=1)
Recheck Cond: (fts @@ '''tom'' & ''lane'''::tsquery)
Heap Blocks: exact=105992
-> Bitmap Index Scan on pglist_fts_idx (cost=0.00..514.27 rows=52569 width=0) (actual time=65.490..65.490 rows=222813 loops=1)
Index Cond: (fts @@ '''tom'' & ''lane'''::tsquery)
Planning Time: 2.989 ms
Execution Time: 634.919 ms
RUM(orig) -----------
Bitmap Heap Scan on pglist (cost=535.41..111296.90 rows=52569 width=1242) (actual time=162.540..683.670 rows=222813 loops=1)
Recheck Cond: (fts @@ '''tom'' & ''lane'''::tsquery)
Heap Blocks: exact=105992
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..522.27 rows=52569 width=0) (actual time=134.477..134.477 rows=222813 loops=1)
Index Cond: (fts @@ '''tom'' & ''lane'''::tsquery)
Planning Time: 3.116 ms
Execution Time: 705.019 ms
RUM(mod) -----------
Bitmap Heap Scan on pglist (cost=535.41..111296.90 rows=52569 width=1242) (actual time=173.801..682.293 rows=222813 loops=1)
Recheck Cond: (fts @@ '''tom'' & ''lane'''::tsquery)
Heap Blocks: exact=105992
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..522.27 rows=52569 width=0) (actual time=150.079..150.079 rows=222813 loops=1)
Index Cond: (fts @@ '''tom'' & ''lane'''::tsquery)
Planning Time: 2.926 ms
Execution Time: 704.734 ms
Мы помним, что отрицательный оператор, внутри которого есть операнды с весами обязан быть перепроверен. Оригинальный RUM справился быстрее всех, но нашел всего половину результатов! Ниже результат перепроверки последовательным поиском из которого убеждаемся, что именно старое поведение было неправильным, а новое — правильное.
explain analyse SELECT * FROM pglist WHERE fts @@ to_tsquery('pg_catalog.english', 'Oleg & !Bartunov:c');
GIN -----------
Bitmap Heap Scan on pglist (cost=87.09..17440.24 rows=5044 width=1242) (actual time=3.171..120.598 rows=3961 loops=1)
Recheck Cond: (fts @@ '''oleg'' & !''bartunov'':C'::tsquery)
Rows Removed by Index Recheck: 2563
Heap Blocks: exact=5110
-> Bitmap Index Scan on pglist_fts_idx (cost=0.00..85.83 rows=5044 width=0) (actual time=2.398..2.398 rows=6524 loops=1)
Index Cond: (fts @@ '''oleg'' & !''bartunov'':C'::tsquery)
Planning Time: 3.100 ms
Execution Time: 121.402 ms
RUM(orig) -----------
Bitmap Heap Scan on pglist (cost=95.09..17448.24 rows=5044 width=1242) (actual time=3.341..38.289 rows=1846 loops=1)
Recheck Cond: (fts @@ '''oleg'' & !''bartunov'':C'::tsquery)
Heap Blocks: exact=1485
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..93.83 rows=5044 width=0) (actual time=3.123..3.123 rows=1846 loops=1)
Index Cond: (fts @@ '''oleg'' & !''bartunov'':C'::tsquery)
Planning Time: 4.012 ms
Execution Time: 38.774 ms
RUM(mod) -----------
Bitmap Heap Scan on pglist (cost=95.09..17448.24 rows=5044 width=1242) (actual time=4.802..113.275 rows=3961 loops=1)
Recheck Cond: (fts @@ '''oleg'' & !''bartunov'':C'::tsquery)
Rows Removed by Index Recheck: 2563
Heap Blocks: exact=5110
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..93.83 rows=5044 width=0) (actual time=4.016..4.016 rows=6524 loops=1)
Index Cond: (fts @@ '''oleg'' & !''bartunov'':C'::tsquery)
Planning Time: 2.971 ms
Execution Time: 114.117 ms
SET enable_seqscan=on;
SET enable_indexscan=off;
SET enable_indexonlyscan=off;
Gather (cost=1000.00..171915.45 rows=5044 width=1242) (actual time=1.023..2141.792 rows=3961 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on pglist (cost=0.00..170411.05 rows=2102 width=1242) (actual time=0.777..2057.403 rows=1320 loops=3)
Filter: (fts @@ '''oleg'' & !''bartunov'':C'::tsquery)
Rows Removed by Filter: 336603
Planning Time: 3.030 ms
Execution Time: 2142.293 ms
Отрицательный оператор, внутри которого нет операндов с весами перепроверки не требует. Выигрыш времени по сравнению со старым RUM — в три раза. Cелективность этого запроса по весам около 30%.
explain analyse SELECT * FROM pglist WHERE fts @@ to_tsquery('pg_catalog.english', 'Oleg:c & !Bartunov');
GIN -----------
Bitmap Heap Scan on pglist (cost=87.09..17440.24 rows=5044 width=1242) (actual time=1.333..35.188 rows=547 loops=1)
Recheck Cond: (fts @@ '''oleg'':C & !''bartunov'''::tsquery)
Rows Removed by Index Recheck: 1299
Heap Blocks: exact=1485
-> Bitmap Index Scan on pglist_fts_idx (cost=0.00..85.83 rows=5044 width=0) (actual time=1.139..1.139 rows=1846 loops=1)
Index Cond: (fts @@ '''oleg'':C & !''bartunov'''::tsquery)
Planning Time: 2.902 ms
Execution Time: 35.359 ms
RUM(orig) -----------
Bitmap Heap Scan on pglist (cost=95.09..17448.24 rows=5044 width=1242) (actual time=3.055..37.500 rows=547 loops=1)
Recheck Cond: (fts @@ '''oleg'':C & !''bartunov'''::tsquery)
Rows Removed by Index Recheck: 1299
Heap Blocks: exact=1485
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..93.83 rows=5044 width=0) (actual time=2.861..2.861 rows=1846 loops=1)
Index Cond: (fts @@ '''oleg'':C & !''bartunov'''::tsquery)
Planning Time: 2.951 ms
Execution Time: 37.808 ms
RUM(mod) -----------
Bitmap Heap Scan on pglist (cost=95.09..17448.24 rows=5044 width=1242) (actual time=5.921..12.629 rows=547 loops=1)
Recheck Cond: (fts @@ '''oleg'':C & !''bartunov'''::tsquery)
Heap Blocks: exact=476
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..93.83 rows=5044 width=0) (actual time=5.770..5.770 rows=547 loops=1)
Index Cond: (fts @@ '''oleg'':C & !''bartunov'''::tsquery)
Planning Time: 3.126 ms
Execution Time: 12.947 ms
И, наконец, запрос с фразовым поиском, с метками и селективностью около 10%. Новый RUM обрабатывает его в 8 раз быстрее старого!
explain analyse SELECT * FROM pglist WHERE fts @@ to_tsquery('pg_catalog.english', 'Oleg:c <-> !Bartunov');
GIN -----------
Bitmap Heap Scan on pglist (cost=87.09..17440.24 rows=5044 width=1242) (actual time=2.635..116.817 rows=551 loops=1)
Recheck Cond: (fts @@ '''oleg'':C <-> !''bartunov'''::tsquery)
Rows Removed by Index Recheck: 5973
Heap Blocks: exact=5110
-> Bitmap Index Scan on pglist_fts_idx (cost=0.00..85.83 rows=5044 width=0) (actual time=1.935..1.935 rows=6524 loops=1)
Index Cond: (fts @@ '''oleg'':C <-> !''bartunov'''::tsquery)
Planning Time: 3.706 ms
Execution Time: 117.020 ms
RUM(orig) -----------
Bitmap Heap Scan on pglist (cost=95.09..17448.24 rows=5044 width=1242) (actual time=5.585..103.667 rows=551 loops=1)
Recheck Cond: (fts @@ '''oleg'':C <-> !''bartunov'''::tsquery)
Rows Removed by Index Recheck: 4447
Heap Blocks: exact=4081
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..93.83 rows=5044 width=0) (actual time=4.965..4.965 rows=4998 loops=1)
Index Cond: (fts @@ '''oleg'':C <-> !''bartunov'''::tsquery)
Planning Time: 3.306 ms
Execution Time: 103.998 ms
RUM(mod) -----------
Bitmap Heap Scan on pglist (cost=95.09..17448.24 rows=5044 width=1242) (actual time=4.116..8.386 rows=551 loops=1)
Recheck Cond: (fts @@ '''oleg'':C <-> !''bartunov'''::tsquery)
Heap Blocks: exact=480
-> Bitmap Index Scan on pglist_rum_idx (cost=0.00..93.83 rows=5044 width=0) (actual time=4.034..4.034 rows=551 loops=1)
Index Cond: (fts @@ '''oleg'':C <-> !''bartunov'''::tsquery)
Planning Time: 3.062 ms
Execution Time: 8.693 ms
Обработка меток лексем внутри индекса RUM позволяет избежать существенной потери времени на перепроверку результатов. Полнотекстовый поиск с разнородными лексемами в одном индексе в результате модификации ускоряется в несколько раз. Чем выше селективность запроса по меткам, тем больше будет выигрыш времени. На запросах без меток или в случае, когда операнд с меткой стоит внутри отрицания и должен быть перепроверен модификация работает так же, как оригинальный метод RUM: в случае фразового оператора — в разы быстрее, чем GIN, в случае чисто логических операторов — примерно на 10% медленнее.
Кроме того новое поведение индексов RUM и GIN в отличие от старого корректно выдает результаты при использовании оператора ! с операндами, которые содержат отбор по весам.