Как вы делаете schema evolution для метаданных документов в RAG?

Краткий тезис

Schema evolution (эволюция схемы) — это процесс изменения структуры метаданных документов в RAG-системе без потери совместимости с уже проиндексированными данными. Ключевая идея — использовать сериализационные форматы с поддержкой эволюции (Avro, Protobuf), делать все поля optional с default values, версионировать схемы и выполнять backfill (обратную миграцию) старых документов в новую схему. Это позволяет добавлять новые поля метаданных, не ломая существующие индексы и запросы.


1. Термин: Schema Evolution (эволюция схемы)

Schema evolution — это способность системы обрабатывать данные, записанные по старой схеме, после того как схема была изменена. В контексте RAG метаданные документов (например, дата создания, автор, категория, версия) хранятся вместе с векторными эмбеддингами в векторной БД или отдельном индексе. Когда бизнес-требования меняются, приходится добавлять, удалять или изменять поля метаданных. Без продуманной эволюции это приведёт к ошибкам десериализации, потере данных или необходимости полной переиндексации.

Почему это важно в RAG

  • Метаданные используются для фильтрации (например, «только документы за 2024 год») и ранжирования (например, по рейтингу).
  • Векторные БД (Pinecone, Weaviate, Qdrant) позволяют хранить метаданные как payload или filter attributes.
  • Если схема меняется, старые документы с отсутствующими полями должны корректно обрабатываться, а новые запросы — учитывать новые поля.

2. Проблема: метаданные в RAG

Метаданные — это структурированная информация о документе: заголовок, дата, автор, теги, источник, версия, права доступа и т.д. В RAG они обычно хранятся в двух местах:

  • Векторная БД — как часть записи (point) вместе с вектором.
  • Отдельный индекс (например, Elasticsearch) для гибридного поиска.

При изменении схемы возникают проблемы:

  • Несовместимость типов: поле было строкой, стало числом.
  • Отсутствие поля: старые документы не имеют нового поля.
  • Удаление поля: старые документы могут содержать поле, которого больше нет в схеме.

Пример изменения

  • Изначально метаданные: { "title": str, "date": str }.
  • Через год нужно добавить "author": str и изменить "date" на "timestamp": int.

3. Подходы к schema evolution

Сравним популярные форматы сериализации:

ФорматПоддержка эволюцииТипизацияПроизводительностьИспользование в RAG
AvroОтличная (backward/forward совместимость)Сильная (схема в JSON)Высокая (бинарный)Часто в Kafka, Hadoop
ProtobufХорошая (правила эволюции)Сильная (.proto файлы)Очень высокаяgRPC, микросервисы
JSON SchemaСредняя (ручная валидация)Слабая (динамическая)Низкая (текстовый)REST API, простые случаи
ThriftХорошаяСильнаяВысокаяУстаревает, редко

Рекомендация для RAG Avro или Protobuf — они обеспечивают строгую типизацию и встроенные механизмы эволюции. JSON Schema подходит для прототипов, но не для продакшена из-за отсутствия бинарной сериализации и риска ошибок.


4. Принципы backward compatibility (обратная совместимость)

Backward compatibility означает, что новый код может читать данные, записанные по старой схеме. Основные правила:

  • Все новые поля — optional (с default value, например null или 0).
  • Не удаляйте поля — помечайте их как deprecated и оставляйте в схеме.
  • Не меняйте тип существующего поля — вместо этого добавляйте новое поле с другим именем.
  • Используйте default values для полей, которые могут отсутствовать.

Пример в Avro

{
  "type": "record",
  "name": "DocumentMetadata",
  "fields": [
    {"name": "title", "type": "string"},
    {"name": "date", "type": ["null", "string"], "default": null},
    {"name": "author", "type": ["null", "string"], "default": null}
  ]
}

Здесь date и author — optional (union с null). Старые документы без author будут читаться с null.


5. Версионирование схем

Каждая версия схемы получает уникальный идентификатор (например, v1, v2). В RAG-системе можно хранить версию схемы в самом документе или в отдельном реестре.

Практика

  • Schema Registry (Confluent, Apicurio) — централизованное хранение схем, проверка совместимости при регистрации новой версии.
  • Векторная БД может хранить версию как одно из полей метаданных.

Пример версионирования

v1: { title, date }
v2: { title, date, author }  // backward compatible (author optional)
v3: { title, timestamp, author } // несовместимо с v1 (date удалено, timestamp новый тип)

В v3 нужно либо сохранить date как deprecated, либо выполнить миграцию.


6. Хранение метаданных в векторной БД

Большинство векторных БД (Pinecone, Weaviate, Qdrant, Milvus) позволяют хранить метаданные как payload или filter attributes. При эволюции схемы:

  • Все поля делаем optional — в БД они могут отсутствовать.
  • Индексы по метаданным (например, для фильтрации) должны поддерживать null значения.
  • При добавлении нового поля — оно автоматически становится доступным для фильтрации, но старые документы его не имеют.

Пример в Qdrant (Python):

from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct, Filter, FieldCondition, MatchValue

client = QdrantClient("localhost")

# Добавляем точку с метаданными v2
client.upsert(
    collection_name="docs",
    points=[
        PointStruct(
            id=1,
            vector=[0.1, 0.2],
            payload={"title": "Doc1", "date": "2023-01-01", "author": "Alice"}
        )
    ]
)

# Старая точка (v1) без author
client.upsert(
    collection_name="docs",
    points=[
        PointStruct(
            id=2,
            vector=[0.3, 0.4],
            payload={"title": "Doc2", "date": "2022-05-10"}
        )
    ]
)

