vk_hackathon/doc/output.md

329 lines
13 KiB
Markdown
Raw Permalink 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.

# Report: Go Nova Data Audit
Дата: 2026-04-18
## Что анализировал
Под "Go Data.js" интерпретировал файл [data/Go Nova.json](/home/q/doc/hackaton/data/Go%20Nova.json), потому что в репозитории это единственный релевантный датасет для `index` и `search`.
## Краткая статистика по данным
- всего сообщений: `25`
- сообщений с пустым верхнеуровневым `text`: `15`
- сообщений с `parts`: `14`
- сообщений с `mentions`: `4`
- сообщений с `member_event`: `1`
- сообщений с `file_snippets`: `1`
- сообщений с `is_forward = true`: `2`
- сообщений с `is_quote = true`: `5`
- сообщений с `thread_sn`: `0`
- сообщений с zero-width символом `\u200b`: `1`
## Что это значит для индексации
Текущий `index` теряет заметную часть смысла, потому что:
- слишком сильно полагается на `message.text`
- не различает `mediaType` внутри `parts`
- не превращает `member_event` в индексируемый текст
- не разбирает JSON в `file_snippets`
- не нормализует артефакты вроде zero-width символов
На этом датасете это критично: существенная доля сообщений живет целиком внутри `parts[*].text`.
## Наблюдаемые паттерны в сообщениях
### 1. Системные сообщения
Есть системное сообщение без текста, но с `member_event`:
- `type = addMembers`
- список участников лежит в `members`
Такое сообщение нельзя отбрасывать. Его нужно материализовать в текст вроде:
```text
system event: add members
actor: n.lebedev@corp.example
members: l.smirnova@corp.example, m.orlova@corp.example, v.baranova@corp.example, n.lebedev@corp.example
```
### 2. Сообщения, где весь смысл в `parts`
Во многих сообщениях `text == ""`, а контент лежит в `parts[*].text`.
Наблюдаемые `mediaType`:
- `text`
- `quote`
- `forward`
Следствие:
- `parts` должны быть первичным источником текста, а не вторичным придатком к `text`
### 3. Цитаты
У quote-part встречаются:
- `mediaType = quote`
- `sn` как источник цитаты
- `time`
- `text`
Quote нельзя просто склеивать с ответом. Нужна разметка, например:
```text
quote_from: n.ermakova@team.example
quote_text: ...
reply_text: ...
```
Иначе dense/sparse видят просто один большой комок текста и теряют отношение "на что отвечали".
### 4. Forward-сообщения
Forward приходит как `parts[*].mediaType = forward`, часто с длинным телом анонса.
Для них полезно явно сохранять:
- что это пересланное сообщение
- источник `sn`
- текст forwarded-блока
Пример нормализованного вида:
```text
forwarded_from: 48377@chat.example
forward_text: ...
```
### 5. Файлы и ссылки
В `file_snippets` лежит JSON-строка, внутри которой есть полезные поля:
- `name`
- `mime`
- `original_url`
- `date_create`
Это нужно разбирать локально и добавлять в нормализованный текст, а не хранить сырой JSON.
Минимально:
```text
attachment_name: IMG_8471.webp
attachment_mime: image/webp
attachment_url: https://redacted.example/resource/001
```
Ссылки из текста тоже нельзя выбрасывать полностью. Их нужно:
- сохранять в `page_content`
- извлекать как отдельные токены/сигналы в `sparse_content`
### 6. Технический шум
В данных уже видны артефакты:
- zero-width символ `\u200b`
- лишние пустые строки
- неравномерные пробелы
Но чистить нужно осторожно, чтобы не повредить:
- email
- URL
- имена файлов
- термины вроде `CGO`, `Go 1.18`, `Mutex.TryLock`
## Предлагаемая локальная логика очистки сообщений
Вся очистка должна жить локально внутри `index`, без внешних API.
### Шаг 1. Извлечение сигналов из raw message
Из каждого сообщения собрать:
- `message.text`
- `parts[*]`
- `mentions`
- `member_event`
- `file_snippets`
- `sender_id`
- флаги `is_system`, `is_forward`, `is_quote`
### Шаг 2. Нормализация Unicode и whitespace
Безопасная очистка:
- удалить `\u200b`, `\u200c`, `\u200d`, `\ufeff`
- заменить `\r\n` на `\n`
- схлопнуть повторяющиеся пробелы внутри строки
- схлопнуть `3+` пустых строк до `2`
- обрезать пробелы по краям строк
Не делать агрессивную очистку:
- не удалять email
- не удалять URL
- не переводить все в lower
- не выкидывать цифры и версии
### Шаг 3. Нормализация `parts`
Правила:
- `mediaType = text`: добавить как обычный текстовый блок
- `mediaType = quote`: добавить маркеры `quote_from` и `quote_text`
- `mediaType = forward`: добавить маркеры `forwarded_from` и `forward_text`
- неизвестный `mediaType`: сохранять как `part_type: <value>` + текст, не терять содержимое
### Шаг 4. Нормализация системных событий
Для `member_event` генерировать текстовую форму.
Минимум поддержать:
- `addMembers`
- любые неизвестные события сохранять как `system_event_type: ...`
### Шаг 5. Нормализация файлов
`file_snippets` распарсить из JSON-строки локально.
Из каждого файла вытаскивать:
- имя
- mime
- url
- дату
Если JSON битый:
- не падать
- сохранить исходную строку как `attachment_raw`
### Шаг 6. Сборка трех текстовых представлений
`page_content`:
- читабельный текст для payload
- с маркерами quote/forward/system/file
`dense_content`:
- нормализованный текст с ролями и источниками
- без мусорных повторов и с понятной структурой
`sparse_content`:
- keyword-heavy версия
- email, mentions, file names, MIME, URL host/path, технические термины
### Шаг 7. Правила пропуска
Сообщение можно пропускать только если после нормализации одновременно пусты:
- основной текст
- `parts`
- `member_event`
- `file_snippets`
Иначе его нужно индексировать.
## Что обновил в документации
- [doc/prompt.md](/home/q/doc/hackaton/doc/prompt.md): добавил локальную логику очистки сообщений и требование логировать каждую правку
- [doc/output.md](/home/q/doc/hackaton/doc/output.md): создал этот отчет
## Что делать следующим шагом
1. Реализовать `index/rendering.py` и `index/cleaning.py` по этим правилам.
2. Добавить unit tests на системные, quote, forward и file-based сообщения.
3. Только после этого менять chunking и retrieval, чтобы не тюнить поиск на грязном тексте.
---
# Отчёт: Рефакторинг search и index (2026-04-18)
## Что изменено
### P0 — исправлен критический баг в search
**Файл:** `search/main.py` (до рефакторинга)
**Баг:** строка `must_conditions: []` была type annotation, а не присваивание. Любой запрос с `date_range` или `asker` вызывал `NameError` на `.append()`.
**Исправление:** присваивание `must_conditions = []` перенесено в `search/retrieval.py` корректно.
### search — модульная декомпозиция
**Было:** монолит `search/main.py` (~390 строк)
**Стало:** 6 модулей + тонкий main
| Модуль | Назначение |
|---|---|
| `search/config.py` | env vars, validate_required_env (теперь в lifespan, не при импорте) |
| `search/schemas.py` | pydantic модели |
| `search/query_builder.py` | построение dense/sparse запросов из question |
| `search/retrieval.py` | qdrant prefetch с multi-query + фильтры |
| `search/rerank.py` | reranker + сохранение хвоста |
| `search/aggregation.py` | dedup, top-50 |
| `search/main.py` | только FastAPI wiring |
**Логические изменения:**
- primary dense query: `search_text` с fallback на `text`
- дополнительные dense queries: `variants`, `hyde` — отдельные Prefetch
- sparse query: `keywords` или primary query при их отсутствии
- rerank: сортирует top-60, хвост retrieval сохраняется
- финал: dedup + top-50 из head+tail
- timeout=30s, retry до 2 раз на 5xx/сеть
**Параметры:** DENSE_PREFETCH_K=50, SPARSE_PREFETCH_K=100, RETRIEVE_K=80, RERANK_LIMIT=60, TOP_K=50
### index — модульная декомпозиция
**Было:** монолит `index/main.py` (~268 строк, char-based chunking)
**Стало:** 5 модулей + тонкий main
| Модуль | Назначение |
|---|---|
| `index/schemas.py` | pydantic модели |
| `index/cleaning.py` | локальная очистка, без внешних API |
| `index/rendering.py` | три представления: page/dense/sparse |
| `index/chunking.py` | message-based windowing + overlap |
| `index/sparse.py` | sparse embedding |
| `index/main.py` | только FastAPI wiring |
**Логические изменения:**
- **Базовая единица чанка**: сообщение, не символ
- **Окно**: ≤10 сообщений И ≤2048 символов И без time gap >1h
- **Overlap**: последние 3 сообщения из предыдущего окна
- **page_content**: читабельный текст с `sender: текст`
- **dense_content**: timestamp + role markers + mentions + файлы
- **sparse_content**: sender + mentions + filenames + url + текст
**Очистка (cleaning.py):**
- удаление zero-width chars (`\u200b`, `\u200c`, `\u200d`, `\ufeff`)
- mediaType-aware нормализация parts (text/quote/forward/unknown)
- member_event → человекочитаемый текст
- file_snippets → safe JSON parse + extract (name/mime/url/date)
- сообщение пропускается только если пусты text+parts+member_event+file_snippets
## Файлы изменены
**Изменены:** `search/main.py`, `index/main.py`
**Созданы:** `search/config.py`, `search/schemas.py`, `search/query_builder.py`, `search/retrieval.py`, `search/rerank.py`, `search/aggregation.py`, `search/__init__.py`, `index/schemas.py`, `index/cleaning.py`, `index/rendering.py`, `index/chunking.py`, `index/sparse.py`, `index/__init__.py`, `tests/` (5 test files)
## Проверка
- `python3 -m py_compile` — пройден на всех 13 новых/изменённых Python-файлах
- `pytest tests/ -q` — 68 тестов, все прошли
- API контракты не изменены: `POST /index`, `POST /sparse_embedding`, `POST /search`
## Что осталось
- metadata-aware boost/filter (participants, mentions, contains_quote, contains_forward, thread_sn)
- тюнинг параметров DENSE_PREFETCH_K / RETRIEVE_K / RERANK_LIMIT под реальные запросы
- regression test file с контрольными вопросами по Go Nova.json
- docker-compose / Makefile / README alignment (`--platform linux/amd64`)
- решение про `.ai_update/` в `.gitignore`