asopi tech
asopi techIndie Developer
Alopex DB v0.8.8チュートリアル 第1回:キーと値を保存する

【2026年8月版】

Alopex DB v0.8.8チュートリアル 第1回:キーと値を保存する

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

Alopex DBは、キーバリューストア・SQL・ベクトル検索を一つのエンジンで扱うデータベースです。同じデータファイルのまま、アプリに組み込んだ状態から、サーバー、クラスタ構成へ移せます。

ベクトル検索を足すためにSQLiteとFaissを併用したり、規模が大きくなった段階で組み込みDBからサーバー型DBへ移行したりする作業を、製品を変えずに済ませることを狙った設計です。

第1回で扱うのはキーバリューストアのAPIです。キーを指定して値を書き、同じキーで読み出します。

説明はPythonで進めます。Alopex DBの本体はRustで、Pythonバインディングはそれを包んだものです。どちらの言語から書いたデータも同じ形式で残るので、片方で作ったデータディレクトリをもう片方から開けます。

1. 環境とインストール

この記事の実行結果は、次の環境で確かめています。

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

Pythonから使います。

pip install --no-cache-dir "alopex==0.8.8"

バージョンを固定しているのは、この記事に書いた実行結果と手元の結果を突き合わせられるようにするためです。

ここではPyPIに公開された0.8.8を使います。v0.8.8のリリース確認では、ソースコードを混ぜずに公開版パッケージだけをインストールし、ライブラリ・組み込み・サーバー・クラスタが同じデータを扱えることを確認しています。

2. putとget

putで値を書き、getで読みます。hello.pyという名前で次の内容を保存します。

from alopex import Database, TxnMode

db = Database.new()
with db.begin(TxnMode.READ_WRITE) as txn:
    txn.put(b"greeting", b"hello")
    txn.commit()

実行しても何も表示されないので、保存した値を取り出す行を足します。

with db.begin(TxnMode.READ_ONLY) as txn:
    print(txn.get(b"greeting"))
b'hello'

get()は、保存した値をそのまま返します。b"..."が付いているのは、キーも値もバイト列だからです。文字列として読むなら.decode()します。

print(txn.get(b"greeting").decode())
hello

Database.new()はメモリ上にデータベースを作ります。プロセスを終了すれば消えるので、書き捨てで試せます。

実行結果はVHSでも録画しています。コンテナ内でPyPIのalopex==0.8.8をインストールし、putget・存在しないキーの結果を同じコードで確認したものです。

公開版Alopex DB v0.8.8でput・getを実行した結果

動画を開く

3. 存在しないキーに対するget

保存していないキーを引いてみます。

print(txn.get(b"missing"))
None

例外にはならず、Noneが返ります。キーがあるかどうかは、この戻り値で判断します。

4. バイト列と文字列の変換

キーと値はバイト列なので、日本語を書くときは.encode()で変換します。

このチュートリアルは、技術記事を管理するデータを題材にします。キーにarticle:1のような接頭辞を付けると、記事以外のデータが増えても区別できます。

from alopex import Database, TxnMode

db = Database.new()

with db.begin(TxnMode.READ_WRITE) as txn:
    txn.put(b"article:1", "Rustで書くLSMツリー".encode())
    txn.put(b"article:2", "SQLパーサをNimで実装する".encode())
    txn.put(b"article:3", "ベクトル検索の基礎".encode())
    txn.commit()

with db.begin(TxnMode.READ_ONLY) as txn:
    print(txn.get(b"article:1").decode())
    print(txn.get(b"article:3").decode())
Rustで書くLSMツリー
ベクトル検索の基礎

日本語は.encode()でバイト列にしてから渡します。取り出すときは.decode()で戻します。

5. 上書きとdelete

同じキーに二度書くと、後から書いた値が残ります。

