Qt 5 の未来は明るいブログ

QtSql の SQLITE で拡張を利用する方法

Published: 2018-01-21

この記事は 2018年1月20日に開催された Qt 勉強会 @ Tokyo #55 で学んだ内容になります。

経緯と概要

勉強会の最後にした成果発表の内容を主催者の @hermit4 さんがうまくまとめてくれていました。

QtSql の SQLITE で拡張を利用する方法

画像を特徴ベクトルの変換は 転移学習 / Deep Features [TensorFlowでDeep Learning 12] を参考に C++ で TensorFlow の inception-v3 の学習済みモデルを利用して 2048次元のベクトル(8KB)に変換しています。

特徴ベクトルの類似検索は Deep Features と Faiss [TensorFlowでDeep Learning 15] を参考に、Faiss を利用して効率よく行っていたのですが、ウェブサービス化するにあたり、1枚 8KB * 1M 枚のデータをメモリ上で扱うと 8GB 以上のメモリが必要になってしまうのが悩みどころでした。

今回は速度はある程度は妥協できる代わりに精度は妥協できないモノを作っているため、Brute Force にして SQLITE からすべての画像の特徴ベクトルを取得しユークリッド距離をアプリのコードで計算するようにしたところ「多少」マシにはなったのですが、なんとなくデータベースのレイヤーで計算したほうが良さそうだと思ったので SQLITE の拡張を書きました。

/* Add your header comment here */
#include <stdio.h>
#include <sqlite3ext.h> /* Do not use <sqlite3.h>! */
SQLITE_EXTENSION_INIT1

/* Insert your extension code here */
static void euclidean(sqlite3_context *context, int argc, sqlite3_value **argv)
{
    int i;
    float *x1, *x2;
    double distance = 0;
//    printf("argv[0]:%d, %d, %d\n", sqlite3_value_type(argv[0]), SQLITE_BLOB, sqlite3_value_bytes(argv[0]));
//    printf("argv[1]:%d, %d, %d\n", sqlite3_value_type(argv[1]), SQLITE_BLOB, sqlite3_value_bytes(argv[1]));
    if( sqlite3_value_type(argv[0])!=SQLITE_BLOB ) return;
    if( sqlite3_value_type(argv[1])!=SQLITE_BLOB ) return;
    if( sqlite3_value_bytes(argv[0])!=sqlite3_value_bytes(argv[1]) ) return;
    x1 = (float *)sqlite3_value_blob(argv[0]);
    x2 = (float *)sqlite3_value_blob(argv[1]);
    for (i = 0; i < sqlite3_value_bytes(argv[0]) / 4; i++) {
        distance += (x1[i] - x2[i]) * (x1[i] - x2[i]);
    }
//    printf("%lf\n", distance);
    sqlite3_result_double(context, distance);
}

#ifdef _WIN32
__declspec(dllexport)
#endif
/* TODO: Change the entry point name so that "extension" is replaced by ** text derived from the shared library filename as follows:  Copy every ** ASCII alphabetic character from the filename after the last "/" through ** the next following ".", converting each character to lowercase, and ** discarding the first three characters if they are "lib". */
int sqlite3_euclidean_init(
  sqlite3 *db, 
  char **pzErrMsg, 
  const sqlite3_api_routines *pApi
){
  int rc = SQLITE_OK;
  SQLITE_EXTENSION_INIT2(pApi);
  (void)pzErrMsg;
  rc = sqlite3_create_function(db, "euclidean", 2, SQLITE_UTF8 | SQLITE_DETERMINISTIC, 0, euclidean, 0, 0);
  return rc;
}

これを Qt から使いたいのですが、単純に QSqlQuery で SELECT load('euclidean') を呼んでも失敗します。

Qt Forum の Low-level SQLite (load_extension) という記事を参考に以下のようなコードで拡張をロードすることができました。

    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName("/home/tasuku/food.sqlite");
    db.open();

    sqlite3_initialize();
    sqlite3 *db_handle = *static_cast<sqlite3 **>(db.driver()->handle().data());
    qDebug() << db_handle;
    if (db_handle != 0) {
        qDebug() << sqlite3_enable_load_extension(db_handle, 1);
        qDebug() << sqlite3_load_extension(db_handle, "/home/tasuku/euclidean", 0, 0);
    }

これにより多少速度が改善し、メモリの使用量も若干改善しました。