RAG
Pinecone, pgvector of Qdrant: drie maanden in productie
Op een dinsdag in maart vroeg een Nederlandse archivaris onze pilot-RAG naar een raadsbesluit uit 2014. Drie maanden later weten we welke vector store bleef en waarom.

Op een dinsdag in maart opende een archivaris van een middelgrote Nederlandse gemeente onze pilot-zoekbalk en typte: 'wat besloot de raad in 2014 over het busstation?' Het oude systeem, een SharePoint-index bovenop vijftien jaar raadsnotulen, gaf nul resultaten. Onze pilot leverde de juiste notulen, de juiste pagina en het bijbehorende besluit in 1,8 seconden. De archivaris stuurde één regel terug: 'eindelijk.'
Die pilot is inmiddels drie maanden oud. We beloofden de klant op te schrijven wat we hadden geleerd over de drie vector stores die we parallel draaiden: Pinecone, pgvector en Qdrant. Geen benchmark op synthetische data, geen Reddit-discussie. Het resultaat van hetzelfde Nederlandse corpus van 400.000 documenten op drie systemen, door een kwartaal echte vragen van echte inwoners.
De opdracht
Het corpus is vijftien jaar gemeentelijke output. Raadsnotulen, motieteksten, ambtelijke nota's, omgevingsvergunningen en een lange staart aan gescande PDF's van vóór de digitale workflow. Rond de 400.000 documenten na deduplicatie, met een zware OCR-slag over de oudere scans. Volledig Nederlandstalig, met de bekende gemeentelijke vocabulaire (raadsbesluit, ruimtelijke ordening, motie, amendement, omgevingsvergunning). Ongeveer 11 miljoen chunks na onze windowing-stap.
Drie keiharde eisen kwamen van inkoop, niet van ons:
- Data blijft binnen de EU. Frankfurt of Amsterdam, geen uitzonderingen.
- Back-ups moeten passen binnen de bestaande BIO-checklist (Baseline Informatiebeveiliging Overheid) die de gemeentelijke DBA al draait tegen elke database in het pand.
- Eén verwerkersovereenkomst, geen drie. Hoe minder externe verwerkers op het AVG-register, hoe beter.
Dat laatste punt slaan de meeste engineering-stukken over. Voor een Nederlandse gemeente betekent elke extra subverwerker een overleg met de FG (Functionaris Gegevensbescherming) en een nieuwe regel op een register dat al dertig regels telt. De technische en de contractuele keuze zijn niet los te koppelen. Vergeet je dat, dan blokkeert iemand die nog nooit een whitepaper over vector databases las jouw benchmark-winnaar, en terecht.
Waarom we alle drie een kwartaal draaiden
Een benchmark zonder deadline is een hobby. Een benchmark op synthetische data zegt je niets over hoe een reindex op vrijdagmiddag botst met leesverkeer op maandagochtend. Dus bouwden we dezelfde retrieval-pipeline drie keer: één die schreef naar Pinecone serverless in eu-west, één naar pgvector op dezelfde Postgres als onze documentmetadata, en één naar een self-hosted Qdrant-cluster op de bestaande Kubernetes van de gemeente.
Alle drie indexeerden dezelfde 11 miljoen chunks uit dezelfde 1024-dimensionale meertalige embeddings. Dezelfde chunker, dezelfde rerank-stap, hetzelfde prompt-template. De enige variabele was de vector store. We spiegelden het hele kwartaal productieverkeer naar alle drie en routeerden leesverkeer steeds maar naar één tegelijk, wekelijks roterend zodat elke store echte belasting voelde.
Pinecone, de managed gok
Pinecone stond het snelst overeind. Een handvol API-calls en we waren vectoren aan het inserten. De eu-west-regio hield de data binnen de EU at rest en in transit. De mediane latency was de laagste van de drie: rond de 35 ms server-side voor een top-10 ANN-search met metadata-filters.
De filter-performance was de aangename verrassing. We filteren hard op afdeling, jaar en documenttype, want een inwoner geeft zelden om het hele archief. Die geeft om de afdeling wonen in 2019. De metadata-filtering van Pinecone deed dit zonder de recall-drop die we soms zien als filters achteraf op een ANN-index worden gelegd.
Wat het bij deze klant deed sneuvelen, was niet de techniek. Het was de inkoopadviseur die 'Pinecone Systems Inc., Palo Alto' op de verwerkersovereenkomst las. Het technische eu-west-datapad was prima. Contractueel kwam er een extra subverwerker met hoofdkantoor in de VS bij op een AVG-register dat zijn geduld al verloren had. We draaiden het hele kwartaal door zodat we konden aantonen dat we geen sterke optie hadden weggegooid, maar het oordeel viel in week één in een kamer waar wij niet bij waren.
pgvector, Postgres doet het zware werk
pgvector is tegenwoordig waar we als eerste naar grijpen voor projecten van deze omvang. Het is een Postgres-extensie. Staat jouw Postgres in eu-central, dan staan je vectoren in eu-central. Je back-ups zijn al een opgelost probleem omdat het Postgres-back-ups zijn, en je DBA weet al hoe ze die om 03:00 terugzet.
Voor dit corpus gebruikten we HNSW (toegevoegd aan pgvector in 0.5.0), 1024-dim vectoren, cosine distance:
-- 400k documents, ~11M chunks, 1024-dim multilingual embeddings
CREATE INDEX documents_embedding_idx
ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- query-time recall/latency knob
SET hnsw.ef_search = 80;
De index-build op een db.r6g.large duurde ongeveer 47 minuten voor de volledige 11 miljoen chunks. Mediane zoek-latency bij ef_search = 80 kwam rond de 55 ms server-side uit. Trager dan Pinecone, ruim binnen ons end-to-end budget van 1,8 seconden. De rerank-stap domineert toch al, en geen enkele vector database wint die race.
Het voordeel dat we niet hadden voorspeld, was filter pushdown. Omdat de chunk-metadata in dezelfde tabel staat als de embedding, wordt een query als 'raadsbesluiten van afdeling ruimte uit 2014' een gewone WHERE-clause die de planner combineert met de HNSW-index. We hoefden geen dual-store query-laag te ontwerpen of twee systemen in sync te houden. We schreven SQL. De gemeentelijke DBA las het en knikte. Dat is veel waard.
HNSW-indexen houden niet van bulk-reindexes tijdens kantooruren. Toen we in maand twee 30.000 nieuw gescande vergunningen backfillden, gaf vacuum-churn op de chunks-tabel lock-contention met het leespad en spikte de zoek-latency naar 600 ms voor een kwartier. pg_repack en een 03:00-cron losten het definitief op, maar dit leer je alleen in productie.
Qdrant, de tussenweg
Qdrant was technisch de interessantste optie. Geschreven in Rust, kleine memory footprint, ingebouwde sparse-plus-dense hybrid search, payload-filtering die echt expressief is. De self-hosted variant is overzichtelijk: een Helm-chart, een paar volumes en je draait.
Een filter op Qdrant ziet er aan de client-kant zo uit:
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue
client = QdrantClient(host="qdrant.internal", port=6333)
hits = client.search(
collection_name="raadsstukken",
query_vector=embedding,
query_filter=Filter(
must=[
FieldCondition(key="afdeling", match=MatchValue(value="ruimte")),
FieldCondition(key="jaar", match=MatchValue(value=2014)),
]
),
limit=10,
)
De latency lag tussen Pinecone en pgvector in, rond de 45 ms server-side. De sparse-plus-dense hybrid was een echt voordeel bij Nederlandse lexicale queries waarbij een naam, een dossiernummer of een straat exact moest matchen. Pinecone kan dit met sparse vectors, maar dat is meer opzet. pgvector vereist een aparte full-text index en een fusie-stap in je applicatiecode.
De operationele kosten waren echt. Qdrant is één systeem extra op de on-call rotatie. Eén Helm-chart extra, één set dashboards extra, één ding extra dat kan OOM'en tijdens collection-optimalisatie (gebeurde één keer, tijdens piekuren; we hebben optimalisatie naar 03:00 verplaatst en sindsdien is het stil). Voor een team dat al Kubernetes draait en de appetite heeft, is het een prima keuze. Voor een gemeentelijke IT-afdeling die al Postgres draait voor dertig systemen en Qdrant voor geen enkel, is het één nieuwe buur om te leren kennen.
De cijfers die we echt gemeten hebben
Geen van deze cijfers is universeel. Ze gelden voor deze klant, dit corpus en deze hardware. We delen ze omdat de vorm van de vergelijking belangrijker is dan de getallen.
Recall@10 tegen een evaluatie-set van 1.200 vragen, geschreven door de archivaris (de vragen zijn niet synthetisch; het zijn de echte vragen die inwoners de afgelopen twee jaar bij de helpdesk hebben gesteld):
- Pinecone serverless, default-instellingen: 0,93
- pgvector HNSW, ef_search=80: 0,94
- Qdrant, default-instellingen: 0,94
De recall-verschillen vallen binnen de ruis van de evaluatie-set. Voor de eindgebruiker is het verschil onzichtbaar. Wat niet onzichtbaar is, zijn de operationele en contractuele kosten. De kwartaalfactuur van Pinecone was met afstand de hoogste van de drie, omdat serverless storage en read units oplopen als je filtert over 11 miljoen chunks. pgvector kostte ons de marginale CPU en storage op een Postgres-instance waar we toch al voor betaalden. Qdrant kostte ons een kleine Kubernetes-namespace en één extra rotatie op de on-call kalender.
Wat we hielden, en waarom
We hielden pgvector. Niet omdat de benchmark doorslaggevend was. Dat was 'ie niet. We hielden hem omdat:
- De vectoren staan naast hun metadata, in één database, met één back-up, op één verwerkersovereenkomst.
- De gemeentelijke DBA draait Postgres al voor dertig andere systemen. De on-call belasting groeide met geen enkele pagina in PagerDuty.
- Als de vraag is 'wat gebeurt er als dit op zaterdag om 23:00 omvalt', is het antwoord hetzelfde als voor elke andere Postgres-instance in het pand.
Qdrant hielden we op de bank voor de sparse-hybrid experimenten die we niet aan productie hebben gecommitteerd. Als de archivaris straks vraagt om retrieval op dossiernummer dat exact moet matchen, dan is dat het pad dat we volgend kwartaal pakken. We zijn eerlijk tegen onszelf: groeit het sparse-hybrid verhaal uit tot een dagelijkse eis, dan verdient Qdrant zich terug.
Pinecone hebben we afgebouwd. De techniek was de meest gepolijste van de drie. Contractueel paste 'ie niet bij deze klant. We zouden 'm zonder aarzelen weer draaien voor een private-sector klant waarvan inkoop niet schrikt van een Amerikaanse subverwerker.
Wat we onszelf drie maanden geleden zouden vertellen
Drie dingen. Eén: kies de saaie optie die al in je stack zit, tenzij je een technische reden hebt die zwaarder weegt dan inkoop. Voor het meeste gemeentelijke en mkb-werk in Nederland betekent dat pgvector. Twee: schrijf de eval-set vóórdat je de database kiest. Twaalfhonderd echte vragen uit helpdesk-transcripten leerden ons in één middag meer dan een week aan synthetische benchmarks ooit had gedaan. Drie: het reindex-verhaal is belangrijker dan het zoekverhaal. Zoeken gebeurt miljoenen keren en is saai. Reindexen gebeurt één keer per kwartaal en is het ding waar je 's nachts wakker van ligt.
Toen we de gemeentelijke kennis-agent voor deze klant bouwden, was de vraag nooit 'welke vector database is de beste', maar 'welke kan ons ops-team om 23:00 op zaterdag dragen'. Dat antwoord schreef zichzelf zodra we het operations-diagram op een whiteboard zetten en bij elk vakje een naam zetten.
De audit van vijf minuten die je maandag kunt doen: open je vector store dashboard, zoek je grootste collectie en vraag je DBA wat er gebeurt als die vannacht omvalt. Het eerlijke antwoord vertelt je of je de juiste hebt gekozen.
Kern
De beste vector store is degene die je ops-team om 23:00 op zaterdag kan dragen. Voor onze Nederlandse gemeentelijke RAG was dat pgvector, met Qdrant op de bank.
FAQ
Is pgvector snel genoeg voor 400.000 documenten?
Ja. Met HNSW en ef_search rond de 80 maten we mediane zoek-latency van ongeveer 55 ms op een db.r6g.large voor 1024-dim embeddings. De rerank-stap is meestal de echte bottleneck, niet de vector-lookup.
Waarom niet gewoon Pinecone kiezen en doorgaan?
Voor veel teams zou dat prima zijn. Onze klant wilde alle data en metadata onder één verwerkersovereenkomst binnen de EU, en inkoop gaf de voorkeur aan zo min mogelijk externe subverwerkers. Pinecone slaagde technisch, sneuvelde contractueel.
Waar past Qdrant het beste?
Als je out of the box sparse-plus-dense hybrid search wilt en je al Kubernetes draait. Wij hielden Qdrant op de bank voor exact-match dossier-retrieval die we nog niet aan productie hebben gecommitteerd.
Hoe reindex je zonder downtime?
Tijdens een switchover-window schrijven we naar twee collecties, valideren met een smoke-test van 200 queries en flippen daarna het leespad met een config-wijziging. Op pgvector gebruiken we CREATE INDEX CONCURRENTLY en pg_repack om lock-contention te voorkomen.