Strona główna / Artykuły

Jeden Retriever, trzy backendy: przenoszenie funkcji wyszukiwania z NumPy do Chroma, FAISS i Pinecone

Przenieś ręcznie opracowany narzędzie do wyszukiwania kosinusów w NumPy do Chromy, FAISS i Pinecone, a także dowiedz się, jakie funkcje takie jak indeksy, filtry metadanych i mechanizmy przechowywania wpływają na efektywność, oraz jakie błędy psują trafność wyników.

Wydawca: REACTAPP.TOP11 min czytania2493 słów

Źródła: Vector Databases, Hands-On — From Your numpy Array to Pinecone

Najprostszym magazynem wektorowym jest macierz NumPy z embeddingami wraz z jedną linią kodu arytmetycznego: chunk_vectors @ q_vec oblicza wyniki dla każdego fragmentu w odniesieniu do zapytania, a sortowanie wybiera najlepsze rezultaty. To jest prawdziwa baza danych wektorowych, tylko w najmniejszym możliwym stopniu rozbudowana. Ten przewodnik bierze dokładnie taki mechanizm wyszukiwania i przenosi go na trzy rzeczywiste backendy – Chroma, FAISS oraz Pinecone – abyś mógł dokładnie zobaczyć, co każdy z nich dodaje, który z wielu dostępnych produktów (Chroma, FAISS, Qdrant, Weaviate, Milvus, pgvector, Pinecone) nadaje się do danej sytuacji oraz jakie błędy potajemnie psują trafność wyników.

Przykłady lokalne nie wymagają kluczy API i są bezpłatne. Pinecone oferuje darmowy, serverless poziom startowy, który wymaga rejestracji i klucza, ale w momencie pisania tego tekstu nie wymaga karty płatniczej, więc możliwe jest również bezpłatne wypróbowanie tej usługi. Kod zakłada istnienie jednego skryptu, vdb.py, który uruchamiamy blok po bloku.

Gdzie tablica NumPy już nie wystarcza

Ręcznie stworzony mechanizm wyszukiwania oblicza podobieństwo kosinusowe dla każdego przechowywanego fragmentu przy każdym zapytaniu. Przy kilkuset fragmentach jest to natychmiastowe. Przy kilku milionach proces staje się powolny, cała macierz musi być przechowywana w pamięci RAM, a wszystkie reprezentacje wektorowe znikają po zakończeniu procesu. Baza danych wektorowych eliminuje trzy te problemy:

  1. Szybkość w skali. Zamiast dokonywać dokładnego porównania O(n) z każdym wektorem, tworzy się przybliżony indeks najbliższego sąsiada (ANN), takiego jak HNSW, który znajduje niemal najbliższe wektory w czasie około O(log n). Rezygnuje się z niewielkiej ilości dokładności w zamian za znaczne przyspieszenie.
  2. Persistencja. Wektory są przechowywane na dysku lub na serwerze zarządzanym i przetrwają restarty.
  3. Metryki i filtrowanie. Każdy wektor może zawierać atrybuty takie jak {"source": "doc12", "date": "2026-01"}, a zapytanie można do nich ograniczyć, na przykład do wektorów najbliższych, których źródło to doc12.

Różnica między tymi dwoma sposobami przetwarzania zapytań mieści się w dwóch liniach:

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)

To jest cała wartość oferowana przez tę technologię. Wszystko, co następuje, to tylko aspekty techniczne.

