asopi tech
asopi techIndie Developer
Alopex DB v0.8.8チュートリアル 第5回:サーバーで開く

【2026年8月版】

Alopex DB v0.8.8チュートリアル 第5回:サーバーで開く

公開日: 2026/08/27
読了時間: 約 14分

第4回までは、Pythonのプロセスの中でデータベースを開いてきました。第5回では、同じデータディレクトリをalopex-serverで開き、HTTPから同じSQLを実行します。

v0.8.8では、公開版パッケージだけを使ったリリース検証で、ライブラリ・組み込み・CLI・HTTP・gRPC・cluster-awareの各入口が同じデータを読めることまで確認されています。ここでは、その確認を手元で再現するためのサーバー操作を扱います。

アプリが育ってデータベースを別プロセスへ分けるとき、通常はデータの移行が要ります。ダンプして、変換して、投入して、件数と中身を突き合わせる作業です。Alopex DBでは、第1回から使ってきたデータディレクトリをそのまま開きます。

1. 環境

項目
Alopex DB0.8.8(PyPI / crates.io 公開版)
Python3.11.11
OSLinux(WSL2、glibc 2.35)
確認日2026年8月17日

サーバーとCLIを入れます。

cargo install alopex-cli --version "=0.8.8" --locked
cargo install alopex-server --version "=0.8.8" --locked

2. エンジンへの五つの入口

Alopex DBには、同じデータへ届く経路が五つあります。

サーフェス到達経路データ
SF-MEMDatabase.new() / CLI --in-memoryメモリのみ
SF-FILEDatabase.open(path) / CLI --data-dirデータディレクトリ
SF-HTTPalopex-server/api/sql/queryデータディレクトリ
SF-GRPCalopex-server のgRPCサーフェスデータディレクトリ
SF-CLUSTERalopex-server[cluster] mode=cluster_awareデータディレクトリ

第1回のDatabase.new()がSF-MEM、Database.open()がSF-FILEです。永続化しないのはSF-MEMだけで、残る四つは同じデータディレクトリを開けます。

3. データディレクトリを作る

CLIでarticlesテーブルを作ります。次の内容をsetup.sqlに書きます。

CREATE TABLE articles (
  id INTEGER PRIMARY KEY,
  title TEXT,
  author TEXT,
  views INTEGER,
  rating REAL
);
INSERT INTO articles VALUES (1, 'Rustで書くLSMツリー', 'mio', 1200, 3.5);
INSERT INTO articles VALUES (2, 'SQLパーサをNimで実装する', 'mio', 800, 4.5);
INSERT INTO articles VALUES (3, 'ベクトル検索の基礎', 'ren', 1500, 4.5);

--data-dirでディレクトリを指定して実行すると、そこにデータが書かれます。

alopex --batch --output json --data-dir ./srv-data sql -f setup.sql

./srv-dataディレクトリができます。このディレクトリは、Pythonからも読めます。

from alopex import Database

db = Database.open("./srv-data")
print(db.execute_sql("SELECT id, title, views FROM articles ORDER BY id"))
[{'id': 1, 'title': 'Rustで書くLSMツリー', 'views': 1200},
 {'id': 2, 'title': 'SQLパーサをNimで実装する', 'views': 800},
 {'id': 3, 'title': 'ベクトル検索の基礎', 'views': 1500}]

CLIで書いたデータを、Pythonがそのまま読み出します。第1回からDatabase.open()で開いてきたのと同じディレクトリです。

4. サーバーで開く

同じディレクトリをalopex-serverで開きます。設定をalopex.tomlに書きます。

data_dir = "./srv-data"
http_bind = "127.0.0.1:18080"
grpc_bind = "127.0.0.1:19090"
admin_bind = "127.0.0.1:18081"

Pythonのプロセスを終了してから起動します。

alopex-server --config alopex.toml

ログに設定が出ます。

