Add hackathon audit notes

This commit is contained in:
q 2026-04-18 11:13:56 +03:00
parent 5491153935
commit 33f8880807

290
doc/ai_update.md Normal file
View file

@ -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