Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik11 min czytania

Qdrant, baza wektorowa na wlasnym serwerze

Qdrant to otwarta baza wektorowa w Rust z filtrowaniem, kwantyzacja i wyszukiwaniem hybrydowym. Self hosting, cennik chmury i porownanie z Pinecone.

Qdrant, otwarta baza wektorowa, którą uruchomisz u siebie

Qdrant przechowuje wektory i wyszukuje po podobieństwie, podobnie jak konkurencja, ale różni się dwiema rzeczami. Napisany jest w Rust, więc jedna instancja obsługuje duży ruch bez strojenia, a licencja Apache 2.0 pozwala uruchomić go na własnym serwerze bez opłat i bez ograniczeń funkcji.

Dla kogo to rozwiązanie

Wybór między bazą wektorową jako usługą a bazą na własnym serwerze rzadko sprowadza się do ceny. Sprowadza się do tego, gdzie mogą leżeć dane i kto ma czas na utrzymanie.

Qdrant wygrywa w trzech sytuacjach. Gdy dane nie mogą opuścić firmy, bo dotyczą zdrowia, finansów albo objęte są wewnętrzną polityką. Gdy potrzebujesz precyzyjnego filtrowania po metadanych, bo tutaj jest ono pierwszorzędnym mechanizmem, a nie dodatkiem. Gdy zbiór jest na tyle duży, że rozliczenie za operacje w usłudze zewnętrznej przestaje być przewidywalne.

Przegrywa tam, gdzie nikt nie chce utrzymywać kolejnej bazy. Kontener trzeba aktualizować, dysk monitorować, kopie zapasowe robić i sprawdzać. Zespół bez tej gotowości szybciej dowiezie wynik na Pineconie albo na Postgresie, który już ma.

Uruchomienie w minutę

Code
Bash
docker run -p 6333:6333 -p 6334:6334 \
  -v $(pwd)/qdrant_storage:/qdrant/storage \
  qdrant/qdrant

Pod adresem localhost:6333/dashboard dostajesz interfejs do przeglądania kolekcji, co przy diagnozowaniu wyników bywa wygodniejsze niż odpytywanie API.

Code
Python
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct

klient = QdrantClient(url="http://localhost:6333")

klient.create_collection(
    collection_name="dokumentacja",
    vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)

klient.upsert(
    collection_name="dokumentacja",
    points=[
        PointStruct(
            id=1,
            vector=embedding,
            payload={"sekcja": "zwroty", "jezyk": "pl", "wersja": 3},
        )
    ],
)

Identyfikator punktu nadajesz sam i warto, żeby wynikał z pochodzenia fragmentu, na przykład z identyfikatora dokumentu i numeru części. Wtedy ponowne przetworzenie dokumentu nadpisuje stare wektory zamiast tworzyć duplikaty, a usunięcie dokumentu ze źródła da się odwzorować w bazie bez zgadywania, które punkty do niego należały.

Pole payload to dowolny obiekt JSON przypisany do wektora. Trzymasz w nim wszystko, po czym chcesz filtrować, oraz identyfikator pozwalający pobrać pełną treść z własnej bazy.

Filtrowanie, czyli mocna strona tego rozwiązania

W wielu bazach filtr po metadanych jest doklejony do wyszukiwania wektorowego i przy wąskich warunkach potrafi zwrócić mniej wyników, niż prosiłeś. Qdrant traktuje filtr jako element indeksu, więc zapytanie z ostrym warunkiem nadal zwraca komplet trafień.

Code
Python
from qdrant_client.models import Filter, FieldCondition, MatchValue, Range

wyniki = klient.query_points(
    collection_name="dokumentacja",
    query=embedding_zapytania,
    limit=4,
    query_filter=Filter(
        must=[
            FieldCondition(key="jezyk", match=MatchValue(value="pl")),
            FieldCondition(key="wersja", range=Range(gte=3)),
        ]
    ),
).points

Ma to konkretne przełożenie na produkt. Aplikacja wielodostępna, w której każde zapytanie musi ograniczyć się do jednego klienta, działa tu poprawnie także przy tysiącu klientów i wąskim wycinku danych. Warto tylko dodać indeks na polu, po którym filtrujesz najczęściej, bo bez niego przy dużych kolekcjach filtr zwalnia.

Kwantyzacja, czyli jak zmieścić dziesięć razy więcej

Wektor o 1536 wymiarach zapisany jako liczby zmiennoprzecinkowe zajmuje około sześciu kilobajtów. Milion takich wektorów to sześć gigabajtów pamięci, a pamięć jest najdroższym elementem rachunku.

Kwantyzacja zamienia te liczby na mniejsze reprezentacje. Wariant skalarny tnie rozmiar czterokrotnie przy minimalnej stracie jakości. Wariant binarny sprowadza każdą wartość do jednego bitu, co daje kompresję trzydziestodwukrotną, ale wymaga sprawdzenia jakości na własnych danych, bo dla części modeli embeddingowych strata jest zauważalna.

