I oktober 2026 slog embeddingmodeller och vektordatabaser igenom som den infrastruktur alla pratar om, inte bara stora språkmodeller. Perplexity Research släppte pplx-embed-v2-context-9b-preview i början av oktober, en kontextuell embeddingmodell med 9 miljarder parametrar byggd specifikt för RAG (Retrieval-Augmented Generation). Samtidigt öppnade Mistral en förhandsgranskning av Large 4 och Anthropic sänkte priset på Claude Haiku 5.5 radikalt, vilket gör det billigare än tidigare att koppla ihop sökresultat med en språkmodell som formulerar svaret.

Den här guiden visar hur du bygger en egen RAG-pipeline från grunden: du samlar dokument, delar upp dem i chunkar, skapar embeddings, lagrar vektorerna i PostgreSQL med tillägget pgvector och kopplar till sist ihop sökresultaten med en språkmodell som svarar med källhänvisning. Vi använder ett svenskt nyhetsarkiv som exempel, men samma mönster fungerar för juridiska dokument, supportartiklar eller interna wikisidor. Du behöver grundläggande Python-kunskap och tillgång till ett terminalfönster, resten går vi igenom steg för steg.

För svenska och nordiska organisationer finns ett extra skäl att bygga den här typen av sökfunktion själv istället för att enbart luta sig mot en extern leverantörs färdiga chattbot. Du behåller kontrollen över var texten lagras, vilket blir allt viktigare i takt med att fler verksamheter omfattas av EU:s krav på datahantering, och du kan anpassa sökningen efter svensk grammatik och sammansatta ord på ett sätt en generisk engelskspråkig produkt sällan gör lika bra.

Vad är RAG och varför behövs en vektordatabas

RAG löser ett konkret problem: en språkmodell kan inte svara korrekt på frågor om information den aldrig tränats på, till exempel gårdagens nyhetsartikel eller ett internt dokument från förra veckan. Istället för att träna om modellen varje gång ny information dyker upp, hämtar man relevanta textutdrag från en egen datakälla och skickar dem till modellen tillsammans med frågan. Modellen läser utdragen och formulerar ett svar baserat på dem, ungefär som när en journalist får ett researchunderlag innan intervjun.

För att hitta rätt textutdrag snabbt krävs ett sätt att mäta hur “lika” två texter är i betydelse, inte bara i exakt ordval. Det är där embeddings kommer in. En embeddingmodell omvandlar text till en lista av tal, en vektor, där texter med liknande betydelse hamnar nära varandra i det matematiska rummet. En vektordatabas är helt enkelt en databas optimerad för att snabbt hitta de vektorer som ligger närmast en given sökvektor, bland miljoner eller miljarder rader.

Skillnaden mot en vanlig textsökning är stor. Söker du efter “cyberattacker mot kommuner” i en vanlig databas hittar du bara rader som innehåller just de orden. Med semantisk sökning via embeddings hittar du även artiklar om “ransomware mot Svedala” eller “dataintrång hos Miljödata”, eftersom innebörden ligger nära även om orden skiljer sig åt.

Tänk på embeddingmodellen som en mycket noggrann bibliotekarie som redan läst hela arkivet. Istället för att leta efter exakta ordträffar i registret minns bibliotekarien vad varje artikel faktiskt handlar om, och kan därför peka ut rätt hyllor även när frågan ställs med helt andra ord än de som står i artikeln. Vektordatabasen är hyllsystemet som gör det möjligt att hitta rätt bland miljontals artiklar på millisekunder istället för att bibliotekarien måste tänka efter för varje enskild fråga.

Så fungerar embeddings: från text till siffror

En embeddingmodell tar emot en textsträng och lämnar tillbaka en vektor, oftast mellan 384 och 4 096 tal beroende på modell. Perplexitys nya pplx-embed-v2-context-9b-preview producerar som standard 2 048-dimensionella vektorer, men stödjer även 1 024 dimensioner för den som vill spara lagringsutrymme och sökningstid. Modellen är dessutom tränad med int8-kvantisering inbyggd, vilket gör det enklare att köra den med lägre precision i produktion utan att bygga om hela pipelinen efteråt.

Det viktiga att förstå är att dimensionsantalet måste vara konsekvent genom hela systemet. Om du bäddar in dina dokument med 1 024 dimensioner men sedan skickar en sökfråga inbäddad med 2 048 dimensioner, går jämförelsen inte att göra. Vektorkolumnen i databasen måste alltså definieras med exakt det dimensionsantal du bestämmer dig för i embeddingsteget, och den inställningen bör du låsa innan du bäddar in några dokument alls.

