forked from zovos/vk_hackathon
6.7 KiB
6.7 KiB
Объяснение изменений в 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.
- Зачем: контролируем размер ответа и уменьшаем шум.
Итоговый пайплайн (коротко)
- Собираем базовые query:
search_text/text + variants. - Для каждого query делаем dense+sparse retrieval.
- Для
hydeделаем dense-only retrieval. - Объединяем и дедуплицируем кандидатов по point id.
- Делаем rerank для head, сохраняем tail.
- Преобразуем кандидаты в
message_idи агрегируем score. - Берем
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стоит донастроить на локальном наборе регрессионных вопросов.