Słownictwo, z którym będziesz się często spotykać

  • Embedding lub wektor: lista liczb, która koduje znaczenie; przykłady tutaj używają all-MiniLM-L6-v2.
  • Miara odległości: sposób mierzenia bliskości. Kosinus (kąt między wektorami) jest standardowym wyborem dla tekstu; alternatywami są iloczyn skalarny oraz euklidesowy (L2). Użyj tego, do czego został przeszkolony twój model embeddingów.
  • Indeks: struktura, która przyspiesza wyszukiwanie. HNSW, czyli graf nawigowalny, jest standardem w większości baz danych; IVF i PQ w różny sposób kompromitują pamięć w zamian za szybkość.
  • Upsert: wstawienie wektora za pomocą ID lub jego aktualizacja, jeśli ID już istnieje.
  • Filtrowanie metadanych: zawężanie wyszukiwania za pomocą przechowywanych atrybutów.
  • Przestrzeń nazw lub kolekcja: podział wewnątrz jednej bazy danych, dzięki któremu dane niespowiązane nigdy się nie mieszają.
  • Chroma: naturalna pierwsza aktualizacja

    Chroma działa wewnątrz twojego procesu Pythona po jednej instalacji, bez konieczności używania serwera ani Dockera. Dzięki temu stanowi najprostszy krok w rozwoju od tablic NumPy. Zainstaluj ją razem z biblioteką do embeddingów:

    pip install chromadb sentence-transformers
    

    Poniższy skrypt wykorzystuje niewielki zbiór dokumentów opisujący fikcyjny produkt do notowania zwanym Nimbus, w którym każdy dokument ma tag tematyczny. Tworzy klient pamięciowy, otwiera kolekcję skonfigurowaną do obliczania odległości kosinowej za pomocą hnsw:space, wyraźnie embeduje dokumenty i dodaje je wraz z identyfikatorami oraz metadanymi. Następnie embeduje pytanie i prosi o dwa najbliższe dokumenty, wyświetlając każdy wraz z jego odległością. Zwróć uwagę na komentarz dotyczący chromadb.Client(): wystarczy zastąpić go PersistentClient(path=...), aby dane zostały przechowywane na dysku.

    # 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}")
    

    Jeśli porównać to z wersją ręcznie tworzoną, nie ma tu nic nowego pod względem koncepcyjnym. collection.add zastępuje proces kodowania dokumentów oraz przechowywanie macierzy. collection.query zastępuje iloczyn macierzy, funkcję argsort oraz operację wybierania pierwszych k elementów. Chroma wprowadza indeks HNSW oraz funkcje obsługi trwałości danych i metadanych, oparte na tej samej koncepcji.

    Chroma może również wygenerować embeddy dla tekstu za pomocą wbudowanego modelu, jeśli podasz parametr documents= bez embeddings=. Wyraźne określenie embeddów, jak powyżej, umożliwia zrozumienie mechanizmów i pozostawia wybór modelu w twoich rękach, co jest ważne, ponieważ ten sam model musi być używany zarówno przy dodawaniu, jak i wyszukiwaniu danych.

    Filtrowanie według metadanych

    Filtrowanie to funkcja, której zwykły tabliczka nigdy nie miała. Następne zapytanie szuka „bezpieczeństwa danych”, ale ogranicza wyniki do wektorów, których topic to security, za pomocą argumentu 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])
    

    W środowisku produkcyjnym to właśnie ono umożliwia zapytania takie jak „podobne zgłoszenia od tego klienta” lub „dokumentacja zaktualizowana w tym roku”. Filtr jest stosowany razem ze szukaniem wektorowym, a nie jako krok poobróbki, dzięki czemu nadal otrzymuje się do n_results wyników, które go spełniają. Jako ćwiczenie zmień filtr na where={"topic": "pricing"} i upewnij się, że dokumenty dotyczące zwrotów pieniędzy oraz bezpieczeństwa nigdy się nie pojawią.

    FAISS: szybka biblioteka indeksów, a nie baza danych

    FAISS od Meta wcale nie jest bazą danych. Jest to wysoce optymalizowana biblioteka do wyszukiwania najbliższych sąsiadów, która nie posiada metadanych ani własnej warstwy przechowywania i jest przeznaczona do włączenia bezpośrednio do aplikacji. Wiele innych systemów wykorzystuje ją wewnętrznie. Zainstaluj wersję dla CPU:

    pip install faiss-cpu
    

    Kod koduje dokumenty za pomocą parametru normalize_embeddings=True i przekształca je na typ float32, który wymaga FAISS. Tworzy się wtedy indeks typu IndexFlatIP, który bazuje na iloczynie wektorowym; ponieważ wektory są znormalizowane do długości jednostkowej, iloczyn wektorowy odpowiada podobieństwu kosinowemu. Po dodaniu wektorów zapytania również koduje się je w ten sam sposób, a następnie wywołuje funkcję search, aby znaleźć dwa najlepsze wyniki, po czym przyporządkowuje zwrócone pozycje do oryginalnych dokumentów.

    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]}")
    

    To jest niemal dosłownie odpowiednikiem narzędzia NumPy: IndexFlatIP to metoda wyszukiwania kosinusowego metodą siłowej, zaimplementowana w optymalizowanym C++. Korzyści pojawią się, gdy zastąpisz je indeksem przybliżonym, takim jak IndexHNSWFlat lub IndexIVFFlat, które zachowują tę samą interfejs, ale przeprowadzają wyszukiwanie w czasie subliniowym. Wybierz FAISS, gdy chcesz maksymalnej szybkości w obrębie jednego procesu i jesteś gotów samodzielnie zarządzać trwałością danych oraz metadanymi, na przykład poprzez przechowywanie oddzielnego mapowania od pozycji do rekordów dokumentów.

    Pinecone: w pełni zarządzany i bezserwerowy

    Wszystko do tej pory działało w twoim własnym procesie. Pinecone to zarządzana baza danych wektorowych dostępna przez sieć: nie ma potrzeby uruchamiania serwera, a system skaluje się automatycznie. Jest to opcja dla zespołów, które nie chcą zajmować się infrastrukturą.

    Należy pamiętać, że SDK Pinecone uległo znacznym zmianom. Starszy styl pinecone.init(api_key=, environment=) już nie istnieje; teraz tworzy się obiekt klienta Pinecone, a pakiet nosi nazwę pinecone, a nie pinecone-client. Sprawdź aktualną dokumentację SDK, jeśli coś poniżej nie zadziała.

    Blok zaczyna się od polecenia instalacji, a następnie od kodu w Pythonie (rozdziel je, jeśli go kopiujesz). Tworzy klienta oraz indeks bez serwera o nazwie nimbus tylko wtedy, gdy jeszcze nie istnieje, ustawiając dimension=384, aby był zgodny z MiniLM, oraz metric="cosine", po czym łączy się z nim. Wstawia lub aktualizuje pary (id, vector, metadata), przechowując oryginalny tekst w metadanych, ponieważ Pinecone zwraca tylko to, co zostało zapisane. Na koniec wyszukuje dwa najlepsze wyniki, ograniczając wyszukiwanie za pomocą filtru typu Mongo $eq na polu topic, i wyświetla każdą ocenę oraz tekst. W rzeczywistym kodzie klucz należy odczytać z zmiennej środowiskowej lub sklepu tajemnic, zamiast umieszczać go w kodzie źródłowym.

    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']}")
    

    Kształt dokładnie odpowiada Chromie: utwórz indeks, wykonaj operację upsert dla ID, wektorów i metadanych, a następnie zapytaj top-k z opcjonalnym filtrem. Krok pobierania danych jest taki sam jak wcześniej, teraz wykonywany na infrastrukturze autoscaling należącej do kogoś innego za pośrednictwem API. Pinecone obsługuje również namespaces, które dzielą jeden indeks, oraz integrated inference, w którym funkcje upsert_records i search_records samodzielnie wstawiają tekst, dzięki czemu nie musisz ręcznie używać narzędzi do wstawiania.

    Ogólnie rzecz biorąc, przechodząc z Chromy na Pinecone zmienił się jedynie sposób łączenia się i wywoływania sklepu, a nie logika RAG. W dobrze zorganizowanej aplikacji sklep wektorowy jest komponentem wymiennym, dlatego metody frameworku takie jak .as_retriever() z LangChain wyglądają identycznie niezależnie od backendu.

    Wybór backendu dostosowanego do Twoich ograniczeń

    Żadna baza danych wektorowych nie jest najlepsza w ogólności; jedna nadaje się najlepiej do określonego zestawu ograniczeń. Mniej więcej:

    • Tablica NumPy (stworzona ręcznie): działa wewnątrz procesu; odpowiednia do około 10 tys. wektorów, przy nauki lub gdy chcesz mieć pełną kontrolę.
    • Chroma: baza danych wbudowana, działająca wewnątrz procesu lub jako lokalny serwer; idealna do prototypów oraz aplikacji małych i średnich rozmiarów – najprostszy punkt wyjścia.
    • FAISS: biblioteka indeksowania, działająca wewnątrz procesu; maksymalna szybkość przy samodzielnym zarządzaniu przechowywaniem i metadanymi.
    • pgvector: rozszerzenie Postgresa wewnątrz istniejącej bazy danych; idealne, jeśli już używasz Postgresa i chcesz przechowywać wektory obok danych relacyjnych.
    • Qdrant: baza danych, hostowana samodzielnie za pomocą Dockera lub w chmurze; silne filtrowanie, wydajność dzięki językowi Rust, otwarty kod źródłowy, hybrydowe wyszukiwanie.
  • Weaviate: baza danych, do zarządzania lokalnie lub w chmurze; wbudowane moduły, wyszukiwanie hybrydowe oraz API GraphQL.
  • Milvus: baza danych, do zarządzania lokalnie (wymagająca większych zasobów) lub w chmurze; zaprojektowana do przechowywania miliardów wektorów.
  • Pinecone: wyłącznie w formie usługi zarządzanej, w własnej chmurze; brak konieczności obsługi oraz automatyczne skalowanie bez serwerów.
  • Często poruszana decyzja dotyczy wyboru między usługami zarządzanymi a samodzielnym hostowaniem. Usługi zarządzane, takie jak Pinecone czy chmurowe wersje innych rozwiązań, eliminują obowiązki operacyjne, ale wiążą się z ciągłymi kosztami i przechowują twoje dane na serwerach dostawcy. Samodzielne hostowanie Chromy, Qdrant, Weaviate, Milvusa lub pgvector oznacza ich uruchamianie we własnym zakresie, przy jednoczesnym zachowaniu kontroli nad kosztami i lokalizacją danych. Powszechnie przyjętą zasadą jest to, że samodzielne hostowanie zaczyna się opłacać, gdy miesięczny rachunek za usługę zarządzaną przekracza kilkaset dolarów; poniżej tego progu godziny pracy inżynierów przeznaczone na obsługę zwykle kosztują więcej, niż oszczędzają. Traktuj to jako przybliżoną zasadę, a nie prawo niezmienne, ponieważ zdolności operacyjne twojego zespołu mają takie samo znaczenie jak wysokość rachunku.

    Dla nauki oraz większości projektów pobocznych wystarczy Chroma lub pgvector. Pinecone lub Qdrant Cloud mają sens, gdy infrastruktura jest czymś, czego zespół woli nie posiadać, natomiast Milvus jest odpowiedni, gdy faktycznie przetwarzasz miliardy wektorów. Jeśli zastanawiasz się, czy w ogóle potrzebujesz dedykowanego magazynu wektorów, przeczytaj dlaczego RAG w produkcji wykracza poza dedykowane bazy danych wektorowych.

    Błędy, które powstają po uruchomieniu bazy danych

    • Niezgodność wymiarów to najczęstszy błąd. Wymiara indeksu dimension musi być równa rozmiarowi wyjściowemu modelu embedującego: 384 dla MiniLM, 1536 dla wielu modeli embedujących OpenAI. Mieszanie modeli w jednym indeksie gwarantuje błąd.
  • Niespójność metryki w tajemnicy niszczy trafność wyników, na przykład poprzez użycie odległości euklidesowej z modelem zaprojektowanym do obliczeń kosinusowych. Należy dopasować metrykę do modelu.
  • Aproxymacja to nie dokładność. Indeksy typu ANN czasami pomijają prawdziwego najbliższego sąsiada; to cena szybkości. Większość baz danych oferuje parametr, takie jak ef_search dla HNSW lub nprobe dla IVF, który poprawia dokładność wyników kosztem opóźnienia. Aby dowiedzieć się więcej na temat tych parametrów, zapoznaj się z dostrojeniem indeksów HNSW do zastosowań produkcyjnych RAG.
  • Ponowne wgranie danych przy każdym uruchomieniu występuje, gdy nic nie jest przechowywane trwale, więc za każdym razem ponosimy koszt przetwarzania danych. Użyj PersistentClient z Chromy lub bazy danych wspieranej serwerem.
  • Filtracja ma swój własny koszt. Intensywna filtracja metadanych może spowolnić zapytania; indeksuj pola, których używasz do filtracji, jeśli twoja baza danych to umożliwia.
  • Dobrym ćwiczeniem jest wzięcie istniejącego skryptu RAG opartego na NumPy i zastąpienie wyłącznie jego funkcji pobierania danymi kolekcji Chroma przedstawionej wcześniej. Część generowania w ogóle się nie zmienia, co pokazuje, że pobieranie danych to rzeczywiście odrębna, wymienna warstwa.

    Szybki przewodnik rozwiązywania problemów

    • dimension does not match: twoje wektory nie mają rozmiaru oczekiwanego przez indeks. Stwórz indeks ponownie z właściwą wartością dimension lub użyj narzędzia do embedowania, dla którego został on stworzony.
    • Wyniki Chroma wyglądają losowo: metryki są prawdopodobnie pomieszane lub do dodawania i wyszukiwania danych użyto różnych narzędzi embedowania. Obie muszą być identyczne.
  • Błędy Pinecone has_index lub create_index: upewnij się, że używasz aktualnego SDK (from pinecone import Pinecone) a nie przestarzałego stylu pinecone.init.
  • FAISS search zwraca ID -1: indeks zawiera mniej wektorów niż k. Dodaj dane lub zmniejsz wartość k.
  • We wszystkim występuje spowolnienie przy dużych skaliach: najprawdopodobniej używasz prostego, dokładnego indeksu. Przejdź na HNSW lub IVF i dostosuj parametr召回.
  • Główne wnioski

    • Baza danych wektorowych to specjalnie zaprojektowany mechanizm wyszukiwania, który jest szybki, trwały i umożliwia filtrowanie. Dodawanie wektorów polega na ich kodowaniu oraz przechowywaniu; zapytania realizowane są za pomocą algorytmu kosinusowego top-k.
    • Indeks ANN to jedyna naprawdę nowatorska cecha: dzięki akceptowaniu przybliżonych odpowiedzi wyszukiwanie pozostaje szybkie nawet wraz ze wzrostem ilości danych.
    • Filtrowanie metadanych oraz ich przechowywanie to funkcje produkcyjne, których brakuje zwykłym tablicom.
    • Gdy jakość wyszukiwania jest niska, najpierw należy ustalić, czy problem leży w embeddingach (niewłaściwy model lub metryka) czy w indeksie (zbyt niski wskaźnik odnalezienia). Zwrócone odległości zazwyczaj wskazują, co jest przyczyną.
    • Zachowaj magazyn za wąskim interfejsem, aby przejście z Chroma na Pinecone lub jakikolwiek inny system dotyczyło jedynie kodu łączenia i wywołań.