Själva likhetsmåttet räknas oftast med cosinuslikhet, som jämför vinkeln mellan två vektorer istället för deras absoluta längd. Det gör måttet robust mot att vissa texter är längre än andra. pgvector, tillägget vi använder i den här guiden, stödjer cosinuslikhet, euklidiskt avstånd och inre produkt som färdiga operatorer direkt i SQL.

Vilket avståndsmått ska du välja

I praktiken spelar det mindre roll vilket av de tre måtten du väljer, så länge du håller dig till det måttet embeddingmodellen faktiskt är tränad för. De flesta moderna embeddingmodeller, inklusive pplx-embed-v2-context-9b-preview, normaliserar vektorerna internt så att cosinuslikhet och inre produkt i praktiken ger samma rangordning. Euklidiskt avstånd används mer sällan för textembeddings men kan vara ett naturligt val om du senare bygger vidare med bild- eller ljudvektorer i samma databas, där absolut avstånd ibland är mer meningsfullt än vinkel.

Förkunskaper: verktyg och versioner du behöver

Innan du börjar bör du ha följande installerat och klart. Guiden förutsätter en vanlig Linux- eller macOS-maskin, men stegen fungerar även i WSL på Windows.

  • Python 3.12 eller senare
  • PostgreSQL 16 eller 17, med behörighet att skapa tillägg
  • pgvector-tillägget för PostgreSQL, senaste stabila version från det officiella GitHub-repot
  • Paketen psycopg, pgvector för Python och requests (installeras i steg 1)
  • En API-nyckel till en embeddingleverantör, till exempel Perplexity, Mistral eller OpenAI
  • En API-nyckel till en språkmodell för svarsgenerering, till exempel Claude Haiku 5.5 eller Mistral Large 4
  • Ett terminalfönster och en texteditor

Du behöver inte ett dedikerat GPU-kort eller en separat vektordatabas-tjänst för att följa guiden. pgvector kör ovanpå en vanlig PostgreSQL-instans, vilket gör det till ett naturligt förstahandsval för de flesta mindre och medelstora projekt.

Steg 1 och 2: Sätt upp projektet och installera beroenden

Skapa en projektmapp och en virtuell miljö, installera sedan de paket pipelinen behöver. Håll beroendelistan kort i det här skedet, du kan alltid lägga till fler senare.

mkdir rag-sokmotor && cd rag-sokmotor
python3 -m venv venv
source venv/bin/activate
pip install "psycopg[binary]" pgvector requests python-dotenv

Skapa sedan en .env-fil för dina nycklar och anslutningssträngar. Lägg aldrig riktiga nycklar i kod som checkas in i ett delat repo, använd miljövariabler genom hela projektet.

DATABASE_URL=postgresql://rag_user:losenord@localhost:5432/rag_db
EMBEDDING_API_KEY=din_nyckel_har
LLM_API_KEY=din_andra_nyckel_har

Steg 3 och 4: Skapa databasen och aktivera pgvector

Logga in i PostgreSQL och skapa en ny databas för projektet, aktivera därefter pgvector-tillägget. Det kräver superanvändarrättigheter första gången tillägget installeras på servern, men när det väl är gjort kan vanliga användare aktivera det per databas.

CREATE DATABASE rag_db;
\c rag_db

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE dokument_chunks (
    id SERIAL PRIMARY KEY,
    dokument_id TEXT NOT NULL,
    rubrik TEXT,
    chunk_text TEXT NOT NULL,
    sprak TEXT DEFAULT 'sv',
    kategori TEXT,
    publicerad_datum DATE,
    embedding VECTOR(1024)
);

CREATE INDEX ON dokument_chunks USING hnsw (embedding vector_cosine_ops);

Lägg märke till att vektorkolumnen definieras som VECTOR(1024), alltså 1 024 dimensioner. Det betyder att varje embedding du genererar i nästa steg måste vara exakt 1 024 tal långt, annars avvisar PostgreSQL insättningen. HNSW-indexet gör sökningar snabba även när tabellen växer till miljoner rader, utan det söker databasen igenom varenda rad för varje fråga.

Steg 5 och 6: Samla dokument och dela upp dem i chunkar

En hel artikel är sällan lämplig att bädda in som en enda vektor, texten blir för lång och embeddingen tappar precision. Istället delar man upp varje dokument i mindre chunkar, oftast 200 till 400 ord, med ett litet överlapp mellan varje chunk så att meningar inte klipps av mitt i ett sammanhang.

def chunka_text(text, max_ord=300, overlapp=50):
    ord_lista = text.split()
    chunkar = []
    start = 0
    while start < len(ord_lista):
        slut = start + max_ord
        chunk = " ".join(ord_lista[start:slut])
        chunkar.append(chunk)
        start = slut - overlapp
    return chunkar