7. Query с учётом эволюции

При поиске по новым полям (например, author == "Alice") старые документы, у которых author отсутствует (null), не будут соответствовать условию. Это ожидаемое поведение.

Пример фильтрации

# Ищем документы, где author = "Alice"
filter_condition = Filter(
    must=[
        FieldCondition(
            key="author",
            match=MatchValue(value="Alice")
        )
    ]
)
results = client.search(
    collection_name="docs",
    query_vector=[0.1, 0.2],
    query_filter=filter_condition
)

Документ с id=2 не попадёт в результаты, так как у него author == null.

Важно Если нужно, чтобы старые документы тоже участвовали в поиске по новому полю, можно задать default value (например, пустая строка) и при backfill заполнить его.


8. Миграция (backfill) — оффлайн конвертация

Backfill — это процесс преобразования старых документов в новую схему. Выполняется оффлайн, обычно в фоновом режиме.

Шаги:

  1. Определить новую схему (v2).
  2. Считать все старые документы из векторной БД.
  3. Для каждого документа:
    • Добавить новые поля с default значениями.
    • Преобразовать типы, если необходимо (например, datetimestamp).
  4. Перезаписать документы в БД (или создать новую коллекцию).
  5. Переключить приложение на новую коллекцию.

Пример backfill на Python (псевдокод):

def backfill_v1_to_v2(old_points):
    new_points = []
    for point in old_points:
        payload = point.payload
        # Добавляем author с default
        if "author" not in payload:
            payload["author"] = ""
        # Преобразуем date в timestamp (если нужно)
        if "date" in payload:
            from datetime import datetime
            payload["timestamp"] = int(datetime.strptime(payload["date"], "%Y-%m-%d").timestamp())
            # Можно удалить старое поле, но лучше оставить для совместимости
        new_points.append(PointStruct(id=point.id, vector=point.vector, payload=payload))
    return new_points

Инструменты Spark, Apache Beam, простые Python-скрипты с параллельной обработкой.


9. Инструменты и практики

  • Schema Registry (Confluent, Apicurio) — для управления версиями схем и проверки совместимости.
  • CI/CD — автоматическая проверка совместимости при каждом изменении схемы.
  • Тестирование — написать unit-тесты, которые проверяют, что старые документы корректно десериализуются новой схемой.
  • Мониторинг — логировать ошибки десериализации, чтобы быстро обнаружить несовместимость.

Пример проверки совместимости в Avro

from avro.schema import parse
from avro.datafile import DataFileReader, DataFileWriter

# Проверка backward compatibility
old_schema = parse(open("v1.avsc").read())
new_schema = parse(open("v2.avsc").read())
# Используем библиотеку avro-compatibility или встроенные проверки

10. Пример кода: эволюция схемы с Protobuf

Файл metadata.proto

syntax = "proto3";

message DocumentMetadata {
  string title = 1;
  optional string date = 2;        // v1
  optional string author = 3;      // v2 (new)
  optional int64 timestamp = 4;    // v3 (new, replaces date)
}

Генерация Python-классов protoc --python_out=. metadata.proto

Использование

from metadata_pb2 import DocumentMetadata

# Старый документ (v1)
old = DocumentMetadata(title="Doc1", date="2023-01-01")
# Новый код читает — author будет None
print(old.author)  # None

# Новый документ (v2)
new = DocumentMetadata(title="Doc2", date="2024-05-10", author="Bob")

Сериализация в векторную БД можно хранить protobuf как bytes в payload, но удобнее использовать встроенные поля БД. Protobuf полезен для передачи между микросервисами.


11. Плюсы и минусы разных подходов

ПодходПлюсыМинусы
Avro + Schema RegistryПолная совместимость, интеграция с Kafka, бинарный форматДополнительная инфраструктура, сложность
Protobuf + gRPCВысокая производительность, строгая типизация, поддержка optionalНеобходимость компиляции, сложнее для ad-hoc запросов
JSON Schema + NoSQLПростота, человекочитаемость, гибкостьНет бинарной сериализации, риск ошибок при ручной валидации
Встроенные механизмы БД (например, Qdrant payload)Минимум кода, автоматическая обработка nullЗависимость от вендора, ограниченные возможности эволюции

Рекомендация для продакшн RAG используйте Avro или Protobuf с Schema Registry и backfill. Для прототипов — JSON Schema.


Пет-проект для закрепления

Задача Создать RAG-систему для хранения документов с метаданными, которая поддерживает эволюцию схемы. Реализовать добавление нового поля rating (float) и миграцию старых документов.

Инструменты

Шаги:

  1. Определить схему v1: { "title": str, "date": str }.
  2. Загрузить 100 документов с этой схемой в Qdrant.
  3. Определить схему v2: добавить "rating": float с default 0.0.
  4. Написать скрипт backfill, который читает все точки, добавляет rating=0.0 и перезаписывает.
  5. Проверить, что новые документы с rating корректно фильтруются, а старые — нет (или участвуют с default).
  6. Добавить тест на backward compatibility: прочитать старый документ новой схемой.

Ожидаемый результат Работающая RAG-система, где можно добавлять новые поля метаданных без остановки сервиса и потери данных. Код с комментариями и тестами.


Связь с другими вопросами

ВопросТема
525Как вы обновляете документы в существующей RAG-системе?
527Как вы обрабатываете дубликаты документов в RAG?
510Как вы выбираете векторную БД для RAG?
515Как вы храните метаданные в векторной БД?
530Как вы делаете гибридный поиск (векторный + ключевой)?
505Как вы оцениваете качество retrieval'а в RAG-системе?

Навигация