INFO alopex_server::server: Cluster startup configuration applied
  cluster_mode=SingleNode node_id=local role=Gateway
  http_bind=127.0.0.1:18080 grpc_bind=127.0.0.1:19090 admin_bind=127.0.0.1:18081

HTTPが18080、gRPCが19090、管理用が18081で待ち受けます。

データディレクトリは、一度に一つのプロセスから開きます。Pythonで開いたままサーバーを起動しないでください。

5. HTTPから同じSQLを実行する

別の端末から/api/sql/queryへPOSTします。

curl -s -X POST http://127.0.0.1:18080/api/sql/query \
  -H 'Content-Type: application/json' \
  -d '{"sql":"SELECT id, title, views FROM articles ORDER BY id"}'
{"columns":[{"name":"id","data_type":"INTEGER"},
            {"name":"title","data_type":"TEXT"},
            {"name":"views","data_type":"INTEGER"}],
 "rows":[[{"Integer":1},{"Text":"Rustで書くLSMツリー"},{"Integer":1200}],
         [{"Integer":2},{"Text":"SQLパーサをNimで実装する"},{"Integer":800}],
         [{"Integer":3},{"Text":"ベクトル検索の基礎"},{"Integer":1500}]],
 "affected_rows":null}

SQLは3章のPythonと同じ文です。書き換えていません。

上のJSONは、記事に載せるためにcolumnsrowsaffected_rowsだけを抜き出しています。実際の応答には、これらに加えてresultsrouting_diagnosticsが入ります。

返る形式は違います。Pythonが辞書のリストを返したのに対し、HTTPは列の定義と行の配列を分けて返し、値には型名が付きます。アプリのテストでこの二つを比べるときは、形ではなく値と列名で照合します。実行時間のようなメタデータまで固定すると、サーバーの更新のたびにテストが落ちます。

ここで、v0.8.8のサーバーへHTTPで行を追加する瞬間を録画しています。レスポンスのaffected_rows: 1と、local_onlyのルーティング判断を実際の実行結果で確認できます。

HTTPで1行を追加し、affected_rowsとlocal_onlyの実際の応答を確認

動画を開く

6. サーバー経由で書いた行をファイルから読む

HTTPから1行追加します。

curl -s -X POST http://127.0.0.1:18080/api/sql/query \
  -H 'Content-Type: application/json' \
  -d "{\"sql\":\"INSERT INTO articles VALUES (4, 'HTTPから追加した記事', 'sora', 500, 4.0)\"}"
{"columns":[],"rows":[],"affected_rows":1}

サーバーを停止し、Pythonから開き直します。

from alopex import Database

db = Database.open("./srv-data")
for row in db.execute_sql("SELECT id, title, author FROM articles ORDER BY id"):
    print(row)
{'id': 1, 'title': 'Rustで書くLSMツリー', 'author': 'mio'}
{'id': 2, 'title': 'SQLパーサをNimで実装する', 'author': 'mio'}
{'id': 3, 'title': 'ベクトル検索の基礎', 'author': 'ren'}
{'id': 4, 'title': 'HTTPから追加した記事', 'author': 'sora'}

HTTP経由で追加したid = 4を、ファイル経路から読み出せます。データディレクトリは、どの経路から書いても同じ場所に残ります。

7. healthzとadmin/status

管理用のポートでヘルスチェックを返します。

curl -s http://127.0.0.1:18081/healthz

より詳しい状態は/api/admin/statusにあります。

curl -s http://127.0.0.1:18080/api/admin/status
{"version": "0.8.8",
 "uptime_secs": 20,
 "cluster": {
   "mode": "single_node",
   "identity": {"node_id": "local", "role": "gateway",
                "lifecycle_state": "unconfigured"},
   "membership": {"source": "local_default", "members": []},
   "routing_capabilities": {"local_only": true,
                            "future_distributed_execution_required": true}}}