För ett nyhetsarkiv bör chunkningen bevara struktur snarare än att klippa text helt mekaniskt. Behåll rubrik, ingress och brödtext som separata fält i dokumentobjektet innan du chunkar brödtexten, så att varje chunk ändå kan kopplas till rätt rubrik och publiceringsdatum när resultaten senare visas för en läsare.

Ett vanligt nybörjarmisstag är att chunka på exakt antal ord utan hänsyn till meningsgränser, vilket kan dela en mening mitt itu mellan två chunkar. En enkel förbättring är att chunka på stycken eller meningar istället för en fast ordräkning, och bara falla tillbaka på ordgränsen om ett enskilt stycke är betydligt längre än måltalet. Det gör att varje chunk blir lite olika lång, men innehållet hänger ihop bättre när det senare ska läsas av en språkmodell.

Steg 7 och 8: Generera embeddings med ett aktuellt API

Nästa steg är att skicka varje chunk till en embeddingmodell och spara vektorn som kommer tillbaka. Exemplet nedan visar mönstret med en generisk funktion, byt ut url och fältnamn mot det som din leverantör anger i sin aktuella dokumentation, eftersom detaljerna skiljer sig mellan Perplexity, Mistral och OpenAI.

import requests
import os

def hamta_embedding(text, modell="pplx-embed-v2-context-9b-preview", dimensioner=1024):
    svar = requests.post(
        "https://api.perplexity.ai/embeddings",
        headers={"Authorization": f"Bearer {os.environ['EMBEDDING_API_KEY']}"},
        json={"model": modell, "input": text, "dimensions": dimensioner},
        timeout=30
    )
    svar.raise_for_status()
    return svar.json()["data"][0]["embedding"]

Skicka inte chunkarna en och en om du har tusentals dokument. De flesta embedding-API:er stödjer batchanrop där du skickar en lista av texter och får tillbaka en lista av vektorer i samma ordning, vilket minskar både kostnad och väntetid avsevärt jämfört med ett anrop per chunk.

Bygg in en liten fördröjning eller köhantering mellan batcherna om du bäddar in ett stort befintligt arkiv för första gången. Att skicka hundratals batcher i rask följd mot samma API-nyckel riskerar att träffa en hastighetsgräns, vilket i sin tur stoppar hela inmatningen mitt i om du inte har någon felhantering som försöker igen efter en kort väntetid.

Steg 9: Lagra vektorer och metadata i databasen

När du har en embedding för varje chunk är det tid att spara den tillsammans med metadata som rubrik, kategori och publiceringsdatum. Metadata är avgörande, utan den kan du senare filtrera resultat eller visa en trovärdig källa, men med den blir träfflistan bara en lista av textsnuttar utan sammanhang.

from pgvector.psycopg import register_vector
import psycopg
import os

conn = psycopg.connect(os.environ["DATABASE_URL"])
register_vector(conn)

def spara_chunk(conn, dokument_id, rubrik, chunk_text, kategori, datum, embedding):
    with conn.cursor() as cur:
        cur.execute(
            """
            INSERT INTO dokument_chunks
            (dokument_id, rubrik, chunk_text, kategori, publicerad_datum, embedding)
            VALUES (%s, %s, %s, %s, %s, %s)
            """,
            (dokument_id, rubrik, chunk_text, kategori, datum, embedding)
        )
    conn.commit()

Funktionen register_vector måste anropas en gång per anslutning innan du försöker sätta in en vektor. Den talar om för psycopg hur Python-listan ska omvandlas till det format pgvector förväntar sig i databasen, hoppar du över den raden får du ett felmeddelande om att typen inte kan anpassas.

Välj indextyp utifrån hur arkivet växer. HNSW ger snabbare sökningar och klarar löpande tillägg av nya rader bättre, vilket passar ett nyhetsarkiv som uppdateras varje dag. IVFFlat bygger istället om hela indexet i omgångar och passar bättre när data i huvudsak läggs in i stora engångsimporter med få uppdateringar däremellan. För de flesta löpande RAG-projekt är HNSW det säkrare förstahandsvalet.

Steg 10 och 11: Bygg sökfunktionen och filtrera resultat

Själva sökningen görs med en SQL-fråga som sorterar raderna efter avstånd till din sökvektor. Operatorn <=> i pgvector räknar ut cosinusavstånd direkt i databasen, vilket gör att du inte behöver hämta alla rader till Python för att jämföra dem manuellt.

SELECT rubrik, chunk_text, kategori, publicerad_datum,
       1 - (embedding <=> %(fraga_vektor)s) AS likhet
FROM dokument_chunks
WHERE (%(kategori)s IS NULL OR kategori = %(kategori)s)
  AND (%(fran_datum)s IS NULL OR publicerad_datum >= %(fran_datum)s)
