Wszystkie wpisy

Bazy wektorowe - biblioteka ułożona po nastrojach

Książki o miśkach leżą obok siebie, choć tytuły zaczynają się na różne litery. Tak działa baza wektorowa.


Dwie biblioteki #

Wyobraź sobie dwie biblioteki w Twoim mieście.

Biblioteka A (tradycyjna baza danych):

  • Książki ułożone alfabetycznie po tytule.
  • Szukasz „Kubuś Puchatek”? Idziesz do półki K.
  • Ale chcesz „książkę o tym, jak misiu uczy się dzielić” - nie wiesz, gdzie szukać. Tytuł nic Ci nie powie.

Biblioteka B (baza wektorowa):

  • Książki ułożone według znaczenia.
  • Książka o Kubusiu Puchatku leży obok „Paddington” i „Kłopoty Niedźwiedzia”. Tytuły różne, ale wszystkie o miśkach, które się uczą.
  • Mówisz bibliotekarce: „chcę coś o misiu, który się uczy dzielić” - ona zabiera Cię prosto do tej półki.

Jak to działa - „kod znaczenia” #

Każda książka dostaje od komputera unikalny kod - nie tytuł, nie numer, ale listę liczb opisujących jej nastrój i tematykę. Na przykład:

Kubuś Puchatek:  [0.8 dziecko, 0.9 miś,  0.7 przyjaźń, 0.1 horror, ...]
Paddington:      [0.8 dziecko, 0.9 miś,  0.6 przyjaźń, 0.0 horror, ...]
Książka kucharska: [0.1 dziecko, 0.0 miś, 0.2 przyjaźń, 0.0 horror, ...]

Te listy liczb nazywają się embeddings (osadzenia). Komputer patrzy: „o, Kubuś i Paddington mają bardzo podobne liczby”. Kładzie je blisko siebie na półce.

Po co to w 2026? #

Bo tak działa ChatGPT z Twoimi dokumentami (RAG). Pod spodem to zwykłe REST API - aplikacja pyta bazę wektorową, baza oddaje listę pasujących fragmentów, reszta to już tylko sklejka:

  1. Ładujesz do bazy wektorowej PDF z dokumentacją firmy. Każdy paragraf dostaje swój „kod znaczenia”.
  2. Ktoś pyta: „Jak zwolnić urlop w systemie?”.
  3. Pytanie też dostaje „kod znaczenia”.
  4. Baza nie szuka słów - szuka podobnych wibracji.
  5. Znajduje paragraf o „zgłaszaniu dni wolnych” - chociaż w pytaniu słowa „urlop” i „zwolnić” nie padają w tym paragrafie.
  6. AI dostaje znaleziony paragraf i układa odpowiedź.

To dlatego „chatboty firmowe” działają - nie szukają po słowach kluczowych, ale po znaczeniu.

W kodzie #

// Zamiana tekstu na "kod znaczenia" (wektor)
const wektor = await model.embed("miś, który się uczy dzielić");

// Szukanie 3 najbliższych książek w bibliotece
const wyniki = await vectorDB.query({
  vector: wektor,
  topK: 3,
});

// Wyniki: [Kubuś Puchatek, Paddington, Kłopoty Niedźwiedzia]

Popularne biblioteki (dosłownie) #

  • Pinecone - chmurowa, prosta w użyciu.
  • Weaviate - open source, z dodatkową warstwą metadanych.
  • Qdrant - napisany w Rust, szybki.
  • Chroma - lekka, dobra na start lokalny.

Najważniejsza lekcja #

W starym świecie pytałeś komputera: „znajdź słowo X”. W nowym świecie pytasz: „znajdź coś, co znaczy to, co czuję”.

Tym jest AI w 2026: komputer, który rozumie nastrój tekstu, nie tylko litery.

Embedding - funkcja tekst → wektor #

Embedding to odwzorowanie obiektu (tekst, obraz, audio) w wektor gęstych liczb zmiennoprzecinkowych o ustalonej wymiarowości, tak że podobieństwo geometryczne koreluje z podobieństwem semantycznym.

Dla popularnych modeli:

  • text-embedding-3-small (OpenAI) - 1536 wymiarów, redukowalne do 256/512 (Matryoshka embeddings).
  • text-embedding-3-large - 3072.
  • all-MiniLM-L6-v2 (sentence-transformers, open) - 384, szybki.
  • bge-large-en-v1.5 - 1024, dobry do RAG, open.

