# Vektorsøgning

## Hvad det er

Vektorsøgning finder de lagrede elementer, hvis vektorer ligger tættest på
vektoren for en forespørgsel. På engelsk hedder det vector search. En vektor er
her en liste af tal med fast længde, som en embeddingmodel danner ud fra et
stykke tekst, et billede eller en produktpost. Modellen er trænet, så elementer
med beslægtet indhold får vektorer, der ligger tæt på hinanden, og derfor kan et
spørgsmål og det afsnit, der besvarer det, ligge tæt sammen, selv om de ikke har
et eneste ord til fælles.

»Tættest« kræver et mål. pgvector, den PostgreSQL-udvidelse som artiklens
eksempler bygger på, understøtter seks: L2-afstand (euklidisk afstand), indre
produkt, cosinusafstand og L1-afstand samt Hamming- og Jaccard-afstand til
binære vektorer. Embeddingmodellens egen dokumentation oplyser, hvilket mål dens
vektorer er beregnet til. For vektorer, der er normaliseret til længden 1, giver
cosinusafstand og indre produkt naboerne i samme rækkefølge, og her anbefaler
pgvector indre produkt, fordi det giver den bedste ydelse.

Der er to måder at besvare spørgsmålet på. Eksakt søgning sammenligner
forespørgslen med hver eneste lagrede vektor og returnerer de sande nærmeste
naboer. Det gør pgvector som standard, og dokumentationen beskriver det som
perfekt recall, det vil sige at alle de sande naboer kommer med. Approksimativ
søgning bruger et indeks, der kun besøger en del af dataene, og bytter noget
recall for fart: de fleste af de sande naboer kommer med, men der er ingen
garanti for dem alle. Derfor advarer pgvector om, at et approksimativt indeks i
modsætning til et almindeligt indeks ændrer det, en forespørgsel returnerer.

pgvector har to typer approksimativt indeks. HNSW bygger en graf i flere lag, en
metode som Malkov og Yashunin første gang offentliggjorde i 2016: hvert lag
rummer en delmængde af laget nedenunder, en søgning begynder i det lille øverste
lag og arbejder sig ned til det fulde, og deres artikel rapporterer, at
søgningens omkostning vokser logaritmisk. IVFFlat deler vektorerne op i lister
og søger kun i de lister, der ligger nærmest forespørgslen.

## Hvorfor det betyder noget

Fuldtekstsøgning matcher ord. En kunde, der skriver »skærmen er helt sort«,
finder ikke manualens afsnit med overskriften »displayet tænder ikke«, fordi de
to ikke deler et eneste ord, heller ikke når endelserne er skåret af.
Vektorsøgning sammenligner betydning, sådan som embeddingmodellen har opfanget
den, så klagen og afsnittet kan lande tæt på hinanden. Samme idé virker ud over
tekst: et billede, der ligner dette, en supportsag, der ligner en allerede løst,
et produkt, der ligner det i kurven.

Vektorsøgning er også én måde at klare hentningen på i retrieval-augmented
generation, hvor en stor sprogmodel svarer ud fra tekstafsnit, der er hentet til
spørgsmålet. Spørgsmålet laves om til en vektor med den samme model som
afsnittene, og de nærmeste afsnit sendes med i prompten. Svaret kan aldrig blive
bedre end det, søgningen fandt, så søgningen fortjener den samme omhu som
modellen.

At beholde vektorerne i den database, man allerede kører, har en praktisk side,
og pgvector indleder sin dokumentation med netop det argument. Vektorerne får de
samme transaktioner, den samme point-in-time recovery og de samme joins som alle
andre kolonner, og fordi pgvector skriver til write-ahead-loggen, følger de også
med ved replikering. Et filter på kunde, dato eller rettigheder er en almindelig
`WHERE`-betingelse på den samme række, og et slettet dokument holder op med at
blive fundet i samme øjeblik, som transaktionen bliver committet. Et separat
vektorlager er et system mere, der skal fyldes og holdes i takt med det første.