ORDER BY embedding <=> %(fraga_vektor)s
LIMIT 5;

Genom att lägga till villkor på kategori och datum i samma fråga kan du kombinera semantisk sökning med klassisk filtrering. En läsare som bara vill se artiklar från de senaste 30 dagarna om "ransomware" får då relevanta resultat i betydelse, men avgränsat till rätt tidsperiod och ämnesområde.

Håll filtervillkoren som egna parametrar i applikationskoden snarare än att bygga ihop SQL-strängen med textformatering. Det skyddar mot SQL-injektion om någon del av filtret, till exempel en kategori, någonsin kommer från användarens egen inmatning, och det är samma goda vana du redan bör ha från all annan databaskod.

Steg 12 och 13: Koppla sökningen till en språkmodell med källhänvisning

Nu finns alla delar för att bygga själva RAG-flödet: ta emot en fråga, bädda in den, hämta de mest relevanta chunkarna och skicka dem tillsammans med frågan till en språkmodell som formulerar ett svar. Instruera modellen tydligt att bara använda de medskickade källorna och att ange rubrik och datum för varje påstående.

def svara_med_kallor(fraga, conn, sok_sql):
    fraga_vektor = hamta_embedding(fraga)
    with conn.cursor() as cur:
        cur.execute(sok_sql, {
            "fraga_vektor": fraga_vektor,
            "kategori": None,
            "fran_datum": None
        })
        rader = cur.fetchall()

    kallor = "\n\n".join(
        f"[{rubrik}, {datum}] {text}" for rubrik, text, kat, datum, likhet in rader
    )
    prompt = (
        "Svara på frågan nedan enbart med hjälp av källorna. "
        "Ange rubrik och datum för varje källa du använder.\n\n"
        f"Källor:\n{kallor}\n\nFråga: {fraga}"
    )
    return anropa_sprakmodell(prompt)

Funktionen anropa_sprakmodell kan peka mot valfri aktuell modell. Claude Haiku 5.5, som Anthropic släppte den 7 oktober 2026 för 0,10 dollar per miljon indata-token och 0,50 dollar per miljon utdata-token vid korta prompter, passar bra för den här typen av svarsgenerering eftersom kostnaden per fråga blir låg även vid många sökningar per dag. Mistral Large 4, som öppnade i offentlig förhandsgranskning den 6 oktober, är ett annat alternativ med öppna vikter som väntas släppas under slutet av oktober.

Lägg märke till att prompten ovan explicit instruerar modellen att bara använda de medskickade källorna. Utan den instruktionen tenderar språkmodeller att fylla i luckor med egen bakgrundskunskap när de hämtade chunkarna inte räcker till för ett fullständigt svar, vilket undergräver hela poängen med att bygga en RAG-pipeline i första hand. Är du osäker på om modellen håller sig till källorna, be den explicit svara "Det framgår inte av källorna" när underlaget faktiskt saknas, istället för att gissa.

Steg 14: Testa pipelinen och mät prestanda

Innan du sätter pipelinen i produktion bör du testa den mot minst 20 till 30 verkliga frågor som dina faktiska användare skulle kunna ställa. Kör varje fråga genom hela kedjan och kontrollera manuellt om de hämtade chunkarna faktiskt är relevanta, inte bara om språkmodellens svar låter rimligt.

Ett körexempel kan se ut ungefär så här i terminalen:

$ python sok.py "cyberattacker mot svenska kommuner"

1. [Ransomware Mot Svedala, 2026-05-10] likhet: 0.84
2. [Miljödata-attacken: 200 kommuner, 2026-03-02] likhet: 0.79
3. [Electrolux, Aarsleff Drabbade, 2026-04-18] likhet: 0.71

Svar: Flera svenska kommuner och företag har drabbats av ransomware
under 2026, bland annat Svedala kommun (2026-05-10) och en attack
som påverkade cirka 200 kommuner via Miljödata (2026-03-02).

Mät även svarstiden för hela kedjan, från fråga till färdigt svar. Embeddinganropet och själva databasfrågan tar oftast bara några tiotals millisekunder vardera med ett HNSW-index, medan anropet till språkmodellen för att formulera svaret vanligtvis är den klart långsammaste delen av flödet.

Det är även värt att räkna på kostnaden innan du skalar upp. Med Claude Haiku 5.5 och dess pris på 0,10 dollar per miljon indata-token blir till exempel fem hämtade chunkar på 300 ord styck, cirka 2 000 tokens totalt inklusive frågan och instruktionerna, en kostnad på en bråkdel av en hundradels dollar per sökning. Kör du 10 000 sökningar per dag landar den löpande kostnaden för själva svarsgenereringen fortfarande på en nivå de flesta mindre team och publicister kan hantera utan särskild budgetering, även om embeddingkostnaden för inmatningen av nya dokument tillkommer separat.

