Пошук уроків, статей та іншого контенту
Налаштуєте індексацію повнотекстового пошуку через tsvector і підберете відповідний GIN або GiST індекс.
Повнотекстовий пошук у PostgreSQL працює з двома основними типами даних:
tsvector — нормалізований документ, представлений набором лексем;
tsquery — пошуковий запит, також представлений у формі, придатній для порівняння.
Оператор @@ перевіряє, чи відповідає tsvector заданому tsquery:
search_vector @@ search_queryБез індексу PostgreSQL мусить обчислити tsvector і перевірити кожен рядок таблиці. На великій таблиці це призводить до повного сканування.
Індекс на tsvector дає змогу швидко знаходити документи, які відповідають пошуковому запиту.
tsvectorРозглянемо таблицю статей:
CREATE TABLE articles (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
search_vector tsvector GENERATED ALWAYS AS (
setweight(
to_tsvector('simple'::regconfig, coalesce(title, '')),
'A'
) ||
setweight(
to_tsvector('simple'::regconfig, coalesce(body, '')),
'B'
)
) STORED
);У цьому прикладі:
заголовок і текст перетворюються на tsvector;
заголовок отримує вагу A;
основний текст отримує вагу B;
coalesce захищає від NULL;
STORED означає, що результат зберігається в таблиці й оновлюється під час INSERT або UPDATE.
Ваги використовуються під час ранжування результатів. Вони не змінюють сам факт збігу, але дають змогу вважати збіг у заголовку важливішим за збіг у тексті.
Конфігурація
simpleне виконує стемінг слів. Наприклад, різні форми слова не обов’язково будуть зведені до однієї лексеми. Для потрібної мови слід явно вибрати відповідну конфігурацію, доступну в конкретній інсталяції PostgreSQL.
Додамо тестові дані:
INSERT INTO articles (title, body)
VALUES
(
'Індекси в PostgreSQL',
'GIN індекс прискорює повнотекстовий пошук у великих таблицях.'
),
(
'Транзакції бази даних',
'Транзакції забезпечують узгодженість і надійність операцій.'
),
(
'Пошук у документах',
'Повнотекстовий пошук використовує tsvector і tsquery.'
);Перевірити сформоване поле можна звичайним запитом:
SELECT id, search_vector
FROM articles;Для повнотекстового пошуку найчастіше використовують індекс типу GIN:
CREATE INDEX articles_search_vector_gin_idx
ON articles
USING GIN (search_vector);GIN зберігає відповідність між лексемами та рядками, у яких вони зустрічаються. Це добре підходить для пошуку за tsvector.
Приклад запиту:
SELECT
id,
title,
ts_rank(
search_vector,
websearch_to_tsquery('simple'::regconfig, 'GIN пошук')
) AS rank
FROM articles
WHERE search_vector @@ websearch_to_tsquery(
'simple'::regconfig,
'GIN пошук'
)
ORDER BY rank DESC, id;Умова WHERE використовує індекс, а ts_rank обчислює оцінку відповідності знайдених рядків.
Для запитів, сформованих програмою, часто зручно використовувати websearch_to_tsquery. Вона підтримує звичний синтаксис пошуку, зокрема фрази в лапках і оператор - для виключення слова.
Якщо потрібен простіший контрольований синтаксис, можна використати plainto_tsquery:
SELECT id, title
FROM articles
WHERE search_vector @@ plainto_tsquery(
'simple'::regconfig,
'повнотекстовий пошук'
);Окремий стовпець tsvector не є обов’язковим. Можна створити індекс безпосередньо на виразі:
CREATE TABLE documents (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title text NOT NULL,
body text NOT NULL
);
CREATE INDEX documents_search_gin_idx
ON documents
USING GIN (
to_tsvector(
'simple'::regconfig,
coalesce(title, '') || ' ' || coalesce(body, '')
)
);Запит повинен містити такий самий вираз:
SELECT id, title
FROM documents
WHERE to_tsvector(
'simple'::regconfig,
coalesce(title, '') || ' ' || coalesce(body, '')
) @@ websearch_to_tsquery(
'simple'::regconfig,
'індекси'
);PostgreSQL зможе використати виразний індекс лише тоді, коли вираз у запиті відповідає виразу індексу. Якщо змінити конфігурацію, порядок об’єднання полів або обробку NULL, індекс може не використовуватися.
Окремий стовпець tsvector часто зручніший, тому що:
вираз не потрібно дублювати в кожному запиті;
tsvector можна використовувати для ранжування;
структуру пошукового документа легше перевіряти;
до нього простіше додавати ваги полів.
PostgreSQL підтримує два основні типи індексів для tsvector.
GIN зазвичай є найкращим вибором для таблиць, у яких:
пошук виконується часто;
дані змінюються рідше, ніж читаються;
важлива мінімальна затримка пошуку;
таблиця містить багато документів.
Переваги:
швидке виконання повнотекстових запитів;
добре підходить для великих наборів документів;
є типовим вибором для tsvector.
Недоліки:
індекс може займати більше місця;
вставки та оновлення можуть бути дорожчими;
після великої кількості змін може знадобитися обслуговування індексу.
GiST можна створити так:
CREATE INDEX articles_search_vector_gist_idx
ON articles
USING GIST (search_vector);GiST може бути корисним, коли:
таблиця часто змінюється;
важливіші швидкі вставки й оновлення;
потрібен один тип індексної інфраструктури для різних GiST-сумісних типів;
дещо повільніший пошук є прийнятним.
GiST використовує сигнатури й може повертати приблизні збіги, які PostgreSQL додатково перевіряє за таблицею. Через це він може виконувати більше зайвої роботи, ніж GIN.
У більшості типових сценаріїв повнотекстового пошуку починають із GIN. GiST варто розглядати після вимірювань або для навантажень із великою кількістю змін.
План виконання можна переглянути за допомогою EXPLAIN:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, title
FROM articles
WHERE search_vector @@ websearch_to_tsquery(
'simple'::regconfig,
'GIN пошук'
);У плані для використання GIN зазвичай з’являється операція на кшталт:
Bitmap Index Scan on articles_search_vector_gin_idxНа невеликій таблиці PostgreSQL може навмисно вибрати послідовне сканування, навіть якщо індекс існує. Це нормально: для кількох рядків повний перегляд іноді дешевший.
Оцінювати індекс потрібно на даних, обсязі та запитах, наближених до робочого навантаження.
Для вже заповненої таблиці звичайне створення індексу може блокувати операції запису. У робочому середовищі індекс часто створюють конкурентно:
CREATE INDEX CONCURRENTLY articles_search_vector_gin_idx
ON articles
USING GIN (search_vector);CREATE INDEX CONCURRENTLY дає змогу продовжувати вставки, оновлення й видалення під час побудови індексу, але:
операція триває довше;
створення не можна виконувати всередині транзакції;
під час міграцій потрібно враховувати спеціальні обмеження цього режиму.
Оскільки заголовок у прикладі має вагу A, можна врахувати це під час ранжування:
SELECT
id,
title,
ts_rank_cd(
search_vector,
websearch_to_tsquery('simple'::regconfig, 'PostgreSQL')
) AS rank
FROM articles
WHERE search_vector @@ websearch_to_tsquery(
'simple'::regconfig,
'PostgreSQL'
)
ORDER BY rank DESC;Індекс використовується для відбору рядків, а ts_rank_cd додатково оцінює якість збігу. Ранжування саме по собі не замінює умову @@: без неї запит може обчислювати оцінку для всіх рядків.
Індекс і запит повинні використовувати однакову конфігурацію:
to_tsvector('simple'::regconfig, body)має відповідати:
websearch_to_tsquery('simple'::regconfig, 'запит')Використання різних конфігурацій може призвести до неправильних збігів або неможливості ефективно використати індекс.
Якщо індекс створено для:
to_tsvector(
'simple'::regconfig,
coalesce(title, '') || ' ' || coalesce(body, '')
)то запит із пропущеним coalesce, іншим порядком полів або іншою конфігурацією не є тим самим виразом.
Індекс на title або body типу btree не прискорює умову:
search_vector @@ queryДля повнотекстового пошуку потрібен GIN або GiST-індекс на tsvector чи відповідному виразі.
NULLКонкатенація з NULL дає NULL:
title || ' ' || bodyЯкщо одне з полів може бути NULL, використовуйте:
coalesce(title, '') || ' ' || coalesce(body, '')simpleКонфігурація simple нормалізує текст, але не виконує мовного стемінгу. Якщо пошук має враховувати форми слів, необхідно обрати відповідну текстову конфігурацію та послідовно використовувати її під час формування tsvector і tsquery.
tsvector зберігає нормалізоване представлення документа.
Для пошуку використовується оператор @@ і значення типу tsquery.
GIN зазвичай є найкращим вибором для швидкого читання та великих таблиць.
GiST може бути корисним для навантажень із частими змінами.
Конфігурація текстового пошуку в індексі та запиті повинна збігатися.
Індекс можна створити на окремому стовпці tsvector або безпосередньо на виразі.
EXPLAIN (ANALYZE, BUFFERS) допомагає перевірити, чи використовує PostgreSQL індекс.