Реалізація пошуку по синонімах для веб-додатків
Синоніми розширюють охоплення пошуку: користувач шукає "ноутбук" — знаходить результати з "лаптоп" та "notebook". Без синонімів пошук прив'язаний до конкретних словоформ і втрачає релевантні результати.
PostgreSQL: thesaurus словник
PostgreSQL FTS підтримує thesaurus — файл з правилами заміни слів при індексуванні.
Створюємо /etc/postgresql/14/main/thesaurus_ru.ths:
# Синтаксис: входові слова : замінюється на
ноутбук лаптоп notebook : ноутбук
смартфон телефон мобільник : смартфон
навушники headphones : навушники
телевізор тв tv : телевізор
холодильник фридж : холодильник
Створюємо конфігурацію текстового пошуку:
CREATE TEXT SEARCH DICTIONARY thesaurus_ru (
TEMPLATE = thesaurus,
DictFile = thesaurus_ru,
Dictionary = russian_ispell -- базовий словник для нормалізації входу
);
CREATE TEXT SEARCH CONFIGURATION search_ru (COPY = russian);
ALTER TEXT SEARCH CONFIGURATION search_ru
ALTER MAPPING FOR asciiword, word, numword
WITH thesaurus_ru, russian_stem;
Перевіряємо:
SELECT to_tsvector('search_ru', 'лаптоп Dell з SSD');
-- Результат: 'dell':2 'ноутбук':1 'ssd':4
-- "лаптоп" замінений на "ноутбук"
Оновлюємо індекс з новою конфігурацією:
UPDATE products SET search_vector =
setweight(to_tsvector('search_ru', coalesce(title, '')), 'A') ||
setweight(to_tsvector('search_ru', coalesce(description, '')), 'C');
-- Запит тепер знайде "лаптоп" при пошуку "ноутбук":
SELECT id, title
FROM products
WHERE search_vector @@ plainto_tsquery('search_ru', 'ноутбук');
Обмеження: PostgreSQL застосовує синоніми тільки при індексуванні, не при пошуку. Додавання нового синоніма вимагає переіндексування даних.
Elasticsearch: Synonym Token Filter
Elasticsearch обробляє синоніми як при індексуванні, так і при пошуку (через search_analyzer).
Варіант 1: файл синонімів:
# config/synonyms_ru.txt
ноутбук, лаптоп, notebook
смартфон, телефон, мобільник
навушники, headphones
тв, телевізор, tv
PUT /products
{
"settings": {
"analysis": {
"filter": {
"synonym_ru": {
"type": "synonym",
"synonyms_path": "synonyms_ru.txt",
"updateable": true
},
"russian_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"russian_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"ru_with_synonyms": {
"tokenizer": "standard",
"filter": [
"lowercase",
"russian_stop",
"russian_stemmer",
"synonym_ru"
]
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ru_with_synonyms",
"search_analyzer": "ru_with_synonyms"
}
}
}
}
"updateable": true — оновити синоніми без переіндексування через API:
POST /products/_reload_search_analyzers
Варіант 2: синоніми в запиті (query-time):
{
"query": {
"bool": {
"should": [
{
"match": {
"title": {
"query": "ноутбук",
"analyzer": "ru_with_synonyms"
}
}
}
]
}
}
}
Query-time синоніми гнучкіші: не потрібна переіндексація при зміні словника.
Варіант 3: граф синонімів (synonym_graph) для мультисловних фраз:
{
"filter": {
"synonym_graph_ru": {
"type": "synonym_graph",
"synonyms": [
"стиральна машина => стиралка",
"мобільний телефон => смартфон, мобільник",
"ssd накопичувач => твердотільний диск"
]
}
}
}
synonym_graph коректно обробляє мультисловні синоніми — стандартний synonym ломає позиції токенів при фразовому пошуку.
Meilisearch: вбудовані синоніми
import meilisearch
client = meilisearch.Client('http://localhost:7700', 'masterKey')
index = client.index('products')
# Оновлення словника синонімів
index.update_synonyms({
'ноутбук': ['лаптоп', 'notebook', 'laptop'],
'смартфон': ['телефон', 'мобільник', 'мобільний телефон'],
'навушники': ['headphones', 'earphones', 'гарнітура'],
'тв': ['телевізор', 'tv'],
})
Meilisearch застосовує синоніми при пошуку — переіндексація не потрібна. Словник оновлюється через API за секунди.
Управління словником синонімів
Синоніми повинні керуватися бізнесом, а не тільки розробниками:
# api/synonyms.py (FastAPI)
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter(prefix='/admin/synonyms')
class SynonymGroup(BaseModel):
words: list[str] # усі слова групи — взаємні синоніми
@router.get('/')
async def list_synonyms():
return index.get_synonyms()
@router.put('/')
async def update_synonyms(groups: list[SynonymGroup]):
"""Замінити весь словник синонімів."""
synonym_dict: dict[str, list[str]] = {}
for group in groups:
for word in group.words:
# кожне слово посилається на інші в групі
synonym_dict[word.lower()] = [
w.lower() for w in group.words if w.lower() != word.lower()
]
task = index.update_synonyms(synonym_dict)
return {'task_uid': task.task_uid, 'status': 'accepted'}
@router.delete('/')
async def clear_synonyms():
return index.reset_synonyms()
Тестування синонімів
import pytest
def test_synonym_search():
results_notebook = index.search('ноутбук', {'limit': 5})
results_laptop = index.search('лаптоп', {'limit': 5})
ids_notebook = {h['id'] for h in results_notebook['hits']}
ids_laptop = {h['id'] for h in results_laptop['hits']}
# Результати повинні перетинатися
assert len(ids_notebook & ids_laptop) > 0, (
f"Синоніми не працюють: {ids_notebook} vs {ids_laptop}"
)
Графіки
PostgreSQL thesaurus (словник, конфігурація, переіндексування): 1 день. Elasticsearch з synonym_graph та admin API: 1–2 дні. Meilisearch (синоніми + API управління): половина дня–1 день.