1 024 mot 2 048 dimensioner: kostnad, lagring och kvalitet

Valet av dimensionsantal påverkar både hur mycket lagringsutrymme databasen kräver och hur snabbt sökningar går, samtidigt som det kan påverka hur exakt sökningen blir. pplx-embed-v2-context-9b-preview stödjer båda alternativen, vilket gör det möjligt att göra en medveten avvägning istället för att tvingas välja en leverantörs standardvärde.

Egenskap1 024 dimensioner2 048 dimensioner
Lagring per miljon chunkarLägreCirka dubbelt så hög
IndexeringstidSnabbareLängre
Sökhastighet vid stora volymerSnabbareNågot långsammare
Semantisk precisionGod för de flesta användningsfallPotentiellt högre för nyanserade frågor
Stöds nativt av pplx-embed-v2-context-9b-previewJaJa (standard)

Som utgångspunkt är 1 024 dimensioner ett rimligt förstahandsval för de flesta svenska och nordiska projekt, särskilt vid mindre arkiv under några miljoner dokument. Går du upp mot 2 048 dimensioner bör du göra det medvetet, efter att ha testat om den extra precisionen faktiskt märks i dina egna sökresultat, inte bara eftersom det är leverantörens standardvärde.

Det enklaste sättet att testa skillnaden konkret är att bygga ett litet utvärderingsset innan du bestämmer dig. Skriv ner 20 till 30 frågor du vet att arkivet borde kunna besvara, bädda in samma dokument med båda dimensionsstorlekarna i separata tabeller, och kör sedan samma frågor mot båda. Räkna hur många gånger den chunk du egentligen letade efter faktiskt hamnar bland de fem bästa träffarna i varje variant, så får du ett konkret mått på om den högre dimensionen var värd merkostnaden i just din data.

Vilken vektordatabas ska du välja 2026?

pgvector är inte det enda alternativet, och valet beror mycket på vilken infrastruktur du redan har. Om du redan kör PostgreSQL är pgvector det naturliga valet eftersom du slipper driva ytterligare ett system. Har du inget existerande databasbehov finns renodlade vektordatabaser som ofta är byggda för ännu större skala från start.

LösningTypLicensBäst lämpad för
pgvectorTillägg till PostgreSQLÖppen källkod (PostgreSQL-licens)Projekt som redan kör PostgreSQL
QdrantFristående vektordatabasÖppen källkod (Apache 2.0)Hög skala, skriven i Rust
WeaviateFristående vektordatabasÖppen källkod (BSD)Hybridsökning med inbyggda moduler
MilvusFristående vektordatabasÖppen källkod (Apache 2.0)Mycket stora datamängder, distribuerad drift
PineconeHanterad molntjänstProprietärTeam som vill undvika egen drift

För de flesta mindre och medelstora svenska projekt, som ett nyhetsarkiv, en supportportal eller en intern kunskapsbas, räcker pgvector gott och väl. Byt bara till en fristående vektordatabas om du faktiskt når gränserna för vad en vanlig PostgreSQL-instans klarar, inte i förväg som en teoretisk försäkring.

Ett byte senare i projektets liv är inte gratis. Att flytta miljoner vektorer från pgvector till Qdrant eller Milvus kräver antingen att du bäddar om allt innehåll på nytt eller exporterar de befintliga vektorerna i ett format den nya databasen förstår, vilket i båda fallen tar tid och kostar beräkningskraft. Av det skälet lönar det sig att vara ärlig mot sig själv om den förväntade datavolymen redan i planeringsfasen, snarare än att behöva göra bytet akut efter lansering.

De senaste AI-modellerna som driver RAG-pipelines just nu

Modellandskapet för både embeddings och svarsgenerering förändrades snabbt under september och oktober 2026. Det är värt att känna till de senaste släppen innan du låser fast ditt val av leverantör, eftersom pris och kapacitet skiljer sig kraftigt mellan modellerna. Enligt en löpande sammanställning av AI-modellsläpp 2026 har flera stora leverantörer släppt nya versioner bara under de senaste veckorna.