Pod spodem: transformer kodujący, bierzemy wektor z ostatniej warstwy (zwykle po pooling - CLS token lub mean pooling).

Miara podobieństwa #

  • Cosine similarity - cos(θ) = (a · b) / (||a|| ||b||). Zakres [-1, 1]. Niezależne od długości wektora. Najczęściej używane.
  • Dot product - a · b. Gdy wektory są znormalizowane (L2) - równoważne cosine. Szybsze do liczenia.
  • Euclidean distance - ||a - b||. Dla gęstych embeddings zwykle skorelowane z cosine, ale wrażliwe na długość.

Większość produkcyjnych embeddings jest L2-znormalizowanych → dot product = cosine, wybieramy dot product dla wydajności.

Problem: naiwne wyszukiwanie jest O(n·d) #

Baza z 10M wektorów, 1536 wymiarów, chcesz top-10 podobnych do query. Naiwnie: liczymy cosine dla każdej pary → 10M * 1536 = 15 mld operacji. Pojedyncze zapytanie trwa sekundy. Nie skaluje się.

ANN - Approximate Nearest Neighbors #

Rezygnujemy z gwarancji znalezienia dokładnie top-k, w zamian dostajemy sub-linear query (O(log n) lub O(√n)), z recall 95-99%. Dla RAG / rekomendacji - wystarczająco.

HNSW (Hierarchical Navigable Small World) #

Graf wielowarstwowy:

  • Warstwa 0 zawiera wszystkie punkty, gęsto połączone.
  • Wyższe warstwy coraz rzadsze, działają jak “highway” - szybki skok nad duże odległości.
  • Query zaczyna się na górze, schodzi w dół greedy, rafinuje rezultat na warstwie 0.

Parametry: M (liczba sąsiadów na węzeł, 8-64), ef_construction (jakość podczas budowy), ef_search (jakość przy zapytaniu - wyżej = dokładniej + wolniej).

Plusy: top recall, szybki. Minusy: pamięć (graf w RAM), koszt budowy O(n log n), re-indeksowanie przy dużych zmianach.

IVF-PQ (Inverted File + Product Quantization) #

  1. K-means grupuje wektory w nlist centroidów (np. 1024-65536).
  2. Query porównujesz tylko z nprobe najbliższymi centroidami (nie wszystkimi).
  3. Product Quantization kompresuje wektor - dzielisz go na podwektory, każdy podwektor koduje się najbliższym wpisem małego codebooka. 1536 * 4B = 6 KB → 48 bajtów / wektor.

Plusy: low memory footprint, dobre dla miliardów wektorów. Minusy: mniejszy recall niż HNSW, trzeba dostroić.

Bazy wektorowe - krajobraz #

Self-hostedWbudowana ANNFiltrowanie metadataHybryda BM25+vector
Qdranttak (Rust)HNSWtaktak
WeaviatetakHNSWtaktak
MilvustakHNSW, IVF, DiskANNtaktak
Pineconemanaged onlywłasnetaktak
pgvectortak (Postgres)HNSW, IVFFlattak (SQL WHERE)tak (przez ts_rank)

Dla <10M wektorów i już używanego Postgresa - pgvector wygrywa (prostota, transakcje, jedna baza). Dla dedykowanego RAG-a z filtrowaniem i większej skali - Qdrant albo Weaviate.

RAG - Retrieval-Augmented Generation #

Architektura kanoniczna:

  1. Indeksowanie - chunker dzieli dokument na fragmenty (500-1500 tokenów, overlap 10-20%), każdy idzie przez embedder, wektory + metadane lądują w bazie.
  2. Query time - pytanie użytkownika → embedding → top-k najbliższych fragmentów.
  3. Generation - znalezione fragmenty wstawiasz do system promptu LLM wraz z pytaniem, LLM generuje odpowiedź zakorzenioną w kontekście.

Dobry RAG to głównie retrieval quality. LLM “nic nie wymyśli” - jeśli retrieval nie znajdzie właściwego fragmentu, odpowiedź będzie niepełna.

Implementacja w C# - OpenAI + Qdrant #

