Как осуществляется поиск по истории сообщений
Что проверяют
Умеете ли вы разложить поиск на части: как хранить сообщения, как индексировать, как ранжировать и как оценить качество. В задаче на кодинг часто просят собрать простой обратный индекс, а следом спрашивают, что изменится на миллиардах сообщений и как добавить поиск по смыслу.
Как отвечать
- обратный индекс: словарь «термин → отсортированный список id сообщений». Запрос из нескольких слов это пересечение списков, его делают слиянием двумя указателями за O(n + m), начиная с самого короткого списка;
- нормализация: нижний регистр, токенизация, для русского стемминг (Snowball) или лемматизация (
pymorphy3), иначе «заказ» не найдёт «заказы»; - ранжирование: BM25 учитывает частоту слова, его редкость в коллекции и длину сообщения, в Elasticsearch и OpenSearch он стоит по умолчанию. Для истории чата к нему обычно подмешивают свежесть;
- готовые движки: Elasticsearch или OpenSearch, либо PostgreSQL с
tsvectorи GIN-индексом, если объём умеренный и вторую систему заводить не хочется; - смысловой поиск: эмбеддинги сообщений и ANN-индекс (HNSW в
pgvector, FAISS, Qdrant). Гибрид с BM25 объединяют через Reciprocal Rank Fusion, так находятся и точные номера заказов, и перефразы; - масштаб и доступ: шардирование по
chat_idили пользователю, фильтр по правам внутри запроса. Новые сообщения становятся видны послеrefresh, в Elasticsearch по умолчанию раз в секунду; - качество: размеченный набор запросов и метрики recall@k, MRR, nDCG.
Где спотыкаются
Предлагают LIKE '%слово%' по таблице сообщений. Без триграммного индекса такой запрос читает таблицу целиком, а морфологию не понимает вовсе.
Фильтруют права после поиска: берут top-10 и уже потом отсекают чужие чаты. Выдача оказывается почти пустой, а счётчики результатов выдают существование чужих сообщений.
Сразу уходят в эмбеддинги. Чистый векторный поиск плохо находит точные строки вроде номера телефона или артикула, без лексической части он проигрывает.