ModellLeverantörSläpptCentrala egenskaper
pplx-embed-v2-context-9b-previewPerplexityOktober 20269 miljarder parametrar, 2 048 eller 1 024 dimensioner, int8-kvantisering
Claude Haiku 5.5Anthropic7 oktober 20261 miljon tokens kontext, 128 000 tokens max utdata, 0,10/0,50 dollar per miljon token
Mistral Large 4Mistral AI6 oktober 2026 (förhandsgranskning)Öppna vikter (MoE), 1 miljon tokens kontext, cirka 1,05 biljoner parametrar
Gemini 4 ArgonGoogle13 oktober 2026Allmän tillgänglighet, introduktionspris 2/10 dollar per miljon token
Meta Muse WonderMeta12 oktober 2026Första slutna frontier-modellen från Meta, 4/20 dollar per miljon token

Du behöver inte välja samma leverantör för embeddings och svarsgenerering. Ett vanligt mönster är att använda en kostnadseffektiv embeddingmodell för själva sökningen och en separat, ofta något kraftfullare, modell för att formulera det slutgiltiga svaret till användaren. Enligt Mistrals officiella changelog väntas öppna vikter för Large 4 släppas innan oktober är slut, vilket kan göra modellen till ett ännu billigare alternativ för svarsgenerering i egna miljöer.

Bygg gärna in ett fallback-steg som byter till en andra leverantör om det primära API:et svarar långsamt eller inte alls. Eftersom flera modeller nu ligger på jämförbar prisnivå kostar det inte mycket extra att hålla en reservväg klar, och en kortare avbrottstid för sökfunktionen är sällan acceptabelt för en publik webbplats eller ett internt verktyg som andra team är beroende av dagligen.

Vanliga fallgropar när du bygger RAG med embeddings

De flesta misstag i en RAG-pipeline upptäcks inte vid utveckling, eftersom koden fungerar felfritt rent tekniskt. Problemen visar sig först när sökresultaten börjar kännas fel eller irrelevanta för faktiska användare, långt efter att pipelinen satts i drift. Här är de vanligaste fallgroparna att undvika redan från start.

  • Blandade dimensioner: Du bäddar in dokument med 1 024 dimensioner men byter sedan embeddingmodell till en som ger 2 048, utan att uppdatera databaskolumnen eller bädda om det gamla innehållet.
  • Fel chunkstorlek: Chunkar på bara 20 till 30 ord ger för lite sammanhang, medan hela artiklar som en enda chunk gör embeddingen för diffus för att träffa rätt vid specifika frågor.
  • Saknad metadata: Om du inte sparar källa, rubrik och datum tillsammans med chunken kan du aldrig visa en trovärdig källhänvisning i svaret, bara en lös textsnutt.
  • Inget vektorindex: Utan ett HNSW- eller IVFFlat-index skannar PostgreSQL varje rad för varje sökning, vilket fungerar bra för tiotusen rader men blir orimligt långsamt vid miljoner.
  • För mycket kontext till modellen: Att skicka tjugo chunkar istället för fem ökar kostnaden och gör ofta svaret mer vagt, eftersom modellen måste väga in mer irrelevant text.
  • Ingen gräns för antal resultat: En SQL-fråga utan LIMIT kan i värsta fall returnera tusentals rader om filtreringen är för bred, vilket belastar både databasen och efterföljande steg.

Felsökning: 9 vanliga problem och hur du löser dem

Nedan listas de felmeddelanden och symptom du mest sannolikt stöter på när du bygger och driftsätter pipelinen, tillsammans med den konkreta åtgärd som löser varje fall.

  • "extension vector does not exist": pgvector är inte installerat på servern. Installera paketet på operativsystemsnivå innan du kör CREATE EXTENSION.
  • Dimension mismatch vid insert: Kontrollera att VECTOR(n) i tabelldefinitionen exakt matchar dimensions-parametern du skickar till embeddingmodellen.
  • "can't adapt type" i psycopg: Du har glömt anropa register_vector(conn) på anslutningen innan du försöker sätta in en vektor.
  • Sökningen ger irrelevanta resultat: Kontrollera att chunkarna faktiskt innehåller sammanhängande text på rätt språk, och att du inte blandar embeddings från olika modellversioner i samma tabell.
  • API:et svarar med 429 Too Many Requests: Inför exponentiell backoff och batcha flera chunkar per anrop istället för ett anrop per chunk.
  • Frågor blir långsamma vid skalning: Skapa eller bygg om HNSW-indexet, och kör ANALYZE på tabellen efter stora datainmatningar.
  • Svaret saknar källhänvisning: Instruera språkmodellen explicit i prompten att ange rubrik och datum för varje källa, lita inte på att den gör det av sig själv.
  • Minnesfel vid stora batcher: Dela upp inmatningen i mindre omgångar, exempelvis 500 rader i taget, och commit:a efter varje omgång.
  • Dubbletter i träfflistan: Gruppera resultat per dokument_id i applikationskoden, eller filtrera bort chunkar från samma dokument som redan är representerat högre upp i listan.

GDPR och datahantering för svenska RAG-projekt

