純粋なZigでゼロから構築する検索・推論データベース
A search-and-inference database from scratch in pure Zig (antfly.io)
要約
Antflyは、当初Goで開発されたドキュメントストレージ、フルテキスト、ベクトル、グラフインデックスエンジンでしたが、依存関係をゼロにし、パフォーマンスと移植性を向上させるために、純粋なZigで再構築されました。著者は、Zigの可読性、C言語との相互運用性、速度、テスト容易性といった特性が、特にAIやセマンティック検索といった新しいユースケースに対応するために最適であると説明しています。
全文翻訳
純粋なZigでゼロから構築した検索・推論データベース
1年以上前、同僚のRowanはAntflyの野心的な目標を要約しました。それは「完璧な検索」です!この目標は馬鹿げていて、野心的で、達成不可能ですが、私たちにはぴったりです。偉大な技術者たちが完璧な検索について語るのを聞くのは、今でも『シリコンバレー』のエピソードから抜け出してきたような気分になりますが、私は不可能な目標が好きですし、私の内なるテック・ヒップスターは皮肉を理解しています。完璧な検索を目指せば、それは外れますが、興味深い場所で外れるのです。そこで今回は、新しいスタートアップでは絶対にやってはいけないこと、つまり製品の書き直しを行った理由を説明します。Antfly v0.1は、Goでドキュメントストレージ、フルテキスト、ベクトル、グラフのインデックスエンジンをリリースしました。Antfly v0.2は、依存関係ゼロ(まあ、主役として動くものはないですが、後述します)で、同じエンジンをZigでリリースします。
原理原則#
最近は原理原則についてあまり聞かなくなりましたが、私や私の意思決定においては依然として重要です。Antflyの最初のバージョンを構築したとき、データベース市場のギャップを埋めるという考えがありました。それは、Elasticsearchのようなインデックス作成のための、スキーマのような、フレンドリーなクエリエンジンで、スケールはIcebergよりもPostgresに近く、Mongoのように使いやすく、GoogleのSpannerやBigtableのように運用しやすいものです。(そして、完璧な検索は高望みすぎると考えていました…ハッ!)。
最初のバージョンを構築したとき、私は(コホン、コホン、Codex、Claude、Aider、Piのような)ソフトウェアツールを開発支援に利用できませんでした。そして、私が取り組みたい困難な問題は、CockroachDBがGoを選んだのと同じ理由でした。それは分散システムと並行性です。Rustはライフタイムを通じてメモリ安全性を保証できますが、メモリ安全性は難しい部分ではなく、私にとってGoはRustよりもはるかに読みやすいです。さらに、Goには最も実証済みのRaft実装(etcdのもの)がありました。私は一人で、週末と金曜日に、Bleveの上にElasticsearch DSLを構築しようとしていました。
エンベディングではなく、モデルを重視する#
少し時間が経つと、エンベディングが注目されるようになりました。word2vecは、ベクトルが実際に意味を運ぶことができることを示し、最初の実用的なエンベディングモデルは、平均的な開発者がアプリのためにGoogle風のセマンティック検索を構築できることを意味しました。職場では、ベクトルのストレージ規模が非常に小さかったため、トップkは網羅的な検索で済みましたが、古い同僚が馬鹿げたPineconeのシードラウンドについて鼻で笑った後、Pinecone、Vertex AI Vector Search、Elastic、Mongo、CockroachDB、pgvectorの背後にある楽しいアルゴリズムについて読みました。Pineconeは過大評価されていたかもしれませんが、その最も興味深いアイデアの1つが見過ごされていたと思います。それは、ユーザーがエンベディングを気にする必要がなく、モデルを気にするだけでよいということです。これはPostgresのことを思い出させました。ユーザーはほとんどの場合、Bツリーやその他のインデックスアルゴリズムを知る必要がないこと、あるいはBleveやLuceneではS2インデックス、有限状態トランスデューサーなどを知る必要がないことです。一方、ベクトルデータベースやインデックスでは、HNSW、SPFresh、RaBitQなどを知り、気にする必要がありました。
私はAntflyの最初のバージョンで、これらのアルゴリズムを論文やブログ記事から実装することに挑戦し、それは大成功でした。ラップトップ、Antfly、Ollamaを使って、セマンティックに検索可能なWikipediaを作成することができました!エンベディング生成は非常に遅かったため、初期実装ではデータベースが少し遅くても問題になりませんでした。
Zigの登場#
これと並行して、数年前にTigerBeetleに出会った後、ZigでVSR、Protobuf、LSMを実装しようとしました。Zigの読みやすさ、並行性プリミティブ、言語を実装している人々、そしてCとの相互運用性が非常に気に入りました。しかし、Zigは私の週末データベースプロジェクトにはまだ青すぎ、多くの重労働(Raft、フルテキストインデックス、ポータブルで実証済みのLSMなど)が不足していました。それでも、TigerBeetleの人々の精神の多くを受け継ぎ、TLA+トレース検証によるVOPRテストを、新しいGoのモック時間とetcdのRaft TLA+仕様とトレース検証からの先行事例に基づいて組み込む作業を始めました(Rowanのコーディングエージェントによる形式検証に関する投稿で詳しく説明されています)。
その後、Zigが言語に大幅なオーバーホールを加える決定をしたとき、std.Ioがテクノロジー界で大きな話題を呼びました。私はしばらくの間、この件に関するGitHubのデザインスレッドを読んでおり、エンジニアリングの観点からはすべて素晴らしいと思っていました。Antflyはv0.1をリリースしたばかりで、次に何が来るかを考える余裕がありました。そこで、Zigと新しいstd.Ioの作業を使い、Zigが公開し始めた非同期I/Oとstd.Io.Eventedの仕組みを実装して、LSMを実装しようとしました。同時に、コードのソフトウェア分解をどこまで進められるかを見たかったのです。Raft、TLA+仕様、明確なトレース、言語に依存しないテストが、登るべき完璧な山のように感じられました。そこで、交通整理と、Raft、フルテキストインデックス、LSM、そして…HTTP/2(そしてHTTP/3とQUIC上のRaftトランスポート)の構築に取り掛かりました。また、GoベースのエンドツーエンドテストのほとんどをPythonで書き直し、コーディングエージェントがGoコードにアクセスして「不正行為」できないようにし、Python SDKが堅牢であることを確認しました。これは、Zigへのトランスパイルにも最適な言語に依存しないフレームワークであることが判明しました。
なぜ書き直したのか#
Jamesとの会話と、Drewとの別の会話が、私たちが何を最適化しているかについて話したときに、非対称な結果と信頼性という考えを固めました。ほとんどのスタートアップは失敗するので、高パフォーマンスの依存関係をすべて自社で書くという高価な賭けは、行う価値のあるものです。ユーザーは、データベースが機能することを「知って」信頼しなければならず、そのデータベースが疾走する(驚くほど速くなければならない)ことを望んでいます。すでにさまざまな興味深いプロジェクトがAntflyの上に構築されていました。しかし、他のどのプログラミング言語からでも、あるいはブラウザで実行するには、C互換、WASM、WebGPU互換のインターフェースを提供するために、RustやZigのようなものが必要でした。私はAntflyをデータベースのグランドユニファイドセオリーにしたかったのです。古いユースケースや従来のアプリのためのマチェットであり、まだ誰も見たことのないAIやセマンティックなユースケースのための完璧なスイスアーミーナイフ(完璧な検索はもはや高望みではないように思えました)。開発者がサンドボックス環境(ラップトップ、単体テスト、Lambda)に埋め込み、平均的なアプリケーションワークロード(PostgreSQL、Cockroach、Mongo)で実行できるもの、または分析スケール(データウェアハウス、サーバーレス)で実行できるものです。そして、設定したパフォーマンス目標を維持または超えながら、すべての環境でAntflyをさらに信頼性の高いものにしたかったのです。すべてをゼロから書き直すことで、リソース管理、優先度スケジューリング、テストの考え方を最初から組み込み、依存関係を目的志向の形状で構築する機会が得られます。
では、なぜRustではなくZigなのでしょうか?4つの理由、すべて原理原則に戻ります。ポータビリティ、Cインターop、スピード、テスト容易性です。
ポータビリティとCインターop:Zigは、すべてのターゲットにlibcを備えたCコンパイラです。CUDA、ONNX、WasmtimeをリンクしてAntflyをクロスコンパイルするのは1つのフラグで、それらを呼び出すのは@cImportです。Rustは純粋なRustをうまくクロスコンパイルしますが、最初のC依存関係で壊れます。だからcargo-zigbuildはZigなのです。
スピード:同じLLVMなので、コード生成は同等です。違いは、高速なバージョンがデフォルトであることです。すべての割り当てはアロケータを取り、何もバックグラウンドで割り当てたり分岐したりせず、SIMDとコンパイル時(comptime)の特殊化が組み込まれています。Rustでは、ホットパスはunsafeになります。Goと比較すると、GC、スケジューラ、CGOはありません。
テスト容易性:std.Ioは世界をパラメータにします。パッケージにシミュレートされたIoを渡すと、ディスク、ネットワーク、クロックの障害など、すべてが提供されます。Rustでは、tokioをmadsimやturmoilに置き換えて、依存関係ツリーが協力することを願うことになります。
人々は通常、形式検証とメモリ安全性がバグのクラス全体から保護してくれると考えていますが、それは事実ですが、Rustでのライフタイムの妥当性と同様に、文脈に依存します…Rustには借用チェッカーがありますが、Zigにはありません。しかし、私を悩ませるバグはuse-after-freeではありません。レプリカが遅延した、メッセージが2回届いた、fsyncが嘘をついた、といったことです。借用チェッカーはそれらをカバーしません。そして、KaniやVerusのようなツールは、Rust関数について証明しますが、あなたのRaftが線形化可能かどうかについては証明しません。RustでもZigでも、システム全体に障害を投げかけるAntithesisのようなものが必要です。どちらの言語でも、テスト中はすべての境界チェックとオーバーフローチェックを有効にしておきたいです。なぜなら、1つのチェック漏れは誰かのデータになるからです。Rustはリリース時には他のすべてと同様にオーバーフローチェックをオフにしますが、Zigは安全性を切り替え可能なビルドモードにします。そのため、プロトコル、TLA+仕様、トレース検証を、実行中のRaftに対して、そしてパッケージごとに検証します。これらはすべて、あなたがどの言語で書いたかには関係ありません。メモリ安全性はテストで獲得します。すべてのスイートは、デバッグモードとリリースセーフモードで、同じテスターアロケータとシミュレータを使用して実行され、リリースファストで出荷します。
Goバージョンからずっと欲しかった、もう1つの大きな書き直しの理由がありました。すでに主要な依存関係のすべてをフォークして所有していました。もしすべての依存関係を完全に所有していれば、プロセス全体でメモリ、CPU、GPUリソースをホリスティックに制御できます。これは、TigerBeetleがTiger Styleでゼロ依存ポリシーを掲げているのと同じ議論です。Zigを使えば、ゼロ依存は実際に維持できるものであり、何かを速くする必要があるたびに移動する純粋さの目標ではありません。Cライブラリのリンクはイベントではなく、CUDA、Metal、Wasmtimeは、CGOが国境を越えるたびに請求する料金ではなく、数行で済みます。そのため、ネイティブコードは適切なツールである場合には引き続き使用しますが、実行させることはありません。外部ライブラリは私たちのエンジンにスナップインしますが、それをラップすることはできません。メモリ、スレッド、ストレージ、ネットワークに触れるすべては私たちのものです。LSM、B+ツリー、Raft、HTTP/3をゼロから書くことは、いくつかの派手な新機能のためのひどい取引ですが、歩く代わりに飛ぶことができるための素晴らしい取引です。
書き直しを難しくしたこともいくつかあります。Goバージョンはすでに非常に信頼性が高く、競合他社と比較するとGoプログラムとしては驚くほど高速でした。Zigは1.0未満であり、標準ライブラリは移動します。ツールはまだ新しく、コードを読める人も少なくなっています。また、スタートアップを立ち上げながら家族を始めることにもなりました。これは成功のための簡単なレシピではありません。
何が得られたか#
これが書き直しを行った理由ですが、何を得られたのでしょうか?まず、私が最も欲しかったものです。初日から、すべての依存関係と構築したすべてのパッケージでVOPRシミュレーションテストを組み込むことができました。世界を完全に制御できます。Antflyがノードになり、std.Ioは、システム、ネットワーク、並行性、クロックの障害をどこでもシミュレートし、シミュレーションテストを単なるテストに変えることができる完璧な環境でした。ベクトル中心のバッチ指向ワークロードに合わせてLSMを調整し、LSMのキャッシュがフルテキストおよびベクトルインデックスのキャッシュと連携するようにすることができました。
次に、ゼロから構築して、私たちのビジョンのための素晴らしい基盤を築くことができた他のすべてがあります。
データエンジン#
- ドキュメント(Mongo、Elasticsearch):スキーマのようなJSONドキュメントで、カラム形式で格納されたフィールドとプロジェクションプッシュダウン
- リレーショナル(Postgres):クローズドスキーマと型付きパック行、フルテキスト、ベクトル、スパース、グラフ、代数インデックスが同じベースストアから派生
- レイク(Iceberg、Parquet):オブジェクトストレージ上の外部テーブルをインプレースでクエリ、派生インデックスはサイドカーとしてマテリアライズ
- 外部ソース:論理レプリケーション経由でPostgreSQLからテーブルをストリーム
ストレージエンジン#
| モード | 類似 | 自動スケーリング | 耐久性/HA |
|---|---|---|---|
シングルファイル(Lite、.aflite) | SQLite | 垂直 | fsync |
| シングルノード | Postgres | 垂直 | フェンシング付きホットスタンバイ |
| 分散 | CockroachDB | 垂直+水平 | マルチRaft、オンラインシャード分割、クロスシャードトランザクション |
| サーバーレス | Neon、Lakebase | 垂直+水平 | オブジェクトストレージ |
すべての下に:独自のLSM、LMDB互換のB+ツリー、WAL、TTL、ポータブルバックアップと復元。それぞれに独自のシミュレーションハーネスがあります。
インデックスエンジン#
- フルテキスト(Lucene、Elasticsearch):Luceneスタイルのセグメント、Snowballステマー、ストップワード;ジオ、キーワード、n-gram、数値、ワイルドカード、正規表現、ファジークエリ;ハイライトと集計
- 密ベクトル(Weaviate、pgvector):SPFreshスタイルの増分投稿メンテナンスを備えたRaBitQ圧縮、他のインデックスでフィルタリング
- スパースベクトル:SPLADE学習スパース検索とBM25を併用
- 遅延相互作用:マルチベクトルドキュメント上のColBERTスタイルのMaxSim(ColQwen2)
- ハイブリッドフュージョン: reciprocal rank fusion、相対スコアフュージョン、およびこれらすべてを1つのクエリプランで結果をプルーニング
- 代数:集計とマテリアライズ(合計、最小、最大)
- グラフ(Neo4j):トラバーサル、パスファインディング、PageRankスタイルのクエリ、アドホック外部キーとビュー
エンリッチメントエンジン#
1つのメカニズム:アセットプロデューサーが生のフィールドを永続的な成果物に変換し、どのインデックスもその成果物からソースを取得できます。成果物は再処理可能で、ストリームごとに準備状況が追跡され、何もサイレントにドロップされません。
- 任意の入力形状:テキスト、JSON、PDF、画像、音声、ビデオ(URL経由または直接挿入)
- ドキュメント:生まれたままのPDF解析、テキストを持たないページ用のFlorence-2 OCR、ドキュメントレイアウト用のLayoutLMv3
- 音声:検索可能なトランスクリプトへのWhisper文字起こし、ネイティブ音声エンベディング用のCLAP
- 画像:CLIPとGemmaマルチモーダルエンベディング、テキストクエリで画像を取得し、画像クエリでテキストを取得
- チャンキング:固定および学習済みチャンカー、ドキュメントレベルおよびチャンクレベルのベクトルを1つのインデックスに
- 非構造化から構造化へ:GLiNER2エンティティ抽出、分類、要約により、フィルタリング、集計、結合可能な型付きフィールドを書き込みます
- Autograph:抽出成果物からソースされたグラフインデックス、エンティティと関係のエッジはドキュメントが到着するとマテリアライズされ、個別のグラフロードステップはありません
- Bring your own:アプリケーション管理のベクトルと事前計算された説明は、同じインデックスにプラグインします
クエリエンジン#
- HTTP JSON API:Lucene、Elasticsearch、Bleve、MongoスタイルのDSL、SSE経由のストリーミングRAG
- SQLとPostgresワイヤプロトコル:DDL、CTE、JOIN、ウィンドウ関数、
RETURNING、search_pathセッション。SQLテキストではなくネイティブ型付きプランに低下し、JSONテーブル上のドキュメントSQL方言も提供 - エージェント:検索エージェント、クエリビルダーエージェント、エージェントツール、MCPおよびA2Aインターフェース
エンベディングインターフェース#
- シングルバイナリ、自動スケーリング付きKubernetesオペレーター、またはAntfly Cloud
- 他の言語からのインプロセス使用のためのC APIと
antfly-embedded - 推論ランタイム(wasm32およびwasm64)のWASMビルド
- ユーザーコードをエンジン内で実行するためのWASM拡張ランタイム(Wasmtime)
コンピューティングバックエンド#
- CPU(SIMD)
- Metal
- CUDA(カーネルJIT付き)
モデルランタイム#
- フォーマット:GGUF、safetensors、ONNX;ネイティブ量子化、エクスポート、コンパイル済みアーティファクト
- アーキテクチャ:BERT、ModernBERT、NomicBERT、DeBERTa、T5、GPT、Qwen2/2-VL/3-VL、Gemma 3/4(マルチモーダル)、Whisper、Florence、LayoutLMv3、GLiNER
- タスク:エンベディング(テキスト、画像、音声、遅延相互作用)、チャンキング、リランキング、分類、NER、OCRとレイアウト、文字起こし、ツール呼び出し付き生成
- リモートプロバイダー:OpenAI、AWS Bedrock、Vertex、Ollama
ファインチューニング#
- 手法:LoRA、QLoRA(NF4)、再帰的LoRA、PEFT;SFT、DPO、ORPO、SimPO、GRPO、RFT;NEFTune、勾配チェックポインティング、シーケンスパッキング
- レシピ:Gemma 4、GLiNER2、ColQwen2、LayoutLMv3、リランカーヘッドとLoRA、フュージョンSPLADEチャンカー
検証#
- LMDBバックエンド、LSM、WAL、トランザクション、Raft、メタデータ、インデックスマネージャーのVOPRシミュレーションハーネス
- RaftのTLA+トレース検証
- Goバージョンと共有される言語に依存しないPythonエンドツーエンドスイート
正確性Beyond#
厳密な正確性テストに加えて、有効性、ユーザビリティ、パフォーマンステストにも力を入れてきました。モデルカードは、LLMの大きくて強力な機能を要約するための新しいトレンドであり、データベースリリースにもこのアイデアを借用しました。Antfly 0.2リリースカードは私たちのモデルカードです。Rowanは、さまざまなベンチマークとドメイン、そしてそれぞれが専門とするドメインのさまざまな競合他社に対して、Antflyのパフォーマンスを評価する多くの素晴らしい作業を行いました。RAG、OCR、メモリシステムのためのソリューションとしてAntflyを評価してきました。また、Antflyが本当に輝く場所を示す新しいベンチマークと評価を構築しています。それは、あなたが気にする機能を簡単に実装し、データベースのユースケースをスキャフォールディングしてスケールさせ、AIがこれまで以上に多くの情報とアセットを生成するようになった今、検索を改善するために必要な機能と能力を発見することです。
完璧な検索は依然として馬鹿げていて、野心的で、達成不可能です。Goの時よりもずっと近づきましたが、今回は残りの道のりを進むために基盤を書き直す必要はありません。
これらすべてを実行したい場合は、クイックスタートでAntflyをインストールし、10,000件のWikipedia記事をロードし、フルテキスト、セマンティック、ハイブリッド、画像検索とRAGを、CLI、cURL、TypeScript、Python、Goのすべてのステップで実行します。
リンク#
- クイックスタート: antfly.io/docs/guides/quickstart
- リリースカード: antfly.io/releases/v0.2
- GitHub: github.com/antflydb/antfly/tree/main/zig
- ドキュメント: docs.antfly.io
- サイト: antfly.io
- Discord: discord.gg/zrdjguy84P