в этой задаче решается этап поиска: по тексту вопроса пользователя найти статьи справки, которые стоит передать дальше в RAG-пайплайн.
двухэтапный гибридный пайплайн: на первой стадии кандидаты параллельно извлекаются лексическим (BM25) и векторным (FAISS) поиском, списки объединяются через Reciprocal Rank Fusion (RRF), затем верхушка объединённого пула переранжируется кросс-энкодером. итоговый топ-10 статей на запрос сохраняется в answer.csv.
тело каждой статьи приходит в HTML. перед индексацией выполняется очистка: блоки <script> и <style> удаляются вместе с содержимым, чтобы их текст не попадал в индекс. оставшаяся разметка конвертируется в Markdown через html2text с сохранением структуры таблиц и списков — заголовки, маркированные и нумерованные списки и строки таблиц остаются отдельными строками, а не склеиваются в один абзац. к телу статьи добавляется заголовок (с повтором), чтобы его слова получали больший вес в term frequency и в эмбеддингах.
- лемматизация:
pymorphy3с мемоизацией черезlru_cache— словарь корпуса сильно повторяется, поэтому большинство обращений идёт из кэша; - кастомные синонимы и стоп-слова: словарь синонимов (
data/synonyms.json) схлопывает доменные синонимы на общий кластерный токен, кастомные стоп-слова (data/stopwords.txt) объединяются со списком NLTK; применяются только в лексической ветке; - лексический индекс:
BM25Okapiпо нормализованным токенам полных статей; - векторная база: FAISS (
IndexFlatIP) по чанкам статей (окна по 1500 символов с перекрытием 200); эмбеддинги L2-нормализуются, поэтому скалярное произведение равно косинусной близости; - би-энкодер:
BAAI/bge-m3— кодирует чанки корпуса и сырой текст запроса; - кросс-энкодер:
BAAI/bge-reranker-v2-m3— переранжирует топ-15 объединённого пула по лучшему чанку каждой статьи; - гибридное слияние: взвешенный RRF (
score = sum(weight / (rrf_k + rank))) по ранжированиям BM25 и векторного поиска; - агрегация чанков: выбор стратегии схлопывания скоров чанков в скор статьи —
max_p(лучший чанк),avg_p(среднее),sum_p(сумма).
качество проверялось локально на размеченном датасете calibration.f: пайплайн делает предсказания для каждого калибровочного запроса, после чего считается метрика MAP@10 — та же, по которой оценивается финальное решение. предсказания сохраняются в формате сабмита, поэтому метрика считается на той же структуре данных, что уходит в ответ. фиксированный seed и воспроизводимое сэмплирование позволяют сравнивать значения между итерациями.
| подход | MAP@10 на calibration.f |
особенности |
|---|---|---|
| baseline | 0,5082 | гибридный поиск, реранкинг, чанкинг по 1500 символов |
главная выявленная проблема — асимметрия языка пользователей и базы знаний: пользователи пишут разговорно («посылка не пришла», «кинули на деньги»), а статьи справки — формальным языком («доставка», «безопасная сделка»). из-за расхождения лексики BM25 не находил релевантные статьи, а плотный поиск вытягивал их недостаточно высоко.
решение — нормализация лексической ветки:
- словарь синонимов: доменные синонимы (товар / заказ / посылка и т. п.) отображаются на один кластерный токен после лемматизации, поэтому BM25 матчит запрос со статьёй по всему кластеру;
- кастомные стоп-слова: разговорные слова-паразиты запросов (здравствуйте, подскажите, пожалуйста и т. п.) не несут сигнала и выкидываются из токенизации, чтобы не размывать скоринг BM25.
убедитесь, что установлен uv.
uv sync
uv run pre-commit installпайплайн полностью автоматизирован и выполняется в main.py: код сам управляет жизненным циклом индексов и стадиями обработки.
export PYTHONUTF8=1 # в Windows cmd: set PYTHONUTF8=1
# запуск решения
uv run main.py
# текстовый отчёт об ошибках на calibration.f
uv run python -m src.analysis
# локальный дашборд phoenix для анализа ошибок
uv run python -m src.launch_dashboard
# текст статьи после этапа обогащения
uv run python -m src.inspect_article <id>для работы пайплайна необходимы три файла в формате Feather:
-
articles.f— база статей справки:article_id— целочисленный идентификатор статьи;title— заголовок статьи;body— текст статьи в HTML. -
calibration.f— размеченные запросы для локальной проверки подхода:query_id— идентификатор запроса;query_text— текст вопроса пользователя;ground_truth— правильныеarticle_id, разделённые пробелами. -
test.f— запросы, для которых нужно подготовить ответ:query_id— идентификатор запроса;query_text— текст вопроса пользователя.
решения оцениваются по MAP@10: для одного запроса считается AP@10 (чем выше в списке релевантные документы, тем больше их вклад), затем значение усредняется по всем запросам. если для запроса есть несколько правильных статей, учитываются все найденные документы, а не только первый.
- при первом старте библиотеки автоматически скачают веса эмбеддера и реранкера с Hugging Face в кэш;
- пути к данным и параметры пайплайна прописаны в
configs/.
