【メモ】LevelDBをPythonから使用する

LevelDBを使用することがあったので、その使い方をメモします。

LevelDBGoogleの以下のリポジトリによって公開されているデータストアの仕組みになります。

github.com

LevelDBGoogleで書かれた高速なKey-Valueストレージ・ライブラリで、文字列キーから文字列値への順序付きマッピングを提供します。

Key-ValueスタイルということなのでNoSQL的な動作をするものになるでしょうか。

LevelDBの主な機能

LevelDBの特徴は以下のようになります(GitHubより抜粋

  • キーと値は任意のバイト配列を使用可能
  • データはキーでソートされて保存される
  • ユーザーが独自の比較関数を提供してソート順をカスタマイズ可能
  • 基本操作は以下の3つ:
    • Put(key,value): データの追加・更新
    • Get(key): データの取得
    • Delete(key): データの削除
  • アトミックなバッチ処理をサポート
    • 複数の変更を1つの単位として実行可能
    • バッチ内の全操作が成功するか、全て失敗するかのどちらか
    • 処理の一貫性を保証(中間状態が外部から見えない)
    • パフォーマンスの向上(特に大量のデータ操作時に効果的)
    • 例:複数のユーザーデータを一度に追加・更新・削除
  • 一貫性のあるデータビューを得るための一時的なスナップショット作成が可能
  • データに対する前方および後方のイテレーション(繰り返し処理)をサポート
  • Snappy圧縮ライブラリを使用してデータを自動的に圧縮(Zstd圧縮もサポート)
  • 外部アクティビティ(ファイルシステム操作など)は仮想インターフェースを通じて行われるため、ユーザーがOSとの相互作用をカスタマイズ可能

スキーマがあるタイプのデータベースではないので、作りはシンプルのようです。

LevelDBPythonから使用してみる

LevelDBを使用してデータの操作を行ってみます。

動作する環境

今回実験したOSやミドルウエアは以下となります。

環境設定

PythonからLevelDBを使用するにはpipコマンドplyvelというライブラリを導入します。

plyvel.readthedocs.io

pypi.org

では以下のようにライブラリをインストールします。pipを使用するので、venv環境も作成しています。

# 実験用のディレクトリ作成と移動
$ mkdir levelDB && cd $_
# 仮想環境の構築(今回は**.levelDB**としました)
$ python -m venv .levelDB
# 仮想環境の有効化
$ source .levelDB/bin/activate
# ライブラリのインストール
(.levelDB) $ pip install plyvel

LevelDBの操作サンプル

基本的な操作のサンプルは以下となります。(以下のソースコードには型ヒントをつけています

leveldb_sample.py

import plyvel
from typing import Optional

def leveldb_operations(db_path: str) -> None:
    """
    LevelDBの基本的な操作を実行します:データベースの開封、書き込み、読み取り、削除。

    この関数は以下の操作を示します:
    1. LevelDBデータベースを開く(データベースがなければ作成後に開く)
    2. データベースにデータを書き込む
    3. データベースからデータを読み取る
    4. データベースからデータを削除する
    5. データベースを閉じる

    引数:
        db_path (str): LevelDBデータベースが保存されるファイルパス。

    戻り値:
        None
    """
    # データベースを開く(存在しない場合は作成される)
    db: plyvel.DB = plyvel.DB(db_path, create_if_missing=True)

    try:
        # データの書き込み
        key: bytes = b'key'
        value: bytes = b'value'
        db.put(key, value)

        # データの読み取り
        retrieved_value: Optional[bytes] = db.get(key)
        print(f"取得された値: {retrieved_value}")  # b'value' が出力される

        # データの削除
        db.delete(key)

        # 削除されたことを確認
        deleted_value: Optional[bytes] = db.get(key)
        print(f"削除後の値: {deleted_value}")  # None が出力される

    finally:
        # データベースを閉じる
        db.close()

if __name__ == "__main__":
    leveldb_operations('/tmp/testdb/')

LevelDBの特徴として、Key-Valueそれぞれがバイト列である点です。そのため、文字列を使用する場合はencode()メソッドを使ってバイト列に変換する必要があります。文字列にbがついているのはそんな理由のためです。

データの扱い部分抜粋

        # データの書き込み
        key: bytes = b'key'
        value: bytes = b'value'
        db.put(key, value)

上記のコードを実行すると以下のようになります。

(.levelDB) $ python leveldb_sample.py
取得された値: b'value'
削除後の値: None

無事に動作しました。データが格納されるとデータベースのファイルが生成されます。

辞書データからのデータの格納

使用していると1つ1つのデータを登録するのは大変なので、辞書データ(Dict形式)を使用してを登録する関数を作成します。

leveldb_insert_data_sample.py

import plyvel
from typing import Dict, Union

def insert_Dict(db_path: str, data: Dict[Union[str, bytes], Union[str, bytes]]) -> int:
    """
    辞書のデータを LevelDB に一括で登録します。

    この関数は以下の操作を行います:
    1. 指定されたパスの LevelDB データベースを開く
    2. 与えられた辞書のすべてのキーと値のペアをデータベースに書き込む
    3. データベースを閉じる

    引数:
        db_path (str): LevelDB データベースが保存されているファイルパス
        data (Dict[Union[str, bytes], Union[str, bytes]]): 
            登録するデータを含む辞書。キーと値は文字列またはバイト列。

    戻り値:
        int: 登録されたキーと値のペアの数

    注意:
        - キーと値が文字列の場合、自動的に UTF-8 でエンコードされます。
        - すでに存在するキーの場合、値が上書きされます。
    """
    db = plyvel.DB(db_path, create_if_missing=True)
    inserted_count = 0

    try:
        with db.write_batch() as batch:
            for key, value in data.items():
                # 文字列の場合、バイト列に変換
                if isinstance(key, str):
                    key = key.encode('utf-8')
                if isinstance(value, str):
                    value = value.encode('utf-8')
                
                batch.put(key, value)
                inserted_count += 1
        
        print(f"登録されたデータ数: {inserted_count}")
    finally:
        db.close()

    return inserted_count

if __name__ == "__main__":
    db_path = './testdb/'
    
    # テストデータ
    test_data = {
        'key1': 'value1',
        'key2': 'value2',
        b'key3': b'value3',
        'キー4': '値4',
        b'\xe3\x81\x82': 'あ',
    }

    # データを一括登録
    inserted = insert_Dict(db_path, test_data)
    print(f"合計 {inserted} 件のデータが登録されました。")

    # 登録されたデータを確認
    db = plyvel.DB(db_path, create_if_missing=True)
    try:
        print("\n登録されたデータ:")
        for key, value in db:
            print(f"キー: {key}, 値: {value}")
    finally:
        db.close()

実行すると以下のようになります。

(.levelDB) $ python leveldb_insert_data_sample.py
登録されたデータ数: 5
合計 5 件のデータが登録されました。

登録されたデータ:
キー: b'key', 値: b'value'
キー: b'key1', 値: b'value1'
キー: b'key2', 値: b'value2'
キー: b'key3', 値: b'value3'
キー: b'\xe3\x81\x82', 値: b'\xe3\x81\x82'
キー: b'\xe3\x82\xad\xe3\x83\xbc4', 値: b'\xe5\x80\xa44'

データベース上のすべてデータの取得と削除

取得・削除もできれば一気に行いたいので、その処理を行う関数も作成してみます。

leveldb_all_data_sample.py

import plyvel
from typing import Dict, List, Tuple

def extract_all_data(db_path: str) -> Dict[bytes, bytes]:
    """
    LevelDBからすべてのデータを抽出します。

    引数:
        db_path (str): LevelDBデータベースが保存されているファイルパス

    戻り値:
        Dict[bytes, bytes]: 抽出されたすべてのキーと値のペアを含む辞書
    """
    db = plyvel.DB(db_path, create_if_missing=True)
    extracted_data: Dict[bytes, bytes] = {}

    try:
        with db.iterator() as it:
            for key, value in it:
                extracted_data[key] = value
        
        print(f"抽出されたデータ数: {len(extracted_data)}")
    finally:
        db.close()

    return extracted_data

def delete_all_data(db_path: str) -> List[bytes]:
    """
    LevelDBからすべてのデータを削除します。

    引数:
        db_path (str): LevelDBデータベースが保存されているファイルパス

    戻り値:
        List[bytes]: 削除されたキーのリスト
    """
    db = plyvel.DB(db_path, create_if_missing=True)
    deleted_keys: List[bytes] = []

    try:
        with db.write_batch() as batch:
            for key, _ in db:
                batch.delete(key)
                deleted_keys.append(key)
        
        print(f"削除されたキー数: {len(deleted_keys)}")
    finally:
        db.close()

    return deleted_keys

if __name__ == "__main__":
    db_path = './testdb/'
    
    # データを抽出
    extracted = extract_all_data(db_path)
    print("\n抽出されたデータの一部:")
    for i, (key, value) in enumerate(list(extracted.items())):
        print(f"{i+1}. キー: {key}, 値: {value}")
    
    # データを削除
    deleted = delete_all_data(db_path)
    print("\n削除されたキーの一部:")
    for i, key in enumerate(deleted):
        print(f"{i+1}. {key}")

こちらのコードを実行すると以下のように表示されます。

(.levelDB) $ python leveldb_all_data_sample.py
抽出されたデータ数: 6

抽出されたデータの一部:
1. キー: b'key', 値: b'value'
2. キー: b'key1', 値: b'value1'
3. キー: b'key2', 値: b'value2'
4. キー: b'key3', 値: b'value3'
5. キー: b'\xe3\x81\x82', 値: b'\xe3\x81\x82'
6. キー: b'\xe3\x82\xad\xe3\x83\xbc4', 値: b'\xe5\x80\xa44'
削除されたキー数: 6

削除されたキーの一部:
1. b'key'
2. b'key1'
3. b'key2'
4. b'key3'
5. b'\xe3\x81\x82'
6. b'\xe3\x82\xad\xe3\x83\xbc4'

LevelDBにはトランザクション機能はない

他のデータベースにはトランザクション機能があるものも多いのですが、LevelDBには実はその機能はありません。その代わりになるものとしてバッチ機能があります。

使い方はwith db.write_batch() as batch:とブロックを作りその中でデータを処理する形です。以下がコード例になります。

import plyvel

# データベースを開く
db = plyvel.DB('./testdb/', create_if_missing=True)

# バッチ処理を作成
with db.write_batch() as batch:
    batch.put(b'key1', b'value1')
    batch.put(b'key2', b'value2')
    batch.delete(b'key3')
    # ここでバッチが自動的にコミットされる

# データベースを閉じる
db.close()

バッチ処理はパフォーマンスを向上させる機能で、トランザクションのような機能はありません。完全なデータ一貫性を保証する必要がある場合は、トランザクションをサポートするデータベースを採用したほうがよいでしょう。


LevelDBの操作はこれで概ねのデータを作成することができました。

おわりに

LevelDBPythonでの基本的な使用方法をまとめてみました。キーバリューストアであるLevelDBは、ログ保存などの用途に適しているように感じます。これまではSQLiteを使用することが多かったですが、スキーマなどが不要であれば、LevelDBを使用するのもいいかなと思います。特にJSONなどのデータを使用することを考えるのであれば、こちらでも問題は出てこないかなと思います。

とはいえ、実際のアプリケーション開発での使用では、大規模データの取り扱いや並行アクセスを考えると別のアプローチをしたほうが適切だと思います。

/* -----codeの行番号----- */