13 KiB
Report: Go Nova Data Audit
Дата: 2026-04-18
Что анализировал
Под "Go Data.js" интерпретировал файл data/Go Nova.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
Такое сообщение нельзя отбрасывать. Его нужно материализовать в текст вроде:
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:
textquoteforward
Следствие:
partsдолжны быть первичным источником текста, а не вторичным придатком кtext
3. Цитаты
У quote-part встречаются:
mediaType = quotesnкак источник цитатыtimetext
Quote нельзя просто склеивать с ответом. Нужна разметка, например:
quote_from: n.ermakova@team.example
quote_text: ...
reply_text: ...
Иначе dense/sparse видят просто один большой комок текста и теряют отношение "на что отвечали".
4. Forward-сообщения
Forward приходит как parts[*].mediaType = forward, часто с длинным телом анонса.
Для них полезно явно сохранять:
- что это пересланное сообщение
- источник
sn - текст forwarded-блока
Пример нормализованного вида:
forwarded_from: 48377@chat.example
forward_text: ...
5. Файлы и ссылки
В file_snippets лежит JSON-строка, внутри которой есть полезные поля:
namemimeoriginal_urldate_create
Это нужно разбирать локально и добавлять в нормализованный текст, а не хранить сырой JSON.
Минимально:
attachment_name: IMG_8471.webp
attachment_mime: image/webp
attachment_url: https://redacted.example/resource/001
Ссылки из текста тоже нельзя выбрасывать полностью. Их нужно:
- сохранять в
page_content - извлекать как отдельные токены/сигналы в
sparse_content
6. Технический шум
В данных уже видны артефакты:
- zero-width символ
\u200b - лишние пустые строки
- неравномерные пробелы
Но чистить нужно осторожно, чтобы не повредить:
- URL
- имена файлов
- термины вроде
CGO,Go 1.18,Mutex.TryLock
Предлагаемая локальная логика очистки сообщений
Вся очистка должна жить локально внутри index, без внешних API.
Шаг 1. Извлечение сигналов из raw message
Из каждого сообщения собрать:
message.textparts[*]mentionsmember_eventfile_snippetssender_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_textmediaType = 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. Правила пропуска
Сообщение можно пропускать только если после нормализации одновременно пусты:
- основной текст
partsmember_eventfile_snippets
Иначе его нужно индексировать.
Что обновил в документации
- doc/prompt.md: добавил локальную логику очистки сообщений и требование логировать каждую правку
- doc/output.md: создал этот отчет
Что делать следующим шагом
- Реализовать
index/rendering.pyиindex/cleaning.pyпо этим правилам. - Добавить unit tests на системные, quote, forward и file-based сообщения.
- Только после этого менять 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