← Harvester

Pipeline

harvester · workzone

Внутренний конвейер обработки: как сырьё из источника превращается в нормализованную сущность и попадает в хранилище. Три стадии — Extract Transform Load. Source-specific — только вход (extract) и первый шаг трансформации (normalize); всё, что дальше, — общее для любого источника и для любого режима синхронизации.

1 Extract Вход конвейера: поток RawItem.
Тонкий коннектор на источник коннектор
Один коннектор на источник — тонкий async-клиент (httpx) поверх REST API; официальные SDK не используются: они синхронны и несут свои retry-слои, а надёжность платформы живёт на сыром HTTP-ответе.
Все коннекторы прячутся за единым интерфейсом fetch(since) → RawItem и выдают поток RawItem — это и есть вход в Transform. Саму механику выборки — подключение, аутентификацию, пагинацию и инкремент по since — держит sources.
2 Transform Цепочка full sync и incremental.
1 normalize коннектор
Сырые данные источника единая модель Entity. Единственный шаг трансформации, который пишется под конкретный источник.
2 filter
Отсев того, что не нужно индексировать: пустые сущности, системные события («X присоединился к каналу»), сообщения ботов. Базовые правила встроены и не настраиваются. Что тянуть сверх них — набор переключателей объявляет манифест коннектора (как и объекты scope): переопределения (вернуть отсеянное — ботов в чатах) и доп-включения (вложения, комментарии и треды, приватные каналы и DM). Карточка показывает релевантные типу — настройки источника. Scope на уровне объектов — соседняя тема, sources.
3 classify
Статус сущности: draft / final / archived.
4 resolve
Схлопывание дублей внутри одного прогона и выбор победителя при конфликте версий: один fetch может вернуть сущность дважды. Идемпотентность между прогонами — не здесь, её даёт Load через upsert по source_id + source_type + source_entity_id. Глубокий entity resolution между источниками держит data-model.
5 enrich
Связи. Ребро проводится сразу, когда целевой узел уже загружен; иначе — ссылка на другой источник или на ещё не пришедший узел своего — сохраняется упоминанием (refs) в метаданных и материализуется позже, когда оба конца в базе. Граница — data-model.
6 clean
Снятие разметки для вектора: размеченный текст плоский. Делается поздно, после enrich: ссылки и упоминания в разметке нужны для связей, а заголовки — для резки на фрагменты. Чистится только копия под эмбеддинг; оригинал в Entity остаётся нетронутым.
7 chunk
Длинный текст фрагменты под эмбеддинг. Одна Entity много фрагментов, у каждого свой вектор. Целевой размер — 512 токенов (рабочий диапазон 256–1024), тюнируемый параметр. Окно модели — не цель, а потолок: effective = min(target_chunk_tokens, model_max_tokens). Большое окно эмбеддера (у text-embedding-38192) даёт запас от обрезки длинных секций, а не разрешение делать фрагмент на всё окно: чем длиннее текст под одним вектором, тем сильнее он «усредняется» и тем хуже попадания поиска.
Стратегия — по типу контента, не один глобальный размер на всё: Slack — тред или окно, сообщения остаются целыми; тикет Jira или MR GitLab — тикет вместе с комментариями одним документом; код — по синтаксису (функции, AST), не по токен-окну; длинная страница Confluence идёт дефолтным маршрутом ниже. Это валидный современный дефолт — просто маршрутизируемый по типу.
  • structural Основной рез — по структуре из normalize: заголовки, секции, сообщение или тред. Граница естественная — нахлёст не нужен.
  • recursive Fallback: блок крупнее целевого размера добираем по абзацам, пока не влезет.
  • overlap ~15% (≈75 токенов) — нахлёст на стыке, чтобы не рвать контекст границы. Включается только на recursive-fallback: при структурном резе граница естественная, а короткий контент (сообщение Slack) остаётся целым, overlap = 0.
8 contextualize v2 AI
Перед эмбеддингом к каждому фрагменту дописывается короткий контекст, приземляющий его в документе — из какого источника и раздела, о чём речь; приписку пишет модель, видя документ целиком и сам фрагмент. Эмбеддинг фрагмента вместе с ней заметно поднимает попадания семантического поиска: фрагмент перестаёт быть вырванным из контекста.
Нюанс v2 для контент-кеша: приписка меняет финальную строку, значит хешируется строка вместе с ней — переэмбеддинг происходит сам собой. Сгенерированный blurb при этом сохраняется рядом с фрагментом: модель недетерминирована, и без сохранённой приписки хеш «плыл» бы при каждом прогоне.
9 embed AI контент-кеш
Каждый фрагмент вектор. Модель берётся из реестра AI-моделей — Admin назначает её функции embedding (назначение по функциям); это первая конкретная функция и точка стыка Harvester с управлением моделями. Свою настройку Harvester не заводит: использует ту модель, что назначена в реестре. Назначение — предусловие приёма: дефолта нет, и пока embedding-модель не выбрана (встроенные модели платформы доступны из коробки или своя), приём не стартует — иначе размерность chunks.embedding нечем зафиксировать.
Ключ контент-кеша — хеш точной финальной строки, уходящей в эмбеддер, плюс всего, что влияет на вектор: SHA256(model_id@version | instruction_prefix | normalized(текст фрагмента)). Смена модели, токенизатора, обязательного префикса (query:/passage:) или правил нормализации меняет ключ переэмбеддинг. Так неизменный фрагмент переиспользует вектор, а реальное изменение бьёт инференсом точечно. Как общий рантайм держит этот поток и масштабируется под нагрузку — embeddings-рантайм.
Где живут параметры. Конвенция «no hardcoding» делит ручки chunk и embed на два дома. Интринсики модели — макс. токенов окна, обязательный префикс (query:/passage:), размерность вектора — живут в реестре AI-моделей; чанкер читает потолок оттуда. Тюнируемые ручки — target_chunk_tokens, overlap_pct, размеры по типам контента — в конфиге. На стыке стоит валидатор: target_chunk_tokens ≤ model.max_input_tokens.
3 Load Идемпотентный upsert — выход конвейера.
Loader
Идемпотентный upsert по ключу source_id + source_type + source_entity_id: каждая сущность пишется ровно в свою строку, повторный прогон обновляет её, а не плодит дубли. Идемпотентность распространяется и на фрагменты: их вектора — дочерние строки сущности, и тем же прогоном их набор сверяется целиком — устаревшие удаляются, новые добавляются, осиротевших векторов от прежней версии не остаётся. Неизменные фрагменты при этом не переэмбеддятся: вектор переиспользуется по контент-хешу, инференс бьёт только по тому, что реально изменилось. Главная гарантия модуля: повторная синхронизация и подключение нового источника спустя время не ломают уже собранные данные. Одна сущность при этом ложится сразу в несколько приёмников хранения:
  • relational Тело записи, метаданные и ACL — структура для точных фильтров.
  • vector Эмбеддинги фрагментов — поиск по смыслу.
  • graph Узел сущности и связи внутри источника — обход по графу.
Полную раскладку — какое поле в какую базу, плюс поля, историю и удаления — держит data-model.
RawItem Entity сохранено
Контракты

Конвейер держится на двух контрактах. Каждая стадия знает только их — поэтому всё после normalize остаётся общим.

RawItem
Connector → Transform
Сырьё источника вместе с его метаданными. Выход коннектора и вход трансформации.
Entity
Transform → Loader
Нормализованная сущность. Разные типы внутри источника (тикет, страница, сообщение) — подвиды одной Entity и проходят один и тот же конвейер. Полную модель держит data-model.