メインコンテンツへスキップ
見出し画像

【次世代高速RDB 劔"Tsurugi"】Tsurugi UDFを利用してリモート環境のAI処理をSQL一本で実行する

    はじめに

    Tsurugi 1.8.0では試験的機能という位置付けで ユーザー定義関数 (UDF、以降Tsurugi UDFと記述) に対応しました。

    TsurugiのUDF機能は他のRDMBSの同等機能には見られない非常に興味深い特色を持っています。というのもTsurugiではgRPCサービスとして作成したユーザープログラムをSQLの関数として呼び出せるようにしているからです。つまりgRPCで構築したマイクロサービスをSQLから直接呼び出せるのです。

    gRPCサービスをUDFとして利用できることで「DBに登録されているデータをリモートのGPUサーバー上で稼働するAIモデルに処理させる」「DBに登録されたデータを起点にして複数のマイクロサービス間でデータを受け渡しし、最終的な結果をDBに戻すというデータフローをSQLで構築する」といった様々なユースケースが考えられます。

    本記事では上に挙げた「DBに登録されているデータをリモートのGPUサーバー上で稼働するAIモデルに処理させる」例を簡単なデモンストレーションで紹介したいと思います。AIモデルを動かしますが、通常のPCでも動くようなモデルを使うので是非読者の皆様も実際に自分の手元でプログラムを動かしてみてTsurugi UDFの面白さを体感してみてください。

    注意事項

    今回紹介するサンプルプログラムは Tsurugi 1.9.0 で新規に実装された機能も利用します。必ず1.9.0以上のバージョンで実行してください。

    また事前に Tsurugi UDF ユーザガイド にも目を通しておくことを強く推奨します。

    今回作るデモンストレーションの概要

    全体構成

    今回作るデモンストレーションの構成は次の通りです。Tsurugi搭載サーバーとAIサーバーが別々のノードになっていますが、もちろん同じノード上で実行することも可能です。以降の解説では別々のノードで実行する場合に必要となる設定についても都度解説します。

    画像

    処理の流れは次の通りです。 この一連の処理は一本のSQLで実行されます!

    1. AIモデルに処理させる画像をTsurugi上のテーブルに保存しておきます

    2. Tsurugi UDFを含むSQLを実行し、テーブルに保存されている画像を引数としてリモートのAIサーバー上で稼働しているgRPCサーバー上で稼働しているTsurugi UDFを呼び出します

    3. Tsurugi UDFは内部の処理で物体認識AIモデルに画像を渡し、物体認識処理を行わせます

    4. Tsurugi UDFはAIモデルの処理結果 (認識結果をバウンディングボックスとして書き込んだ画像とバウンディングボックスの座標情報) を返します

    5. SQLはTsurugi UDFの処理結果を結果格納テーブルにinsertするように掛かれているので、処理結果がTsurugiに保存されます

    対象となるテーブル

    デモンストレーションでは次の2つのテーブルを使います。

    画像

    処理対象となる画像を保存するテーブルが `images` テーブルです。ファイル名をキーとし (`file_name` 列) 、画像データそのものをblob型の列 `image` に保存します。

    Tsurugi UDFを通してAIの処理させた結果を `prediction_results` テーブルに保存します。1つの画像から複数の物体を検出する可能性があるので、検出したオブジェクト別に個別の行に保存します。そのためファイル名 (`file_name`) に加えて連番 (`sequence`) を加えた複合キーになっています。

    `prediction_results` テーブルの属性には次の情報を保存します。

    • 認識結果をバウンディングボックスとして記入した画像 (`predicted_image` 列)

    • バウンディングボックスの座標情報

      • X座標、Y座標、幅、高さをそれぞれ `bbox_x` 、 `bbox_y` 、 `bbox_width` 、 `bbox_height` 列に保存

    • 認識の確度 (`confidence` 列)

    デモンストレーションの実装

    それではプログラムを作っていくことにしましょう。

    Tsurugi上にテーブルを作成

    予めTsurugi上にテーブルを作成しておきます。次のDDLを使ってテーブルを作成してください。

    create table prediction_results (
      file_name varchar(*) not null,
      sequence int not null,
      bbox_x real not null,
      bbox_y real not null,
      bbox_width real not null,
      bbox_height real not null,
      confidence real not null,
      predicted_image blob not null,
      primary key (file_name, sequence)
    );
    
    create table images (
      file_name varchar(*) not null,
      image blob not null,
      primary key (file_name)
    );

    gRPCサーバーの作成

    AIサーバー上で稼働するgRPCサーバーを構築する手順について説明していきます。

    gRPCサーバーはPythonで構築します。実行環境を汚さないようにまずは仮想環境を作っておきましょう。

    $ python -m venv udf-sample
    $ source udf-sample/bin/activate

    Windows環境の場合は次のような手順になります。

    > python -m venv udf-sample
    > udf-sample\Scripts\activate

    以降の説明ではこの仮想環境を有効化していることを前提としています。

    必要となるライブラリをインストールします。gPRCサーバーの構築に必要な `grpcio-tools` モジュールとYOLOモデルを使うために `ultralytics` モジュールをインストールしておきます。

    $ pip install grpcio-tools
    $ pip install ultralytics

    Tsurugi UDFを実装する際に利用するライブラリを次の手順でインストールします。
    tsurugi-udf プロジェクトのリリースアーカイブをダウンロードし、その中に含まれている `udf-library/python` をpipで取り込みます。

    $ curl -OL https://github.com/project-tsurugi/tsurugi-udf/archive/refs/tags/0.2.0.tar.gz
    $ tar xf 0.2.0.tar.gz
    $ cd tsurugi-udf-0.2.0/udf-library/python
    $ pip install .

    それではgRPCサービスの定義をしましょう。先ほど作ったvenv環境のディレクトリの直下にコードを作成する前提で説明をします。

    まずはgRPCサービス定義を記述したprotoファイル `udf-sample.proto` を作成します。今回のサービス定義ではTsurugi独自のデータ型を利用するため、上で展開したtsurugi-udfのソースアーカイブの下に置かれている `proto/tsurugidb/udf/tsurugi_types.proto` もコピーします (`proto` より下のディレクトリ階層をコピーしてください) 。

    venv環境下のprotoファイルの配置は次のようになります。

    udf-sample
    ├── tsurugidb
    │   └── udf
    │       └── tsurugi_types.proto
    └── udf-sample.proto

    `udf-sample.proto` の記述内容は次のようになります。

    syntax = "proto3";
    
    import "tsurugidb/udf/tsurugi_types.proto";
    
    package udf_sample;
    
    service UdfSample {
      // 1
      rpc predict_image (ImageData) returns (stream PredictionResult);
    }
    
    // 2
    message ImageData {
      string file_name = 1;
      tsurugidb.udf.BlobReference image_data = 2;
    }
    
    // 3
    message PredictionResult {
      int32 sequence = 1;
      float bbox_x = 2;
      float bbox_y = 3;
      float bbox_width = 4;
      float bbox_height = 5;
      float confidence = 6;
      tsurugidb.udf.BlobReference predicted_image = 7;
    }

    protoファイルの記述内容のポイントについて説明します。

    1. UDFとなるrpcメソッド定義はServer Streaming RPCとなっており、複数のレコードをストリームで返します

    2. 入力メッセージ `ImageData` はTsurugi上の `images` テーブルの列に合わせたフィールドを定義し、同テーブルのデータを受け取れるようにしています

    3. 出力メッセージ `PredictionResult` はTsurugi上の `prediction_results` テーブルの列に合わせたフィールドを定義しており、UDFが返した結果をそのままテーブルの1行に書き込めるようにしています

      • `file_name` 列は `images` テーブルの値をそのまま渡します (後で解説するSQLを見ればどのように渡しているかが分かります)

    rpcメソッドをServer Streaming RPCとして定義したことにより、Tsurugiではユーザー定義表値関数 (UDTF) として認識されます。UDTFとはTsurugi 1.9.0にて新たに実装された機能で、スカラー値を引数として受け取り、複数行・複数列の表示 (テーブル) を返す関数です。従ってSQL上ではUDFのレスポンスをテーブルをして扱うことができるようになります。

    サービス定義を作ったので次のようにprotocを使ってPythonソースコードを生成します。

    $ python -m grpc_tools.protoc -I. -I./tsurugidb/udf --python_out=. --grpc_python_out=. udf-sample.proto

    上のコマンドを実行した結果 `udf-sample` ディレクトリの下に `udf_sample_pb2.py` 、 `udf_sample_pb2.pyi` 、 `udf_sample_pb2_grpc.py` の3ファイルが出力されるはずです。

    それではgRPCサーバーを実装しましょう。同じ `udf-sample` ディレクトリの下に `udf_server.py` というファイルを作成し、次のように実装します。

    from concurrent import futures
    import os
    from pathlib import Path
    
    import grpc
    from grpc import ServicerContext
    from tsurugidb.udf import create_blob_client
    from udf_sample_pb2 import ImageData, PredictionResult
    from udf_sample_pb2_grpc import UdfSampleServicer, add_UdfSampleServicer_to_server
    from ultralytics import YOLO
    
    
    class SampleServer(UdfSampleServicer):
    
        def __init__(self):
            # 1
            self.model = YOLO("yolo26n.pt")
    
        def predict_image(self, request: ImageData, context: ServicerContext):
            file_name = request.file_name
            print(f"ファイル {file_name} に対して推論を実行します")
            with create_blob_client(context) as blob_client:
                # 2
                download_path = Path(file_name)
                if download_path.exists():
                    os.remove(download_path)
                blob_client.download_blob(request.image_data, download_path)
    
                # 3
                results = self.model(file_name)
                for i, result in enumerate(results):
                    # 4
                    result_file_path = f"{file_name}_{i}.jpg"
                    result.save(result_file_path)
    
                    # 5
                    boxes = result.boxes
                    for j, conf in enumerate(boxes.conf):
                        sequence = i + j
                        xywh = boxes.xyxy[j]
                        bbox_x = xywh[0]
                        bbox_y = xywh[1]
                        bbox_width = xywh[2]
                        bbox_height = xywh[3]
    
                        # 6
                        upload_path = Path(result_file_path)
                        blob_ref = blob_client.upload_blob(upload_path)
    
                        # 7
                        yield PredictionResult(sequence=sequence, bbox_x=bbox_x, bbox_y=bbox_y,
                                               bbox_width=bbox_width, bbox_height=bbox_height, confidence=conf,
                                               predicted_image=blob_ref)
    
    
    def serve():
        try:
            server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
            add_UdfSampleServicer_to_server(SampleServer(), server)
            # 8
            server.add_insecure_port("[::]:50051")
            server.start()
            server.wait_for_termination()
        except KeyboardInterrupt:
            print("停止します")
        finally:
            server.stop(0)
    
    
    if __name__ == "__main__":
        serve()

    サーバーの実装上のポイントについて説明します。

    1. gRPCサーバー起動時にYOLOのモデルをロードしておきます

      • このスクリプトの初回実行時はモデルのダウンロードが発生します

    2. Tsurugiのblob型の列の値を受け取るためにはudf-libraryが提供する `tsurugidb.udf.BlobRelayClient` を利用します

    3. ここでYOLOのモデルにTsurugiから受け取った画像のパスを渡して推論を実行しています

    4. YOLOの機能を使って推論結果であるバウンディングボックスを書き込んだ画像を保存しています

    5. ここでバウンディングボックスの座標情報を取得しています

    6. 4で保存した画像のパスを `BlobRelayClient.upload_blob` メソッドに渡して実行し、画像のデータをTsurugiへ送り込みます

    7. 5で取得したバウンディングボックスの座標情報、6でTsurugiに送信したBLOBデータへの参照をレスポンスメッセージ `PredictionResult` に渡して返却しています

    8. サーバーは50051番ポートで待機しており、後ほどTsurugi側にはこのポート番号を設定することになります

    次のコマンドを実行することでサーバーが立ち上がり、リクエストを待機します。

    $ python udf_server.py

    サーバーを終了する場合は `Ctrl-C` を入力します。

    Tsurugi側の準備

    それではUDFを実行する側であるTsurugiサーバーの準備を行っていきます。

    データの準備

    まずはAIモデルに解析する元データを準備します。今回はUltralyticsが用意しているデータセットである COCO8データセット を利用します。
    次のようにUltralyticsのGitHubリポジトリから直接zipデータを取得し、適当なディレクトリに展開します。

    $ curl -OL https://github.com/ultralytics/assets/releases/download/v0.0.0/coco8.zip
    $ unzip coco8.zip

    展開したディレクトリの下は次のような構成になっているはずです。

    coco8
    ├── LICENSE
    ├── README.md
    ├── images
    │   ├── train
    │   └── val
    └── labels
        ├── train
        └── val

    今回は `images/train` の下にある4枚の画像を使います。

    本記事執筆時点ではtgsqlコマンドでblobデータを登録する手段がないので、簡単なJavaプログラムを書いて登録することにします。
    `Uploader.java` というファイル名で次のJavaプログラムを作成します。

    package org.example;
    
    import com.tsurugidb.iceaxe.TsurugiConnector;
    import com.tsurugidb.iceaxe.sql.parameter.TgBindParameters;
    import com.tsurugidb.iceaxe.sql.parameter.TgBindVariables;
    import com.tsurugidb.iceaxe.sql.parameter.TgParameterMapping;
    import com.tsurugidb.iceaxe.sql.type.TgBlob;
    import com.tsurugidb.iceaxe.transaction.manager.TgTmSetting;
    import com.tsurugidb.iceaxe.transaction.option.TgTxOption;
    
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    
    public class Uploader {
        private final TsurugiConnector connector;
    
        public Uploader() {
            this.connector = TsurugiConnector.of("ipc:tsurugi");
        }
    
        public static void main(String[] args) throws Exception {
            if (args.length < 1) {
                System.out.println("アップロードする画像のディレクトリを指定してください");
                System.exit(1);
            }
            new Uploader().execute(Path.of(args[0]));
        }
    
        public void execute(Path dir) throws IOException, InterruptedException {
            var sql = "insert or replace into images (file_name, image) values (:file_name, :image)";
            var variables = TgBindVariables.of().addString("file_name").addBlob("image");
            var mapping = TgParameterMapping.of(variables);
            try (var session = connector.createSession();
                 var stmt = session.createStatement(sql, mapping);
                 var fileListStream = Files.list(dir)) {
                var filePathList = fileListStream.toList();
                var tgTmSetting = TgTmSetting.of(TgTxOption.ofOCC());
                var tm = session.createTransactionManager(tgTmSetting);
                tm.execute(tx -> {
                    for (Path filePath : filePathList) {
                        var fileName = filePath.getFileName().toString();
                        var blob = TgBlob.of(filePath);
                        var params = TgBindParameters.of().addString("file_name", fileName).addBlob("image", blob);
                        tx.executeAndGetCount(stmt, params);
                    }
                });
            }
        }
    }

    Iceaxeを使ったJavaアプリケーションの開発方法についてはここでは説明しません。詳細については 劔本 の「8章 Tsurugiを使う~Iceaxe (Javaライブラリ) 」を参照してください。

    プログラムを作成したら次のように `coco8/images/train` のパスを引数に渡して実行します。

    $ java -cp "/path/to/lib/*" org.example.Uploader /path/to/coco8/images/train

    `images` テーブルに次のように画像が登録されているはずです。

    tgsql> select * from images;
    start transaction implicitly. option=[
      type: OCC
      label: "tgsql-implicit-transaction2026-03-05 20:40:21.985+09:00"
    ]
    Time: 33.951 ms
    [file_name: VARCHAR(*), image: BLOB]
    [000000000009.jpg, blob@0]
    [000000000025.jpg, blob@1]
    [000000000030.jpg, blob@2]
    [000000000034.jpg, blob@3]
    (4 rows)

    UDFプラグインの生成

    それではTsurugiからgRPCサービスとして稼働するUDFを呼び出せるようにUDFプラグインを生成します。

    UDFプラグインを生成にするにはtsurugi-udfプロジェクトに含まれるudf-pluginを利用します。Pythonコマンドとして利用するため、次のようにpipでインストールしてください。

    $ cd tsurugi-udf-0.2.0/udf-plugin
    $ pip install .

    プラグインをインストール後、gRPCサービス定義である `udf-sample.proto` に対して 次のように `udf-plugin-builder` コマンドを使ってプラグインを生成します。

    $ udf-plugin-builder --proto udf-sample.proto

    コマンド実行後、次のように `libudf-sample.ini` 、 `libudf-sample.so` 、 `libtsurugi_types.so` という3種類のファイルが出力されています。

    $ ls -l
    total 1828
    -rwxr-xr-x 1 aoe aoe  180176 Mar  5 21:04 libtsurugi_types.so
    -rw-r--r-- 1 aoe aoe      81 Mar  5 21:04 libudf-sample.ini
    -rwxr-xr-x 1 aoe aoe 1674200 Mar  5 21:04 libudf-sample.so
    drwxr-xr-x 9 aoe aoe    4096 Mar  5 21:02 tmp
    drwxr-xr-x 3 aoe aoe    4096 Mar  5 20:58 tsurugidb
    -rwxr-xr-x 1 aoe aoe     491 Mar  5 20:57 udf-sample.proto

    `libudf-sample.ini` がプラグインの設定ファイルです。初期状態では次のように記載されているはずです。

    [udf]
    enabled=true
    endpoint=dns:///localhost:50051
    secure=false
    transport=stream

    gRPCサーバーとTsurugiサーバーを別々のノードで実行する場合、 `endopoint` のホストとポートをgRPCサーバーのホストとポートに書き換えてください。

    [udf]
    enabled=true
    endpoint=dns:///192.168.201.3:50051 # リモート接続の場合はgRPCサーバーのアドレスに変更
    secure=false
    transport=stream

    設定の変更が終わったらこの3ファイルを `$TSURUGI_HOME/var/plugins` の下に配置してください。

    RPCサーバーとTsurugiサーバーを別々のノードで実行する場合はさらにTsurugi本体の設定変更も必要になります。
    次のように `[grpc_server]` セクションの `endpoint` のホストを Tsurugiサーバーのホスト に変更してください (gPRCサーバーから見たアドレスになります) 。

    [grpc_server]
    #enabled=true
    endpoint=dns:///192.168.201.10:52345
    #listen_address=0.0.0.0:52345

    全ての準備が終わったら `tgctl start` でTsurugiを起動します。

    デモンストレーションの実行

    実行するSQLについて

    デモンストレーションを実行するにあたって、実行するSQL文について解説します。次のSQLを実行します。

    insert or replace into prediction_results
    select
      i.file_name,
      p.sequence,
      p.bbox_x,
      p.bbox_y,
      p.bbox_width,
      p.bbox_height,
      p.confidence,
      p.predicted_image
    from images as i
    apply predict_image(i.file_name, i.image) as p;

    UDTFの実行には `apply` 演算子を使います。UDTFが返す表値をテーブルに見立ててJOINする演算子と理解してもらえればいいです。 `apply` を `join` に、UDTFの部分をテーブルに置き換えてみると分かりやすいかと思います。

    入力値は `images` テーブルに格納されているので、このテーブルの `file_name` 、 `image` 列をUDFの引数に渡しています。

    そして `select` の出力結果を `insert-select` 構文を使ってそのまま `prediction_results` テーブルに挿入しています。

    SQLの実行

    ではtgsqlコンソールでこのSQLを実行してみましょう。

    tgsql> insert or replace into prediction_results
         | select
         |   i.file_name,
         |   p.sequence,
         |   p.bbox_x,
         |   p.bbox_y,
         |   p.bbox_width,
         |   p.bbox_height,
         |   p.confidence,
         |   p.predicted_image
         | from images as i
         | apply predict_image(i.file_name, i.image) as p;
    start transaction implicitly. option=[
      type: OCC
      label: "tgsql-implicit-transaction2026-03-06 17:09:26.065+09:00"
    ]
    Time: 1.034 ms
    (9 rows merged)
    Time: 550.167 ms
    transaction commit(DEFAULT) finished implicitly.
    Time: 5.568 ms

    `prediction_results` テーブルに9行登録されたとありますね。また、UDFサーバー側のコンソール出力を見るとリクエストを受け取っていることを確認できるはずです。

    `prediction_results` テーブルの中身を見てみましょう。

    tgsql> select * from prediction_results;
    start transaction implicitly. option=[
      type: OCC
      label: "tgsql-implicit-transaction2026-03-06 17:09:35.389+09:00"
    ]
    Time: 0.803 ms
    [file_name: VARCHAR(*), sequence: INT, bbox_x: REAL, bbox_y: REAL, bbox_width: REAL, bbox_height: REAL, confidence: REAL, predicted_image: BLOB]
    [000000000009.jpg, 0, 311.58472, 0.0, 630.928, 238.57057, 0.91180897, blob@18]
    [000000000009.jpg, 1, 0.0, 190.48422, 619.58685, 480.0, 0.7473526, blob@19]
    [000000000009.jpg, 2, 263.9724, 231.55132, 571.2544, 479.85583, 0.6965864, blob@20]
    [000000000009.jpg, 3, 0.0, 8.265778, 439.20117, 345.323, 0.6897867, blob@21]
    [000000000009.jpg, 4, 325.6366, 408.4717, 424.6326, 479.71454, 0.48934183, blob@22]
    [000000000025.jpg, 0, 378.4063, 68.28476, 601.26447, 356.92383, 0.934395, blob@23]
    [000000000030.jpg, 0, 239.04587, 154.69746, 406.92102, 350.6156, 0.88299567, blob@24]
    [000000000030.jpg, 1, 200.10843, 31.734947, 461.2537, 352.72113, 0.333447, blob@25]
    [000000000034.jpg, 0, 0.77467346, 21.598114, 439.7422, 400.88147, 0.95875555, blob@26]
    (9 rows)
    Time: 3.264 ms
    transaction commit(DEFAULT) finished implicitly.
    Time: 3.372 ms
    

    ファイルごとの検出結果がちゃんと記録されています。ファイル `000000000009.jpg` は5か所検出していますね。

    保存されている画像も見てみましょう。5か所検出した `000000000009.jpg` に対する結果を見てみます。
    tgsqlでのblobデータのダウンロードはselect文の実行と同じトランザクション内で行う必要があるため、まずは `begin` コマンドで明示的にトランザクションを開始します。

    tgsql> begin;
    transaction started. option=[
      type: OCC
    ]
    Time: 0.698 ms

    続いてファイル `000000000009.jpg` に対する結果を検索するselect文を実行します。

    tgsql> select * from prediction_results where file_name = '000000000009.jpg';
    [file_name: VARCHAR(*), sequence: INT, bbox_x: REAL, bbox_y: REAL, bbox_width: REAL, bbox_height: REAL, confidence: REAL, predicted_image: BLOB]
    [000000000009.jpg, 0, 311.58472, 0.0, 630.928, 238.57057, 0.91180897, blob@36]
    [000000000009.jpg, 1, 0.0, 190.48422, 619.58685, 480.0, 0.7473526, blob@37]
    [000000000009.jpg, 2, 263.9724, 231.55132, 571.2544, 479.85583, 0.6965864, blob@38]
    [000000000009.jpg, 3, 0.0, 8.265778, 439.20117, 345.323, 0.6897867, blob@39]
    [000000000009.jpg, 4, 325.6366, 408.4717, 424.6326, 479.71454, 0.48934183, blob@40]
    (5 rows)
    Time: 3.35 ms

    `predicted_image` 列に出力されているblobポインタを使って `\store` コマンドで画像を保存します。保存後は `commit` または `rollback` を実行してトランザクションを終了しておきましょう。

    tgsql> \store blob@36 /path/to/000000000009_result.jpg
    tgsql> commit;
    transaction commit(DEFAULT) finished.
    Time: 3.997 ms

    保存した画像を確認してみます。YOLOによる検出結果がバウンディングボックスとして書き込まれていますね!

    画像

    ということでデータベース上のデータに対するAI処理の実行と結果のデータベースへの保存が SQL一本で 実行できてしまいました。面白いとは思いませんか?

    おわりに

    ここまでTsurugi UDFの機能を利用することでリモートに存在するAI処理をSQL一本で呼び出せることを見てきました。冒頭でも述べましたが他のRDMBSのUDF機能には見られないユニークな機能であると言えます。

    Tsurugiを中心に外部の様々なマイクロサービスを結びつけてシステムを構築できるようになるため、様々な可能性が広がっています。読者の皆様も是非Tsurugi UDFの色々な使い方を考えてみてください。


    その他の劔"Tsurugi"を使う際に役立つTips集はコチラ!

    次世代高速RDB劔"Tsurugi"は、オープンソースで公開中です。
    ぜひダウンロードしてみて、触ってみてください。

    ダウンロードはコチラから

    https://www.tsurugidb.com/

    あなたへのおすすめ