Code
Python
from qdrant_client.models import ScalarQuantization, ScalarQuantizationConfig, ScalarType

klient.create_collection(
    collection_name="dokumentacja",
    vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
    quantization_config=ScalarQuantization(
        scalar=ScalarQuantizationConfig(type=ScalarType.INT8, always_ram=True)
    ),
)

Praktyka wygląda tak: włącz kwantyzację skalarną domyślnie, bo prawie nic nie kosztuje pod względem jakości. Po binarną sięgnij dopiero, gdy zbiór przekracza kilka milionów wektorów, i zawsze zmierz trafność przed i po. Darmowy plan chmury dostawca wycenia na około milion wektorów o 768 wymiarach, czyli mniej więcej pół miliona przy 1536 wymiarach, a z kwantyzacją binarną wielokrotnie więcej, co dobrze pokazuje skalę różnicy.

Wyszukiwanie hybrydowe

Samo podobieństwo semantyczne gubi zapytania precyzyjne. Pytanie o kod błędu albo numer wersji znajdzie fragmenty o zbliżonej tematyce, niekoniecznie ten jeden właściwy.

Qdrant obsługuje wektory rzadkie obok gęstych, więc jedno zapytanie łączy wyszukiwanie znaczeniowe ze słownym. Wyniki obu kanałów scala się w jednym zapytaniu, bez odpytywania dwóch systemów i sklejania list po stronie aplikacji.

Wektor rzadki trzeba czymś policzyć, a to zwykle oznacza drugi model w potoku obok modelu osadzeń. BGE omija ten problem, bo jeden model zwraca reprezentację gęstą i rzadką w jednym przejściu, a licencja pozwala uruchomić go u siebie bez opłaty za token. Kosztem jest utrzymanie własnego serwowania modelu, więc przy niewielkim ruchu usługa zewnętrzna wychodzi taniej.

Code
Python
from qdrant_client.models import Prefetch, FusionQuery, Fusion

wyniki = klient.query_points(
    collection_name="dokumentacja",
    prefetch=[
        Prefetch(query=wektor_gesty, using="dense", limit=20),
        Prefetch(query=wektor_rzadki, using="sparse", limit=20),
    ],
    query=FusionQuery(fusion=Fusion.RRF),
    limit=4,
).points

Zanim to wdrożysz, sprawdź, czy problem w ogóle występuje. Zbiór dwudziestu realnych zapytań z oczekiwanym dokumentem pokaże, czy samo wyszukiwanie wektorowe wystarcza. Przy dokumentacji technicznej zwykle nie wystarcza, przy tekstach opisowych często tak.

Ile to kosztuje

WariantKosztCo dostajesz
Self hostingcena serweraPełna funkcjonalność, licencja Apache 2.0, brak limitów
Chmura, plan darmowy0 USD1 GB pamięci, 0,5 rdzenia, 4 GB dysku, bez karty
Chmura, klaster 2 GBokoło 26 USD miesięcznieJeden węzeł, 0,5 rdzenia, 8 GB dysku, rozliczenie godzinowe
Chmura, klaster 16 GBokoło 137 USD miesięcznieJeden węzeł, 2 rdzenie, 64 GB dysku, typowa produkcja
Hybryda i plan premiumwycena indywidualnaTwoja infrastruktura, zarządzanie po stronie dostawcy, gwarancja dostępności

Ceny z tabeli pochodzą z kalkulatora dostawcy dla Wirginii Północnej i są przybliżone, bo zależą od regionu. Ten sam węzeł 4 GB kosztuje tam 0,0467 USD za godzinę, a w São Paulo 0,0745 USD, czyli o ponad połowę drożej. Jedna stawka dla wszystkich regionów w tej usłudze nie istnieje, więc licz zawsze dla regionu, w którym faktycznie postawisz klaster.

Przy szacowaniu klastra pamiętaj też, że decyduje pamięć, a nie liczba wektorów sama w sobie, i że same wektory to nie wszystko. Milion wektorów o 1536 wymiarach zajmuje w postaci surowej około sześciu gigabajtów, ale kalkulator dostawcy przewiduje dla takiego zbioru dziesięć gigabajtów pamięci, bo dochodzi graf indeksu i miejsce na indeksy pól. Klaster musi więc być rzędu dwunastu do szesnastu gigabajtów, a nie ośmiu. Ten sam zbiór z kwantyzacją skalarną schodzi wielokrotnie niżej i zmieści się w wariancie znacznie tańszym. To pierwsza rzecz do policzenia przed wyborem planu, a nie po pierwszym rachunku.

