Найти в Дзене

Как хранить и использовать текстовые эмбеддинги без «тяжёлых» векторных БД? Parquet + Polars спасут день

В последние пару лет текстовые эмбеддинги, порождённые крупными языковыми моделями (LLM), показали себя как мощный инструмент для сравнения и кластеризации текстов. Однако после генерации возникает вопрос: «Что делать со всеми этими векторами?». Очень часто мы натыкаемся на советы использовать специализированные векторные базы данных наподобие Faiss, Qdrant или Pinecone. И действительно, в крупных проектах с миллионами и миллиардами эмбеддингов такая инфраструктура абсолютно оправдана. Но если ваш проект поменьше (тысячи или десятки тысяч векторных представлений) или вы просто хотите «поиграться» локально, то от внешних сервисов можно отказаться и всё отлично поместится в обычную оперативную память. Автор оригинальной статьи продемонстрировал это на примере 32 254 эмбеддингов для карт Magic: the Gathering. Получилось порядка 94 МБ в памяти, что удобно умещается в современные конфигурации — в том числе и на бесплатных облачных инстансах. Главное — хранить в удобном формате и иметь возмо
Оглавление

В последние пару лет текстовые эмбеддинги, порождённые крупными языковыми моделями (LLM), показали себя как мощный инструмент для сравнения и кластеризации текстов. Однако после генерации возникает вопрос: «Что делать со всеми этими векторами?». Очень часто мы натыкаемся на советы использовать специализированные векторные базы данных наподобие Faiss, Qdrant или Pinecone. И действительно, в крупных проектах с миллионами и миллиардами эмбеддингов такая инфраструктура абсолютно оправдана. Но если ваш проект поменьше (тысячи или десятки тысяч векторных представлений) или вы просто хотите «поиграться» локально, то от внешних сервисов можно отказаться и всё отлично поместится в обычную оперативную память.

Автор оригинальной статьи продемонстрировал это на примере 32 254 эмбеддингов для карт Magic: the Gathering. Получилось порядка 94 МБ в памяти, что удобно умещается в современные конфигурации — в том числе и на бесплатных облачных инстансах. Главное — хранить в удобном формате и иметь возможность быстро считывать данные вместе с метаданными (названиями, типами, описанием). Ниже — идея, почему Parquet + Polars делают это почти идеально.

Почему не CSV или Pickle?

При работе с эмбеддингами есть несколько популярных (но не всегда удачных) способов хранения:

🔴 CSV (текстовый формат)

  • В каждой строке много чисел с плавающей точкой, весящих намного больше, чем бинарные float32.
  • На больших объёмах вырастает размер файла и время парсинга.
  • Чтобы восстановить матрицу (n,d)(где n — число объектов, d — размерность эмбеддинга), придётся изрядно повозиться.

🔴 Pickle

  • Быстрый способ сбросить всё, как есть в памяти, в файл.
  • Но форматы Pickle небезопасны (могут выполнять произвольный код) и не всегда совместимы между разными версиями Python/операционными системами.
  • Лучше избегать Pickle в продакшене.

🔴 Numpy .npy

  • Уже лучше: хранит ровно бинарную матрицу float32, без overhead.
  • Но привязка: как хранить метаданные (название, тип, текст объекта)? Приходится вести отдельный массив с индексами или придумать собственные решения.

Parquet: хранение колоночных данных с типами

Формат Parquet (развиваемый Apache Parquet) — это столбцовый формат, позволяющий эффективно сохранять и загружать табличные данные со строгими типами. Например, в одной колонке можно хранить строки (названия карт Magic), в другой — вложенные списки float32 (сами эмбеддинги). Преимущества:

🗄️ Колонно-ориентированная структура. Проще загружать только нужные колонки, экономя время и память.
🌐
Совместимость. Parquet понимают многие инструменты (Spark, Arrow, pandas, Polars).
🚀
Сжатие и быстрое чтение. Эмбеддинги не всегда сильно сжимаются, но сама структура позволяет максимально эффективно работать без лишнего парсинга строк.

Polars: быстрые датафреймы с поддержкой списков

Для Python наибольшую популярность имеет pandas, но для эмбеддингов есть минус: pandas испытывает сложности при работе с колонками, содержащими длинные вложенные списки (например, когда каждая строка — массив из 768 элементов типа float32). В результате такие данные часто превращаются в объекты или списки без строгой типизации, что требует использования обходных решений вроде np.vstack(), замедляя обработку.

Альтернатива — Polars, библиотека на Rust, совместимая с Apache Arrow. Она умеет:

