forked from zovos/vk_hackathon
88 lines
6.7 KiB
Markdown
88 lines
6.7 KiB
Markdown
# Объяснение изменений в `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` стоит донастроить на локальном наборе регрессионных вопросов.
|