Rachunek za self hosting jest niższy tylko pozornie. Do ceny maszyny dolicz aktualizacje, monitoring, kopie zapasowe i czas kogoś, kto zareaguje przy awarii. Przy jednej kolekcji i niewielkim ruchu darmowy plan chmury wychodzi taniej niż własny serwer. Przy kilkudziesięciu milionach wektorów proporcje się odwracają.

Wielodostępność, czyli jak nie pomieszać danych klientów

Aplikacja obsługująca wielu klientów potrzebuje pewności, że zapytanie jednego nie sięgnie danych drugiego. Qdrant daje na to trzy podejścia i różnią się one bardziej, niż sugeruje pierwsze wrażenie.

Osobna kolekcja na klienta izoluje najmocniej, ale każda kolekcja ma własny narzut pamięci. Przy pięciu klientach to nie problem, przy pięciuset zjada zasoby i wydłuża start instancji.

Wspólna kolekcja z filtrem po identyfikatorze klienta skaluje się najlepiej i jest zalecanym układem. Warunek jest jeden: filtr musi być narzucany po stronie serwera, na podstawie tożsamości z tokenu, a nie parametru przysłanego przez klienta. Pomyłka w tym miejscu to najpoważniejsza klasa błędów w aplikacjach wielodostępnych.

Code
Python
from qdrant_client.models import PayloadSchemaType

klient.create_payload_index(
    collection_name="dokumentacja",
    field_name="klient_id",
    field_schema=PayloadSchemaType.KEYWORD,
)

Indeks na polu identyfikującym klienta jest tu obowiązkowy, nie opcjonalny. Bez niego każde zapytanie przegląda cały zbiór, zanim zawęzi wynik, a czas odpowiedzi rośnie razem z liczbą klientów.

Trzecie podejście to podział na fragmenty według klucza klienta w trybie klastra. Dane jednego klienta trafiają wtedy fizycznie na wskazany węzeł, co pomaga przy wymaganiach dotyczących lokalizacji danych, na przykład gdy część klientów wymaga przetwarzania wyłącznie w Europie.

Przy usuwaniu klienta z systemu skasowanie punktów po filtrze jest jedną operacją, co bywa istotne przy żądaniu usunięcia danych osobowych.

Qdrant kontra alternatywy

RozwiązanieMocna stronaSłabośćKiedy wybrać
QdrantFiltrowanie, kwantyzacja, wersja na własny serwer bez ograniczeńUtrzymanie po Twojej stronie przy self hostinguDane wrażliwe, wielodostępność, duże zbiory
PineconeZero utrzymania, stabilne opóźnieniaOpłata minimalna, brak wersji lokalnejZespół bez czasu na infrastrukturę
pgvector na SupabaseJedna baza na wszystko, transakcje razem z danymiStrojenie indeksu przy większych zbiorachDo miliona wektorów, dane już w Postgresie
ChromaNajprostszy start, dobre do prototypuMniej opcji przy dużej skaliPrototyp i praca lokalna

Warto też sprawdzić, czy w projekcie nie ma już bazy, która to obsłuży. Postgres z rozszerzeniem wektorowym pokrywa większość zastosowań do miliona wektorów i oszczędza całą warstwę utrzymania.

Migracja między tymi rozwiązaniami nie jest kosztowna, o ile zachowasz ten sam model embeddingów. Znacznie droższa jest zmiana modelu, bo wymaga przeliczenia całego zbioru.

Utrzymanie instancji produkcyjnej

Trzy rzeczy warto ustawić od pierwszego dnia. Uwierzytelnianie kluczem API, bo domyślnie instancja jest otwarta, a wystawienie jej na publiczny adres bez klucza kończy się dokładnie tak, jak można się spodziewać. Kopie zapasowe przez migawki kolekcji, wykonywane automatycznie i przywracane próbnie raz na kwartał. Monitoring pamięci, ponieważ przekroczenie dostępnej pamięci przy indeksie trzymanym w RAM kończy się ubiciem procesu.

Przy większym ruchu przejdź na tryb klastra z replikacją. Kolekcja podzielona na fragmenty i zreplikowana przeżyje awarię jednego węzła, ale wymaga świadomej konfiguracji, nie włączy się sama.

Przy imporcie dużych zbiorów dziel dane na paczki po kilkaset punktów zamiast wysyłać je pojedynczo. Różnica w czasie ładowania miliona wektorów liczy się wtedy w godzinach, a nie w procentach.

Warto też pamiętać o gRPC. Interfejs REST jest wygodniejszy przy testach, natomiast przy dużym wolumenie zapisów gRPC daje wyraźnie lepszą przepustowość i to on powinien obsługiwać import danych.

Strojenie indeksu i kompromis między szybkością a trafnością