// title: RagService.cs
using OpenAI;
using Qdrant.Client;
using Qdrant.Client.Grpc;

public class RagService
{
    private readonly OpenAIClient _openai;
    private readonly QdrantClient _qdrant;
    private const string Collection = "docs";

    public RagService(OpenAIClient openai, QdrantClient qdrant)
    {
        _openai = openai;
        _qdrant = qdrant;
    }

    public async Task IndexAsync(string docId, IEnumerable<string> chunks)
    {
        var points = new List<PointStruct>();
        int idx = 0;
        foreach (var chunk in chunks)
        {
            var embedding = await _openai.GetEmbeddingClient("text-embedding-3-small")
                .GenerateEmbeddingAsync(chunk);

            points.Add(new PointStruct
            {
                Id = (ulong)Guid.NewGuid().GetHashCode(),
                Vectors = embedding.Value.ToFloats().ToArray(),
                Payload =
                {
                    ["doc_id"] = docId,
                    ["chunk_idx"] = idx++,
                    ["text"] = chunk,
                }
            });
        }
        await _qdrant.UpsertAsync(Collection, points);
    }

    public async Task<string> AskAsync(string question, int topK = 5)
    {
        var qEmbedding = await _openai.GetEmbeddingClient("text-embedding-3-small")
            .GenerateEmbeddingAsync(question);

        var results = await _qdrant.SearchAsync(
            Collection,
            qEmbedding.Value.ToFloats().ToArray(),
            limit: (ulong)topK);

        var context = string.Join("\n---\n", results.Select(r => r.Payload["text"].StringValue));

        var chat = _openai.GetChatClient("gpt-4o-mini");
        var response = await chat.CompleteChatAsync(
            new SystemChatMessage($"Answer based ONLY on:\n{context}"),
            new UserChatMessage(question));

        return response.Value.Content[0].Text;
    }
}

Chunking - często najważniejszy krok #

  • Za małe chunki - tracisz kontekst, LLM dostaje urwane zdania.
  • Za duże - rozcieńczona istotność, trudniej trafić w prawidłowy fragment.
  • Złe granice - chunk urwany w środku tabeli / bloku kodu = bezużyteczny.

Strategie:

  • Split po strukturze (headings Markdown, akapity, sentences).
  • Overlap 10-20% żeby nie gubić kontekstu na granicy.
  • Recursive splitting (LangChain RecursiveCharacterTextSplitter) - próbuje po akapitach, potem zdaniach, potem słowach.
  • Semantic chunking - embedding każdego zdania, łączysz sąsiednie o wysokim cosine.

Hybrid search - vector + BM25 #

Czysty vector search gubi match-y na rzadkich słowach (nazwy własne, ID, kody produktów). Hybryda:

  1. BM25 (lexical) + vector (semantic) równolegle.
  2. Reciprocal Rank Fusion łączy wyniki: score(d) = Σ 1 / (k + rank_i(d)).

Dla RAG na dokumentacji technicznej / kodzie - hybryda zwykle wygrywa.

Pułapki #

  • Cross-language - model trenowany na angielskim słabo koduje polski. Wybieraj multilingual model.
  • Metric mismatch - indeks zbudowany z cosine, query z euclidean = niczego nie znajdzie. Konfiguracja bazy i klienta musi się zgadzać.
  • Embedding drift - zmieniłeś model embeddera, stare wektory nie są kompatybilne z nowymi. Musisz reindeksować całość.
  • Brak filtrowania per-tenant - w multi-tenant SaaS filtruj po tenant_id metadanych. Bez tego ryzyko leakage.
  • Koszty embeddingu - OpenAI text-embedding-3-small to ~$0.02 za 1M tokenów. Dla 10M chunków po 500 tokenów - $100. Self-hosted open model (MiniLM) - darmowe po amortyzacji GPU.

Skrótowo #

Baza wektorowa to indeks nad wektorami z ANN (HNSW / IVF) i metryką podobieństwa (cosine / dot). Najważniejsze zastosowanie to RAG: embed → store → retrieve top-k → feed LLM. Jakość = 80% retrieval (chunking + hybrid search), 20% LLM. Dla C#: OpenAI SDK + Qdrant client + sensowne chunking = działa.

Podobne wpisy