modesingle_noderouting_capabilities.local_onlytrueです。この設定では、SQLの実行はこのノードの中で完結します。

8. cluster-awareで起動する

alopex.toml[cluster]を足すと、cluster-awareで起動します。

data_dir = "./srv-data"
http_bind = "127.0.0.1:18080"
grpc_bind = "127.0.0.1:19090"
admin_bind = "127.0.0.1:18081"

[cluster]
mode = "cluster_aware"
node_id = "node-1"
cluster_id = "blog-cluster"
advertised_endpoint = "127.0.0.1:19090"

node_idcluster_idadvertised_endpointの三つは必須で、どれかを欠くと起動時に止まります。

failed to load config: invalid config: cluster.cluster_id is required when cluster.mode is cluster_aware

起動すると/api/admin/statusの内容が変わります。

{"cluster": {
   "mode": "cluster_aware",
   "identity": {"node_id": "node-1", "cluster_id": "blog-cluster",
                "advertised_endpoint": "127.0.0.1:19090",
                "role": "gateway", "lifecycle_state": "active"},
   "membership": {"source": "chirps", "members": []},
   "routing_capabilities": {"local_only": true},
   "degraded": false}}

lifecycle_stateunconfiguredからactiveになり、membership.sourcelocal_defaultからchirpsに変わりますが、local_onlytrueのままです。この設定は複数ノードへSQLを分散実行する機能ではなく、分散実行を必要とする操作はfuture_distributed_execution_requiredとして拒否されます。cluster-awareで得られるのは、ノードのidentityとmembershipを外から読めることです。

監視で見るのはdegradedです。membershipの取得元が利用できない場合、単一ノード相当のフォールバックで起動し、degradedtrueになります。起動したかどうかだけを見ていると、この状態に気づけません。

9. io_statsによる統計の読み出し

メモリとI/Oの統計は、通常のSQLとして読めます。

alopex --batch --output json --data-dir ./srv-data sql "SELECT io_stats()"
{"io_stats": "{\"wal_write_bytes\":0,\"sstable_read_bytes\":0,
               \"buffer_pool_hit_rate\":1,\"buffer_pool_size_bytes\":0,
               \"memtable_size_bytes\":1656,\"compaction_bytes_written\":0}"}

WAL書き込み量、SSTable読み込み量、buffer pool hit rate、memtableサイズ、compaction書き込み量が返ります。値はJSON文字列なので、使うときはもう一度パースします。ほかにmemory_stats()clear_cache()があります。

ダッシュボードへ送る前に、空のデータベースでどう見えるか、書き込み後にどの値が増えるかを手元で確かめておくと、異常値の判断がつきます。

10. ここまでで使ったもの

対象役割
alopex --data-dir <dir> sql -f <file>CLIからデータディレクトリを操作する
alopex.tomldata_dirサーバーが開くディレクトリを指定する
POST /api/sql/queryHTTPからSQLを実行する
GET /healthz(管理ポート)起動したかを確認する
GET /api/admin/statusバージョンとcluster状態を読む
SELECT io_stats()I/Oの統計を読む
Database::open(&Path)Rustから組み込みでディレクトリを開く
ureq::post(...).send_json(...)RustからHTTPでSQLを実行する

11. Rustから組み込みとHTTPで接続する

Rustからも二つの経路でデータを開けます。組み込みでディレクトリを直接開く経路と、HTTPでサーバーへ問い合わせる経路です。

組み込みで開く

6章でHTTPから追加したid = 4の行を、Rustのプログラムから読みます。サーバーを停止してから実行します。