Så fort ett dokumentarkiv innehåller personuppgifter, till exempel kundärenden, anställdas namn eller läsarkommentarer, gäller GDPR även för vektordatabasen. En embedding är i sig en komprimerad representation av texten, men eftersom originaltexten oftast lagras parallellt i samma rad för att kunna visas i träfflistan, räknas hela chunken som en personuppgift om innehållet gör det.

Var bör du lagra embeddings och originaltext

Kör du PostgreSQL med pgvector på egen infrastruktur eller hos en EU-baserad leverantör har du full kontroll över var datan fysiskt ligger. Väljer du istället en hanterad embedding-API eller vektordatabastjänst bör du kontrollera var leverantören processar och i vilket land servrarna står, eftersom texten som skickas för inbäddning i praktiken lämnar din egen miljö under den korta tid anropet pågår.

Rätt till radering i en vektordatabas

Om en person begär att få sina uppgifter raderade måste du kunna ta bort inte bara originaldokumentet utan också alla chunkar och embeddings som skapats från det. Det är en bra anledning att alltid koppla varje rad i dokument_chunks till ett tydligt dokument_id, så att en radering kan göras med en enda SQL-fråga istället för att manuellt leta igenom tusentals vektorer för att hitta vilka som härstammar från den borttagna texten.

Hybrid-sökning: kombinera fulltext och vektor

Semantisk sökning är stark på betydelse men svagare på exakta termer, som personnamn, modellbeteckningar eller felkoder som sällan förekommer i träningsdata. Lösningen är att kombinera PostgreSQL:s inbyggda fulltextsökning med vektorsökningen i samma fråga, så att båda signalerna bidrar till den slutgiltiga rankningen.

ALTER TABLE dokument_chunks ADD COLUMN sok_vektor tsvector
    GENERATED ALWAYS AS (to_tsvector('swedish', chunk_text)) STORED;

CREATE INDEX ON dokument_chunks USING gin (sok_vektor);

SELECT rubrik, chunk_text,
       (1 - (embedding <=> %(fraga_vektor)s)) * 0.7
       + ts_rank(sok_vektor, to_tsquery('swedish', %(sokord)s)) * 0.3
       AS poang
FROM dokument_chunks
WHERE sok_vektor @@ to_tsquery('swedish', %(sokord)s)
   OR embedding <=> %(fraga_vektor)s < 0.5
ORDER BY poang DESC
LIMIT 5;

Viktningen mellan de två signalerna, i exemplet 0,7 mot 0,3, är ett startvärde snarare än en fast regel. Justera den utifrån hur dina egna användare söker, en supportportal med många produktnamn kan behöva högre vikt på fulltextdelen, medan ett nyhetsarkiv ofta fungerar bättre med vektordelen som huvudsignal.

Avancerade tips för produktion

När grundpipelinen fungerar finns flera förbättringar som gör stor skillnad i en skarp driftsättning, utöver hybrid-sökningen beskriven ovan.

Ett första steg är reranking: hämta först 20 till 30 kandidater med vektorsökning, låt sedan en separat modell eller ett enklare poängsystem rangordna om dem innan bara de bästa fem skickas vidare till språkmodellen. Det ger ofta bättre precision än att lita blint på den första sorteringen.

Planera även för modellbyten. Byter du embeddingmodell längre fram måste hela arkivet bäddas om från grunden, eftersom vektorer från olika modeller inte går att jämföra med varandra. Bygg därför in ett fält som anger vilken modell och vilket dimensionsantal varje rad bäddades in med, så att du kan migrera stegvis istället för att tvingas göra allt på en gång.

Slutligen, cachea gärna svar på vanliga eller identiska frågor under en kort tidsperiod. Enligt en sammanställning av oktobers AI-lanseringar har flera leverantörer samtidigt infört prisförändringar, vilket gör att kostnaden per anrop kan skifta snabbt och gör caching ännu mer värt. Det minskar både kostnaden för upprepade språkmodellanrop och den upplevda svarstiden för slutanvändaren.

Logga varje fråga, vilka chunkar som hämtades och hur nöjd användaren verkade vara med svaret, till exempel genom en enkel tumme-upp-knapp i gränssnittet. Den loggen blir ditt bästa verktyg för att upptäcka när pipelinen börjar driva iväg, exempelvis om en viss kategori dokument systematiskt får låga likhetspoäng efter en innehållsuppdatering, långt innan användarna själva hinner klaga på sämre sökresultat.

Så ser hela projektet ut sammansatt

Satt ihop blir mappstrukturen för projektet relativt kompakt. En .env-fil med nycklar, ett schema.sql med tabelldefinitionen från steg 3 och 4, ett inmatningsskript som chunkar och bäddar in dokument enligt steg 5 till 9, samt ett sökskript som kombinerar steg 10 till 13 till en fråga-svar-funktion.

