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:
- Ładujesz do bazy wektorowej PDF z dokumentacją firmy. Każdy paragraf dostaje swój „kod znaczenia”.
- Ktoś pyta: „Jak zwolnić urlop w systemie?”.
- Pytanie też dostaje „kod znaczenia”.
- Baza nie szuka słów - szuka podobnych wibracji.
- Znajduje paragraf o „zgłaszaniu dni wolnych” - chociaż w pytaniu słowa „urlop” i „zwolnić” nie padają w tym paragrafie.
- 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) #
- K-means grupuje wektory w
nlistcentroidów (np. 1024-65536). - Query porównujesz tylko z
nprobenajbliższymi centroidami (nie wszystkimi). - 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-hosted | Wbudowana ANN | Filtrowanie metadata | Hybryda BM25+vector | |
|---|---|---|---|---|
| Qdrant | tak (Rust) | HNSW | tak | tak |
| Weaviate | tak | HNSW | tak | tak |
| Milvus | tak | HNSW, IVF, DiskANN | tak | tak |
| Pinecone | managed only | własne | tak | tak |
| pgvector | tak (Postgres) | HNSW, IVFFlat | tak (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:
- Indeksowanie - chunker dzieli dokument na fragmenty (500-1500 tokenów, overlap 10-20%), każdy idzie przez embedder, wektory + metadane lądują w bazie.
- Query time - pytanie użytkownika → embedding → top-k najbliższych fragmentów.
- 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:
- BM25 (lexical) + vector (semantic) równolegle.
- 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_idmetadanych. Bez tego ryzyko leakage. - Koszty embeddingu - OpenAI
text-embedding-3-smallto ~$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
REST API to kelner w restauracji
Nie idziesz do kuchni. Mówisz kelnerowi, co chcesz - on wie, kogo spytać.
Claude Code - agent w terminalu, nie autocomplete w IDE
Inna filozofia niż Copilot i Cursor. Pełnoprawny agent w CLI, który czyta, pisze, uruchamia testy i sam dochodzi do zielonych checków.
CLAUDE.md - kontrakt z AI, który koduje jak nadgorliwy stażysta
Andrej Karpathy nazwał problem po imieniu. Społeczność odpowiedziała plikiem CLAUDE.md, który zmienia model z chaotycznego generatora w zdyscyplinowanego asystenta.