vk_hackathon/doc/output.md

13 KiB
Raw Blame History

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:

  • text
  • quote
  • forward

Следствие:

  • parts должны быть первичным источником текста, а не вторичным придатком к text

3. Цитаты

У quote-part встречаются:

  • mediaType = quote
  • sn как источник цитаты
  • time
  • text

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-строка, внутри которой есть полезные поля:

  • name
  • mime
  • original_url
  • date_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
  • лишние пустые строки
  • неравномерные пробелы

Но чистить нужно осторожно, чтобы не повредить:

  • 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: добавил локальную логику очистки сообщений и требование логировать каждую правку
  • 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)

Что изменено

Файл: 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