vk_hackathon/doc/ai_update.md
2026-04-18 11:13:56 +03:00

290 lines
14 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.

# AI Update по ТЗ
Дата: 2026-04-18
## Что просмотрено
- `doc/ТЗа_хакатон_Индексация_и_поиск_по_сообщениям.pdf`
- `README.md`
- `docker-compose.yml`
- `index/main.py`, `search/main.py`
- `index/Dockerfile`, `search/Dockerfile`
- `index/Makefile`, `search/Makefile`
- `data/Go Nova.json`
По примеру данных:
- всего сообщений: `25`
- с `parts`: `14`
- с цитатами: `5`
- с пересланными сообщениями: `2`
- с `mentions`: `4`
- с `file_snippets`: `1`
- системных сообщений: `1`
Это важно, потому что в текущем коде часть этих сигналов либо не используется вообще, либо теряет смысл при индексации.
## Что уже соответствует ТЗ
1. В репозитории есть оба требуемых сервиса: `index` и `search`.
2. Обязательные endpoints реализованы:
- `GET /health`, `POST /index`, `POST /sparse_embedding` в `index/main.py`
- `GET /health`, `POST /search` в `search/main.py`
3. Контракты request/response по основным endpoint'ам не менялись и в целом совпадают с шаблоном и ТЗ.
4. `search` использует `Qdrant`, dense endpoint и reranker через HTTP.
5. В обоих Dockerfile sparse-модель предзагружается внутрь образа, что соответствует оффлайн-ограничению контейнеров.
6. `HOST` и `PORT` читаются из env, как требует ТЗ.
## Что отсутствует или реализовано частично
### 1. Обогащенный вопрос из ТЗ почти не используется
В `search/main.py:90-100` описаны поля:
- `search_text`
- `variants`
- `hyde`
- `keywords`
- `entities`
- `date_mentions`
- `date_range`
- `asker`
Но в реальном поиске используется только `question.text`:
- `search/main.py:307-316`
Это главный недобор относительно ТЗ. Само ТЗ явно дает эти поля как сигналы для retrieval, а код их сейчас просто игнорирует.
### 2. Метаданные чанков объявлены, но не участвуют в поиске
В `README.md:52-58` отдельно сказано, что в metadata чанка сохраняются:
- `participants`
- `mentions`
- `contains_forward`
- `contains_quote`
В `search/main.py:132-145` есть модель `ChunkMetadata`, но дальше она никак не используется в `query_points`. Поиск не делает:
- фильтрацию по `mentions`
- фильтрацию по `participants`
- учет `thread_sn`
- учет временного диапазона через `start`/`end`
- отдельную обработку quote/forward чанков
То есть сильный канал улучшения качества уже предусмотрен схемой, но сейчас не задействован.
### 3. Реранк отбрасывает часть кандидатов
Сейчас:
- retrieval берет до `20` чанков: `search/main.py:174-177`
- rerank берет только первые `10`: `search/main.py:278-296`
- после rerank возвращаются только эти `10`, а хвост `11-20` теряется: `search/main.py:321-328`
Это не нарушение контракта, но это реальная потеря recall.
### 4. Выдача не дедуплицируется и не ограничивается по полезному top-K
Сейчас `message_ids` просто конкатенируются:
- `search/main.py:323-328`
Проблемы:
- дубликаты message id не удаляются
- результаты не агрегируются по лучшему score сообщения
- нет явного ограничения на топ полезных `50`, хотя именно `K=50` участвует в метрике из ТЗ
Если один и тот же `message_id` попал в несколько чанков, он тратит место в выдаче.
### 5. Индексация пока очень базовая: фиксированные символьные чанки
В `index/main.py:118-190` чанки строятся просто по длине строки:
- `CHUNK_SIZE = 512`
- `OVERLAP_SIZE = 256`
- разбиение идет по символам, а не по сообщениям, тайм-гепам, тредам или смысловым блокам
Из-за этого:
- длинные пересланные сообщения и цитаты могут резаться в неудобных местах
- один и тот же смысловой блок может быть разнесен по чанкам неестественно
- overlap строится по хвосту текста, а не по границе сообщений
### 6. `page_content`, `dense_content` и `sparse_content` сейчас одинаковые
См. `index/main.py:180-186`.
ТЗ прямо оставляет это место как точку оптимизации качества, но пока этот резерв не используется.
### 7. Индексация берет только `text` и `parts[*].text`, остальное почти теряется
См. `index/main.py:99-115`.
Сейчас не используются как поисковые сигналы:
- `sender_id`
- `mentions`
- `file_snippets`
- `member_event`
- `thread_sn`
- `is_hidden`
- `is_system`
- явное различение `quote` и `forward`
Особенно важные пробелы:
- `member_event` у системных сообщений сейчас фактически пропадает, если обычного текста нет
- `file_snippets` не разбирается, хотя там могут быть имена файлов, URL и служебные поля
- запросы вида "кто писал", "кого упоминали", "какой файл/документ кидали" сейчас поддержаны слабо
### 8. Смысл `quote` и `forward` не маркируется
В `index/main.py:105-113` текст из `parts` просто подшивается в общий текст без явных маркеров вида:
- "цитата:"
- "пересланное сообщение:"
- "автор цитаты:"
В итоге dense/sparse видят просто общий текстовый комок. Для поиска по обсуждениям это ощутимая потеря контекста.
### 9. Есть расхождение между локальной инфраструктурой и ТЗ
По ТЗ для `search` ожидается `API_KEY`.
В коде это поддержано:
- `search/main.py:21-30`
- `search/main.py:42-47`
- `search/Makefile:10-15`
Но локальный `docker-compose.yml:57-60` требует `OPEN_API_LOGIN` и `OPEN_API_PASSWORD`.
Итог:
- сам сервис гибче ТЗ
- локальный compose не повторяет боевую схему из ТЗ один в один
Это не ломает контракт, но может запутать при локальной отладке.
### 10. Есть еще одна инфраструктурная несостыковка со сдачей
В `doc/upload_to_docker.md:45-48` явно сказано собирать образы с `--platform linux/amd64`.
Но `index/Makefile:16-18` и `search/Makefile:26-28` собирают без `--platform linux/amd64`.
На x86 это может пройти незаметно, а на ARM-машине дать неправильный образ для отправки.
### 11. Есть неоднозначность между README и PDF по sparse в `search`
- `README.md:211` говорит, что sparse-модель для `search` должна быть локально внутри образа
- PDF в формулировке требований к `Search Service` делает акцент, что обращения к dense/sparse/rerank идут через проверяющую систему
Текущий код следует логике README/example: sparse считается локально в `search/main.py:148-151` и `search/main.py:198-207`.
Я бы это не считал блокером, но как минимум это место стоит держать в голове как неоднозначное требование.
## Что улучшать в первую очередь
### Приоритет 1. Начать использовать все поля `question`
Минимально стоит задействовать:
- `search_text` как основной нормализованный запрос
- `variants` как дополнительные формулировки
- `hyde` как дополнительные dense-запросы
- `keywords` как основу для sparse
- `entities` для фильтров и lexical boost
- `date_range` и `date_mentions` для ограничения по времени
- `asker` как сигнал по людям и email
Самый логичный путь без смены стэка: несколько dense/sparse запросов + fusion в `Qdrant`.
### Приоритет 2. Перестроить chunking под структуру чата, а не под символы
Нужны чанки по:
- окнам сообщений
- временным разрывам
- границам thread/forward/quote
- ограничению на размер по сообщениям, а не только по символам
Для чатов это обычно дает больше пользы, чем любые косметические тюнинги rerank.
### Приоритет 3. Развести `page_content`, `dense_content`, `sparse_content`
Хорошая схема:
- `page_content`: читабельный исходный текст чанка
- `dense_content`: нормализованный текст с ролями, автором, маркерами quote/forward
- `sparse_content`: keyword-heavy версия с email, mentions, именами файлов, ссылками, документами, леммами
Сейчас эта возможность не используется вообще.
### Приоритет 4. Нормально собирать финальную выдачу
Нужно:
- не терять кандидатов после rerank
- удалять дубликаты `message_id`
- агрегировать по лучшему score сообщения или чанка
- отдавать осмысленный top-50
Это прямой выигрыш по Recall@50 и nDCG@50.
### Приоритет 5. Начать использовать metadata в `Qdrant`
Особенно полезно для:
- `mentions`
- `participants`
- `contains_quote`
- `contains_forward`
- `thread_sn`
- `start` / `end`
Для многих вопросов это позволит не просто "лучше ранжировать", а сразу отрезать нерелевантный шум.
### Приоритет 6. Превратить скрытые сигналы в индексируемый текст
Стоит отдельно материализовать:
- `member_event` в текст вида "пользователь X добавил Y"
- `file_snippets` в текст вида "файл: NAME, url: ..."
- автора сообщения
- список упомянутых пользователей
Сейчас эти сигналы либо не попадают в индекс, либо попадают слишком слабо.
## Что можно добавить по Python-библиотекам, не меняя стек
Стек `Qdrant` менять не нужно. Самые полезные добавки я бы смотрел такие:
- `pymorphy3` для лемматизации русских слов при подготовке `sparse_content`
- `razdel` для аккуратной токенизации русского текста
- `rapidfuzz` для точного lexical match по именам, email, документам, ссылкам и названиям
- `python-dateutil` или `dateparser` для нормализации дат, если захотите усиливать работу с `date_mentions`
- `tenacity` для аккуратных retry/timeout-оберток вокруг dense/rerank HTTP вызовов
Что не нужно делать:
- менять `Qdrant`
- тащить внешние LLM/API
- усложнять архитектуру ради "модности", пока не использованы базовые сигналы из самого ТЗ
## Короткий вывод
Сейчас репозиторий соответствует ТЗ как рабочий базовый шаблон, но почти не использует те сигналы, ради которых это ТЗ вообще интересно:
- обогащение вопроса
- metadata чанков
- структуру chat messages
- сигналы автора, упоминаний, файлов, системных событий, цитат и пересылок
Самый большой потенциал улучшения здесь не в замене базы или модели, а в трех вещах:
1. умный chunking
2. multi-query hybrid retrieval
3. использование metadata и нормальной сборки финального top-50