Почему ваш векторный поиск по JSON работает как пьяный слепой?
Вы загрузили тысячу JSON-документов в вашу RAG-систему. Запросы вроде "найди пользователей из Москвы старше 30 лет" возвращают откровенную чушь. Модель находит что угодно, только не нужные записи. Вы проверяете эмбеддинги, меняете модель, настраиваете косинусное расстояние - результат ноль. Проблема не в математике векторов. Проблема в том, как вы кормите JSON модели.
Современные эмбеддинг-модели (такие как OpenAI text-embedding-3-2026-01, Cohere Embed v4.1 или открытый BGE-M3 Ultra) обучены на естественном языке. Их токенизаторы (чаще всего варианты Byte-Pair Encoding) разбивают текст на кусочки, которые имеют смысл в человеческой речи. JSON для них - это просто странная последовательность символов. Каждая фигурная скобка, кавычка и двоеточие съедают драгоценные токены и ломают семантику.
Ключевой момент: механизм внимания внутри трансформер-модели распределяет вес между токенами. Когда половина вашего контекста - это синтаксические символы JSON, модель не понимает, на что обращать внимание. Сигнал тонет в шуме.
Как токенизатор уродует ваш JSON
Возьмем простой пример. Вот ваш документ:
{
"user": {
"name": "Иван Петров",
"age": 35,
"city": "Москва",
"tags": ["разработчик", "python", "devops"]
}
}
Модель типа text-embedding-3-small видит это примерно так (упрощенно): [ "{", "\"", "user", "\"", ":", "{", "\"", "name", "\"", ... ]. Токены разбиваются по символам, ключи и значения теряют связь. Запрос "разработчик из Москвы" будет сравниваться с вектором, где "разработчик" спрятан глубоко в массиве, а "Москва" окружена кавычками и запятыми. Шансы на совпадение катастрофически малы.
Это одна из причин, почему RAG начинает врать при росте базы - плохие эмбеддинги не масштабируются.
Решение: выпрямить JSON перед тем, как скормить его модели
Flatten (выпрямление) - это преобразование структурированного JSON в плоский, читаемый человеком текст. Мы сохраняем все ключи и значения, но убираем синтаксический мусор. Цель: чтобы модель увидела семантику, а не синтаксис.
1 Извлекаем данные из JSON
Сначала нужно распарсить JSON. Используйте надежную библиотеку. В Python это стандартный json. Важно: обработать возможные ошибки формата.
import json
def load_json_safely(json_str):
try:
return json.loads(json_str)
except json.JSONDecodeError as e:
# Логируем ошибку, возвращаем пустой dict или обрабатываем
print(f"Ошибка парсинга JSON: {e}")
return {}
2 Рекурсивно обходим дерево и строим строку
Напишите функцию, которая проходит по всем узлам и собирает ключи и значения в строку с разделителями. Для вложенных объектов добавляем префиксы.
def flatten_to_text(data, parent_key='', sep=': ', line_break='\n'):
items = []
if isinstance(data, dict):
for k, v in data.items():
new_key = f"{parent_key}.{k}" if parent_key else k
if isinstance(v, (dict, list)):
items.append(flatten_to_text(v, new_key, sep, line_break))
else:
items.append(f"{new_key}{sep}{v}")
elif isinstance(data, list):
for i, v in enumerate(data):
new_key = f"{parent_key}[{i}]"
if isinstance(v, (dict, list)):
items.append(flatten_to_text(v, new_key, sep, line_break))
else:
items.append(f"{new_key}{sep}{v}")
else:
# Если данные - простой тип, но не в dict/list
return f"{parent_key}{sep}{data}"
return line_break.join(items)
3 Чистим и оптимизируем текст
Полученный текст можно дополнительно обработать: удалить лишние пробелы, привести к нижнему регистру (если регистр не важен), заменить спецсимволы. Но не перестарайтесь - иногда регистр имеет значение (например, в названиях компаний).
def clean_flattened_text(text):
# Убираем множественные переносы строк
import re
text = re.sub(r'\n\n+', '\n', text)
# Можно удалить точки перед ключами, если они мешают
# text = re.sub(r'^\.', '', text, flags=re.MULTILINE)
return text.strip()
4 Генерируем эмбеддинг
Теперь, когда у нас есть чистый текст, можно вызывать эмбеддинг-модель. Используйте актуальные API на 2026 год.
# Пример с OpenAI (актуально на 29.01.2026)
# from openai import OpenAI
# client = OpenAI(api_key='your_key')
# response = client.embeddings.create(input=cleaned_text, model="text-embedding-3-large-2026")
# embedding = response.data[0].embedding
# Пример с открытой моделью через Sentence Transformers
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-m3-2026') # Предполагаемая актуальная модель
embedding = model.encode(cleaned_text, normalize_embeddings=True)
Используйте normalize_embeddings=True для нормализации векторов. Это улучшает качество поиска при использовании косинусного сходства.
Что получилось? Сравним результаты
Исходный JSON после flatten превращается в такой текст:
user.name: Иван Петров
user.age: 35
user.city: Москва
user.tags[0]: разработчик
user.tags[1]: python
user.tags[2]: devops
Токенизатор теперь видит осмысленные пары "ключ: значение". Механизм внимания правильно выделяет сущности. Запрос "разработчик из Москвы" будет близок к этому вектору, потому что слова "разработчик" и "Москва" стоят в prominent позициях, а не закопаны в синтаксисе.
Три ошибки, которые сведут на нет все усилия
- Потеря типов данных. Число 35 и строка "35" - это разные вещи для модели. Не конвертируйте числа в строки автоматически. Лучше оставить как есть, функция flatten_to_text преобразует их в строку при конкатенации.
- Слишком длинный текст. Если ваш JSON огромен, flatten-текст может превысить лимит токенов модели (например, 8192 токена для text-embedding-3). Нужно разбивать или агрегировать. Иногда помогает гибридный поиск для обработки больших документов.
- Игнорирование контекста ключей. Ключ "name" сам по себе ничего не значит. "user.name" уже лучше. Но можно еще улучшить, добавляя человеческие описания: "Имя пользователя: Иван Петров". Это требует дополнительной схемы маппинга, но сильно поднимает точность.
Частые вопросы (FAQ)
| Вопрос | Ответ |
|---|---|
| А если у меня массив объектов? | Flatten-функция выше обрабатывает массивы, добавляя индекс в ключ: users[0].name: .... Для семантического поиска это нормально. Но если порядок не важен, можно сортировать элементы по ключу, чтобы получить стабильное представление. |
| Как насчет бинарных данных или null-значений? | Null-значения лучше пропускать или заменять на пустую строку. Бинарные данные (картинки в base64) не стоит включать в текст для эмбеддинга - используйте отдельные мультимодальные модели или извлекайте текстовые метаданные. |
| Это замедлит индексацию? | Незначительно. Парсинг и рекурсивный обход добавляют миллисекунды. Основное время все равно занимает вызов эмбеддинг-модели. Выигрыш в точности того стоит. |
| Есть ли готовые библиотеки? | На 29.01.2026 популярны json-flatten для Python или функции в pandas.json_normalize, но они ориентированы на структурное flatten, а не текстовое. Лучше написать свою функцию, как выше, чтобы контролировать формат вывода. |
А что если flatten - это только половина дела?
Flatten JSON решает проблему синтаксического шума. Но есть и другие ограничения эмбеддинг-моделей, о которых я писал в статье про математический потолок RAG. Например, модели плохо улавливают точные совпадения чисел или дат. Для этого нужен гибридный подход.
Еще один лайфхак: после flatten вы можете применить техники обогащения текста. Добавить синонимы ключей, расшифровать аббревиатуры. Это особенно полезно для узкоспециализированных данных (медицина, юриспруденция). Инструменты вроде Spark NLP (партнерская ссылка) могут автоматически извлекать сущности и добавлять их в текст.
И последнее: не забывайте, что эмбеддинги - это черный ящик. Иногда полезно провести обратную инженерию, чтобы понять, что именно модель запомнила из вашего flatten-текста.
Мой прогноз: к концу 2026 года появятся эмбеддинг-модели, натренированные специально на структурированных данных (JSON, XML, YAML). Они будут понимать типы данных и иерархии без flatten. Но пока что flatten - это самое простое и рабочее решение. Начните применять его сегодня, и ваша RAG-система перестанет возвращать случайные результаты.