Hoppa till innehållet
AI-grafen
E· Universitetrag-informationssokning· ca 60 min· utvecklande· verifierad 2026-09-20

Indexuppdatering och versionering

Kunna uppdatera ett index inkrementellt utan att tappa reproducerbarhet.

Förkunskaper

Intuition

Ett index som byggdes en gång och aldrig uppdateras blir snabbt inaktuellt. Men att bygga om allt varje gång ett dokument ändras är ohållbart vid miljontals dokument.

Fyra operationer, med olika svårighetsgrad:

OperationSvårighetVarför
Lägg tilllättde flesta index stöder det direkt
Uppdateramedelta bort + lägg till, och håll id:t stabilt
Ta bortsvårtHNSW-grafen kan inte enkelt läka efter borttagning
Byta embeddingmodellkräver full ombyggnadgamla och nya vektorer är inte jämförbara

Den sista raden är den viktigaste att planera för. Två vektorer från olika modeller ligger i olika rum — avstånden mellan dem betyder ingenting. Ett index kan aldrig innehålla en blandning.

Formellt

Mjuk borttagning är standardlösningen: markera dokumentet som borttaget i metadatan och filtrera bort det vid sökning. Vektorn ligger kvar i grafen och tar plats, men sökningen ger rätt svar.

När andelen borttagna växer över några tiotal procent försämras både minne och recall, och då byggs indexet om — som ett vakuum i en databas.

Stabila id:n är en förutsättning. Använd ett deterministiskt id från dokumentets identitet, inte ett löpnummer:

id = sha256(f"{kalla}:{dokument_id}:{chunk_index}")

Då går samma stycke alltid att hitta och ersätta, oavsett i vilken ordning indexeringen kördes.

Chunk-hash avgör vad som behöver göras om. Spara innehållets hash per stycke; vid omindexering jämförs den nya hashen med den gamla:

UtfallÅtgärd
Hashen oförändradhoppa över — ingen ny embedding behövs
Hashen ändradbädda in på nytt och ersätt
Stycket saknas numarkera som borttaget
Nytt styckelägg till

För ett korpus där 2 % ändras per vecka innebär det att 98 % av embeddingkostnaden försvinner.

Blågrön ombyggnad vid modellbyte:

  1. Bygg ett nytt index (grönt) med den nya modellen, medan det gamla (blått) fortsätter att svara.
  2. Kör utvärderingen mot båda på samma frågor.
  3. Är grönt bättre — byt alias. Är det sämre — kasta det.
  4. Behåll blått en tid, så att en återgång är ett aliasbyte bort.

Versionera indexet som datan. Varje index ska bära:

FältVarför
Embeddingmodell och versionavgör om vektorerna är jämförbara
Chunkningsparametrarpåverkar vad som hittas
Datasetets hashvilket underlag
Byggtidpunkt och kodversionspårbarhet

Utan de fälten går det inte att svara på varför en fråga gav ett annat svar i förra veckan — och det är en fråga som alltid kommer.

Kod

import hashlib, json, time
from dataclasses import dataclass, asdict
from pathlib import Path

def chunk_id(kalla: str, dok: str, i: int) -> str:
    return hashlib.sha256(f"{kalla}:{dok}:{i}".encode()).hexdigest()[:32]

def innehall_hash(text: str) -> str:
    return hashlib.sha256(text.encode()).hexdigest()[:16]

@dataclass
class Indexversion:
    embeddingmodell: str
    embedding_dim: int
    chunk_storlek: int
    chunk_overlapp: int
    dataset_hash: str
    byggd: str
    git_commit: str

def inkrementell_uppdatering(klient, samling, nya_chunks, befintliga: dict[str, str]):
    """befintliga: chunk_id -> innehålls-hash. Returnerar vad som gjordes."""
    lagg_till, ersatt, oforandrade = [], [], 0
    sedda = set()
    for c in nya_chunks:
        cid = chunk_id(c["kalla"], c["dok"], c["i"])
        h = innehall_hash(c["text"])
        sedda.add(cid)
        if befintliga.get(cid) == h:
            oforandrade += 1                       # hoppa över — spar embeddingkostnad
        elif cid in befintliga:
            ersatt.append((cid, c, h))
        else:
            lagg_till.append((cid, c, h))

    borttagna = [cid for cid in befintliga if cid not in sedda]

    for cid, c, h in lagg_till + ersatt:
        klient.upsert(samling, id=cid, vector=bädda_in(c["text"]),
                      payload={"text": c["text"], "hash": h, "borttagen": False})
    for cid in borttagna:
        klient.set_payload(samling, id=cid, payload={"borttagen": True})   # mjuk borttagning

    return {"nya": len(lagg_till), "ersatta": len(ersatt),
            "oforandrade": oforandrade, "borttagna": len(borttagna),
            "besparad_embeddingandel": round(
                oforandrade / max(len(nya_chunks), 1), 3)}

# Sökningen måste filtrera bort mjukt borttagna
def sok(klient, samling, q, k=10):
    return klient.search(samling, q, limit=k,
                         query_filter={"must": [{"key": "borttagen", "match": {"value": False}}]})

# Blågrön: bygg nytt vid sidan av, byt alias först efter utvärdering
def blagron_utrullning(klient, gammalt, nytt, utvardera, troskel=0.0):
    fore, efter = utvardera(gammalt), utvardera(nytt)
    print(f"recall@10: {fore:.3f} → {efter:.3f}")
    if efter >= fore + troskel:
        klient.update_alias("produktion", nytt)
        return {"bytte": True, "fore": fore, "efter": efter}
    return {"bytte": False, "anledning": "ingen förbättring", "fore": fore, "efter": efter}

besparad_embeddingandel är talet som motiverar hela konstruktionen: ligger den på 0,98 betyder det att en veckovis omindexering kostar två procent av vad en full ombyggnad hade gjort.

Behärskning innebär

  • Uppdaterar ett index inkrementellt
  • Hanterar borttagning och ombyggnad
  • Behåller reproducerbarhet vid modellbyte

Logga in för att göra övningarna och bygga upp din behärskning.

Källor

Alla källor och licenser