vk_hackathon/.ai_explain/search_main_explained.md

6.7 KiB
Raw Permalink Blame History

Объяснение изменений в search/main.py

Что мы улучшили

Цель изменений: сделать retrieval стабильнее и точнее, а финальную выдачу управляемой и объяснимой.

Сделаны следующие шаги:

  • основной запрос берется из question.search_text, fallback на question.text;
  • подключены дополнительные запросы из question.variants;
  • подключены dense-only запросы из question.hyde;
  • sparse-запрос строится по question.keywords (если keywords есть);
  • после rerank кандидаты не теряются;
  • финальная выдача строится через агрегацию score по message_id;
  • ответ ограничивается top-50.

Что было раньше

Ранее пайплайн был линейный:

  • один query;
  • один dense и один sparse вектор;
  • retrieval + rerank только для ограниченного количества кандидатов;
  • после rerank часть кандидатов выпадала;
  • message_id выдавались почти напрямую из chunk'ов.

Это делало результат менее устойчивым при перефразировках и могло терять полезные документы.

Что стало и почему это лучше

1) Источник основного query

  • Файл: search/main.py, search(...), строки около 497-503.
  • Логика: collect_query_variants() сначала берет question.search_text, затем fallback на question.text.
  • Зачем: search_text обычно более нормализован для поиска, чем сырой пользовательский вопрос.

2) Дополнительные query-варианты (question.variants)

  • Файл: search/main.py, collect_query_variants(...), строки около 323-344.
  • Логика: варианты очищаются (strip) и дедуплицируются.
  • Зачем: повышает recall, если один вариант формулировки не попал в нужные chunk'и.

3) Dense-only расширение через question.hyde

  • Файл: search/main.py, collect_hyde_queries(...) и qdrant_search_dense_only(...), строки около 347-360 и 274-320.
  • Логика: hyde-запросы добавляют семантических кандидатов без sparse-компоненты.
  • Зачем: помогает доставать семантически близкие фрагменты даже при слабом лексическом совпадении.

4) Sparse-основа через question.keywords

  • Файл: search/main.py, build_sparse_query_text(...), строки около 363-379; использование в search(...) около 509-510.
  • Логика: если keywords переданы, sparse-текст = объединение keywords; иначе fallback на текущий query-вариант.
  • Зачем: sparse-поиск становится более управляемым и фокусным по ключевым терминам.

5) Кандидаты после rerank больше не теряются

  • Файл: search/main.py, rerank_points(...), строки около 440-467.
  • Логика:
    • head до RERANK_LIMIT проходит через внешний reranker;
    • tail сохраняется и добавляется обратно.
  • Зачем: rerank улучшает порядок, но не выбрасывает потенциально полезные кандидаты.

6) Агрегация score по message_id

  • Файл: search/main.py, aggregate_message_scores(...), строки около 470-478.
  • Логика: score всех chunk'ов, относящихся к одному message_id, суммируется.
  • Зачем: если сообщение встретилось в нескольких сильных chunk'ах, оно получает заслуженный приоритет.

7) Ограничение финального ответа top-50

  • Файл: search/main.py, FINAL_TOP_K = 50 (около 178), select_top_message_ids(...) (около 481-486), применение в search(...) (около 525-527).
  • Логика: сортировка по убыванию aggregated score, затем срез до 50.
  • Зачем: контролируем размер ответа и уменьшаем шум.

Итоговый пайплайн (коротко)

  1. Собираем базовые query: search_text/text + variants.
  2. Для каждого query делаем dense+sparse retrieval.
  3. Для hyde делаем dense-only retrieval.
  4. Объединяем и дедуплицируем кандидатов по point id.
  5. Делаем rerank для head, сохраняем tail.
  6. Преобразуем кандидаты в message_id и агрегируем score.
  7. Берем top-50 и возвращаем в results[0].message_ids.

Как объяснить на созвоне (готовый питч)

  • Мы перешли от single-query к multi-query retrieval, чтобы увеличить recall.
  • Разделили роли сигналов: variants для расширения формулировок, hyde для семантики, keywords для лексики.
  • Убрали потерю кандидатов после rerank: rerank теперь переставляет приоритеты, а не режет выдачу.
  • Финальный ранк делаем на уровне message_id, а не chunk, чтобы учитывать вклад нескольких чанков одного сообщения.
  • Ограничили выдачу до 50, чтобы интерфейс и API получали компактный и релевантный список.

На что обратить внимание (ограничения)

  • Сейчас в агрегации используется сумма score; при необходимости можно экспериментировать с max/mean.
  • tail после rerank использует исходный score из Qdrant, он по шкале может отличаться от reranker score.
  • Параметры DENSE_PREFETCH_K, SPRASE_PREFETCH_K, RETRIEVE_K, RERANK_LIMIT стоит донастроить на локальном наборе регрессионных вопросов.