Et team bør også vide, hvad vektorsøgning ikke gør. Den rangerer efter nærhed,
ikke efter sandhed. Det nærmeste afsnit er det, der ligner spørgsmålet mest; det
kan stadig være forældet, forkert eller stamme fra et dokument, læseren ikke må
se. Nogle spørgsmål står og falder med et bestemt ord, for eksempel et navn, et
varenummer eller en fejlkode, og pgvectors dokumentation foreslår at bruge
vektorsøgning sammen med PostgreSQL's fuldtekstsøgning. Den kombination kaldes
hybrid søgning.

## Sådan ser det ud i praksis

pgvector, der i oktober 2026 er i version 0.8.7, understøtter PostgreSQL 13 og
nyere. Udvidelsen aktiveres én gang i hver database, og derefter får tabellen en
kolonne af typen `vector` med det antal dimensioner, modellen leverer:

```sql
CREATE EXTENSION vector;

CREATE TABLE passages (
    id        bigserial PRIMARY KEY,
    source    text NOT NULL,
    body      text NOT NULL,
    embedding vector(3)
);

CREATE INDEX ON passages USING hnsw (embedding vector_cosine_ops);
```

Tre dimensioner holder eksemplet til at læse; en rigtig model leverer langt
flere, og kolonnen angiver præcis det tal, modellen leverer. Programmet beregner
hver embedding med modellen og indsætter den ved siden af den tekst, den kom
fra. Indekset hører til én bestemt afstand: `vector_cosine_ops` betjener
forespørgsler, der sorteres efter cosinusafstand, og en forespørgsel, der
sorteres efter L2-afstand, kræver et ekstra indeks bygget med `vector_l2_ops`.
HNSW har intet træningstrin, så indekset kan oprettes på en tabel uden rækker.

Forespørgslen laver spørgsmålet om til en vektor med samme model og beder om de
nærmeste afsnit:

```sql
SELECT id, source, body
FROM   passages
ORDER  BY embedding <=> $1
LIMIT  5;
```

`<=>` er cosinusafstand, og `$1` er forespørgslens vektor, sendt som parameter.
To detaljer gør det muligt for PostgreSQL at bruge indekset: forespørgslen har
både `ORDER BY` og `LIMIT`, og `ORDER BY` står på selve afstandsoperatoren i
stigende rækkefølge. En lighedsscore til visning, `1 - (embedding <=> $1)`,
hører hjemme i select-listen. De øvrige operatorer er `<->` for L2-afstand,
`<+>` for L1 og `<#>` for indre produkt. Den sidste returnerer det negative
indre produkt, fordi PostgreSQL kun scanner et indeks på en operator i stigende
rækkefølge.

HNSW-indekset har to byggeparametre: `m`, det største antal forbindelser pr.
lag, 16 som standard, og `ef_construction`, størrelsen på kandidatlisten under
opbygningen, 64 som standard. Når der søges, bestemmer `hnsw.ef_search`
størrelsen på kandidatlisten, 40 som standard. En højere værdi giver bedre
recall på bekostning af hastigheden, og `SET LOCAL` inde i en transaktion hæver
den for én forespørgsel alene. Vil man vide, hvad indekset koster i recall,
kører man den samme forespørgsel i en transaktion med
`SET LOCAL enable_indexscan = off`, som giver det eksakte svar, og sammenligner
de to lister.

Virksomhedens [arbejde med skræddersyede AI-agenter](/da/ai-agents/) lægger
agentens hukommelse om tidligere sager i kundens egen database frem for i en
leverandørs chathistorik. En tabel som `passages` er én form, sådan en
hukommelse kan have: når en ny sag kommer ind, er de tidligere sager, der ligner
den mest, kun én forespørgsel væk, i den samme database og under de samme
rettigheder som resten af dataene.