with db.begin(TxnMode.READ_WRITE) as txn:
    txn.put(b"article:1", "旧タイトル".encode())
    txn.put(b"article:1", "新タイトル".encode())
    txn.commit()

with db.begin(TxnMode.READ_ONLY) as txn:
    print(txn.get(b"article:1").decode())
新タイトル

削除はdelete()です。削除した後に引くとNoneになります。

with db.begin(TxnMode.READ_WRITE) as txn:
    txn.delete(b"article:1")
    txn.commit()

with db.begin(TxnMode.READ_ONLY) as txn:
    print(txn.get(b"article:1"))
None

存在しないキーを削除してもエラーにはなりません。「消えている状態にする」操作なので、何度実行しても結果は同じです。

6. commitを省いたときのput

ここまでのコードには、毎回txn.commit()を書いてきました。これを外して、何が起きるかを確かめます。

db = Database.new()

with db.begin(TxnMode.READ_WRITE) as txn:
    txn.put(b"article:9", "コミットを忘れた記事".encode())
    # commit を書かない

with db.begin(TxnMode.READ_ONLY) as txn:
    print(txn.get(b"article:9"))
None

書いたはずの値がNoneになります。put()はその場でデータベースへ反映するのではなく、トランザクションの中に溜めます。commit()を呼んだ時点で、まとめて反映されます。

7. commitとrollback

この仕組みは、途中で失敗した処理を取り消すために使えます。書き換えを始めたあとで問題に気づいたとき、commit()の代わりにrollback()を呼びます。

db = Database.new()

with db.begin(TxnMode.READ_WRITE) as txn:
    txn.put(b"article:1", "1件目".encode())
    txn.commit()

with db.begin(TxnMode.READ_WRITE) as txn:
    txn.put(b"article:1", "書き換えたが取り消す".encode())
    txn.rollback()

with db.begin(TxnMode.READ_ONLY) as txn:
    print(txn.get(b"article:1").decode())
1件目

rollback()を呼ぶと、そのトランザクションで書いた内容は反映されません。commit()する前の状態が残ります。

まとめると、次の三つです。

  1. put()delete()は、トランザクションの中に変更を溜める。
  2. commit()で、溜めた変更をまとめて反映する。
  3. rollback()で、溜めた変更を捨てる。

withブロックを抜けるときにcommit()を呼んでいなければ、rollback()を書かなくても変更は捨てられます。commit()を省いた先ほどの例で値が残らなかったのは、このためです。

8. TxnModeの二つの値

トランザクションを開くときに指定していたTxnModeには、二つの値があります。

  • READ_WRITE:読み書きの両方
  • READ_ONLY:読み取りのみ

読み取り専用で書き込もうとすると、実行時に止まります。

with db.begin(TxnMode.READ_ONLY) as txn:
    txn.put(b"article:1", b"x")
crate::error.PyAlopexError: transaction is read-only

読むだけの処理にはREAD_ONLYを指定しておくと、書き込みを含むコードを誤って通してしまう事故を防げます。

9. Database.newとDatabase.open

ここまではDatabase.new()を使ってきました。プロセスを終了すればデータも消えます。

ファイルに残すにはDatabase.open()にディレクトリのパスを渡します。

from alopex import Database, TxnMode

db = Database.open("./notes-db")
with db.begin(TxnMode.READ_WRITE) as txn:
    txn.put(b"greeting", b"hello")
    txn.commit()

実行すると./notes-dbディレクトリができ、中にWALのlsm.walとSSTableを置くsstができます。データベースの実体はこのディレクトリです。別のプロセスから開き直します。

from alopex import Database, TxnMode

db = Database.open("./notes-db")
with db.begin(TxnMode.READ_ONLY) as txn:
    print(txn.get(b"greeting").decode())
hello

前のプロセスで書いた値を読み出せます。変えたのはnew()open("./notes-db")にした一行だけで、putgetcommitも同じです。

データディレクトリを開き直すときは、書き込んだプロセスを正常終了させてからにします。データディレクトリは、一度に一つのプロセスから開きます。

