Главная / Статьи

Один ретривер, три бэкенда: портирование поиска NumPy в Chroma, FAISS, Pinecone

Перенесите ручно разработанный инструмент по поиску с использованием NumPy и функции косинуса в Chroma, FAISS и Pinecone, и узнайте, что добавляют индексы, фильтры метаданных и возможность сохранения данных, а также какие ошибки снижают релевантность результатов.

Издатель: REACTAPP.TOP11 мин чтения2493 слов

Источники: Vector Databases, Hands-On — From Your numpy Array to Pinecone

Самый простой хранилище векторов — это матрица NumPy с эмбеддингами плюс одна арифметическая операция: chunk_vectors @ q_vec оценивает каждый фрагмент по отношению к запросу, а сортировка выбирает лучшие результаты. Это настоящая векторная база данных, причем максимально упрощенная. В этом руководстве мы берем именно такой механизм поиска и адаптируем его к трём реальным бэкендам — Chroma, FAISS и Pinecone, чтобы вы могли точно увидеть, что добавляет каждый из них, какие из многочисленных продуктов (Chroma, FAISS, Qdrant, Weaviate, Milvus, pgvector, Pinecone) подходят для той или иной ситуации, а также какие ошибки незаметно портят релевантность результатов.

Для локальных примеров не требуются API-ключи, и они совершенно бесплатны. Pinecone предлагает бесплатный серверless-тариф для начинающих, который требует регистрации и ключа, но на момент написания статьи не требует указания кредитной карты, поэтому управляемый вариант также можно протестировать бесплатно. В коде предполагается наличие одного скрипта vdb.py, который запускается по блокам.

Когда массив NumPy уже недостаточен

Реализованный вручную механизм поиска вычисляет коэффициент косинусного сходства с каждым сохранённым фрагментом при каждом запросе. При нескольких сотнях фрагментов это происходит мгновенно. Однако при нескольких миллионах фрагментов процесс замедляется, вся матрица должна храниться в ОЗУ, и все эмбеддинги исчезают при завершении работы процесса. Векторная база данных устраняет три таких проблемы:

  1. Скорость при работе с большими объемами данных. Вместо точного сравнения со всеми векторами по алгоритму O(n) создается приблизительный индекс ближайших соседей, такой как HNSW, который позволяет находить почти ближайшие векторы примерно за O(log n). Вы теряете небольшую долю точности восстановления результатов в обмен на значительное ускорение.
  2. Постоянство данных. Векторы хранятся на диске или на управляемом сервере, поэтому остаются доступными после перезагрузки.
  3. Метаданные и фильтрация. Каждый вектор может содержать атрибуты, такие как {"source": "doc12", "date": "2026-01"}, и запрос можно ограничить ими, например, найти ближайшие векторы, исходящие из doc12.

Разница между этими двумя способами выполнения запросов можно описать в двух строках:

your numpy array:   query ──► compare to ALL n vectors ──► sort ──► top-k     (exact, slow at scale)
a vector database:  query ──► walk HNSW graph ──► ~top-k     (approximate, fast, on disk, filterable)

В этом заключается вся суть предложения. Все остальное — это технические детали.