Hybrid søgning kører begge søgninger og fletter de to lister. pgvectors
dokumentation nævner Reciprocal Rank Fusion fra en artikel fra 2009 af Cormack,
Clarke og Büttcher: hvert dokument får 1 / (k + rang) for hver liste, det
optræder i, og pointene lægges sammen. Forfatterne fastsatte k til 60, og deres
indledende forsøg viste, at 60 var »near-optimal, but that the choice was not
critical«. Med en `tsvector`-kolonne `search_vec` i samme tabel, bygget som til
fuldtekstsøgning, ser den flettede forespørgsel sådan ud:

```sql
WITH semantic AS (
    SELECT id, row_number() OVER (ORDER BY dist) AS r
    FROM  (SELECT id, embedding <=> $1 AS dist
           FROM   passages
           ORDER  BY dist
           LIMIT  20) AS nearest
),
keyword AS (
    SELECT id, row_number() OVER (ORDER BY score DESC) AS r
    FROM  (SELECT id, ts_rank_cd(search_vec, query) AS score
           FROM   passages, plainto_tsquery('danish', $2) AS query
           WHERE  search_vec @@ query
           ORDER  BY score DESC
           LIMIT  20) AS matches
)
SELECT id,
       coalesce(1.0 / (60 + s.r), 0)
     + coalesce(1.0 / (60 + k.r), 0) AS rrf
FROM   semantic AS s
FULL   JOIN keyword AS k USING (id)
ORDER  BY rrf DESC
LIMIT  5;
```

Hver søgning beholder sine egne 20 bedste og nummererer dem fra 1. Et afsnit,
som begge søgninger placerer højt, får to andele og stiger til tops; et afsnit,
som kun den ene søgning finder, beholder én andel. Fletningen bruger rang frem
for score, fordi en cosinusafstand og en tekstrang ligger på skalaer, der ikke
kan sammenlignes. Sproget i `plainto_tsquery` skal være det samme som i
kolonnen, her `danish`. pgvector nævner en cross-encoder som den anden måde at
kombinere listerne på.

## Hvor det går galt

**Færre rækker, end `LIMIT` bad om.** Med et approksimativt indeks anvendes
`WHERE`-betingelsen først efter indeksscanningen. pgvector viser regnestykket:
hvis en betingelse matcher 10 % af rækkerne, giver et HNSW-indeks med
standardværdien 40 for `hnsw.ef_search` i gennemsnit kun omkring 4 rækker. Siden
version 0.8.0 scanner iterative indeksscanninger mere af indekset, indtil nok
rækker er kommet igennem filteret. Slå dem til med
`SET hnsw.iterative_scan = strict_order`, eller med `relaxed_order` for bedre
recall med resultater, der står lidt ude af rækkefølge. En HNSW-scanning stopper
ved `hnsw.max_scan_tuples`, 20.000 som standard. Har filteret kun få forskellige
værdier, virker et partielt indeks pr. værdi også.

**En forespørgsel, der aldrig rører indekset.**
`ORDER BY 1 - (embedding <=> $1) DESC` sorterer efter et udtryk, og en
forespørgsel uden `LIMIT` beder om alt, så begge læser hver eneste række. Et
indeks bygget med `vector_l2_ops` gør intet for en forespørgsel, der sorteres
efter `<=>`. Sortér efter selve operatoren, stigende og med `LIMIT`, byg ét
indeks for hver afstand, der søges med, og tjek planen med
`EXPLAIN (ANALYZE, BUFFERS)`.

**Et IVFFlat-indeks bygget for tidligt.** IVFFlat kører k-means over de rækker,
der findes, når indekset bygges, og deler vektorerne op i lister ud fra
resultatet. Et indeks, der er oprettet med for lidt data til sit antal lister,
returnerer færre rækker, end forespørgslen bad om, og pgvectors råd er at droppe
det, indtil tabellen rummer flere data. Indlæs dataene først, og byg indekset
bagefter. pgvector foreslår at begynde med rækker / 1000 lister op til en
million rækker og kvadratroden af antallet af rækker derover, og at sætte
probes, der som standard er 1, til kvadratroden af antallet af lister. HNSW har
intet tilsvarende træningstrin.

