
slater v0.24.4
低メモリ向けGraphDBで、Bolt+TLSサポート、保存時暗号化、ベクトルを備え、ローカルレプリカのグラフユースケース向けに設計されています。
Slater
現在のバージョン: v0.24.4 — すべてのリリース.
一言で言うと: Slater は、メモリに収まらないグラフ — 数億ノード・数十億エッジをわずか数百MBのRAMで — を標準の Bolt 上で提供し、あらゆる neo4j ドライバがそのまま動作します。グラフの隣にはディスクネイティブなベクトル検索が置かれ、しかも ライブで永続的な書き込み も犠牲にしません。常駐メモリはグラフのサイズではなく、あなたが選ぶキャッシュ予算によって決まります。
ショートカット
| Slaterが存在する理由 | 読み取りと書き込み | 得られるもの | 機能 | Dockerでの実行 |
| 動作の仕組み | 書き込み可能レイヤー | ストレージバックエンド | マウント | 設定 |
| ACL | ヘルスチェック | 動作例 | 開発 | パフォーマンス |
| ライセンス | 📖 完全マニュアル |
Slaterが存在する理由
グラフデータベース は、データを もの(ノード)と それらの間の関係(エッジ)として格納し、関係を第一級市民として扱います。質問が行ではなく接続に関する場合 — 「このアカウントから3ホップ以内にいるのは誰か?」「このビルドの背後にある完全な依存チェーンは何か?」「どのアカウントがデバイス、住所、カードを共有しているか?」 — つまりSQLでは再帰的結合の沼になりがちだが、グラフでは自然に導き出せるクエリこそ、これが求められるものです。
グラフデータベースに関する最も一般的な不満は、RAMに収まる範囲を超えてスケールしないことです。 その多く(neo4j、Memgraph、FalkorDB など)はグラフ全体をメモリに保持します。40 GBのグラフには40 GBのメモリが必要です — インスタンスごとに。リージョンごと、テナントごと、ポッドごとにレプリカが欲しいですか? 請求額はその倍になります。さらに、一定のサイズを超えるとそもそもロードできません。たとえば、9000万ノード / 15億エッジの Wikidata グラフは約64〜128 GiBの常駐メモリを必要とするため、インメモリエンジンではまったく開けません。
Slater はその反論です。グラフをメモリにロードするのではなく、オフラインで一度だけコンパイルします。slater-build はデータをコンテンツアドレス型の不変なオンディスクイメージに変換し、任意の数の Slater サーバーがそのイメージを Bolt 経由で提供します(既存の neo4j ドライバがそのまま動作します)。ブロックをオンデマンドでページングし、固定されたキャッシュ予算だけを常駐させます。これが、同じ9000万ノードのグラフを 数百MBのRAM で提供できる仕組みです — グラフのサイズとメモリコストは切り離されています。4 GBのグラフも400 GBのグラフも、提供に必要なRAMは同じ なので、安価でステートレスな読み取りレプリカを展開し、グラフをヒープではなくストアに保持させることができます。
そのため、RAGの背後にある知識グラフ、レコメンデーションやアイデンティティのグラフ、依存関係グラフなど、大規模で接続された、安価かつ頻繁にクエリしたいあらゆるものに自然に適合します。ディスクネイティブなベクトル検索はグラフのすぐ隣に置かれるため、同じエンジンが埋め込みの検索レイヤーにもなります。
ただし、「一度コンパイル」は「凍結」を意味しません。そのイメージはベースであり、最終状態ではありません。オプトインの書き込みレイヤーがその上に載るため、ライブなグラフを再構築なしで修正・拡張できます。
読み取りと書き込み
コアは不変ですが、グラフは不変ではありません。書き込み可能レイヤーをオン(delta.enabled)にすると、Bolt 経由で書き込めます — プロパティを1つ修正し、ノードを追加し、エッジを撤回する — そしてその変更はイメージの再構築なしで永続的に記録されます。読み取り側で安価に保つのは、書き込みがどこにあるかです。
書き込みは、不変コアの上にあるログ構造マージ(LSM)レイヤーに蓄積されます。書き込み先行ログとインメモリテーブルがあり、不変のデルタセグメントにスピルし、定期的な**統合(consolidation)**によって新しいコアに折り戻されます。これにより得られるものは次のとおりです:
- 書き込みのないグラフへの読み取りは、以前とまったく同じコストです。 空のデルタはマージではなく単一の予測可能な分岐であり、書き込み可能レイヤーがオンかどうかに関わらず読み取りパスはバイト単位で同一です。
- 書き込みの読み取りコストは、グラフのサイズではなくデルタのサイズに比例します。 グラフ全体の回答 —
count(*)、ラベルとリレーションシップタイプの周辺度数 — は、書き込みが未処理でもメタデータ読み取りのままです。デルタは独自のカウンターを保持するため、91.6Mノードのコアに50万件の保留中書き込みがあっても、count(*)は単一ブロックに触れることなく数十ミリ秒で応答します。 - 確認応答は永続性を意味します。 単一のライターがキューを排出し、書き込みをカバーする
fsyncの後にのみSUCCESSを返します。書き込みをグループ化すれば安価です — 書き込みUNWINDは行ごとではなくバッチごとに1回のfsyncをコミットします。 - ビジネスキー書き込み、どちらの方言でも。 ノードのIDプロパティをキーにした
MERGE/MATCH … SET/DELETE(およびCREATE/REMOVE、デタッチ削除、リレーションシップ書き込み)— または同等のISO GQLデータ変更ステートメント(INSERT/SET/REMOVE/DELETE)。これらは同じ書き込みパスに変換されます。ノードとエッジに対して、データがすでに持つアドレス指定方法で、修正・挿入・アップサート・撤回ができます。
レイヤーをオフにすると — デフォルト — Slater は純粋な不変コアのみを提供し、書き込みを拒否します。完全なモデルについては 書き込み可能レイヤー を参照してください。
名前の由来. Slater は Archer(素晴らしい番組)に登場するCIAエージェントにちなんで名付けられました。 彼は単一の名前だけで通すことにこだわっています — 「ただ…Slater」— そして私のお気に入りの キャラクターの一人です。詳細は キャラクターwikiページ を参照してください。
得られるもの
- RAMはグラフサイズではなくキャッシュ予算で決まる — 読み取りレプリカは好きなだけ展開できます。グラフをメモリに収める必要は一切ありません。
- グラフへのドロップイン置換 — Bolt を話すため、標準の neo4j ドライバ(JS、Python、Go…)がそのまま動作します。Cypher(およびISO GQLの一部、読み取りと書き込み)なので、学ぶべき新しいことはありません。
- ライブで永続的な書き込み — 不変コア上のオプトインLSMレイヤー: ノードとエッジに対するビジネスキー
MERGE/SET/DELETE。グループコミットされfsyncで永続化され、統合によって新しいコアに折り戻されます。読み取りはそのコストを負担しません。 - ファイル交換によるデプロイ — 新しいコンテンツハッシュの generation をオフラインでビルドし、
currentポインタを原子的に切り替えると、サーバーがそれを取得します。すべてのブロックがチェックサム検証されるため、半分だけコピーされたイメージは提供されず拒否されます。 - ベクトル検索を内蔵 — ディスクネイティブな近似最近傍探索(コサイン、L2、ドットKNN)がグラフのすぐ隣にあります。これがRAGパイプラインの背後にある検索レイヤーとなる場合に備え、埋め込みはその場で書き込み可能です — ベクトルの追加や変更にオフラインでの再構築は不要です。
- 設計によるロックダウン — 読み取り権限と書き込み権限は独立しており、オプションの保存時暗号化、TLS Bolt、argon2idハッシュACL、読み取りレプリカ用の読み取り専用コンテナrootfsも備えています。マスターキーを設定すると、オンディスクイメージは暗号化されるだけでなく認証もされます。マニフェストはキー付きMACを保持するため、データディレクトリへの書き込みアクセスはあってもキーを持たない攻撃者は、サーバーが受け入れるマニフェストを偽造できません。キーがなくてもコンテンツハッシュは得られます。これは半分だけコピーされた、または破損したイメージを検出しますが、意図的な改ざんは検出できません。各設定が何をもたらすか。
機能
| 機能 | あなたにとっての意味 |
|---|---|
| 制限付きで予測可能なメモリ | 常駐メモリはあなたが設定した3つのキャッシュ予算に追従し、エントリごとのオーバーヘッドとアロケータオーバーヘッドは境界内に収まります。グラフサイズに応じて増えません。グラフ全体にプロビジョニングする代わりに、パフォーマンスとRAMのトレードオフをチューニングします。バックグラウンドパージを備えたjemallocアロケータは、大量のクエリバースト後に解放されたメモリをOSに返すため、常駐サイズはバースト後の高水位に張り付かず、アイドル時の下限に向かって戻ります。 |
| マルチテナント標準装備 | 1台のサーバーがユーザーごとの読み取り許可を持つ多数のグラフをホストします。これは、ほとんどのグラフDBが有料/エンタープライズ層に限定しているマルチデータベース分離です。 |
| 保存時および転送時の暗号化 | ブロックごとのXChaCha20-Poly1305シーリング(キーはディスクに書き込まれません)に加え、オプションのTLS(bolt+s://)。設計上GDPRに準拠します。暗号化は認証済み整合性ももたらします。ビルダーはマニフェストをキー付きMACでシールし、キーを保持するサーバーはそれを検証して、マニフェストが偽造・改変・MAC剥奪されたgenerationの提供を拒否します。キーなし(平文)イメージは、キーなしのコンテンツハッシュによってのみ保護されます。これは完全性と破損の検出であり、改ざんの検出ではありません。各設定における整合性の意味 を参照してください。 |
| 小さなインストール | distroless glibcベース(シェル/aptなし)上の小さなストリップ済みバイナリ。マルチアーキテクチャ(amd64/arm64)イメージは約22MB、サーバーのみの slater:latest-lite タグでは約12MBです。ピュアRust TLSで、OpenSSLは不要です。プルして実行するだけです。 |
| 定期的な公開に最適 | グラフをオフラインでビルドし、不変として提供し、その後ゼロダウンタイムで新バージョンに原子的に切り替えます。データウェアハウス / スケジュール更新ワークロードに理想的です。 |
| 負荷に強い | サーバーとオフラインビルダーはどちらも #![forbid(unsafe_code)] でコンパイルされます。エンジンで唯一の unsafe は、監査済みのjemallocアロケータクレートにあります。コアは不変であるため、読み取りはロックを取らず、ライターを待つこともありません。単一のライターが書き込みパスだけで変更を直列化します。GCによる一時停止もデータ競合もありません。1つの悪いクエリでサーバーをダウンさせることはできません。 |
| 既存のneo4jツールと連携 | Bolt 5.4 / 4.4 / 4.1 に対応。標準のneo4jドライバ(JS、Python、Go、Java…)、cypher-shell、グラフブラウザをそのまま使えます。 |
| 豊富なCypherクエリサーフェス | 幅広い読み取りサーフェス: MATCH/WHERE/WITH/UNION、CALL {…} サブクエリ、70以上の関数と集計、時間値・地理空間値、正規表現。 |
| ライブで永続的な書き込み | 不変コア上のオプトインの単一ライターLSMレイヤー(delta.enabled)。ノードとリレーションシップに対するビジネスキー MERGE / SET / DELETE / CREATE / REMOVE、バッチ書き込み UNWIND(バッチごとに1回の fsync)、および CALL slater.consolidate()。グループコミットされ fsync で永続化され、統合によって新しいコアに折り戻されます。デルタが空のとき、読み取りパスはバイト単位で同一です。 |
| ISO GQL、読み取りと書き込み | 同じBolt接続上でISO GQL(ISO/IEC 39075)のサブセットを話します。量化パス、パス制限子、最短パスセレクタ、ラベル/タイプのブール式、FOR、CAST、オプションの GQL/CYPHER 方言プレフィックス。さらに、書き込み可能レイヤーをオンにすると、GQLのデータ変更ステートメント(INSERT / SET / REMOVE / [DETACH] DELETE)は同じ永続的な書き込みパスに変換されます。CypherとGQL、読み取りと書き込みを1つのエンジンで。 |
| 1つのエンジンにベクトル+グラフ | 埋め込み/RAGのためのディスクネイティブなANNベクトル検索(Vamana + PQ; コサイン / L2 / ドット)に加え、グラフアルゴリズム(PageRank、BFS、媒介中心性、WCC…)。数百万のベクトルがあってもメモリは境界内に収まります。埋め込みは書き込み可能です(FreshDiskANNスタイルの書き込みラダー)。ベクトルの挿入・更新・削除が即座にKNNに反映され、再構築なしでベースに折り込まれます。 |
| ネットワークストレージでも安全 | すべてのファイルはBLAKE3でコンテンツハッシュ化され、オープン時に検証されます。破れた、または半分コピーされたイメージは提供されず拒否されます。NFS/リモートボリューム向けに設計されています(mmapの驚きはありません)。 |
| プラグ可能なストレージバックエンド | ローカルファイルシステム、S3(S3互換)バケット、または Google Cloud Storage バケットから同じgenerationフォーマットを提供します。一度公開すればステートレスレプリカに展開でき、オブジェクトストアの前段にオプションのローカルSSDキャッシュ層も置けます。ストレージバックエンド を参照してください。 |
ワークスペースは2つのバイナリで構成されます:
| バイナリ | 役割 |
|---|---|
slater | オンラインBoltサーバー(コンテナのENTRYPOINT)。読み取りを提供し、delta.enabled の場合、単一ライターの永続的な書き込みパスも提供します。 |
slater-build | オフラインコンパイラ。プリミティブCypherダンプを不変でコンテンツハッシュ化されたgenerationディレクトリに変換します。 |
Slater は バルクビルド と 提供 を分離します。slater-build はオフラインで重労働を行います —
データを取り込み、不変のgenerationにコンパイルする — そのため、コールドグラフが提供ホットパス上で組み立てられることはありません。サーバー内では、読み取り
サーフェスが広範なCypherスライスに応答します — パターンマッチング、WITH/UNION/CALL {…}
サブクエリ、70以上のスカラー・集計関数、時間値・地理空間値、グラフ
アルゴリズム(algo.*)、ディスクネイティブなベクトルKNN(db.idx.vector.queryNodes)—
一方、書き込み可能レイヤーのデルタオーバーレイはそのサーフェスの下にあり、空のときはゼロコスト
なので、読み取りは書き込み側の機構を一切引き受けません。グラフの更新方法は2つあります:
Bolt経由でライブに書き込むか(書き込み可能レイヤー を参照)、
または新しいgenerationをオフラインでビルドし、current ポインタを原子的に切り替えます。実行中のサーバーは
generationガードを介してそれを取得します(
generationガード を参照)。
ドキュメント
完全なユーザーマニュアルは docs/manual/ にあります —
すべての機能について、それが何か、なぜ存在するのか、どう使うのかを説明する機能別ガイドで、
同梱のサンプルグラフに対して実行できる動作例も含まれています。この概要以外のことはすべてそこから始めてください。
- 初めてですか? クイックスタート で5つのステップでグラフをビルドして提供できます。
- クエリを書きますか? クエリ、 関数と式、 プロシージャとアルゴリズム、 ベクトル検索、 データの書き込み。
- グラフをビルドしますか? グラフのビルド と ビルドCLIリファレンス。
- Slaterを運用しますか? デプロイ、 ストレージ、 設定リファレンス、 セキュリティ、 パフォーマンスチューニング。
Dockerでの実行
Slater は Dockerデプロイ として実行されるように設計されています — これが想定された使い方です。プリビルドのマルチアーキテクチャイメージ(linux/amd64 + linux/arm64)は Docker Hub の
hikarisystems/slater に公開されており、
毎リリース :latest および :vX.Y.Z タグが付けられます:```sh
docker pull hikarisystems/slater:latest
Dockerコマンドのみの使用・設定・運用ガイドは
[`DOCKERHUB.md`](https://github.com/hikari-systems/slater/blob/HEAD/DOCKERHUB.md) にあります(Docker Hubの概要ページにもミラーリングされています)—
**デプロイする場合はここから始めてください。** 要するに:```sh
# Build a graph generation with the offline writer:
docker run --rm -v slater-data:/data -v "$PWD/dumps:/dumps:ro" \
--entrypoint /app/slater-build hikarisystems/slater:latest \
--input /dumps/people.cypher --graph people --data-dir /data
# Serve it over Bolt on 7687 (read-only unless `delta.enabled`):
docker run -d --name slater -p 7687:7687 \
-v slater-data:/data:ro -v "$PWD/acl.json:/config/acl.json:ro" \
hikarisystems/slater:latest
代わりにイメージをローカルでビルドするには(例:開発用):```sh
Build the image (both binaries).
docker compose build
Serve (expects generations under the slater-data volume / your /data mount).
docker compose up slater
Build a generation with the offline writer (profile build):
docker compose run --rm builder
--input /dumps/people.cypher --graph people --data-dir /data
ビルダーステージは、rustls の `aws-lc-rs` バックエンド用に `cmake`、`clang`、`libclang-dev` をインストールします。`git`(ベースイメージに既に含まれています)は `hs-utils` の git+tag 依存関係に必要であり、`.cargo/config.toml` はこれを git CLI 経由で取得します。
以下のセクションでは、ディスク上の形式、設定、ACL、およびローカル(非 Docker)の実行例について説明します。
## 動作の仕組み```
slater-build slater (Bolt server)
dump.cypher ──────────▶ /data/<graph>/<uuid>/ ──────────▶ neo4j driver
(offline, atomic) MANIFEST.json, *.blk, (bolt / bolt+s)
range/*.isam, vector/*.{vamana,pq},
current → <uuid>
- generation は、1 つの不変ディレクトリです:
MANIFEST.json(シンボルテーブル、 インデックス記述子、オプションの暗号化ヘッダー), カラムナブロックファイル (node_props.blk,node_labels.blk,edge_props.blk,topology.csr.blk,vectors.f32.blk), レンジインデックス (range/<name>.isam), 閾値以上の ANN インデックス (vector/<label>.<prop>.{vamana,pq}), およびcurrentテキストポインター。 - すべてのブロックは zstd 圧縮され、BLAKE3 チェックサムが付与されます;
--encryptを指定すると、各 ブロックはさらに XChaCha20-Poly1305 (保存時 AEAD) でシールされます。 - サーバーは、マニフェストに対してすべてのファイルを再ハッシュして generation を開く際、 中途半端なコピー / 切り詰められたイメージ — データディレクトリ (リモート/ネットワークストレージの場合もある) への不完全なコピー — は、提供されず拒否されます。
- 読み取りは3 つの制限付きキャッシュプールを通過します — 展開済みブロック LRU、 ベクターインデックスプール (常駐 PQ コード + Vamana ブロック LRU)、および結果 LRU — それぞれが独自のバイト予算を持ちます。各プールは、保持しているものを重み付けし、予算内に収まるように エビクトします。これにより、RSS は予算に追従し、エントリごとの有限なオーバーヘッドと アロケータのオーバーヘッドの範囲内に収まり、グラフとともに増大することはありません。
書き込み可能レイヤー
delta.enabled を指定すると、不変 generation は、小さなログ構造化マージツリーの完全に圧縮された最下位
レベル ("core") となり、ライブ書き込みはその上に
載ります:```
write (Bolt) read (Bolt)
│ │
▼ ▼
┌──────────────┐ flush ┌──────────────┐ ┌──────────────────────┐
│ WAL + active │ ───────▶ │ L0 delta │ │ a query pins one │
│ memtable │ │ segments │ │ (core, delta) view │
└──────────────┘ └──────┬───────┘ │ and reads the merge │
(fsync = ack) │ └──────────────────────┘
consolidation │ (folds core + delta → fresh core)
▼
┌─────────────┐
│ new core │ (atomic current swap)
└─────────────┘
* **永続性の下限 — WAL。** 各ミューテーションはグラフごとに単一のライターの背後で直列化され、グラフごとの書き込み先行ログに追記され、Bolt の `SUCCESS` が返される前に `fsync` されます — つまり *確認応答された ⇒ 永続化済み* であり、破れた末尾はリプレイ時に破棄されます。バッチ書き込みの `UNWIND` はその行を追記し、バッチ全体で **1回** の `fsync` でコミットします。WAL は **ローカルディスクのみ** で(ストレージバックエンド経由にはしません)、これにより *ライター* ノードはステートフルになります。つまり `delta.walDir` に永続的なローカルボリュームが必要です。読み取りレプリカはステートレスを維持します。
* **Memtable → L0 → 統合。** 書き込みはインメモリの memtable(`delta.memtableBytes` で上限設定)に蓄積されます。いっぱいになると、不変のL0デルタセグメントにフラッシュされます。**統合** は、マージされたビューを `slater-build` を通じて再シリアライズし、`current` をアトミックに交換することで `{core + delta}` を新しいコアに畳み込みます — 公開されたどの世代と同じコンテンツハッシュガードです。手動では `CALL slater.consolidate()` で、自動ではコアサイズに対する `delta.deltaCorePercent` で(オプションでオフピークの `delta.consolidateWindow` に制限)、または `delta.deltaHardBytes` スロットルに暴走した成長を防がせます。
* **オーバーレイは読み取り面の下にあります。** エグゼキュータは `ReadView` を通じて読み取ります。これは裸のコア(デルタは常に空)か、マージされた `(core, delta)` ビューのいずれかです。エンジンはその上でモノモーフィズされるため、空のデルタは単一の予測可能なブランチにコンパイルされ、読み取り専用パスはバイト単位で同一になります。グラフ全体のカウンタ(`count(*)`、ラベル/リレーションタイプの周辺度数)は、デルタ自身のライブカウンタから提供されるため、書き込みが保留中でもメタデータ読み取りのままです。
* **クエリは安定したスナップショットを参照します。** クエリはその全寿命にわたって1つの `(core, delta)` タプルを固定します。マルチステートメントトランザクションもロールバックもありません — 書き込みは永続的な、ビジネスキーでアドレス指定された訂正であり、OLTPトランザクションではありません。
正確な書き込み文法と調整項目は、下の [設定](#environment--configuration) テーブル(`delta.*`)と [動作例](#worked-example) にあります。
### レンジインデックス(ISAM)
レンジインデックス(`range/<name>.isam`、インデックス化された `(label, property)` ごとに1つ)により、`MATCH (n:Label {prop: v})` や `WHERE n.prop <op> v` は、**ラベルをスキャンすることなく** 一致するノードIDに解決できます。これは **[ISAM](https://en.wikipedia.org/wiki/ISAM)**(Indexed Sequential Access Method)構造です — 古典的な *静的・ソート済み・ブロック構造* のインデックスであり、不変の世代にとってまさに適切な形状です。リバランスする挿入がないため、ISAMの単純さは、B木の変異メカニズムが複雑にするだけのものを得られます。
* エントリ `(value, entity_id)` は値でソートされ、他のすべてと同様の zstd圧縮された256 KiBブロックに詰め込まれます。
* 小さな **常駐トップレベル** が各ブロックの最初のキーを保持します(スパースインデックス)。ルックアップはこのインメモリのトップレベルをバイナリサーチして、キーが存在しうる *1つの* ブロックを見つけ、そのブロックを読み取り+解凍してスキャンします — したがって等価ルックアップは **1ブロック読み取り** であり、レンジスキャンはそれがまたがる連続したブロックの並びを走査します。(これが、`meshUi` インデックス付きルックアップが一桁ミリ秒である一方、インデックスなしプロパティの同じマッチがラベル全体をスキャンする理由です。)
* プランナは `NodeScan::RangeEq` / `RangeRange` を介してこれを選択します。インデックスなしの述語はラベルスイープまたはフルスキャンにフォールバックし、エグゼキュータはいずれの場合もすべての述語を再チェックします。
### ベクター検索(Vamana + PQ)— cosine、L2、ドット積、読み取り *および* 書き込み
ベクターKNN(`db.idx.vector.queryNodes`)は、**cosine、L2、またはドット積(MIPS)** インデックス上で実行されます。基本インデックスはオフラインで2つの実行パスで構築され、`--ann-threshold`(デフォルト 50 000 ベクター)によってインデックスごとに選択されます。
* **閾値未満 — ブルートフォース。** 完全な `f32` ベクターは `vectors.f32.blk` に格納されます。クエリはインデックスのグループをスキャンし、インデックスのメトリックで正確な距離を計算します。単純かつ正確で、ベクター集合が小さい場合に適しています。
* **閾値以上 — Vamana + PQ**。ベクター数に関係なく常駐メモリを制限し続ける、ディスクネイティブなANNパスです:
* **[Vamana](https://arxiv.org/pdf/2401.11324)** は DiskANN の研究系統によるグラフインデックスで、単一の近傍グラフであり、そのエッジは(`--vamana-r` の出力次数と `--vamana-alpha` の長辺係数で)剪定されます。これにより *貪欲ビームサーチ* — メドイドから開始し、クエリに向かって繰り返しホップし、幅 `vectorQuery.beamWidth` の候補リストを維持 — が数ホップでノードの真の近傍に到達します。つまり **クエリあたりのランダムブロック読み取りが少ない** ことを意味します。グラフブロック(`vector/<label>.<prop>.vamana`)はベクターキャッシュを通じてページインされ、全体を保持するわけではありません。
* **[積量子化(PQ)](https://medium.com/aiguys/product-quantization-k-nn-for-big-datasets-12431d764c4e)** は各ベクターを短いコード(`--pq-subspaces` × `--pq-bits`)に圧縮します。次元はサブスペースに分割され、それぞれが独立に k-means クラスタリングされ、ベクターは最も近いセントロイドIDのタプルとして格納されます。これらのコード(`vector/<label>.<prop>.pq`)は **常駐** させるのに十分小さいため、ビームサーチはRAMから候補をスコアリングし、選択された少数の完全なベクターのみがディスクから読み取られます。その常駐PQセットが `cache.vectorCacheBytes` プールが固定するものです。
**書き込み可能な埋め込み — ベクター書き込みラダー([FreshDiskANN](https://arxiv.org/abs/2105.09613) スタイル)。**
インデックス化された埋め込みは、第一級の書き込み可能な値です。`SET n.embedding = vecf32([…])`(および `REMOVE`)は書き込みデルタに入り、**正確なランクで即座にKNNに可視** になり、その後、セグメントフラッシュ、マージ、統合を生き延びます。クエリは最大3つのレベル — シールされた基本インデックス、シールされたセグメントごとのインデックス、インメモリの **RWインデックス**(書き込みデルタ上のライブな可変Vamana)— をマージするため、書き込みが蓄積してもレイテンシは一定に保たれ、保留中の書き込み数に応じて増加しません。削除は *穴* を残します。ノードは返されなくなりますが、バックグラウンドの **削除統合** がグラフからそれを取り除くまで航行上のウェイポイントとして残るため、削除はクエリIOのコストを止めます。また、ディスク上のグラフはノードIDではなくレイアウト位置で隣接ノードをアドレス指定するため、`CALL slater.consolidate()` は Vamana を **参照で** 運びます — ハードリンクされ、バイト単位で同一 — そして小さなID列だけを書き換え、ベクター書き込みを基本インデックスに O(N·R·L) のグラフ再構築 **なしで** 畳み込みます。測定された数値は、注意事項付きで [パフォーマンスレポート](https://github.com/hikari-systems/slater/blob/HEAD/docs/PERF-REPORT.md) にあります。
## ストレージバックエンド(ファイルシステム / S3 / GCS)
すべての世代ファイルは、`std::fs` を直接使うのではなく **`ObjectStore`** 抽象化を通じて開かれるため、*同じ* ディスク上のバイト形式(ブロック、インデックス、マニフェスト、`current` ポインタ)はどのバックエンドからも変更されずに提供されます。異なるのは *バイトがどこから来るか* だけであり、リーダーもクエリエンジンも整合性チェックも変わりません。ホットパスは位置読み取り(`read_exact_at`)であり、これはローカルファイル上の `pread` とオブジェクトストア上のHTTPバイトレンジリクエストに対応します — Slater は mmap を行わないため、明示的で境界のある読み取りモデルはどこでも同一です。
**3つのファーストクラスのバックエンド**。`dataBackend.kind` で選択します。ファイルシステムが単純なデフォルトです。**Amazon S3 と Google Cloud Storage は同等で、完全にサポートされたオブジェクトストアバックエンドです** — 公開イメージには両方がコンパイル済みで同梱されているため、それぞれ設定のみで有効になり、一度構築した世代は再ビルドなしでそれらのいずれからでも提供できます(`fs` → S3 → GCS への移行も可能)。
| `dataBackend.kind` | 位置読み取り | オープン時の整合性 | 認証情報 |
| --- | --- | --- | --- |
| `fs` *(デフォルト)* | `pread` | 各ファイルの完全なBLAKE3再ハッシュ | — |
| `s3` | HTTP `Range` GET | `HEAD` によるサーバー **SHA-256**(ない場合はBLAKE3ボディ再ハッシュ) | 設定キー、AWSチェーン、または IAM ロール |
| `gcs` | HTTP レンジ読み取り | `get_object` によるサーバー **CRC32C**(ない場合はBLAKE3ボディ再ハッシュ) | ADC / Workload Identity、または サービスアカウントJSON |
両オブジェクトストアは、**ストアがすでに計算して保持しているチェックサム** から整合性を検証し、それをオブジェクトメタデータとして取得します。`slater-build` はアップロード時にチェックサムを送信し(ストアはバイトを検証して保存します)、サーバーはオープン時にそれを読み戻してマニフェストと比較します — ファイルごとに1回のメタデータリクエストで、ボディのダウンロードはありません。これはコンテンツグレードであり、S3(SHA-256)と GCS(CRC32C)で同じ精神です。オブジェクトがサーバー保存のチェックサムを **持たない** 場合(帯域外でコピーされた、または別のデフォルトでアップロードされた)、サーバーはバイト長を信頼するのではなく、**マニフェストのBLAKE3に対してオブジェクトボディを再ハッシュ** します — 要求された整合性チェックが、暗黙のうちにサイズ比較に格下げされることは決してありません。Slaterが公開した世代は常にチェックサムを保持するため、安価なメタデータパスのままです。
この列がすべてのバックエンドでチェックするのは、ファイルが **マニフェストと一致する** ことです。マニフェスト自体を信頼できるかどうかは別の問題であり、それに答えるのはマスターキーです。キーが設定されている場合、マニフェストはキー付きMACを保持し、サーバーは任意のフィールド(これらのハッシュを含む)を信頼する前にそれを検証します。したがって、改ざんされたファイルを記述するように書き換えられたマニフェストは拒否されます。キーがない場合、比較は全体的にキーなしであり、データディレクトリに書き込める人はファイルとマニフェストを一緒に書き換えることができます。[各構成における整合性の意味](https://github.com/hikari-systems/slater/blob/HEAD/THREAT_MODEL.md#what-integrity-means-in-each-configuration) を参照してください。チェック自体は `dataBackend.verifyIntegrity: false` でオフにでき、より高速なオープンと引き換えになります。
### ファイルシステム(`fs`)
デフォルトで、`dataBackend.fs.dir` をルートとします。ほとんどのデプロイに適した選択肢です。ローカルSSD(またはNFS/EBSマウント)上の世代を読み取り専用で提供します。整合性は、オープン時にすべてのファイルを完全にBLAKE3再ハッシュします。
### Amazon S3(`s3`)
S3またはS3互換バケット(AWS、MinIO、localstack)。認証情報は **最初に** 設定(`dataBackend.s3.awsAccessKey` / `awsSecretKey`、一時的なSTS認証情報用の `awsSessionToken` も)から取得され、空の場合は標準のAWSチェーン(`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` 環境変数、共有プロファイル、またはインスタンス/IRSAロール)にフォールバックします。```sh
# serve from S3 (env-var form; see the config table for every key)
dataBackend__kind=s3
dataBackend__s3__bucket=slater
dataBackend__s3__region=eu-west-2
dataBackend__s3__awsAccessKey=… # omit to use the AWS chain / instance role
dataBackend__s3__awsSecretKey=…
# S3-compatible (e.g. MinIO): also set
dataBackend__s3__endpoint=http://minio:9000
dataBackend__s3__pathStyle=true # required by most S3-compatible servers
# publish a generation into the bucket (remote `current` pointer written last)
slater-build --input people.cypher --graph people --data-dir /data \
--publish-s3-bucket slater --publish-s3-region eu-west-2 --publish-s3-prefix prod
# MinIO: add --publish-s3-endpoint http://localhost:9000 --publish-s3-path-style
Google Cloud Storage (gcs)
JSON API 経由でアクセスする GCS バケット。認証は GCP ネイティブです。デフォルトでは
Application Default Credentials(GKE Workload Identity、GCE
メタデータサーバー、または gcloud / GOOGLE_APPLICATION_CREDENTIALS キー)を解決します。
明示的なキーを指定するには、dataBackend.gcs.credentialsPath(サービスアカウントの JSON キーファイル)またはインラインの
credentialsJson を設定します。dataBackend.gcs.endpoint は
fake-gcs-server エミュレータを指し、dataBackend.gcs.anonymous=true は
そのエミュレータのみ で未認証アクセスを有効にします — 実際の GCS に対しては決して有効にしないでください。```sh
serve from GCS (env-var form; see the config table for every key)
dataBackend__kind=gcs dataBackend__gcs__bucket=slater dataBackend__gcs__prefix=prod dataBackend__gcs__credentialsPath=/secrets/sa.json # omit for ADC / Workload Identity
```sh
# publish a generation into the bucket (remote `current` pointer written last)
slater-build --input people.cypher --graph people --data-dir /data \
--publish-gcs-bucket slater --publish-gcs-prefix prod
# explicit key: add --publish-gcs-credentials /secrets/sa.json
すべての場合において、slater-build は完成した世代をまず --data-dir(そのローカルステージング領域)に書き込み、さらにバケットへアップロードします。リモートの current ポインタは最後に書き込まれるため、サービングノードが公開途中の世代を目にすることはありません。
オブジェクトストア(S3またはGCS)を使用する場合
世代をノードのディスクではなく、永続性のある中央オブジェクトストレージに置きたい場合は、s3 または gcs を選びます。典型的には、一度公開して、同じバケットを読む多数のステートレスでディスクレスなサーバーレプリカにファンアウトする、ビルドホストとサーブホストを分離する、あるいはボリュームを管理する代わりにストアの永続性/バージョニング/ライフサイクルに頼る、といったケースです。トレードオフはレイテンシです。コールドブロックはローカル読み取り(~0.1 ms)ではなくネットワーク往復(~10–50 ms)になります。Slaterは、インメモリブロックキャッシュ、並行読み取り先行、および後述のオプションのディスクキャッシュによって、そのほとんどを隠蔽します。世代がすでに高速なローカルストレージにあり、中央バケットモデルが不要な場合は、fs のほうがシンプルで高速です。
ローカルディスクブロックキャッシュ(オブジェクトストアの第2層)
インメモリの BlockCache は意図的に小さく設計されています(RSSが制限されることが主要な保証です)。そのため、RAMより大きいワーキングセットでは、同じブロックがスピルするたびにオブジェクトストアから再取得されることになります。オプションのローカルSSD第2キャッシュ層はこれを解決します。RAMから退避したブロックは、新しいオブジェクトGETではなくローカルディスク(~0.1 ms)から提供され、インメモリ退避を生き延び、オブジェクトストアへのリクエスト数/コストを削減します。これにより、オブジェクトストアをバックエンドに使うノードも、ウォーム化後はローカルファイルシステムに近い性能になります。これは s3 と gcs の両方でオプトインであり、dataBackend.<s3|gcs>.diskCacheBytes > 0 と書き込み可能な diskCacheDir を設定することで有効になります。
- 取得したそのままのシール済みバイトをキャッシュします。つまり、すでに圧縮済みで、(
--encrypt世代の場合は)AEADシールのまま、復号/展開より下層にあります。キャッシュ層は暗号化キーを保持せず、再暗号化もしないため、保存時の状態はそのまま維持されます。暗号化された世代はシールされたままディスクに置かれます。 - 書き込みはライトビハインドです。ミス時は取得したバイトをクエリに即座に返し、その後バックグラウンドスレッドがディスク書き込みとLRUトリムを行います。したがって、クエリパスはディスクI/Oでブロックされません。退避によりキャッシュはバイト予算内に維持されます。各ファイルのチェックサムが読み取りごとに検証され、破損したキャッシュファイルはミスとして自己修復されます(→オブジェクトストアから再取得)。
diskCacheDirは実際に書き込み可能なボリュームを指す必要があり、tmpfsは不可です(tmpfsはRAMであり、RSS制限の保証を無効化します)。これを追跡するインメモリインデックスは少量のRAM(キャッシュブロックあたり数十バイト)を消費し、RSS上限にカウントされるため、ディレクトリはインメモリブロックキャッシュより≫大きくしてください。- この層のもう1つのRAMコストはライトビハインドキューで、ディスクへの書き込み待ちのブロックをステージングします。これは
blockCacheBytes / 8(diskCacheBytesで下限が設定される)に制限されており、デフォルトでは8 MiBで、増加するのではなく切り捨てられるため、コールドスキャンで膨張することはありません。切り捨てられたブロックは、次のミス時に単に再取得されます。設定は不要です。blockCacheBytesに合わせてスケールするため、ディスク層はRSS予算にインデックス以外の新しい数字を追加しません。
マウント
読み取りレプリカは読み取り専用のルートファイルシステムと非rootユーザー(appuser:1000)で実行されます。必要なものはすべて読み取り専用でマウントされます。ライター(delta.enabled)はさらに、WAL用に永続的で書き込み可能なボリュームを1つ必要とします。
| Path | Purpose | Notes |
|---|---|---|
/data | グラフ世代(<graph>/<uuid>/… + current)。 | レプリカでは読み取り専用。slater-build によって生成される。リモート/ネットワークストレージ(例:NFS)上にある場合があるため、読み取りが高速なローカルSSDレイテンシであるとは想定されない。 |
/sandbox | 環境ごとの設定オーバーレイ+シークレット。 | /sandbox/config.json は組み込みの config.json にディープマージされる。acl.json、TLS PEMマテリアル、保存時暗号化キーファイルも保持する。 |
/tmp, /run | スクラッチ(tmpfs)。 | 読み取りレプリカはデフォルトではディスクに書き込まない。 |
(ライター) delta.walDir | delta.enabled の場合のライトアヘッドログ+L0デルタセグメント。 | 書き込み可能であり、永続的な実際のボリュームであり、tmpfs は不可(永続性の下限です)。相対パスはデータディレクトリの下で解決されます。ライターにはここに独自の永続ボリュームを割り当ててください。 |
| (オプション) disk cache | dataBackend.s3.diskCacheBytes / dataBackend.gcs.diskCacheBytes > 0 の場合のローカルディスクブロックキャッシュ。 | 書き込み可能であり、実際のボリュームであり、tmpfs ではない。s3 および gcs バックエンドで使用。ストレージバックエンド を参照。 |
環境・設定
設定はハウススタンダードの階層ローダーで読み込まれます。組み込みの config.json、その上に /sandbox/config.json がディープマージされ、その上に KEY__sub 環境変数オーバーライド(ネストには二重アンダースコアを使用。キーはcamelCase設定と一致)が適用されます。
すべての設定項目(camelCaseキー、KEY__sub 環境変数オーバーライド、デフォルト値、および機能)は、設定リファレンス に表形式でまとめられています。最も調整されるノブは、キャッシュ予算(cache.*)、クエリガード(query.*)、接続上限(server.*)、ストレージバックエンド(dataBackend.*)、および書き込み層(delta.*)です。
常駐メモリは blockCacheBytes + vectorCacheBytes + resultCacheBytes をエントリごとのオーバーヘッドとアロケータオーバーヘッドの範囲内で追跡します。各プールは自身の内容(文字列とコンテナは割り当て容量で計量)を重み付けし、予算内に収まるよう退避させますが、エントリごとのブックキーピングとアロケータのサイズクラス丸めは、設定した数値の上に乗ります。さらに、小さな固定オーバーヘッド(および lazy 次数列については、次数合計 count(endpoint) の高速パスが実行されると、最大 degreeColumnBytes)が加わります。これはグラフサイズに依存しません。これが主要な保証であり、rss_stays_bounded_under_sustained_knn_load 統合テストによって検証されています。このテストは、ピーク時とウォーム時のRSS増加を合計予算の範囲内に十分収めます。接続ごとのバッファはキャッシュ予算の外側に存在するため、この保証は server.maxConnections が同時接続数を制限しているからこそ、敵対的負荷の下でも成立します。
ネットワーク防御体制
Slaterは読み取りレプリカのハンドルです。主要な接続セキュリティ制御はネットワークであり、バイナリではありません。プライベートインターフェースにバインドし、ネットワーク層(セキュリティグループ / NetworkPolicy)で送信元レンジを制限し、信頼できるクライアント以外に公開する場合は、接続を制限するL4プロキシ(HAProxy maxconn +送信元ごとの stick-table、またはnftables connlimit + hashlimit)を前面に配置してください。これはファイルディスクリプタがプロセスに渡される前に存在するため、最も堅牢な制限です。
上記のバイナリ内制限(maxConnections、maxPreAuthConnections、maxConnectionsPerIp、差分バイト上限、および loginTimeoutMs)は多層防御です。デフォルトで有効かつ寛大に設定されているため、正当なクライアント集団には見えませんが、プロキシを忘れた場合でもRSS制限の保証を維持します。完全な防御体制については docs/HARDENING.md を、正規の詳細については THREAT_MODEL.md / SECURITY_WORKLIST.md を参照してください。
世代ガード
Slaterは各グラフの current ポインタを generationPollMs ごとにポーリングします(inotifyではなくポーリング。データディレクトリはNFSのようなリモート/ネットワークストレージ上にある可能性があり、ファイルシステム変更イベントは信頼できないため)。ポインタが変更された場合:
reloadStrategy=exit(デフォルト):サーバーはfatalをログに記録し、非ゼロで終了します。これにより、オーケストレータが新しい世代に対してクリーンに再起動します。reloadStrategy=swap:サーバーは新しい世代を開いて検証し(起動時と同じコンテンツハッシュガード)、アトミックにスワップインし、実行中のクエリは古い世代で完了させます。破損/不完全な新しいイメージは拒否され、古い世代がサービスを継続します。
ACL
acl.json はユーザーをargon2idパスワードハッシュとグラフごとの read / write 権限にマッピングします。ハッシュを生成してください(平文は絶対に保存しないこと)。次のようにして:```sh
slater hash-password 's3cret' # prints a $argon2id$… string for acl.json
スターター用の `acl.json` がリポジトリのルートに同梱されており、その構造は次のとおりです:```json
{
"users": {
"reporting": {
"passwordArgon2id": "$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>",
"grants": {
"people": ["read"],
"products": ["read", "write"]
}
}
}
}
-
users— ログインごとに1エントリで、ユーザー名をキーとします。 -
passwordArgon2id—slater hash-passwordの$argon2id$…文字列 (平文は不可。ファイル自体はプレーンなJSONで、共有ストレージ上に置かれます)。 -
grants— グラフごとの権限リスト。意味を持つ権限は2つです:read— グラフを照会します。ユーザーのgrantsに存在しないグラフは、そのユーザーからは見えません。write— 書き込み可能レイヤー(delta.enabled)を通じてグラフを変更します:MERGE/SET/DELETEステートメントとCALL slater.consolidate()。
これらは独立しています。
read権限が書き込みアクセスを付与することはありません。 したがって、書き込み可能 レイヤーを有効にしても、既存の読み取り専用ユーザーを書き込みユーザーに昇格させることはできません。書き込みには 両方 —["read", "write"]— が必要です。ビジネスキーを解決して書き込むことは読み取りに相当するためです。 認識されない権限文字列は無視されます(何も付与しません)。
aclPath で指定されたパス(デフォルト /config/acl.json)に、読み取り専用でマウントします。
サーバーは世代ホットスワップのたびにこれを再読み込みし、保存時ACLスタンプは
再読み込みのたびに再チェックされます(requireAclStamp を参照)。
ヘルスチェック
slater バイナリは自身の liveness probe を兼ねています:slater healthcheck [host] [port] はサーバーに対して Bolt ハンドシェイク(HTTPリクエストではない)を行い、
プロトコルバージョンのネゴシエーションに成功すると 0 で終了し、それ以外は 1 で終了します。デフォルトは
localhost と設定済みの Bolt ポートです。これがコンテナの
HEALTHCHECK が実行する処理であり、オーケストレーターは単なる
オープンソケットではなく、真に Bolt 対応のサーバーを確認できます:```sh
slater healthcheck localhost 7687 # exit 0 = healthy
docker exec slater /app/slater healthcheck # inside the container
## ワンショットクエリ
スクリプト、CIチェック、およびクイックルックアップのために、`slater query` はグラフの現在の世代をマウントし、プロセス内で単一の読み取り専用Cypherクエリを実行し、結果をJSONオブジェクトとして出力して終了します — サーバーもBolt接続も不要です。これはサーバーと同じ設定(ストレージバックエンド、暗号化キー、クエリ予算)を尊重します:```sh
# GRAPH defaults to `defaultGraph`. Without -q, normal datestamped logging
# (config, "opened generation", …) is written to stdout alongside the result.
slater query mygraph 'MATCH (n) RETURN count(n) AS c'
# -q/--quiet ⇒ logging suppressed, so stdout is *only* the compact result JSON
slater query mygraph -q 'MATCH (c:Company) RETURN c.ticker AS t LIMIT 3' | jq
# {"columns":["t"],"rows":[["AUPH"],["KYMR"],["MREO"]]}
ノードとリレーションシップは、そのラベル/タイプとプロパティに展開されます。機械が解析できる出力が必要な場合は -q を使用してください(結果のJSONのみがstdoutに出力されます)。オペレーター向けのログ付き実行ではこれを省略します。-q を付けない場合、各実行後にメトリクスのみのサマリーがログ出力されます — 例:```text
INFO query executed cost=2389 resultCount=10 execMs=441 limitRowCount=10
carrying the query `cost` (elements charged), `resultCount`, `execMs`, and
`limitRowCount` (only when the query specifies a `LIMIT`) — never the query text
or any result value. Exit status is `0` on success, `1` on a parse/open/execute
error (message on stderr).
## Export a graph (`slater dump`)
`slater dump` exports a graph from a **running** server as business-key `MERGE`
Cypher — the same dialect `slater-build` ingests — so a graph round-trips
(dump → `slater-build` → new generation) for migration or text backup. Unlike
`slater query`, it connects over **Bolt**, authenticates, and honours per-graph
ACLs, so it needs no disk access to the server. The password is read from
`SLATER_DUMP_PASSWORD` or stdin (never a flag, keeping it out of `ps`/history).```sh
# List the graphs the authenticated user may read.
SLATER_DUMP_PASSWORD=pw slater dump --list -u reporting
# Dump a graph to a file (identity keys inferred from range indexes).
SLATER_DUMP_PASSWORD=pw slater dump people -u reporting -o people.cypher
# Rebuild it into a fresh generation.
slater-build --input people.cypher --graph people --data-dir ./data
各ラベルのアイデンティティキーは、そのレンジインデックスが保持するプロパティです。上書きするには --key Label=prop(繰り返し指定可)またはグローバルな --pk <field> を使用します。再構築でインデックスが再作成されるよう、CREATE INDEX DDL が最初に出力されます。マルチラベルノードはすべてのラベルを保持し、MERGE (n:Ident:Other {key: v}) として出力されます。このときアイデンティティラベル(ビジネスキーを供給するラベル)が最初に来て、残りはソートされます。マージはアイデンティティラベルのみをキーとするため、後続のラベルは新しいノードを作成せずにそのノードに書き込まれます。特殊文字を含むラベル、リレーションシップタイプ、プロパティキーは、出力時にバックティックで引用されるため、通常とは異なる名前も忠実に往復でき、再構築に Cypher を注入することはできません。ベクター(および Cypher リテラル表記を持たないその他の値)は MERGE ダンプに載せることができないため、stderr に警告を出して破棄されます。終了ステータスは、成功時は 0、エラー時は 1 です。
動作例
グラフを構築し、それを提供し、neo4j の JavaScript ドライバと Python ドライバで接続してデータを書き込むまでの、完全で実行可能なウォークスルーは、マニュアルの クイックスタート と データの書き込み のページにあります。docs/manual/examples/ にある同梱のサンプルグラフを使用します。
開発```sh
export PATH="$HOME/.cargo/bin:$PATH" cargo build cargo test # unit + the bounded-RSS headline integration test cargo clippy --all-targets -- -D warnings cargo fmt --all -- --check
### オブジェクトストアのバックエンドはオプトインのcargo機能です
通常の`cargo build`は**ファイルシステムのみ**のバイナリを生成します — `s3`および`gcs`
バックエンドはcargo機能の背後にゲートされているため、デフォルトのビルドは小さく保たれます(AWS
またはGoogle SDK、非同期ランタイムなし)。必要な方を使用可能にしてください。**両方**の`slater`
(serve)と`slater-build`(publish)で:```sh
# S3 only / GCS only / both
cargo build -p slater -p slater-build --features s3
cargo build -p slater -p slater-build --features gcs
cargo build -p slater -p slater-build --features s3,gcs
各クレートは、graph-format/{s3,gcs} に転送する対応する s3 / gcs フィーチャーを公開しています。実行時にバックエンドを要求する(dataBackend.kind=s3|gcs、または slater-build --publish-{s3,gcs}-*)際、そのフィーチャーがコンパイルされていないと、"built without the … feature" エラーで即座に失敗します。公開されている Docker イメージは両方を有効化しています(Dockerfile の CARGO_FEATURES)。したがって、プリビルドイメージには追加フラグは不要です。これはソースからビルドする場合にのみ関係します。統合テストも同様にゲートされています: --features s3 --test s3_minio、--features gcs --test gcs_emulator(fake-gcs-server)、--features gcs --test gcs_real(ADC 経由の実 GCS)。それぞれ、SLATER_* 環境変数が設定されない限りスキップされます。
設計、マイルストーン台帳、および決定ログについては、docs/PLAN.md、docs/PROGRESS.md、docs/DECISIONS.md を参照してください。
パフォーマンス
最大6つのエンジン、単一クライアントのスイート、62kノードのおもちゃから Wikidata 91.6Mノード / 1.5Bエッジ までのグラフ。各エンジンは単独で測定されています(他のすべてのコンテナを停止 — RSS とレイテンシはそのエンジン自身のフットプリントです)。以下のレイテンシテーブルは Slater 0.21.0(書き込み可能ビルド)で再測定されました。中小規模グラフ(MeSH、EU-AI-Act)は新規に、91.6Mグラフは新しい 同一マシン・共有アンカー の slater 対 Neo4j のパスとして(そのテーブルを参照)。常駐メモリの数値は以前のパスから引き継がれています(コンテナの cgroup で測定。読み取りパスは書き込み可能レイヤーがアイドル状態のバイト同一のものです)。他のエンジンの数値は確立されたクロスエンジン実行のものです(バージョン/パフォーマンスは不変)。すべての数値は中央値(ms)またはピーク常駐メモリ(MiB)です。どこでも低いほど良く、太字 = 行内で最良。 slater はローカルファイルシステム(fs)バックエンドで実行されました。S3 および GCS バックエンドは、ローカル読み取りレイテンシをオブジェクトストアのラウンドトリップと引き換えにします(インメモリキャッシュとオプションのローカルディスクキャッシュ層で緩和)。したがって、これらの数値はエンジン自体を特徴付けるものであり、ネットワークストレージのデプロイではありません。
| エンジン | クラス | メモリ制約 |
|---|---|---|
| slater | ディスクバックアップ、ページング | query.maxIntermediate がワーキングセットを自動的に上限 |
| Neo4j 5 | ディスクバックアップ、JVM | ~2 GiB ヒープ + オフヒープ。クエリに関係なくコミットされる |
| Memgraph · FalkorDB | インメモリ | グラフ全体が RAM に常駐 |
| ArcadeDB | インメモリ、JVM | グラフ全体が常駐。最も重い |
| LadybugDB | 組み込み、カラム型 | クエリを超える必要がある手動バッファプール |
ディスクからページングする 3つのエンジン(slater、Neo4j 5、LadybugDB)は、5つのグラフすべてをロードします。インメモリの3つ組(Memgraph · FalkorDB · ArcadeDB)は、1.5Bエッジのグラフをまったく保持できません(約64〜128 GiBの常駐が必要)。ArcadeDB のインポーターもそれを完了できません。
常駐メモリ(MiB)— グラフが約1,500倍に成長しても上限を維持
各数値はコミットされたワーキングメモリです — OSが再利用できないものです。slater 以外の すべてのエンジンは、グラフをコミットされた匿名メモリ(独自ヒープ、Neo4j のオフヒープページキャッシュ、またはバッファプール)に保持するため、そのピーク RSS は コミットされたフットプリントです。slater だけは、ディスク上のストアの再利用可能な OS ページキャッシュからサービスを提供するため、その数値は匿名ワーキングセットです。ストアのページキャッシュ(プレッシャー下で追い出し可能 — slater はサービスを継続)は除外され、91.6Mグラフについては括弧内に 合計 として示されます。太字 = 最低。
| グラフ (ノード / エッジ) | slater | Neo4j 5 | Memgraph | FalkorDB | ArcadeDB | LadybugDB |
|---|---|---|---|---|---|---|
| pole — 62k / 106k | 11 | 746 | 114 | 140 | 1,556 | 198 |
| MeSH — 341k / 469k | 63 | 1,083 | 358 | 455 | 1,631 | 121 |
| EU-AI-Act — 21k / 45k (+55 MiB vec) | 99 | 729 | 229 | 312 | 1,948 | 286 |
| Wikidata — 91.6M / 1.5B | 584 (4,595 total) | ~2,900 | ロード不可 | ロード不可 | ロード不可 | ~652 † |
slater はあらゆるスケールで最低であり、グラフが約1,500倍に成長する間も約50倍の増加に留まります。そのフットプリントはグラフではなく クエリのワーキングセット に追従します(全体を通してアイドル時約16〜71 MiB)。インメモリの3つ組はほぼ線形に増加し、1.5Bグラフをロードできません。Neo4j はクエリに関係なく約2 GiBのヒープをコミットします。(† LadybugDB は境界のあるシェイプのみ。1.5Bエッジでの hub / var-length / shortestPath トラバーサルは、slater の自動 maxIntermediate 上限に対し、読み取りプールを ≥2 GiB に引き上げる必要があります。)ビルド時の value→count ヒストグラムは、無視できるほどの常駐メモリしか追加しません。低カーディナリティのインデックス付きカラムでは数 KB、Wikidata のようなユニークキーグラフでは ゼロ(wikidata_id がヒストグラムのカーディナリティ上限を超えるため、何も保存されない)です。したがって、これらの数値はそのフィーチャーによって変わりません。
レイテンシ(中央値 ms)— グラフが RAM に収まる場合(MeSH、341k / 469k)
| クエリ形状 | slater | Neo4j 5 | Memgraph | FalkorDB | ArcadeDB | LadybugDB |
|---|---|---|---|---|---|---|
| count(*) 全ノード | 0.41 | 15.0 | 23.8 | 16.4 | 82.0 | 2.2 |
| ラベルカウント | 0.42 | 4.2 | 20.7 | 1.1 | 4.4 | 4.3 |
| インデックス付きポイントルックアップ | 0.43 | 3.9 | 0.48 | 0.48 | 0.65 | 8.8 |
| idx-eq カウント | 0.42 | 4.9 | 5.0 | 2.0 | 381 | 2.5 |
| 1ホップ(インデックス付きアンカー) | 1.28 | 5.8 | 1.21 | 4.1 | 390 | 4.9 |
| 2ホップ(アンカーなし) | 1.40 | 5.6 | 8.5 | 16.7 | 444 | 6.4 |
| group-by / count(DISTINCT) | 0.45 | 47–51 | 63–64 | 31–39 | 411 | 5.3 |
フルスキャン CONTAINS | 0.43 | 5.4 | 24.1 | 1.7 | 16.3 | 4.1 |
slater はメタデータ / インデックス / スキャンの各クエリ形状(count、label、idx-eq、scan — 約0.4 ms、サービスエンジンの10〜200倍)、インデックス付きポイントルックアップ(0.43 ms、インメモリの2機種の0.48 msに肉薄)、アンカーなしマルチホップ(リレーションシップタイプスキャンによる2ホップ1.40 ms、この分野で最速)、そしてビルド時の value→count ヒストグラムをインデックス付きグループ化キーに使ったラベル全体の group-by / count(DISTINCT)(0.45 ms、LadybugDB のカラム型5.3 msを上回る)で最速です。インメモリサーバーは素の1ホップでのみ最速を維持しています(Memgraph 1.21 ms 対 slater 1.28 ms)。(pole 62k/106k も同様: slater は count/scan で約0.4 ms と単独最速、ホップでは約1.3〜2.6 ms。)
レイテンシ(中央値 ms)— ベクトル(EU-AI-Act kNN、15k × 1024次元)
| クエリ形状 | slater | Neo4j 5 | Memgraph | FalkorDB | LadybugDB |
|---|---|---|---|---|---|
| kNN トップ10 Concept | 2.9 | 8.6 | 1.9 | 1.2 | 2.8 |
| kNN トップ10 Chunk | 2.4 | 5.7 | 1.9 | 1.5 | 3.2 |
slater は kNN を厳密な総当たり(ブルートフォース)スキャンで処理します(これらのセットは 50kベクトルの ANN 閾値を下回ります)。他のエンジンは近似の常駐 HNSW を使用します。そのため slater の結果は厳密です(再現率 1.0)。SIMD 距離カーネルと常駐の事前正規化ベクトル行列により、Concept は約23 → 約2.9 ms、Chunk は約10 → 約2.4 ms に短縮されました。これにより slater は Neo4j と LadybugDB を上回り、Memgraph の約1.4倍以内に迫り、FalkorDB にのみ遅れを取ります — しかも厳密なままです。
ベクトル書き込みラダー — リビルドなしの挿入 / 更新 / 削除
上記のテーブルはクロスエンジンの 読み取り 比較です。ベクトルの 書き込み パス(静的 Vamana ベース上の FreshDiskANN スタイルの書き込みラダー)には、クロスエンジンの対応物がありません — ここにある他のエンジンはディスクネイティブで 書き込み可能な ANN を備えていないためです。したがって、以下の数値は、合成の埋め込み風フィクスチャ(低ランク多様体、次元768、不等ノルム)に対する単一エンジンの コンポーネント ベンチマークであり、crates/slater/benches/ にコミットされ、docs/PERF-REPORT.md に方法論とすべての注意事項を含めて完全に記載されています。再現率は常に実データセットに対する厳密な総当たりに対して測定され、インデックス同士の比較ではありません。ここでのスケールは代表的なものであり、メトリックがサイズ線形である場合にのみ外挿されます。
| プロパティ | 測定値 | 重要な理由 |
|---|---|---|
| KNNレイテンシと保留中の書き込みの比較 | RWインデックスは保留50kまで 約1.5〜2 msでフラット。プリインデックスの総当たりオーバーレイは 1.9 → 115 ms(デルタに線形) — 50kで61倍 | クエリレイテンシは、統合の間に書き込みが蓄積しても劣化しない |
| 埋め込み挿入 | ライブインデックスへのベクトルあたり 約1.5〜2 ms | 書き込みは即座に KNN で可視化される。デルタリビルドの予算は ≈ 2 ms × デルタ上限 |
| 等再現率での削除 IO | 67 %削除時にクエリあたりのノードフェッチが 2.9倍少ない、80 %で 5.2倍(再現率 ≥ 0.90) | 統合されたグラフは、削除されたベクトルに対して読み取りコストを支払わない |
| 統合、純粋な置換 | O(1) — .vamana はハードリンクされバイト単位で同一、id カラムのみが書き換えられる | ベクトル書き込みをベースに折り込むことで O(N·R·L) のリビルドをスキップする |
| ラダー全体の再現率 | cosine、L2、dot のいずれでも統合後 ≥ ベース | 書き込みラダーはすべての段階で再現率を維持する |
専用の性能説明が必要な唯一の数値は、スローパス の統合書き換えスループットです。統合が純粋な置換ではなく削除や新しいベクトルを伴う場合、シングルスレッド zstd とローカルディスクに制約されるシーケンシャル再圧縮となるため、絶対的な MiB/s は環境依存です(レポートはその形状を示し、環境による範囲を説明しています)。
レイテンシ(中央値 ms)— グラフ ≫ RAM(Wikidata 91.6M / 1.5B)
インメモリエンジン(Memgraph / FalkorDB / ArcadeDB)はこのグラフをまったくロードできません(約64〜128 GiB常駐)。ロードできるのは slater と Neo4j 5 だけです。これは 同じマシン・同じ日の新しいパスで、共有された固定アンカーセットに対するものです。すべてのクエリが両エンジンで 同一の ノードにヒットするため、直接比較は同等条件です(中程度の次数のアンカーからなる共通の wikidata_id プール。その重要性については下の注を参照)。slater は両方のファンアウトで示されています(query.maxFanout 1 = スループットデフォルト、8 = コールドブロック読み取りを重ねるレイテンシダイヤル)。太字 = 行内で最良。
| クエリ形状 | slater (fan 1) | slater (fan 8) | Neo4j 5 |
|---|---|---|---|
| count(*) 全ノード | 0.41 | 0.41 | 3606 |
| ポイントルックアップ(インデックス付き) | 0.72 | 0.49 | 6.3 |
| 次数(1ホップカウント) | 0.43 | 0.44 | 6.0 |
| 1ホップ近傍 | 9.8 | 4.5 | 10.1 |
| 2ホップ | 37 | 23 | 34.5 |
| 3ホップ | 32 | 25 | 74 |
可変長 *1..2 distinct | 985 | 1056 | 47 |
正直なところ: slater はメタデータ / インデックス系のクエリ形状で支配的です — count(*) はメタデータで処理され(0.41 ms 対 Neo4j の3.6秒のディスクスキャン、約8800倍)、ポイントルックアップ / 次数 / 3ホップは約2〜10倍高速です。1〜2ホップでは Neo4j と互角(fanout 8 はコールド読み取りで先行)ですが、var-length *1..2 distinct では明確に敗北(約1秒 対 Neo4j の47 ms): slater の可変長 distinct 展開はここでは著しく遅く、独自の調査に値する実際の弱点です。そのすべてが、Neo4j のコミット済み約2 GiBヒープに対して、数百MBの RSS で実現されています。
アンカーについて。 これらのトラバーサル数値は、どの ノードから開始するかに大きく依存します。Wikidata のメガハブ("human"、"country")から1リンク離れたノードは、何百万もの規模の2ホップ近傍を持つため、可変長/ホップのコストはアンカーの選択によって桁違いに変動します。このテーブルの以前の版では、各エンジン自身の「スキャンによる最初のN件」をサンプリングしていましたが、これは安定しておらず比較もできません。今回のパスでは、両エンジンに共通の、次数に制約のある単一のアンカーセットを固定しています。(shortestPath は今回のパスでは省略されています — 任意の2つのアンカー間では経路の有無に依存し、分散が大きすぎて中央値を有意に取れないため。)
マルチホップ count(*) — メモリが結果サイズから切り離される
上限なしのマルチホップ RETURN count(*) は、一致した行を具体化する代わりに、展開 中に カウントします。91.6Mグラフ上の同じハブアンカー、maxIntermediate=20M:
| 3ホップ count(*) @ 91.6M | fanout=1 | fanout=8 |
|---|---|---|
| レイテンシ / ピークワーキングセット | 554 ms / 0.66 GiB | 298 ms / 1.9 GiB |
カウントは O(1) 行を保持します。チャージは変わらないため、メガハブのカウントは依然として 計算(隣接読み取り)の面で maxIntermediate に抵触し、以前と同じように制限されます。
クエリごとの並列性(maxFanout)
query.maxFanout を上げると、クエリのコールドでI/Oバウンドなブロック読み取りがコア間で重なります。大きなコールドワーキングセットを持つディスクバウンドなクエリ形状に効果があり、ウォームな形状ではフラットです。1.5Bグラフでは: shortestPath ≤6 は 918 → 608 ms(1.5倍、最大探索 6,269 → 2,350 ms、2.7倍); 3ホップ count は 547 → 298 ms。maxFanout=1 がデフォルト(スループット指向)で、8 はレイテンシダイヤルであり、より多くの一時的なワーカーメモリを消費します。
slater が勝つ点 / 劣る点
| 項目 | slater | 他エンジンの最良 | 判定 |
|---|---|---|---|
| 常駐メモリ(任意のスケール) | 11–584 MiB (62k → 91.6M) | インメモリ 1.5–2.7 GiB; 1.5B はロード不可 | slater |
| count / メタデータ / スキャン | 約0.4 ms | サービスエンジン 5–80 ms | slater (10–200×) |
| インデックス付きポイントルックアップ | 0.43 ms (MeSH) | Memgraph · FalkorDB 0.48 ms | slater(インメモリの2機種に肉薄) |
| アンカーなしマルチホップ(行) | 1.40 ms (MeSH 2ホップ) | Neo4j 5.6 ms | slater(リレーションシップタイプスキャン) |
| 集計(group-by / DISTINCT) | 0.45 ms | LadybugDB 5 ms(カラム型) | slater(ビルド時ヒストグラム) |
| kNN | 2.4–2.9 ms(厳密) | FalkorDB 1.2 ms(HNSW) | Neo4j/Ladybug を上回る; Memgraph の約1.4倍差; 厳密 |
| 91.6M メタデータ / ポイント / 次数 / 3ホップ | 0.4–32 ms | Neo4j 6–3,600 ms | slater (2–8800×) |
| 91.6M 1〜2ホップ | 4.5–23 ms (fan 8) | Neo4j 10–35 ms | ほぼ互角 |
91.6M 可変長 *1..2 distinct | 約1 s | Neo4j 47 ms | Neo4j(slater の実際の弱点) |
スケールでのマルチホップ count(*) | 0.3–0.6 GiB | インメモリエンジンは行セットを具体化する | slater(上限あり) |
エンジンごとの完全なテーブル(pole、MeSH、EU-AI-Act + blockCacheBytes の RAM↔レイテンシダイヤル、Wikidata 1M & 91.6M)は perf/cross-engine-hs/README.md にあります。新しい slater のみのパス(両ファンアウト、全データセット)は perf/PERF_CURRENT_STATUS.md にあります。
並行性とブラウンアウト(負荷テスト)
上記のベンチマークは単一クライアントです。補完的な軸 — 多くの同時クライアント下での挙動 — には専用のハーネス perf/loadtest/ があります。Bolt 上の Locust ドライバーと、負荷を段階的に増加させ、CALL slater.diagnostics() を読み取り、容量の変曲点を見つけ、制限要因を特定するコーディネーターで構成されます(完全な方法は docs/LOAD-TESTING.md にあります)。Wikidata-1M グラフ(16コアの1台)での 256 MiB キャッシュ実行のハイライト:
| 結果 | 測定値 |
|---|---|
| 1000の同時クライアント、ゼロ障害を維持 | スループットは約2.5k rps でピークに; レイテンシの変曲点は約750クライアントあたりで発生(p99 51 → 750 ms)— コア競合下でのキューイングであり、ハードキャップではない(単回実行、WSL2) |
| ブロックキャッシュは上限付きで効果的 | 100%ヒット率、0エビクション、キャッシュに収まるワーキングセットで50 MB常駐 |
| 持続負荷下で RSS を維持 | jemalloc アロケーターは、100→500クライアントの wiki_cache_churn ランプ全体で RSS を約0.6 GBに維持 — キャッシュに制約され安定しており、MALLOC_* のチューニングは一切不要(以前の MALLOC_ARENA_MAX=2 + trim しきい値は廃止); そのバックグラウンドパージは、バースト後の高水準を固定したままにするのではなく、解放します |
| 総メモリは上限付き | サーバー全体の query.maxIntermediateGlobal + 隣接チャージ型の展開が、wiki_budget の2ホップフラッドを1000クライアントで OOM なしに抑えます(RSS 約0.6 GB; ガードはハブクエリの約60%をリトライ可能なバジェットエラーとして切り捨てます) |
負荷テストで表面化した両方のメモリ問題は現在クローズされています。すべて負荷テストのドキュメントで追跡されています。
ライセンス
Apache License、Version 2.0 の下でライセンスされています。全文は LICENSE を、帰属表示は NOTICE を参照してください。明示的に別段の表明がない限り、Apache 2.0 ライセンスで定義される、この作品への包含を意図的に提出されたいかなるコントリビューションも、追加の条項や条件なしに上記のとおりライセンスされます。
SPDX-License-Identifier: Apache-2.0