diff --git a/doc/ai_update.md b/doc/ai_update.md new file mode 100644 index 0000000..f66288a --- /dev/null +++ b/doc/ai_update.md @@ -0,0 +1,290 @@ +# 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