**Rækker, der aldrig bliver fundet.** NULL-vektorer kommer ikke med i indekset,
og det gør nulvektorer heller ikke under cosinusafstand, for de har ingen
retning at sammenligne. En række, der står uden embedding, er usynlig for en
indekseret søgning. Tilføj `NOT NULL`, når indlæsningen er pålidelig, og lad et
fast job tælle de rækker, der stadig mangler en embedding.

**Vektorer fra to modeller i én kolonne.** To embeddingmodeller, eller to
versioner af den samme, placerer den samme tekst forskellige steder, og deres
vektorer kan ikke sammenlignes, selv når antallet af dimensioner er det samme.
Lav spørgsmålets vektor med den model, der lavede afsnittenes, gem modellens
navn i en kolonne, og beregn alle vektorer igen, når modellen skifter. Til en
kolonne med vektorer i flere størrelser viser pgvector et udtryksindeks med en
betingelse for hver model, `WHERE (model_id = 123)`.

**For mange dimensioner til indekset.** Typen `vector` gemmer op til 16.000
dimensioner, men HNSW og IVFFlat indekserer højst 2.000. Indeksér en cast til
`halfvec` for op til 4.000, brug binær kvantisering for op til 64.000 og rangér
den korte liste igen efter de fulde vektorer, eller indeksér en delvektor, når
modellen understøtter det.

**Én kundes vektorer gør en andens søgning langsommere.** pgvector advarer om,
at når flere kunder deler et approksimativt indeks, kan én kundes vektorer
påvirke recall og hastighed for de andre. Partitionér tabellen efter kunde med
listepartitionering, eller giv hver kunde sin egen tabel.

**En indeksopbygning, der blokerer programmet.** HNSW bygges langt hurtigere,
når grafen kan ligge i `maintenance_work_mem`, og pgvector udskriver en
meddelelse, når den ikke længere kan. At sætte den hukommelse så højt, at
serveren løber tør, giver sin egen nedetid. På et system i drift bygger man med
`CREATE INDEX CONCURRENTLY`, som ikke blokerer skrivninger.

**Afsnit, læseren ikke må se.** En søgning over alle afsnit kan give en
sprogmodel tekst fra et dokument, brugeren ikke har ret til at læse, og modellen
kan citere den. Læg rettighedsbetingelsen i den samme `WHERE`-betingelse som
søgningen, så sådan en række aldrig forlader databasen, og slå iterative
scanninger til, så filteret ikke efterlader prompten med for få afsnit.

**En afstand opfattet som vished.** En cosinusafstand på 0,2 siger, at et afsnit
ligger nærmere end et på 0,4, ikke at det besvarer spørgsmålet. En tærskel, der
er indstillet til én model, siger intet om den næste. Sortér efter afstand,
begræns med `LIMIT`, og bedøm søgningen på et sæt rigtige spørgsmål, hvor man
kender de rigtige afsnit på forhånd.

## Læs mere

- [pgvector](https://github.com/pgvector/pgvector) — udvidelsens egen
  dokumentation: typer, afstandsoperatorer, indstillinger for HNSW og IVFFlat,
  filtrering, iterative indeksscanninger, hybrid søgning og fejlfinding.
- [Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs](https://arxiv.org/abs/1603.09320)
  — Malkov og Yashunins artikel, der introducerede HNSW.
- [Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods](https://cormack.uwaterloo.ca/cormacksigir09-rrf.pdf)
  — SIGIR-artiklen fra 2009 med fletteformlen og de indledende forsøg bag dens
  konstant.
- [Introduction to full text search](https://www.postgresql.org/docs/current/textsearch-intro.html)
  — PostgreSQL's ordbaserede søgning, den anden halvdel af en hybrid
  forespørgsel.
- [Controlling Text Search](https://www.postgresql.org/docs/current/textsearch-controls.html)
  — `plainto_tsquery` og `ts_rank_cd`, som de bruges i nøgleordsdelen ovenfor.

<https://engineer.company/da/glossary/vector-search/>
