Головна / Статті

Один Retriever, три бекенди: міграція пошуку 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 пропонує безкоштовний серверний стартовий рівень, який вимагає реєстрації та ключа, але на момент написання не потребує платіжної картки, тож керований підхід також можна випробувати безкоштовно. Код передбачає наявність одного скрипту vdb.py, який виконується блок за блоком.

Коли масив NumPy вже недостатній

Ручно створений механізм пошуку обчислює косинусову схожість з кожним збереженим фрагментом під час кожного запиту. З кількома сотнями фрагментів це відбувається миттєво. З кількома мільйонами це стає повільним, вся матриця мусить знаходитися у оперативній пам’яті, і кожен ембеддинг зникає після завершення процесу. Векторна база даних усуває три ці проблеми:

  1. Швидкість у масштабі. Замість точного порівняння O(n) з кожним вектором створюється приблизний індекс найближчих сусідів (ANN), такий як 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. Встановіть його разом із бібліотекою embedding:

    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=. Явне використання ембеддингів, як описано вище, забезпечує прозорість механізмів та дає можливість самостійно вибирати модель, що має значення, адже для додавання та пошуку потрібно використовувати одну й ту саму модель.

    Фільтрація за метаданими

    Фільтрація — це функція, якої ніколи не мав звичайний масив. Наступне запитання шукає „data security“, але обмежує результати векторами, у яких 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", а потім під’єднується до нього. Він вставляє або оновлює туплі (id, vector, 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: створюється індекс, виконується операція upsert з ID, векторами та метаданими, а потім виконується запит top-k з необов’язковим фільтром. Крок отримання даних залишається таким самим, як і раніше, але тепер він виконується на інфраструктурі автомасштабування когось іншого через API. Pinecone також підтримує назви просторів, які дозволяють розділяти один індекс, та інтегроване оброблення даних, де функції upsert_records та search_records автоматично вбудовують текст, тож не потрібно самостійно виконувати цю операцію.

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

    Вибір бекенду відповідно до ваших обмежень

    Жодна база даних векторів не є найкращою у загальному сенсі; кожна з них підходить для певного набору обмежень. Ось приблизний огляд:

    • NumPy array (створений вручну): працює в межах процесу; підходить для приблизно 10 тис. векторів, для навчання чи коли потрібен повний контроль.
    • Chroma: вбудована база даних, яка може працювати в межах процесу або як локальний сервер; ідеальна для прототипів та додатків малого чи середнього розміру — найпростіший варіант для початку.
    • FAISS: бібліотека для створення індексів, яка працює в межах процесу; забезпечує максимальну швидкість, коли ви самостійно керуєте зберіганням та метаданими.
    • pgvector: розширення для Postgres у вашій існуючій базі даних; ідеальне рішення, якщо ви вже використовуєте Postgres та хочете зберігати вектори поруч із реляційними даними.
    • Qdrant: база даних, яку можна розмістити самостійно за допомогою Docker або в хмарі; має потужну функцію фільтрації, високу продуктивність завдяки технології Rust, є відкритим кодом та підтримує гібридний пошук.
  • 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.
  • Усе працює повільно при великих обсягах даних: найімовірніше, ви використовуєте простий індекс типу “flat”. Перейдіть на HNSW або IVF та налаштуйте параметр відтворення даних.
  • Основні висновки

    • База даних векторів — це спеціально створений інструмент для пошуку, який забезпечує швидкість, стабільність та можливість фільтрації. Додавання векторів включає їх кодування та зберігання; пошук здійснюється за алгоритмом косинусного коефіцієнта top-k.
    • Індекс ANN є єдиним справді новаторським елементом: завдяки прийняттю наближених відповідей пошук залишається швидким навіть при зростанні обсягу даних.
  • Фільтрація та збереження метаданих — це функції, яких бракує звичайному масиву у продакшен-середовищі.
  • Якщо якість пошуку погана, спочатку вирішіть, чи проблема у ембеддингах (неправильна модель чи метрика), чи у індексі (занадто низький рівень відтворення даних). Отримані відстані зазвичай показують, що саме є причиною.
  • Зберігайте інформацію за вузьким інтерфейсом, щоб перехід від Chroma до Pinecone чи будь-якого іншого інструменту стосувався лише коду підключення та викликів.