Лексика, с которой вы будете часто сталкиваться

  • Эмбеддинг или вектор: список чисел, кодирующий смысл; в примерах здесь используется all-MiniLM-L6-v2.
  • Метрика расстояния: способ измерения близости. Косинус (угол между векторами) — обычный выбор для текста; альтернативами являются скалярное произведение и евклидово (L2) расстояние. Используйте тот вариант, для которого была обучена ваша модель эмбеддингов.
  • Индекс: структура, обеспечивающая быстрый поиск. HNSW, представляющий собой навигируемую графовую структуру, является стандартом в большинстве баз данных; IVF и PQ по-разному компромиссируют между объемом памяти и скоростью.
  • Upsert: вставка вектора по ID или его обновление, если такой ID уже существует.
  • Фильтрация метаданных: сужение поиска с использованием хранящихся атрибутов.
  • Пространство имен или коллекция: разделение внутри одной базы данных, благодаря которому несвязанные данные никогда не смешиваются.
  • Chroma: естественное первое обновление

    Chroma запускается внутри вашего процесса Python после одной установки, без необходимости в сервере или Docker. Это делает его наиболее прямым шагом вперед по сравнению с массивами NumPy. Установите его вместе с библиотекой для эмбеддингов:

    pip install chromadb sentence-transformers
    

    В приведённом ниже скрипте используется небольшой корпус, описывающий вымышленный инструмент для ведения заметок под названием Nimbus, при этом к каждому документу присваивается тег темы. Скрипт создаёт клиент в памяти, открывает коллекцию, настроенную для расчёта косинусного расстояния с помощью hnsw:space, явно встраивает документы и добавляет их вместе с ID и метаданными. Затем он встраивает вопрос и запрашивает два ближайших документа, выводя каждый из них вместе с его расстоянием. Обратите внимание на комментарий к chromadb.Client(): достаточно заменить его на PersistentClient(path=...), чтобы данные сохранялись на диске.

    # vdb.py
    import chromadb
    from sentence_transformers import SentenceTransformer
    
    embedder = SentenceTransformer("all-MiniLM-L6-v2")
    
    # Same Nimbus corpus from the RAG tutorial
    DOCS = [
        "Nimbus Pro costs 8 dollars per month billed annually, with 50 GB storage.",
        "Nimbus offers a 30-day refund policy on all paid plans.",
        "Nimbus Lite (free) includes 1 GB of storage and up to 50 notes.",
        "Nimbus stores notes encrypted at rest with AES-256; E2E encryption is Pro-only.",
        "Live chat support is Pro-only, Mon-Fri 9-6 UTC; free users get 48h email support.",
    ]
    metadatas = [{"topic": t} for t in ["pricing","refund","pricing","security","support"]]
    
    client = chromadb.Client()                       # in-memory; use PersistentClient(path=...) to save to disk
    collection = client.create_collection(
        "nimbus", metadata={"hnsw:space": "cosine"}   # choose the distance metric
    )
    
    # Embed and add. (↔ your chunk_vectors = embedder.encode(...))
    collection.add(
        ids=[f"doc{i}" for i in range(len(DOCS))],
        embeddings=embedder.encode(DOCS).tolist(),
        documents=DOCS,
        metadatas=metadatas,
    )
    
    # Query. (↔ your retrieve(): embed query, find nearest)
    q = "How much does the paid plan cost?"
    res = collection.query(query_embeddings=embedder.encode([q]).tolist(), n_results=2)
    for doc, dist in zip(res["documents"][0], res["distances"][0]):
        print(f"[{dist:.3f}] {doc}")
    

    Если сопоставить это с ручной версией, то концептуально ничего нового нет. collection.add заменяет процесс кодирования документов и хранения матрицы. collection.query заменяет собой операцию умножения матриц, функцию argsort и выделение топ-k элементов. Вклад Chroma заключается в использовании индекса HNSW, возможности сохранения данных и поддержки метаданных на основе той же идеи.

    Chroma также может генерировать вкладыши для текста с помощью встроенной модели, если передать параметр documents= без embeddings=. Явное указание вкладышей, как описано выше, позволяет видеть внутренние механизмы и оставляет выбор модели за пользователем, что важно, поскольку для добавления и поиска должна использоваться одна и та же модель.

    Фильтрация по метаданным

    Фильтрация — это функция, которой никогда не обладал простой массив. Следующий запрос ищет «безопасность данных», но ограничивает результаты векторами, у которых topic равен security, с помощью аргумента where:

    res = collection.query(
        query_embeddings=embedder.encode(["data security"]).tolist(),
        n_results=3,
        where={"topic": "security"},   # only search vectors tagged security
    )
    print(res["documents"][0])
    

    В реальных условиях именно это позволяет выполнять запросы вроде «похожие заявки от этого клиента» или «документация, обновленная в этом году». Фильтр применяется одновременно с поиском векторов, а не в качестве этапа последующей обработки, поэтому всё равно получается до n_results совпадений, соответствующих условию. В качестве упражнения измените фильтр на where={"topic": "pricing"} и убедитесь, что документы, касающиеся возврата средств и безопасности, больше не появляются.

    FAISS: библиотека для быстрого поиска в индексах, а не база данных

    FAISS от Meta вовсе не является базой данных. Это высокооптимизированная библиотека для поиска ближайших соседей без метаданных и собственного слоя хранения, предназначенная для встраивания в ваше приложение. Многие другие системы используют её внутренне. Установите версию для CPU:

    pip install faiss-cpu
    

    В коде документы кодируются с параметром normalize_embeddings=True и преобразуются в тип float32, что требуется FAISS. Создаётся индекс типа IndexFlatIP, представляющий собой индекс скалярного произведения; поскольку векторы нормализованы до единичной длины, скалярное произведение соответствует косинусному сходству. После добавления векторов запрос также кодируется аналогичным образом, затем вызывается функция search для поиска двух лучших результатов, после чего полученные позиции преобразуются обратно в исходные документы.

    import faiss, numpy as np
    
    vectors = embedder.encode(DOCS, normalize_embeddings=True).astype("float32")
    index = faiss.IndexFlatIP(vectors.shape[1])   # IP = inner product = cosine on normalized vectors
    index.add(vectors)                            # ↔ exactly your numpy array, but optimized in C++
    
    q = embedder.encode(["refund window?"], normalize_embeddings=True).astype("float32")
    scores, idx = index.search(q, k=2)            # ↔ your argsort top-k
    for s, i in zip(scores[0], idx[0]):
        print(f"[{s:.3f}] {DOCS[i]}")
    

    Это практически дословная аналогия поискового механизма NumPy: IndexFlatIP представляет собой поиск по косинусу методом грубой силы, реализованный с использованием оптимизированного C++. Положительный эффект проявляется при замене его на приближённые индексы, такие как IndexHNSWFlat или IndexIVFFlat, которые сохраняют тот же интерфейс, но выполняют поиск за время, меньшее линейного. Используйте FAISS, когда вам нужна максимальная скорость внутри одного процесса и вы готовы сами обрабатывать вопросы сохранения данных и метаданных, например, путём ведения отдельной таблицы сопоставлений между позициями и записями документов.

    Pinecone: полностью управляемая и безсерверная система

    До сих пор всё работало внутри вашего собственного процесса. Pinecone — это управляемая векторная база данных, доступная через сеть: нет необходимости запускать сервер, и она автоматически масштабируется. Это опция для команд, которые не хотят заниматься управлением инфраструктурой.

    Обратите внимание, что SDK Pinecone претерпел значительные изменения. Старый способ pinecone.init(api_key=, environment=) больше не существует; теперь необходимо создавать объект клиента Pinecone, причем пакет называется pinecone, а не pinecone-client. Если что-то из приведенного ниже не сработает, ознакомьтесь с документацией к текущему SDK.

    Блок начинается с команды установки, за которой следует код на Python (разделите их при копировании). В нем создается клиент, а также безсерверный индекс с именем nimbus, если такого ещё нет; при этом устанавливаются параметры dimension=384 для совместимости с MiniLM и metric="cosine", после чего осуществляется подключение к индексу. Выполняется операция upsert с использованием кортежей (id, vector, metadata), причём оригинальный текст сохраняется в разделе metadata, поскольку Pinecone возвращает только то, что было сохранено. В заключение выполняется запрос на получение двух лучших результатов с использованием фильтра в стиле Mongo $eq по полю topic, после чего выводятся оценки и тексты. В реальном коде ключ следует читать из переменной окружения или хранилища секретов, а не указывать напрямую в исходном коде.

    pip install pinecone
    
    from pinecone import Pinecone, ServerlessSpec
    
    pc = Pinecone(api_key="YOUR_API_KEY")          # get a free key at app.pinecone.io
    
    # Create a serverless index (only once). dimension must match your embedder: MiniLM = 384.
    if not pc.has_index("nimbus"):
        pc.create_index(
            name="nimbus",
            dimension=384,
            metric="cosine",
            spec=ServerlessSpec(cloud="aws", region="us-east-1"),
        )
    index = pc.Index("nimbus")
    
    # Upsert: (id, vector, metadata) tuples
    index.upsert(vectors=[
        (f"doc{i}", embedder.encode(d).tolist(), {"text": d, "topic": m["topic"]})
        for i, (d, m) in enumerate(zip(DOCS, metadatas))
    ])
    
    # Query with metadata filtering
    res = index.query(
        vector=embedder.encode("how much is pro?").tolist(),
        top_k=2,
        include_metadata=True,
        filter={"topic": {"$eq": "pricing"}},      # Mongo-style filter operators
    )
    for match in res["matches"]:
        print(f"[{match['score']:.3f}] {match['metadata']['text']}")
    

    Формат полностью соответствует Chroma: создается индекс, выполняются операции добавления или обновления ID, векторов и метаданных, затем производится поиск топ-к элементов с необязательным фильтром. Шаг извлечения данных такой же, как и раньше, но теперь он выполняется на инфраструктуре автомасштабирования кого-то другого через API. Pinecone также поддерживает названия пространств, которые разделяют один индекс, а также интегрированную обработку запросов, при которой функции upsert_records и search_records сами встраивают текст, что позволяет избежать необходимости использования отдельного инструмента для встраивания.

    Общий вывод заключается в том, что переход с Chroma на Pinecone изменил лишь способ подключения и вызова хранилища, но не саму логику RAG. В хорошо структурированном приложении векторное хранилище является заменяемым компонентом, и именно поэтому методы фреймворков, такие как .as_retriever() от LangChain, выглядят одинаково независимо от выбранного бэкенда.

    Выбор бэкенда под ваши требования

    Нет универсально лучшей векторной базы данных; оптимальная выбирается в зависимости от конкретных ограничений. В общих чертах:

    • Массив NumPy (самостоятельно созданный): работает внутри процесса; подходит для примерно 10 тыс. векторов, при обучении или когда требуется полный контроль.
    • Chroma: встроенная база данных, работает внутри процесса или как локальный сервер; идеальна для прототипов и приложений небольшого-среднего размера — самый простой способ начала работы.
    • FAISS: библиотека индексации, работает внутри процесса; обеспечивает максимальную скорость при личном управлении хранилищем и метаданными.
    • pgvector: расширение для Postgres внутри существующей базы данных; идеально подходит, если вы уже используете Postgres и хотите хранить векторы рядом с реляционными данными.
    • Qdrant: база данных, которую можно разместить самостоятельно с помощью Docker или в облаке; обеспечивает мощную фильтрацию, высокую производительность благодаря языку Rust, является open source и поддерживает гибридный поиск.
  • Weaviate: база данных, с самостоятельной установкой или в облаке; встроенные модули, гибридный поиск, API GraphQL.
  • Milvus: база данных, с самостоятельной установкой (более сложная в эксплуатации) или в облаке; разработана для обработки миллиардов векторов.
  • Pinecone: исключительно управляемая служба, работающая в собственном облаке; отсутствие необходимости в операциях и автоматическое масштабирование без серверов.
  • Вопрос, который постоянно возникает, — это использование управляемых решений против самостоятельной развертки. Управляемые сервисы, такие как Pinecone или облачные версии других решений, избавляют от оперативных задач, но сопровождаются постоянными расходами и требуют хранения данных на серверах поставщика. Самостоятельная развертка Chroma, Qdrant, Weaviate, Milvus или pgvector означает их эксплуатацию собственными силами, что позволяет контролировать расходы и местоположение данных. Часто приводится правило, согласно которому самостоятельная развертка становится выгодной, когда ежемесячный счет за управляемые сервисы превышает несколько сотен долларов; ниже этого порога время, затрачиваемое на техническое обслуживание, обычно стоит дороже, чем экономия. Рассматривайте это как приблизительную ориентировочку, а не как строгое правило, поскольку оперативные возможности вашей команды имеют такое же значение, как и сумма счета.

    Для обучения и большинства побочных проектов достаточно Chroma или pgvector. Pinecone или Qdrant Cloud подходят тогда, когда команда предпочитает не владеть собственной инфраструктурой, а Milvus — когда вы действительно работаете с миллиардами векторов. Если вы размышляете о необходимости специализированного хранилища векторов, ознакомьтесь с причинами, по которым производственные решения типа RAG выходят за рамки специализированных баз данных векторов.

    Ошибки, которые возникают после запуска базы данных

    • Несоответствие размеров измерений — самая распространенная ошибка. Параметр dimension индекса должен совпадать с размером выходных данных модели генерации векторов: 384 для MiniLM, 1536 для многих моделей OpenAI. Смешивание разных моделей в одном индексе гарантированно приводит к сбоям.
  • Несоответствие метрик тайно уничтожает релевантность, например при использовании евклидова расстояния с моделью, разработанной для косинусного расстояния. Необходимо согласовать метрику с моделью.
  • Приближенное — не точное. Индексы типа ANN иногда упускают настоящего ближайшего соседа; это цена скорости. Большинство баз данных предоставляют параметры, такие как ef_search для HNSW или nprobe для IVF, которые повышают точность поиска за счет увеличения задержки. Чтобы узнать больше об этих параметрах, ознакомьтесь с настройкой индексов HNSW для производственных систем RAG.
  • Повторная генерация эмбеддингов при каждом запуске происходит, когда ничего не сохраняется, поэтому вы оплачиваете затраты на генерацию эмбеддингов при каждом запуске. Используйте PersistentClient от Chroma или базу данных с серверной поддержкой.
  • У фильтров есть своя стоимость. Интенсивная обработка метаданных может замедлить выполнение запросов; индексируйте поля, по которым производится фильтрация, если ваша база данных это поддерживает.
  • Хорошей практикой является взятие существующего скрипта RAG на основе NumPy и замена только его функции поиска на ранее показанный набор Chroma. Часть, отвечающая за генерацию текста, совсем не меняется, что демонстрирует, что слой поиска действительно является отдельным и заменяемым компонентом.

    Краткий справочник по устранению неполадок

    • dimension does not match: размер ваших векторов не соответствует ожидаемому размеру индекса. Создайте индекс с правильным dimension или используйте тот инструмент для генерации векторов, с помощью которого он был создан.
    • Результаты Chroma кажутся случайными: вероятно, метрики смешаны или для добавления данных и выполнения запросов использовались разные инструменты генерации векторов. Оба инструмента должны совпадать.
  • Ошибки Pinecone has_index или create_index: убедитесь, что вы используете текущую версию SDK (from pinecone import Pinecone), а не устаревший способ с pinecone.init.
  • Функция FAISS search возвращает ID -1: в индексе содержится меньше векторов, чем указано в k. Добавьте данные или уменьшите значение k.
  • Медленная работа при больших объемах данных: скорее всего, используется простой индекс с точным поиском. Переключитесь на HNSW или IVF и отрегулируйте параметр восстановления результатов.
  • Основные выводы

    • База данных векторов — это специально созданный механизм поиска, обеспечивающий высокую скорость, стабильность и возможность фильтрации. Добавление векторов включает в себя их кодирование и хранение; поиск осуществляется с использованием метода косинусного сходства top-k.
    • Индекс ANN — единственный по-настоящему новый элемент: благодаря возможности принимать приблизительные ответы поиск остается быстрым даже при увеличении объема данных.
  • Фильтрация метаданных и их сохранение — это функции, отсутствующие у обычных массивов в производственной среде.
  • Если качество поиска низкое, сначала определите, в чём проблема: в эмбеддингах (неправильная модель или метрика) или в индексе (слишком низкий уровень воспроизведения). Возвращаемые расстояния обычно помогают понять причину.
  • Храните данные через узкий интерфейс, чтобы при переходе с Chroma на Pinecone или любую другую систему изменялись только код подключения и вызовов.