vk_hackathon/.ai_explain/search_main_explained.md

88 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Объяснение изменений в `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` стоит донастроить на локальном наборе регрессионных вопросов.