Een zoek- en inferentie-database vanaf nul in puur Zig — Antfly Research
Door AJ Roetker, CTO
Mijn collega Rowan vatte ruim een jaar geleden ons ambitieuze doel voor Antfly samen: de perfecte zoekfunctie! Dit doel is absurd, ambitieus, onbereikbaar en daardoor perfect voor ons. Het horen van grootse technologen die praten over de "perfecte zoekfunctie" doet me nog steeds denken aan een aflevering van Silicon Valley, maar ik hou van onmogelijke doelen, en mijn innerlijke tech-hipster waardeert de ironie. Als je streeft naar de perfecte zoekfunctie en je mist je doel, dan land je waarschijnlijk op een andere, interessante plek.
Ik zal u meenemen in waarom we deden wat je nooit zou moeten doen met een nieuwe startup: we hebben het product volledig herschreven. Antfly v0.1 lanceerde een engine voor documentopslag, full-text, vector- en graph-indexering in Go. Antfly v0.2 lanceert dezelfde engine in Zig, met nul afhankelijkheden (nou ja, geen afhankelijkheden die de leiding hebben, maar daar kom ik later op terug).
Basisprincipes
Ik hoor mensen minder vaak praten over basisprincipes (first principles) dan vroeger, maar ze blijven belangrijk voor mij en voor de manier waarop ik beslissingen neem. Bij het bouwen van de eerste versie van Antfly was het idee om een gat in de databasemarkt te vullen: een "schema-ish", gebruiksvriendelijke query-engine voor indexering zoals Elasticsearch, maar qua schaal dichter bij Postgres dan bij Iceberg, zo eenvoudig in gebruik als Mongo, en zo makkelijk te beheren als Google's Spanner of Bigtable. (En ik dacht dat "perfect search" te hoog gegrepen was... HA!)
Toen ik de eerste versie bouwde, had ik nog niet dezelfde softwaretools (zoals Codex, Claude, Aider, Pi) beschikbaar om me te ondersteunen bij de ontwikkeling. De complexe problemen die ik wilde aanpakken, waren dezelfde redenen waarom CockroachDB destijds voor Go had gekozen: gedistribueerde systemen en concurrency. Rust kan geheugenveiligheid garanderen via lifetimes, maar geheugenveiligheid was niet het moeilijkste deel, en Go is voor mij oneindig leesbaarder dan Rust ooit was. Bovendien had Go de meest beproefde Raft-implementatie (die van etcd), en ik was slechts één persoon die in de weekenden en op vrijdagen werkte, terwijl ik probeerde een Elasticsearch DSL bovenop Bleve te bouwen.
Focus op het model, niet op de embeddings
Kort daarna werden embeddings gemeengoed. Word2vec had aangetoond dat een vector daadwerkelijk betekenis kon dragen, en de eerste praktische embedding-modellen zorgden ervoor dat een gemiddelde ontwikkelaar een "Google-lite" semantische zoekfunctie voor zijn app kon bouwen. Op mijn werk was onze schaal van vectoropslag zo klein dat een top-k zoekopdracht een uitputtende zoekopdracht kon zijn, maar ik las over alle interessante algoritmen achter Pinecone, Vertex AI Vector Search, Elastic, Mongo, CockroachDB en pgvector nadat een oudere collega zijn ogen uitdraaide bij de belachelijke seed-ronde van Pinecone.
Pinecone was misschien overgewaardeerd, maar ik denk dat een van de meest interessante ideeën over het hoofd werd gezien: gebruikers zouden zich zorgen kunnen maken over het model in plaats van over de embeddings. Het deed me denken aan Postgres, waar een gebruiker grotendeels niet hoeft te weten hoe B-trees en andere indexeringsalgoritmen werken, of hoe je in Bleve en Lucene het gebruik van S2-indexen en finite state transducers kunt vermijden. Vectordatabases en -indexen vereisen daarentegen dat je alles weet van HNSW, SPFresh, RaBitQ en dergelijke.
Ik besloot in de eerste versie van Antfly te proberen deze algoritmen te implementeren op basis van hun papers en blogposts, en dat was een groot succes. Met mijn laptop, Antfly en Ollama was ik in staat om een semantisch doorzoekbare Wikipedia te maken! Omdat het genereren van embeddings zo traag was, was het geen groot probleem dat de database zelf iets langzamer was in de initiële implementatie.
De overstap naar Zig
Tegelijkertijd had ik een paar jaar eerder, na het ontdekken van TigerBeetle, geprobeerd om VSR, Protobuf en een LSM in Zig te implementeren. Ik hield erg van de leesbaarheid van Zig, de concurrency-primitieven, de mensen die de taal ontwikkelen en de interoperabiliteit met C. Maar Zig was te onvolwassen voor mijn weekendproject en miste veel van het zware werk: Raft, full-text indexering, een portable en beproefde LSM, enzovoort.
Ik nam echter de geest van de TigerBeetle-ontwikkelaars mee en ging aan de slag met het integreren van VOPR-testing met TLA+-tracevalidering (Rowan's post over formele verificatie met coding-agents behandelt dit), gebouwd op de nieuwe Go mock-tijd en eerdere methoden van etcd's Raft TLA+ spec en tracevalidering.
Toen begon std.Io een grote impact te maken in de techwereld, omdat Zig besloot enkele serieuze herzieningen van de taal door te voeren. Ik had de GitHub-ontwerpdraden over dit onderwerp al een tijd gevolgd en vond het vanuit een engineering-perspectief erg interessant. Antfly had net v0.1 uitgebracht en ik had even ademruimte om na te denken over wat er daarna kwam. Dus begon ik te experimenteren met Zig en het nieuwe std.Io-werk, waarbij ik probeerde onze LSM te implementeren met de async I/O en de std.Io.Evented-machinerie die Zig begon bloot te stellen.
Tegelijkertijd wilde ik zien hoe ver ik kon gaan met de software-factorisatie van onze code. Het voelde alsof Raft, TLA+-specs, duidelijke traces en taal-agnostische tests de perfecte uitdaging waren. Dus begon ik met het bouwen van Raft, full-text indexering, een LSM, en... HTTP/2 (en onze Raft-transportlaag, die over HTTP/3 en QUIC liep). Ik had ook het grootste deel van onze Go-gebaseerde end-to-end tests herschreven in Python, zowel om te voorkomen dat coding-agents konden "valsspelen" door in de Go-code te kijken, als om te garanderen dat onze Python SDK solide was. Dit bleek het perfecte taal-agnostische framework om ook naar Zig te transpileren.
Waarom herschrijven?
Gesprekken met James en Drew bevestigden het idee toen we spraken over waar we op optimaliseerden: asymmetrische uitkomsten en betrouwbaarheid. De meeste startups falen; daarom is de dure gok — het zelf schrijven van elke high-performance dependency — de gok die het waard is. Een gebruiker MOET weten en erop kunnen vertrouwen dat hun database werkt, en een gebruiker wil dat die database "vliegt" in plaats van "sprint" (het moet verbazingwekkend snel zijn).
Mensen bouwden al allerlei interessante projecten op Antfly. Om het echter vanuit elke andere programmeertaal of in de browser te kunnen draaien, hadden we iets nodig als Rust of Zig om de code een C-compatibele, WASM- en WebGPU-compatibele interface te geven. Ik wilde dat Antfly de "grand unified theory" van databases zou worden: een machete voor ouderwetse use cases en traditionele apps, en het perfecte Zwitserse zakmes voor AI- en semantische use cases die niemand heeft voorzien (alleen "perfect search" was blijkbaar niet ambitieus genoeg meer). Iets wat een ontwikkelaar kan embedden in een sandboxed omgeving (laptop, unit tests, Lambda), kan draaien op gemiddelde applicatie-workloads (PostgreSQL, Cockroach, Mongo), of kan draaien op analytische schaal (data warehouse, serverless). En ik wilde dat Antfly in al deze omgevingen nog betrouwbaarder zou zijn, terwijl de prestatiedoelen werden behaald of overtroffen. Een rewrite vanaf de grond geeft je de kans om al je dependencies in een doelbewuste vorm te bouwen, waarbij resourcebeheer, prioriteitsscheduling en testideologie vanaf dag één zijn ingebakken.
Waarom Zig en niet Rust?
Vier redenen, allen terug te voeren op basisprincipes: portabiliteit, C-interop, snelheid en testbaarheid.
- Portabiliteit en C-interop: Zig is een C-compiler met een libc voor elk doelplatform. Het cross-compileren van Antfly met CUDA, ONNX en Wasmtime is slechts één flag, en het aanroepen ervan gebeurt via
@cImport. Rust cross-compileert pure Rust prima, maar loopt vast bij de eerste C-dependency; dat is waaromcargo-zigbuildbestaat (het gebruikt Zig). - Snelheid: Beiden gebruiken LLVM, dus de codegen is vergelijkbaar. Het verschil is dat de snelle versie in Zig de standaard is. Elke allocatie vereist een allocator; er is geen allocatie of branching achter je rug om, en SIMD en comptime specialization zijn ingebouwd. In Rust eindigen de hot paths vaak in
unsafe. Vergeleken met Go is het geen wedstrijd: geen GC, geen scheduler, geen CGO. - Testbaarheid:
std.Iomaakt de wereld tot een parameter. Geef een package een gesimuleerde Io en je krijgt VOPR: disk-, netwerk- en klokfouten, alles. In Rust moet je tokio vervangen door madsim of turmoil en hopen dat de dependency tree meewerkt.
Mensen denken vaak dat formele verificatie en geheugenveiligheid je beschermen tegen een hele klasse bugs, en dat doen ze ook, maar net als de validiteit van je lifetime in Rust hangt dit af van de context. Rust heeft een borrow checker en Zig niet, maar de bugs die mij zorgen baren zijn geen use-after-frees. Het zijn replica's die achterop raken, berichten die twee keer aankomen, of een fsync die liegt. De borrow checker dekt dit niet af. Tools zoals Kani en Verus bewijzen zaken over Rust-functies, niet of je Raft-implementatie lineariseerbaar is. Of je nu Rust of Zig gebruikt, je hebt nog steeds iets nodig als Antithesis om fouten naar het hele systeem te slingeren.
In beide talen wil je elke bounds en overflow check aan hebben staan tijdens het testen, omdat één gemiste check iemands data kan kosten. Rust zet overflow checks in release uit, zoals de meeste talen; Zig maakt veiligheid tot een build-modus die je simpelweg kunt omzetten. We verifiëren dus het protocol, de TLA+-specs, de tracevalidering tegen de draaiende Raft, en VOPR op elk package. Geheugenveiligheid "kopen" we via testen: elke suite draait in Debug en ReleaseSafe met de testing-allocator en dezelfde simulator, en we leveren de ReleaseFast versie.
Er was nog één belangrijke reden voor de rewrite: we hadden al forks van al onze belangrijkste dependencies in handen. Als we elke dependency volledig in eigen beheer hebben, kunnen we het geheugen, de CPU- en GPU-resources holistisch over het hele proces beheren (hetzelfde argument dat TigerBeetle maakt in "Tiger Style" met hun zero-dependencies beleid). Met Zig is "nul afhankelijkheden" iets wat je daadwerkelijk kunt volhouden. Het linken van een C-bibliotheek is een non-event; CUDA, Metal en Wasmtime kosten slechts een paar regels code in plaats van de tol die CGO vraagt bij elke grensovergang. Buitenlandse bibliotheken klikken in onze engine, ze omhullen hem niet. Alles wat geheugen, threads, opslag en het netwerk raakt, is van onszelf. Het vanaf nul schrijven van een LSM, een B+tree, Raft en HTTP/3 is een verschrikkelijke ruil voor een paar nieuwe functies, maar een fantastische ruil om te kunnen "vliegen" in plaats van te lopen.
Sommige zaken maakten de rewrite moeilijker: de Go-versie was al behoorlijk betrouwbaar en krankzinnig snel voor een Go-programma vergeleken met concurrenten. Zig is pre-1.0 en de standaardbibliotheek verandert. De tooling is jonger en minder mensen kunnen de code lezen. Bovendien stond ik op het punt een gezin te stichten terwijl ik een startup probeerde te lanceren, wat geen makkelijk recept voor succes is.
Wat we hebben bereikt
Wat hebben we nu eigenlijk gekregen uit deze rewrite? Ten eerste het belangrijkste: we konden VOPR-simulatietests vanaf dag één inbakken in al onze dependencies en elk package. We hebben volledige controle over de wereld: Antfly werd de node, en std.Io was de perfecte omgeving om systeem-, netwerk-, concurrency- en klokfouten overal te simuleren. Simulatietesten werden simpelweg... testen. We konden de LSM optimaliseren voor onze vector-zware, batch-georiënteerde workloads en ervoor zorgen dat de caching van de LSM goed samenwerkt met de caching van de full-text en vector-indexen.
Daarnaast hebben we een fundament gelegd voor onze visie met de volgende componenten:
Data-engines
- Document (Mongo, Elasticsearch): Schema-ish JSON-documenten met kolomgebaseerde opgeslagen velden en projection pushdown.
- Relationeel (Postgres): Gesloten schema's en getypeerde packed rows, met full-text, vector, sparse, graph en algebraïsche indexen afgeleid over dezelfde basisopslag.
- Lake (Iceberg, Parquet): Externe tabellen die ter plekke worden bevraagd over objectopslag, met dezelfde afgeleide indexen gematerialiseerd als sidecars.
- Externe bronnen: Stream-tabellen uit PostgreSQL via logische replicatie.
Storage-engines
| Modus | Vergelijkbaar met | Autoscaling | Duurzaamheid / HA |
|---|---|---|---|
| Single-file | SQLite | Verticaal | fsync |
| Single-node | Postgres | Verticaal | Hot standby met fencing |
| Distributed | CockroachDB | Verticaal + Horizontaal | Multi-Raft, online shard splits, cross-shard transacties |
| Serverless | Neon, Lakebase | Verticaal + Horizontaal | Object storage |
Onder alle vier: onze eigen LSM, een LMDB-compatibele B+tree, een WAL, TTL, en portable backup en restore, elk met een eigen simulatie-harnas.
Indexerings-engines
- Full text (Lucene, Elasticsearch): Lucene-stijl segmenten, Snowball stemmers, stopwords; geo, keyword, n-gram, numeriek, wildcard, regex en fuzzy queries; highlighting en aggregaties.
- Dense vector (Weaviate, pgvector): RaBitQ-compressie met SPFresh-stijl incrementeel posting-onderhoud, gefilterd door de andere indexen.
- Sparse vector: SPLADE learned sparse retrieval naast BM25.
- Late interaction: ColBERT-stijl MaxSim over multi-vector documenten (ColQwen2).
- Hybrid fusion: Reciprocal rank fusion, relative score fusion, en resultaat-pruning over al het bovenstaande in één queryplan.
- Algebraïsch: Aggregaten en materialisaties (sum, min, max).
- Graph (Neo4j): Traversal, pathfinding, PageRank-stijl queries, ad hoc foreign keys en views.
Enrichment-engines
- Mechanisme: Een asset producer zet een ruw veld om in een duurzaam artefact, en elke index kan putten uit dat artefact. Artefacten zijn opnieuw verwerkbaar, gereedheid wordt per stream bijgehouden en niets wordt stilzwijgend weggegooid.
- Input-vormen: Tekst, JSON, PDF's, afbeeldingen, audio en video, via URL of direct ingevoegd.
- Documenten: Born-digital PDF-parsing, met Florence-2 OCR voor pagina's zonder tekst, en LayoutLMv3 voor documentlayout.
- Audio: Whisper-transcriptie naar doorzoekbare transcripten, en CLAP voor native audio-embeddings.
- Afbeeldingen: CLIP en Gemma multimodale embeddings, zodat tekstqueries afbeeldingen ophalen en afbeelding-queries tekst.
- Chunking: Fixed en learned chunkers, met document-level en chunk-level vectoren in één index.
- Ongestructureerd naar gestructureerd: GLiNER2 entity-extractie, classificatie en samenvatting schrijven getypeerde velden waarop gefilterd, geaggregeerd en gejoined kan worden.
- Autograph: Een graph-index gebaseerd op een extractie-artefact, zodat entity- en relatie-edges materialiseren zodra documenten arriveren, zonder aparte graph-load stap.
- Eigen input: Door de applicatie beheerde vectoren en vooraf berekende beschrijvingen pluggen in dezelfde indexen.
Query-engines
- HTTP JSON API: Lucene, Elasticsearch, Bleve en Mongo-stijl DSL, met streaming RAG over SSE.
- SQL en het Postgres wire protocol: DDL, CTE's, joins, window functies, RETURNING, en search_path sessies, vertaald naar native getypeerde plannen in plaats van SQL-tekst, plus een document SQL-dialect over JSON-tabellen.
- Agents: Retrieval agent, query-builder agent, agent tools, en MCP en A2A surfaces.
Embedding-surfaces
- Single binary, Kubernetes operator met autoscaling, of Antfly Cloud.
- C API en
antfly-embeddedvoor in-process gebruik vanuit andere talen. - WASM-builds van de inference runtime (wasm32 en wasm64).
- WASM-extensie runtime (Wasmtime) voor gebruikerscode binnen de engine.
Compute-backends
- CPU (SIMD)
- Metal
- CUDA, met een kernel JIT
Model-runtime
- Formaten: GGUF, safetensors, ONNX; native kwantisatie, export en gecompileerde artefacten.
- Architecturen: BERT, ModernBERT, NomicBERT, DeBERTa, T5, GPT, Qwen2/2-VL/3-VL, Gemma 3/4 (multimodaal), Whisper, Florence, LayoutLMv3, GLiNER.
- Taken: Embedding (tekst, beeld, audio, late interaction), chunking, reranking, classificatie, NER, OCR en layout, transcriptie, generatie met tool calling.
- Remote providers: OpenAI, AWS Bedrock, Vertex, Ollama.
Fine-tuning
- Methoden: LoRA, QLoRA (NF4), recursive LoRA, PEFT; SFT, DPO, ORPO, SimPO, GRPO, RFT; NEFTune, gradient checkpointing, sequence packing.
- Recipes: Gemma 4, GLiNER2, ColQwen2, LayoutLMv3, reranker heads en LoRA, fused SPLADE chunker.
Verificatie
- VOPR-simulatieharnassen voor de LMDB-backend, LSM, WAL, transacties, Raft, metadata en index manager.
- TLA+ tracevalidering voor Raft.
- Taal-agnostische Python end-to-end suite gedeeld met de Go-versie.
Voorbij correctheid
Naast rigoureuze correctheidstesten hebben we hard gewerkt aan effectiviteit, bruikbaarheid en prestatie-testen. "Model cards" zijn momenteel populair om de grote capaciteiten van LLM's samen te vatten, en we hebben dat idee geleend voor onze database-releases: de Antfly 0.2 release card is onze model card. Rowan heeft veel werk verricht om de prestaties van Antfly te evalueren over verschillende benchmarks en domeinen, tegenover diverse concurrenten die elk gespecialiseerd zijn in een van die domeinen. We hebben Antfly geëvalueerd als oplossing voor RAG, OCR en geheugensystemen. We hebben ook nieuwe benchmarks en evaluaties gebouwd om te laten zien waar Antfly echt uitblinkt: de mogelijkheid om functies te implementeren waar jij om geeft, je database use cases moeiteloos op te zetten en te schalen, en de functies te ontdekken die retrieval verbeteren nu AI meer informatie en assets produceert dan ooit.
De perfecte zoekfunctie is nog steeds absurd, ambitieus en onbereikbaar. We zijn alleen een stuk dichterbij dan we waren in Go, en deze keer hoeven we het fundament niet opnieuw te schrijven om de rest van de weg af te leggen.
Groetjes,