Wyszukiwanie wektorowe jest przybliżone. Baza nie porównuje zapytania z każdym wektorem, tylko przechodzi graf sąsiedztwa, i to od jego parametrów zależy, czy odpowiedź przychodzi w pięć milisekund, czy w pięćdziesiąt, oraz ile trafnych wyników przy okazji pominie.

Dwa parametry decydują o kształcie tego kompromisu. Pierwszy określa, ilu sąsiadów zapamiętuje każdy węzeł grafu przy budowaniu indeksu. Wyższa wartość podnosi trafność i zużycie pamięci, a także wydłuża budowanie indeksu. Drugi określa, jak szeroko baza przeszukuje graf przy konkretnym zapytaniu, i ten da się zmieniać bez przebudowy.

Code
Python
from qdrant_client.models import SearchParams

wyniki = klient.query_points(
    collection_name="dokumentacja",
    query=embedding_zapytania,
    limit=4,
    search_params=SearchParams(hnsw_ef=128, exact=False),
).points

Praktyczna kolejność strojenia wygląda tak. Zacznij od wartości domyślnych i zmierz trafność na zestawie realnych zapytań. Jeśli wyniki są dobre, nie ruszaj niczego, bo domyślne ustawienia są rozsądne dla większości zbiorów. Jeśli brakuje trafień, podnieś szerokość przeszukiwania przy zapytaniu i zmierz ponownie, zanim sięgniesz po przebudowę indeksu.

Osobno warto znać tryb dokładny, który porównuje zapytanie z każdym wektorem i daje wynik wzorcowy. Do produkcji się nie nadaje, bo jest wolny, ale świetnie służy jako punkt odniesienia: uruchamiasz go na dwudziestu zapytaniach i sprawdzasz, ile trafień gubi tryb przybliżony przy Twoich ustawieniach.

Typowe błędy

Pierwszy to brak indeksu na polach używanych w filtrach. Bez niego filtrowanie na dużej kolekcji przegląda dane liniowo i czas odpowiedzi rośnie z rozmiarem zbioru.

Drugi to wrzucanie pełnej treści dokumentu do payloadu. Pole to zwiększa rozmiar kolekcji i zużycie pamięci, a zwykle wystarcza identyfikator, po którym pobierzesz tekst z własnej bazy.

Trzeci to losowe identyfikatory punktów. Ponowne przetworzenie dokumentu tworzy wtedy duplikaty zamiast nadpisać stare wektory, a model dostaje dwie sprzeczne wersje tego samego zapisu.

Czwarty to wystawienie instancji bez uwierzytelniania. Domyślna konfiguracja nie wymaga klucza, więc trzeba go włączyć świadomie przed pierwszym wdrożeniem.

Piąty to włączanie kwantyzacji binarnej bez pomiaru. Kompresja trzydziestodwukrotna brzmi świetnie, ale przy części modeli embeddingowych psuje trafność na tyle, że system przestaje odpowiadać sensownie.

FAQ

Czy Qdrant jest darmowy?

Wersja otwarta na licencji Apache 2.0 jest darmowa także komercyjnie, bez ograniczeń funkcji, i uruchomisz ją na własnym serwerze. Chmura ma plan darmowy z jednym gigabajtem pamięci i bez terminu ważności, ale nieużywany klaster darmowy zostaje uśpiony po tygodniu, a po czterech tygodniach bezczynności skasowany, więc nie nadaje się na uśpiony zapas. Plany płatne rozliczają się godzinowo za przydzielone zasoby.

Qdrant czy Pinecone?

Qdrant wybierasz, gdy potrzebujesz wersji lokalnej, precyzyjnego filtrowania albo kontroli nad kosztem przy dużym zbiorze. Pinecone, gdy nie chcesz utrzymywać niczego i akceptujesz opłatę minimalną. Przy średniej skali oba dowożą podobną jakość wyszukiwania.

Ile pamięci potrzebuje milion wektorów?

Milion wektorów o 1536 wymiarach bez kompresji zajmuje około sześciu gigabajtów. Kwantyzacja skalarna schodzi do około półtora giga, binarna do około dwustu megabajtów, przy czym ta ostatnia wymaga sprawdzenia jakości na własnych danych.

Czy da się połączyć wyszukiwanie wektorowe ze zwykłym?

Tak, przez wektory rzadkie i scalanie wyników w jednym zapytaniu. To rozwiązuje typowy problem zapytań zawierających kody błędów, numery wersji i nazwy własne, których samo podobieństwo semantyczne nie znajduje.

Jak zintegrować Qdrant z aplikacją w Node albo Pythonie?

Oficjalne biblioteki istnieją dla Pythona, TypeScriptu, Rusta, Go i Javy, a gotowe integracje mają też LangChain i LlamaIndex. Wywołania kieruj z serwera, bo klucz API nie może trafić do przeglądarki.

Dokumentacja stoi na qdrant.tech, a kod źródłowy w repozytorium na GitHubie.