Полнотекстовый поиск в PostgreSQL за миллисекунды Коротков А.Е, Бартунов О.С. Полнотекстовый поиск в базе данных: задача Найти документы, которые удовлетворяют запросу Вернуть результаты в порядке релевантности Полнотестовый поиск в базе данных: требования Интеграция с ядром СУБД Поддержка транзакций Конкуретность, recovery Обновление индекса «online» Поддержка языка Расширяемость, масштабируемость Что такое документ? Произвольный текстовый атрибут Комбинация текстовых атрибутов Может быть полностью вирутальным. Например, результатом SQL объединения таблиц doc и autor Title || Abstract || Keywords || Body || Author Операторы полнотекстового поиска Традиционные FTS операторы для атрибутов LIKE, ILIKE, ~, ~* Проблемы – Отсутствие поддержки языка (стемминг, стоп слова) – Отсутствие ранжирования – Последовательное сканирование документов Решение – Предварительная обработка документов – Поддержка индексов FTS в PostgreSQL набор правил по преобразованию документа в его FTS представление – tsvector, tsquery набор функций для получения tsvector, tsquery из текста FTS операторы и индексы функции ранжирования, подсветки результатов FTS в PostgreSQL =# select 'a fat cat sat on a mat and ate a fat rat'::tsvector @@ 'cat & rat':: tsquery; – tsvector – представление документа, оптимизированное для поиска • отсортированный массив лексем • позиции и вес лексем – tsquery – тип данные для полнотекстового запроса • булевы операторы - & | ! () – поисковый оператор tsvector @@ tsquery Возможности FTS Полная интеграция PostgreSQL 27 встроенный конфигураций для 10 языков Поддержка пользовательских конфигураций Встраиваемые словари (ispell, snowball, thesaurus), парсеры Ранжирование по релевантности GiST и GIN индексы с поддержкой concurrency и recovery Богатый язык запросов с поддержкой перезаписывания запросов FTS в PostgreSQL OpenFTS — 2000, Pg как хранилище GiST index — 2000, спасибо Rambler Tsearch — 2001, contrib:без ранжирования Tsearch2 — 2003, contrib:config GIN —2006, спасибо JFG Networks FTS — 2006, в ядре, спасибо EnterpriseDB E-FTS — Enterprise FTS, спасибо??? Накладные расходы на ACID велики Внешние решения: Sphinx, Solr, Lucene.... Скачивание БД в «поисковый движок» (задержка) Затруднен доступ к атрибутам Дополнительная сложность НО: Очень быстро ! Можно ли ускорить встроенный FTS ? Можно ли ускорить встроенный FTS ? 1.Поиск релевантных документов: Index scan — как правило, довольно быстро 2.Расчет релевантности: Heap scan — как правило, медленно 3.Сортировка документов Можно ли ускорить встроенный FTS ? 156676 статей Wikipedia: postgres=# explain analyze SELECT docid, ts_rank(text_vector, to_tsquery('english', 'title')) AS rank FROM ti2 WHERE text_vector @@ to_tsquery('english', 'title') ORDER BY rank DESC LIMIT 3; Limit (cost=8087.40..8087.41 rows=3 width=282) (actual time=433.750..433.752 rows= -> Sort (cost=8087.40..8206.63 rows=47692 width=282) (actual time=433.749..433. Sort Key: (ts_rank(text_vector, '''titl'''::tsquery)) Sort Method: top-N heapsort Memory: 25kB -> Bitmap Heap Scan on ti2 (cost=529.61..7470.99 rows=47692 width=282) (a Recheck Cond: (text_vector @@ '''titl'''::tsquery) -> Bitmap Index Scan on ti2_index (cost=0.00..517.69 rows=47692 wid Index Cond: (text_vector @@ '''titl'''::tsquery) Total runtime: 433.787 ms Можно ли ускорить встроенный FTS ? 156676 статей Wikipedia: postgres=# explain analyze SELECT docid, ts_rank(text_vector, to_tsquery('english', 'title')) AS rank FROM ti2 WHERE text_vector @@ to_tsquery('english', 'title') ORDER BY text_vector>< plainto_tsquery('english','title') LIMIT 3; Если бы был такой план Limit -> (cost=20.00..21.65 rows=3 width=282) (actual time=18.376..18.427 rows=3 loops Index Scan using ti2_index on ti2 (cost=20.00..26256.30 rows=47692 width=282 Index Cond: (text_vector @@ '''titl'''::tsquery) Order By: (text_vector >< '''titl'''::tsquery) Total runtime: 18.511 ms то было бы неплохо! Было бы неплохо Обучить индекс (GIN) считать релевантность и возвращать документы упорядоченно Хранить позиции лексем в индесу — больше не нужна колонка tsvecotr Использовать компрессию Изменить алгоритмы и интерфейсы Оптимизировать случай редкое_слово & частое_слово Инвертированный индекс Инвертированный индекс QUERY: compensation accelerometers INDEX: accelerometers 5,10,25,28,30,36,58,59,61,73,74 RESULT: 30 compensation 30,68 Инвертированный индекс в PostgreSQL E N T R Y Posting list Posting tree T R E E Нет позиционной информации в индексе ! Список изменений • GIN – способ хранения – алгоритм поиска – поддержка ORDER BY – изменения интерфейса • Планировщик Изменение структуры GIN Дополнительная информация (позиции слов) ItemPointer typedef struct ItemPointerData { BlockIdData ip_blkid; OffsetNumber ip_posid; } typedef struct BlockIdData { uint16 bi_hi; uint16 bi_lo; } BlockIdData; 6 bytes WordEntryPos /* * Equivalent to * typedef struct { * uint16 * weight:2, * pos:14; * } */ typedef uint16 WordEntryPos; 2 bytes Varbyte сжатие BlockIdData Varbyte сжатие OffsetNumber O0-O15 – биты OffsetNumber N – NULL бит дополнительной информаци Varbyte сжатие WordEntryPos P0-P13 – биты позиции W0,W1 – биты веса Пример Top-N запросы 1. Сканирование + вычисление релевантности 2. Сортировка 3. Возвращение результатов по одному с помощью gingettuple Быстрое сканирование entry1 && entry2 Изменения интерфейса GIN extractValue Datum *extractValue ( Datum itemValue, int32 *nkeys, bool **nullFlags, Datum *addInfo, bool *addInfoIsNull ) extractQuery Datum *extractValue ( Datum query, int32 *nkeys, StrategyNumber n, bool **pmatch, Pointer **extra_data, bool **nullFlags, int32 *searchMode, ???bool **required??? ) consistent bool consistent ( bool check[], StrategyNumber n, Datum query, int32 nkeys, Pointer extra_data[], bool *recheck, Datum queryKeys[], bool nullFlags[], Datum addInfo[], bool addInfoIsNull[] ) calcRank float8 calcRank ( bool check[], StrategyNumber n, Datum query, int32 nkeys, Pointer extra_data[], bool *recheck, Datum queryKeys[], bool nullFlags[], Datum addInfo[], bool addInfoIsNull[] ) ???joinAddInfo??? Datum joinAddInfo ( Datum addInfos[] ) Оптимзация для планировщика До test=# EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM test ORDER BY slow_func(x,y) LIMIT 10; ---------------------------------------------------Limit (cost=0.00..3.09 rows=10 width=16) (actual t Output: x, y, (slow_func(x, y)) -> Index Scan using test_idx on public.test (co Output: x, y, slow_func(x, y) Total runtime: 103.524 ms (5 rows) После test=# EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM test ORDER BY slow_func(x,y) LIMIT 10; ---------------------------------------------------Limit (cost=0.00..3.09 rows=10 width=16) (actual t Output: x, y -> Index Scan using test_idx on public.test (co Output: x, y Total runtime: 0.164 ms (5 rows) Результаты тестирования avito.ru: 6.7 млн. документов С колонкой tsvector, без патча SELECT itemid, title FROM items WHERE fts @@ plainto_tsquery('russian', 'квартира') ORDER BY ts_rank(fts, plainto_tsquery('russian', 'квартира')) DESC LIMIT 10; С колонкой tsvector, без патча Limit (cost=729272.24..729272.26 rows=10 width=398) (actu Buffers: shared hit=696232 -> Sort (cost=729272.24..731294.81 rows=809028 width=3 Sort Key: (ts_rank(fts, '''квартир'''::tsquery)) Sort Method: top-N heapsort Memory: 26kB Buffers: shared hit=696232 -> Bitmap Heap Scan on items (cost=8661.97..7117 Recheck Cond: (fts @@ '''квартир'''::tsquery Buffers: shared hit=696232 -> Bitmap Index Scan on fts_idx (cost=0.00 Index Cond: (fts @@ '''квартир'''::tsq Buffers: shared hit=612 Total runtime: 1871.349 ms С колонкой tsvector, с патчем SELECT itemid, title FROM items WHERE fts @@ plainto_tsquery('russian', 'квартира') ORDER BY fts >< plainto_tsquery('russian', 'квартира') LIMIT 10; С колонкой tsvector, с патчем Limit (cost=20.00..59.46 rows=10 width=4 -> Index Scan using fts_idx on items Index Cond: (fts @@ '''квартир''' Order By: (fts >< '''квартир''':: Total runtime: 143.952 ms Без колонки tsvector, без патча SELECT itemid, title FROM items2 WHERE (setweight(to_tsvector('russian'::regconfig ORDER BY ts_rank((setweight(to_tsvector('russian' LIMIT 10; Без колонки tsvector, без патча Limit (cost=749132.39..749132.41 rows=10 width=372) (actu Buffers: shared hit=485458 -> Sort (cost=749132.39..751145.79 rows=805360 width=3 Sort Key: (ts_rank((setweight(to_tsvector('russian Sort Method: top-N heapsort Memory: 26kB Buffers: shared hit=485458 -> Bitmap Heap Scan on items2 (cost=8625.55..731 Recheck Cond: ((setweight(to_tsvector('russi Buffers: shared hit=485458 -> Bitmap Index Scan on fts_idx2 (cost=0.0 Index Cond: ((setweight(to_tsvector('r Buffers: shared hit=612 Total runtime: 52685.595 ms Без колонки tsvector, с патчем SELECT itemid, title FROM items2 WHERE (setweight(to_tsvector('russian'::re ORDER BY (setweight(to_tsvector('russian': LIMIT 10; Без колонки tsvector, с патчем Limit (cost=20.02..59.61 rows=10 width=373) (ac Buffers: shared hit=1556 -> Index Scan using fts_idx2 on items2 (cost Index Cond: ((setweight(to_tsvector('rus Order By: ((setweight(to_tsvector('russi Buffers: shared hit=1556 Total runtime: 143.639 ms C колонкой tsvector, без патча SELECT itemid, title FROM items WHERE fts @@ plainto_tsquery('russian', 'квартира арбат') ORDER BY ts_rank(fts, plainto_tsquery('russian', 'квартира арбат')) DESC LIMIT 10; C колонкой tsvector, без патча Limit (cost=6908.03..6908.05 rows=10 width=398) (actual t Buffers: shared hit=1314 -> Sort (cost=6908.03..6912.44 rows=1766 width=398) (a Sort Key: (ts_rank(fts, '''квартир'' & ''арбат''': Sort Method: top-N heapsort Memory: 26kB Buffers: shared hit=1314 -> Bitmap Heap Scan on items (cost=61.69..6869.8 Recheck Cond: (fts @@ '''квартир'' & ''арбат Buffers: shared hit=1314 -> Bitmap Index Scan on fts_idx (cost=0.00 Index Cond: (fts @@ '''квартир'' & ''а Buffers: shared hit=616 Total runtime: 92.069 ms C колонкой tsvector, с патчем SELECT itemid, title FROM items WHERE fts @@ plainto_tsquery('russian', 'квартира арбат') ORDER BY fts >< plainto_tsquery('russian', 'квартира арбат') LIMIT 10; C колонкой tsvector, с патчем Limit (cost=40.00..80.22 rows=10 width=400) (ac Buffers: shared hit=1236 -> Index Scan using fts_idx on items (cost=4 Index Cond: (fts @@ '''квартир'' & ''арб Order By: (fts >< '''квартир'' & ''арбат Buffers: shared hit=1236 Total runtime: 1.579 ms avito.ru: тесты Без патча С патчем С пачем без tsvector Sphinx Размер таблицы 6.0 GB 6.0 GB 2.87 GB - Размер индекса 1.29 GB 1.27 GB 1.27 GB 1.12 GB 216 с 303 с 718 с 180 с* 3,0 млн. 42.7 млн. 42.7 млн. 32.0 мн. Время созадания индекса Запросов за 8 часов Анонимный источник: 18 млн. документов Анонимный источник: тесты Без патча С патчем С патчем, без tsvector Sphinx Размер таблицы 18.2 GB 18.2 GB 11.9 GB - Размер индекса 2.28 GB 2.30 GB 2.30 GB 3.09 GB 258 с 684 с 1712 с 481 с* 2.67 млн. 38.7 млн. 38.7 млн. 26.7 млн. Время создания индекса Запросов за 8 чаос Положение дел 2 из 4 планируемых патчей на текущем commitfest Спасибо за внимание!