Реализация поиска по синонимам для веб-приложения
Синонимы расширяют покрытие поиска: пользователь ищет "ноутбук" — находит результаты с "лэптоп" и "notebook". Без синонимов поиск привязан к конкретным словоформам и теряет релевантные результаты.
PostgreSQL: thesaurus словарь
PostgreSQL FTS поддерживает thesaurus — файл с правилами замены слов при индексировании.
Создаём файл /etc/postgresql/14/main/thesaurus_ru.ths:
# Синтаксис: входные слова : заменяется на
ноутбук лэптоп notebook : ноутбук
смартфон телефон мобильник : смартфон
наушники headphones : наушники
телевизор тв tv : телевизор
холодильник фридж : холодильник
стиральная машина стиралка : стиральная машина
Создаём конфигурацию текстового поиска:
-- Создаём thesaurus dictionary
CREATE TEXT SEARCH DICTIONARY thesaurus_ru (
TEMPLATE = thesaurus,
DictFile = thesaurus_ru,
Dictionary = russian_ispell -- базовый словарь для нормализации входных слов
);
-- Создаём конфигурацию поиска
CREATE TEXT SEARCH CONFIGURATION search_ru (COPY = russian);
-- Применяем thesaurus к существительным и другим токенам
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 thesaurus: синонимы применяются только при индексировании, не при поиске. Это значит, что при добавлении нового синонима нужно переиндексировать данные.
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 synonyms):
{
"query": {
"bool": {
"should": [
{
"match": {
"title": {
"query": "ноутбук",
"analyzer": "ru_with_synonyms"
}
}
}
]
}
}
}
Query-time synonyms гибче: не нужна переиндексация при изменении словаря.
Вариант 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'],
'лэптоп': ['ноутбук', 'notebook', 'laptop'],
'notebook': ['ноутбук', 'лэптоп', 'laptop'],
'laptop': ['ноутбук', 'лэптоп', 'notebook'],
'смартфон': ['телефон', 'мобильник', 'мобильный телефон'],
'телефон': ['смартфон', 'мобильник'],
'наушники': ['headphones', 'earphones', 'гарнитура'],
'headphones': ['наушники', 'earphones'],
'тв': ['телевизор', 'tv'],
'телевизор': ['тв', 'tv'],
'tv': ['тв', 'телевизор'],
})
Meilisearch применяет синонимы при поиске — переиндексация не нужна. Словарь обновляется через API за секунды.
Управление словарём синонимов
Синонимы должны быть управляемы бизнесом, а не только разработчиками. Простой admin-интерфейс:
# api/synonyms.py (FastAPI)
from fastapi import APIRouter, Depends
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 день.