10. RustからKVを使う

Rustからはalopex-embeddedクレートを使います。ここまでのKV操作は、次のように書きます。

Cargo.tomlに依存を書きます。

[dependencies]
alopex-embedded = "=0.8.8"

src/main.rsは次のとおりです。

use alopex_embedded::{Database, TxnMode};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::new();

    let mut txn = db.begin(TxnMode::ReadWrite)?;
    txn.put("挨拶".as_bytes(), "こんにちは".as_bytes())?;
    txn.commit()?;

    let mut txn = db.begin(TxnMode::ReadOnly)?;
    let value = txn.get("挨拶".as_bytes())?;
    println!("{:?}", value.map(|b| String::from_utf8(b).unwrap()));
    Ok(())
}
Some("こんにちは")

Database::new()beginputgetcommitと、名前はPythonと同じです。違いは三つあります。

  1. トランザクションの種類がTxnMode::ReadWriteTxnMode::ReadOnlyになる。PythonのTxnMode.READ_WRITEにあたる
  2. キーと値が&[u8]になる。Pythonが.encode()で作っていたバイト列を、Rustでは.as_bytes()で渡す
  3. getOption<Vec<u8>>を返す。値がなければNoneで、これはPythonと同じ

v0.8.8では、crates.ioから取得したalopex-embeddedのKVプログラムを追加の共有ライブラリ設定なしで実行できます。公開版の実行時依存もリリース検証で確認されています。

読み出した値を文字列として表示するには、Pythonの.decode()にあたるString::from_utf8でバイト列を文字列へ戻します。

ファイルに残すときはDatabase::open()にします。引数は&strではなく&Pathなので、文字列を渡すときはPath::new()で包みます。

use std::path::Path;

let db = Database::open(Path::new("./notes-db"))?;

Pythonで作ったデータディレクトリを、そのままRustから開けます。逆も同じです。保存する形式は言語によらず共通で、片方で書いた値をもう片方で読めます。

11. v0.8.8で確認したデータの引き継ぎ

この回では、Pythonで作った./notes-dbをRustから開きました。v0.8.8のリリース確認では、同じ考え方をサーバーやクラスタまで広げ、ライブラリ・組み込み・CLI・HTTP・gRPC・cluster-awareの各経路で同じデータを読み書きできることを確認しています。

経路この回での位置づけ
PythonライブラリDatabase.new()Database.open()でKVを書く・読む
組み込みRustalopex-embeddedで同じデータディレクトリを開く
CLI・サーバー・HTTP・gRPCv0.8.8のリリース検証で同一結果を確認済み
cluster-aware単一メンバーで既存データを開けることを確認済み

第1回ではまず、アプリに組み込んだPythonとRustの二つの経路を扱いました。サーバーへの接続は、後の回で同じnotes-dbを使って進めます。

12. ここまでで使ったAPI

PythonRust役割
Database.new()Database::new()メモリ上にデータベースを作る
Database.open(path)Database::open(&Path)データディレクトリを開く。なければ作る
db.begin(TxnMode.READ_WRITE)db.begin(TxnMode::ReadWrite)読み書きするトランザクションを開く
db.begin(TxnMode.READ_ONLY)db.begin(TxnMode::ReadOnly)読み取り専用のトランザクションを開く
txn.put(key, value)txn.put(&[u8], &[u8])値を書く。キーが同じなら上書き
txn.get(key)txn.get(&[u8])値を読む。なければNone
txn.delete(key)txn.delete(&[u8])値を消す
txn.commit()txn.commit()変更を反映する
txn.rollback()txn.rollback()変更を捨てる

キーと値はどちらもバイト列です。Pythonでは文字列に.encode().decode()を挟み、Rustでは.as_bytes()String::from_utf8を使います。


次回は、テーブルを定義してSQLで問い合わせます。

参照