rag-sokmotor/
├── .env
├── schema.sql
├── indexera.py      # chunkning + embeddings + insert
├── sok.py           # sökfunktion + svarsgenerering
└── requirements.txt

Med den strukturen kan du köra python indexera.py när nya dokument tillkommer, och python sok.py "din fråga" för att testa pipelinen interaktivt. Lägg till ett enkelt kommandoradsargument i sok.py för att växla mellan kategorier eller datumintervall, så slipper du hårdkoda filtervärden varje gång du vill testa en ny variant av sökningen. Allt som behövs för att gå från ett tomt projekt till en fungerande semantisk sökmotor med källhänvisning finns därmed i de fjorton stegen ovan. Enligt rapporten Svenskarna och internet 2026 söker allt fler svenskar information på andra sätt än via traditionella sökmotorer, vilket gör egna semantiska sökfunktioner mer relevanta för svenska webbplatser och arkiv framöver.

Vanliga frågor

Vad är skillnaden mellan RAG och finjustering av en modell?
Finjustering ändrar modellens inre vikter baserat på träningsdata, vilket är kostsamt och svårt att uppdatera löpande. RAG lämnar modellen orörd och hämtar istället relevant information vid varje fråga, vilket gör det enkelt att lägga till nya dokument utan att träna om något.

Måste jag använda PostgreSQL, eller går det med en annan databas?
Nej, pgvector är bara ett av flera alternativ. Qdrant, Weaviate och Milvus fungerar som fristående vektordatabaser, medan Pinecone är ett hanterat molnalternativ. pgvector passar bäst om du redan kör PostgreSQL och vill undvika att driva ytterligare ett system.

Vilken chunkstorlek bör jag använda för svensk text?
200 till 400 ord per chunk är en rimlig startpunkt för de flesta nyhets- och dokumentarkiv. Korta FAQ-svar eller supportartiklar kan ofta ligga i den nedre delen av intervallet, medan längre utredande artiklar eller juridiska dokument tenderar att behöva den övre delen för att varje chunk ska innehålla ett helt sammanhang. Testa gärna olika storlekar mot dina egna typiska frågor och justera utifrån vilken storlek som ger de mest relevanta träffarna.

Kan jag köra hela pipelinen utan ett molnbaserat API?
Ja, embeddingmodeller går att köra lokalt med verktyg som Ollama eller LM Studio, och PostgreSQL med pgvector körs i alla fall lokalt. Det kräver mer datorkraft och längre uppsättningstid, men ger full kontroll över var datan hanteras.

Hur ofta bör jag uppdatera embeddings för nya dokument?
Nya dokument bör bäddas in så snart de publiceras, antingen direkt vid publicering eller via ett schemalagt jobb som körs flera gånger per dag. Befintliga dokument behöver bara bäddas om när du byter embeddingmodell eller dimensionsantal.

Är pgvector snabbt nog för produktion med stora datamängder?
Med ett HNSW-index hanterar pgvector vanligtvis miljoner rader utan problem på en normal serverkonfiguration. Vid betydligt större volymer, flera hundra miljoner vektorer, kan en fristående vektordatabas som Qdrant eller Milvus vara bättre anpassad från start.

Är det tillåtet enligt GDPR att bädda in svenska personuppgifter i en vektordatabas?
Ja, men samma regler som för all annan personuppgiftsbehandling gäller. Du behöver en rättslig grund för behandlingen, möjlighet att radera en persons uppgifter ur hela arkivet vid en begäran, och kontroll över i vilket land texten processas när den skickas till en embedding-API. Anonymisera eller pseudonymisera gärna känsligt innehåll innan det chunkas och bäddas in, om det inte är just personuppgifterna sökfunktionen behöver hitta.

Vilken språkmodell ska jag koppla till RAG-pipelinen för svenska texter?
Både Claude Haiku 5.5 och Mistral Large 4 hanterar svenska bra och har låga priser per token efter sina respektive släpp i oktober 2026. Testa gärna båda mot dina egna frågor, eftersom kvaliteten kan skilja sig åt beroende på ämnesområde.

Fungerar samma uppsättning för flera nordiska språk i samma arkiv?
Ja, så länge embeddingmodellen är flerspråkig kan svenska, norska och danska dokument ligga i samma tabell och sökas igenom samtidigt, eftersom en bra embeddingmodell placerar liknande innebörd nära varandra oavsett språk. Spara ändå språkkoden som egen metadata i varje rad, så att du kan filtrera resultat per språk när det behövs eller visa träffar på läsarens eget språk först.