De verhouding is instelbaar. Omdat gebruikers problemen vaak in eigen woorden beschrijven, test ik 30% BM25 en 70% vector search als eerste hypothese. Bij scorefusie moeten de scores vergelijkbaar zijn; Reciprocal Rank Fusion combineert rangposities in plaats van oorspronkelijke scores.
Snel zoeken zonder alles te vergelijken: HNSW
Zonder index zou een zoekvraag vergeleken moeten worden met elke opgeslagen embedding. Dat wordt langzamer naarmate de dataset groeit. Voor dit project koos ik HNSW (Hierarchical Navigable Small World), een index die embeddings verbindt in meerdere lagen: bovenaan weinig punten voor grote sprongen door de ruimte, onderaan steeds meer punten om nauwkeuriger te zoeken. Een zoekopdracht daalt van boven naar beneden af en hoeft daardoor maar een fractie van alle embeddings te bekijken.
HNSW is een approximate nearest neighbor-index: snel, maar het kan een vector missen die bij een volledige vergelijking wel tot de dichtstbijzijnde resultaten zou behoren. Een paar instellingen sturen die afweging. m bepaalt het maximale aantal verbindingen per laag; ef_construction beïnvloedt de kwaliteit en bouwtijd van de index; ef_search stuurt tijdens het zoeken de afweging tussen recall en snelheid. pgvector levert hiervoor standaardwaarden, maar of die goed werken, moet je op je eigen dataset meten.
Een praktische beperking: dimensies en opslag
Text-embedding-3-large van OpenAI levert zonder aangepaste dimensions-parameter embeddings met 3.072 dimensies. Daar loop je tegen een technische grens aan: een HNSW-index in pgvector op het standaardtype vector ondersteunt maximaal 2.000 dimensies. Een HNSW-index op halfvec ondersteunt maximaal 4.000 dimensies; het type halfvec zelf kan maximaal 16.000 dimensies opslaan. Elke waarde wordt met halve precisie opgeslagen. Dat is geen dimensionality reduction: de embedding blijft 3.072 dimensies groot, maar het is wel een compactere representatie, met ongeveer de helft van de opslag voor de vectorwaarden. Of het mogelijke verlies aan numerieke precisie merkbaar is in de zoekkwaliteit, moet je in de praktijk meten.
Het resultaat
De gevonden tickets of chunks gaan als extra context samen met de oorspronkelijke vraag naar een LLM, dat op basis daarvan een antwoord formuleert. Dit patroon heet Retrieval-Augmented Generation (RAG).
Voor de Clientbox tickethistorie levert dat een zoekfunctie op die niet blijft hangen op exacte woorden. “Hoe koppel ik mijn Google Agenda” en “hoe zorg ik dat afspraken uit Clientbox in mijn eigen agenda verschijnen” kunnen daardoor bij dezelfde relevante tickets uitkomen, terwijl BM25 de kans vergroot dat een ticketnummer of foutcode hoog eindigt, mits zo’n identifier bij het tokeniseren intact blijft. Als een exacte treffer gegarandeerd moet worden, is daarnaast een aparte exacte zoekroute nodig. De precieze verhouding tussen BM25 en vector search test ik nog verder met realistische zoekvragen, maar de basis staat: embeddings voor betekenis, HNSW om dat snel doorzoekbaar te houden en BM25 voor sterke lexicale signalen.
Voor support betekent dat vooral dit: minder vaak hetzelfde uitzoeken en sneller bij het antwoord dat er al lag.