[dependencies]
alopex-embedded = "=0.8.8"
use alopex_embedded::{Database, SqlResult};
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::open(Path::new("./srv-data"))?;
    if let SqlResult::Query(q) = db.execute_sql("SELECT id, title, author FROM articles ORDER BY id")? {
        for row in &q.rows {
            let cells: Vec<String> = row.iter().map(|v| format!("{v:?}")).collect();
            println!("{}", cells.join(" | "));
        }
    }
    Ok(())
}
Integer(1) | Text("Rustで書くLSMツリー") | Text("mio")
Integer(2) | Text("SQLパーサをNimで実装する") | Text("mio")
Integer(3) | Text("ベクトル検索の基礎") | Text("ren")
Integer(4) | Text("HTTPから追加した記事") | Text("sora")

CLIで作り、HTTPから1行足したディレクトリを、Rustがそのまま読み出します。6章のPythonと同じ4行です。書いた経路と読む経路が違っても、データディレクトリは一つです。

HTTPで開く

同じ問い合わせを、今度はサーバー越しに実行します。alopex-embeddedは要りません。HTTPクライアントとJSONの読み取りがあれば足ります。

[dependencies]
ureq = { version = "2", features = ["json"] }
serde_json = "1"

サーバーを起動し直してから実行します。

use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let body: serde_json::Value = ureq::post("http://127.0.0.1:18080/api/sql/query")
        .send_json(json!({"sql": "SELECT id, title, views FROM articles ORDER BY id"}))?
        .into_json()?;

    let names: Vec<&str> = body["columns"].as_array().unwrap().iter()
        .map(|c| c["name"].as_str().unwrap()).collect();
    println!("{}", names.join(" | "));

    for row in body["rows"].as_array().unwrap() {
        let cells: Vec<String> = row.as_array().unwrap().iter()
            .map(|v| v.as_object().unwrap().values().next().unwrap().to_string())
            .collect();
        println!("{}", cells.join(" | "));
    }
    Ok(())
}
id | title | views
1 | "Rustで書くLSMツリー" | 1200
2 | "SQLパーサをNimで実装する" | 800
3 | "ベクトル検索の基礎" | 1500
4 | "HTTPから追加した記事" | 500

行の各要素は{"Integer": 1}のように、型名をキーにした一つ組のオブジェクトです。上のコードがvalues().next()で最初の値だけを取り出しているのは、この形のためです。組み込みのSqlValue::Integer(1)を、JSONで表したものが{"Integer":1}です。

二つのプログラムで、SQLの文は同じです。変えたのは接続の方法だけです。組み込みの側はDatabase::open()でディレクトリを指し、HTTPの側はURLを指しています。

アプリを組み込みで書き始めて、後からサーバーへ移すとき、書き換えるのはこの接続の部分です。SQLとデータディレクトリはそのまま使えます。

まとめ

この回では、一つの./srv-dataを四つの経路から開きました。CLIの--data-dirarticlesを作り、PythonのDatabase.open()で読み、alopex-serverのHTTPでid = 4を追加し、RustのDatabase::open()でその4行を読み出しています。2章の表でいうSF-FILEとSF-HTTPです。v0.8.8の公開検証では、ここにgRPCとcluster-awareを加えた全サーフェスのモード一致も確認されています。

このときSELECT id, title, views FROM articles ORDER BY idという一つのSQL文を、経路を変えても書き換えていません。変えたのは、Database.open("./srv-data")POST /api/sql/queryという接続の指定だけです。ダンプと投入によるデータの移行は挟んでいません。

経路によって違うのは結果の形式です。Pythonは辞書のリスト、Rustの組み込みはSqlResult::Query、HTTPはcolumnsrowsを分けたJSONを返します。値と列名は一致するので、経路をまたぐテストは形ではなくこの二つで照合します。

[cluster] mode=cluster_awareを足すと、/api/admin/statuslifecycle_stateactiveに、membership.sourcechirpsに変わります。v0.8.8でここまでが実際に使えるようになった一方、local_onlytrueのままです。単一メンバーでのcluster-aware起動と既存データの読み出しはできますが、複数ノードへの分散実行を要する操作はfuture_distributed_execution_requiredとして拒否されます。この設定で外から読めるのは、ノードのidentityとmembershipです。

参照