🟢 Корректно хранить колонку типа «Array<float32>» (именно то, что нужно для эмбеддингов).
🟢
Легко передавать эти данные в NumPy. Вызов df["embedding"].to_numpy() может вернуть реальную двумерную матрицу без копирования.
🟢
Быстро фильтровать и выбирать подмножество строк (например, «хочу карточки только определённого цвета»), после чего доставать связанную часть эмбеддингов.

Так что схема выглядит так:

1️⃣ Чтение: df = pl.read_parquet("dataset.parquet")
2️⃣
Получаем нужные эмбеддинги: emb_matrix = df["embedding"].to_numpy(allow_copy=False)
3️⃣
Считаем косинусное сходство через быстрое dot_product(query, emb_matrix.T) (благодаря NumPy).
4️⃣
Находим индексы топ-K близких эмбеддингов и смотрим метаданные df[idx].

Всё это не требует никакого сервера или векторной БД. Если у вас 30 000—100 000 векторов по 768 float32, всё, скорее всего, влезет в память и будет быстро работать.

Пример: Magic: the Gathering

Автор оригинального поста сгенерировал эмбеддинги для 32 254 карт Magic (включая текст карты, стоимость, редкость и т.д.). 94 МБ занимает вся матрица float32 (32 254 строк × 768 столбцов). Для поиска ближайших соседей (например, топ-3 самых похожих карт) требуется около 1 мс на M3 Pro MacBook. Это довольно шустро.

Когда надо хранить метаданные (название, цвет, тип карты и т.д.), всё идёт в один parquet-файл: одна колонка — name, другая — embedding. Можно добавлять и другие, например, type, rarity, а потом фильтровать: «Покажи похожие, но только с цветом BB и типом Sorcery».

Когда всё-таки нужна векторная база?

Существуют ситуации, где Parquet + Polars уже недостаточно:

🪨 Сотни миллионов эмбеддингов — не поместятся в память или придётся делать сложный шардинг.
⏱️
Требование реального времени с экстремальным количеством запросов. Векторные БД (Faiss, Milvus, Qdrant, Pinecone) оптимизированы для ускоренной ANN (Approximate Nearest Neighbors).
Широкомасштабный проект: когда удобно иметь удалённый сервис, к которому подключается несколько микросервисов.
🔀
Объединение с другой бизнес-логикой и встроенная фильтрация «на лету», сильная параллелизация.

Но если у вас небольшой проект или вы делаете что-то локально, Parquet + Polars дают:

🔹 Единый портативный файл, который можно быстро перенести на другой компьютер.
🔹 Никакой привязки к Docker/внешним сервисам.
🔹 Встроенное хранение строк-метаданных и списка float32 под эмбеддингами в одной таблице.
🔹 Лёгкую фильтрацию и супербыструю загрузку/выгрузку.

Технические подробности реализации

🛠️ Чтение Parquet:

import polars as pl

df = pl.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])
emb_matrix = df["embedding"].to_numpy(allow_copy=False) # (n, d) float32

🛠️ Поиск похожих (простейшая версия на косинусном сходстве при нормализованных векторах):

import numpy as np

def fast_dot_product(query, matrix, k=3):
dot_products = query @ matrix.T # скалярные произведения
idx = np.argpartition(dot_products, -k)[-k:]
idx = idx[np.argsort(dot_products[idx])[::-1]]
score = dot_products[idx]
return idx, score

idx, scores = fast_dot_product(query_embed, emb_matrix, k=4)
related_rows = df[idx]

🛠️ Добавление новых эмбеддингов:

# Допустим, у нас уже есть df (Polars DataFrame) и новый массив embeddings
df = df.with_columns([pl.Series("embedding", embeddings)])
df.write_parquet("updated-embeddings.parquet")

Итог

🔑 Parquet хранит данные столбцами с нужными типами.
🔑
Polars отлично умеет работать со столбцом вида «список float32», не требуя лишнего копирования.
🔑
NumPy обеспечивает быструю реализацию dot-продуктов и сортировок.

Вместе они дают лёгкое и эффективное решение для небольших проектов, где не нужен «тяжёлый» движок векторной БД. Да, при масштабировании свыше сотен тысяч-векторов может потребоваться Faiss/Qdrant/Pinecone, но для личных экспериментов или умеренных задач Parquet + Polars часто закрывают все потребности.

Ссылки на оригинал и дополнительную информацию

📝 Исходная статья «The best way to use text embeddings portably is with Parquet and Polars»