空谷に吼える

ブロックチェーン/DLTまわりのなにかしらを書いていく所存

Blockchain TableにドキュメントのPDFファイルを保存してOracle Textで全文検索してみた

なんの話

  • Oracle DatabaseのBlockchain Table上にドキュメントファイル(PDF)をBLOB形式のカラムで保存する
  • そのドキュメントファイルの内容に対してOracle Textで全文検索する

イントロ

Blockchain Tableとドキュメントファイル

Oracle Databaseの機能にBlockchain Tableというのが追加されたよ、という話を前にしました。そんなん知らんという方はまずこちらのエントリとその中のスライドを見てみてください。

で、このBlockchain Tableは更新、削除に制限があるのと自動的にハッシュチェーンが貼られる以外にはおおよそふつうのテーブルです。監査ログや所有権移転などのイベントを記録する台帳的な使い方はもちろん、JSON、BLOBやCLOBのカラムも扱えるので、JSONドキュメント、WORDやPDFなどのドキュメントファイル、画像ファイル、長大なテキストのデータをBlockchain Tableに突っ込んでしまうこともできます(ただし大きいサイズのデータを扱うときは削除制約によってサイズが膨れ上がってにっちもさっちもいかなくなる事態にならないようDELETE日数制約などをくれぐれも気をつけて設計しましょう)。

いわゆるブロックチェーン/DLTは、分散したノード間でトランザクションに対してコンセンサスを取りながら分散台帳を更新していくという仕組み上、大きなサイズのデータを扱うのがとても苦手です。そのため「(契約書や請求書、貿易関連書類)企業間でやり取りするドキュメントをブロックチェーン/DLTで共有」といったようなユースケースでも、ドキュメントのファイル(例えばPDF)そのものを分散台帳上に書き込むのではなく、そのファイル自体はどこかのストレージやIPFSなどに格納したうえで、後の改ざん検知のためにファイルデータから計算したハッシュ値メタデータ(格納場所へのアドレス等)のみを分散台帳で保持しておくといったやり方がプラクティスになっています。ブロックチェーン/DLTとは別にストレージを用意して維持し、必要に応じてアクセス権なども制御しなければならないのでなかなか面倒です。

一方Blockchain Tableでは前述の通りPDFなどのドキュメントファイルそのものもテーブルに載せてしまえるので、そのようなブロックチェーン/DLTでのプラクティスに比べると格段に楽にドキュメントを扱えます。

「書類を改ざんできないかたちで、かつ、改ざんされていないことを確認可能なかたちで保存する必要がある」というニーズがありそうなところについてちょっと考えてみると、契約書、請求書、行政書類など、かなりありふれていることに気づきます。PDFファイルなどでの電子化もしているんだけど、(改ざんされていないことを担保するために)原本やらファイルから印字した紙やらも保管されがちな類の書類、といったところをイメージするとわかりやすいでしょうか。こういう類の書類も、PDFにしてBlockchain Tableに突っ込んでしまえば紙の保存は不要とできるかもしれませんね。

Oracle Textによるドキュメント全文検索

最近知ったんですがOracle DatabaseにはOracle Textという全文検索およびドキュメント分類のための機能がありますOracle Textについては日本語ではこちらのOracle Text 詳細解説のスライドに詳しく説明されていました。

テーブル上のBLOBに突っ込んだドキュメントファイルの中身を自動的にトークン化して全文検索できるそうで、サポートされているドキュメントのファイル形式もPDF、MS Office(Word、ExcelPowerPoint)などなど多岐にわたっており、使いこなせればなかなか便利そうです。

Oracle Textで全文検索 on Blockchain Table

ドキュメントを改ざんできないかたちで保存しておくだけでなく、その中身の全文検索もできたらなかなか便利ですよね。というわけでBlockchain Table上に突っ込んだPDFファイルの中身をOracle Textで全文検索できるかやってみました。

Blockchain Tableを作成

PDFを格納するBlockchain Tableを作成します。シンプルにドキュメント番号、ドキュメントのファイル名と、ドキュメントファイルそのものを格納する用のBLOBから成るテーブルにしています。また、テストなのでいつでもDROPできるよう、NO DROP UNTIL 0 DAYS IDLEを指定しました。

-- persistent_docsという名前のBlockchain Tableを作成
CREATE BLOCKCHAIN TABLE persistent_docs (doc_number NUMBER NOT NULL, doc_name VARCHAR2(256) NOT NULL, doc_content BLOB NOT NULL)
    NO DROP UNTIL 0 DAYS IDLE
    NO DELETE
    HASHING USING "SHA2_512" VERSION "v1"
;

BLOBにPDFファイルをロード

↑で作成したBlockchain TableにPDFファイルを保管していきます。後述の制約に引っかかってしまってこのステップが一番難しかったです。

SQL Developerでファイルを登録する方法

こちらのブログを参考にSQL DeveloperからBlockchain TableのBLOBにPDFファイルを登録しようとしたんですが、以下のエラーが出てしまい登録できません。

INSERT INTO "BCT"."PERSISTENT_DOCS" (DOC_NUMBER, DOC_NAME, DOC_CONTENT) VALUES ('5', 'ほげほげ.pdf', empty_blob())
UPDATE "BCT"."PERSISTENT_DOCS" SET DOC_CONTENT=? WHERE ROWID=:sqldevrowid AND ORA_ROWSCN=:sqldevrowscn
ORA-05715: ブロックチェーン表に対する操作は許可されません

変更を表"BCT"."PERSISTENT_DOCS"へ保存中にエラーが1つ発生しました:
行5: ORA-05715: ブロックチェーン表に対する操作は許可されません

どうもSQL Developerのファイルアップロード機能はまずBLOBの値をempty_blob()としてINSERTしたのち、ファイル内容をUPDATEで流し込むような動きをしており、これがBlockchain TableのUPDATE制約に引っかかってしまってエラーになっているようです。困ったなこれは。わしのPL/SQLちからではSQL Developerさんに助けてもらわないとアップロードできないのじゃが。。。困った。。。

ついったーで困った困ったとつぶやいていたところ救世主が現れました。

なるほどね。つまりSQL Developerのアップロード機能で通常のテーブルにファイルを登録したのち、その通常のテーブルからBlockchain Tableにデータをコピーさせちゃえばいいんじゃよという寸法なわけ。天才か。

というわけでBlockchain Tableと同一レイアウトで通常のテーブルを作成します。

-- temporary_docsという名前の同一レイアウトの通常テーブルを作成
CREATE TABLE temporary_docs (doc_number NUMBER NOT NULL, doc_name VARCHAR2(256) NOT NULL, doc_content BLOB NOT NULL);

そんで前述のブログの方法でSQL Developerのアップロード機能でPDFファイルを登録していきます。これはすんなり成功しました。

そしたら通常テーブル上のデータをBlockchain Tableにまるっとコピーしましょう。これもすんなり成功しました。

-- 通常テーブルからBlockchain Tableにコピー
INSERT INTO persistent_docs SELECT * FROM temporary_docs;
COMMIT;

ということで、ここまででBlockchain Tableにいくつか(画像では4つ)のPDFファイルが格納された状態を作り出すことができました。

f:id:gakumura:20210325105848p:plain
Blockchain Tableに4つのPDFが格納された状態

PL/SQLでやる場合は…

上記ではSQL Developerでファイルをアップロードしたんですが、アプリケーションからBlockchain Tableにファイルを保存する場合、PL/SQLを使うことになります。

通常、BLOBにファイル内容を書き込む場合は、DBMS_LOB.LOADBLOBFROMFILEという関数を使います。この関数を使う際の注意点があります。調べた限りいったんBLOBの値をempty_blob()でINSERTしてからそのポインタに向けてファイル内容を流し込んでいく、という方法が紹介されていることが多いんですが、おそらく(未検証です)この方法だと前述のSQL Developerでアップロード失敗したのと同様にUPDATE制約に引っかかって実行できないと思われます。

代わりに、以下のブログに紹介されているように、ファイル内容をバッファしておいてINSERT一発でBLOBにバッファを流し込む方法にすればおそらく実行できると思います(未検証です)。

renaps.com

Oracle Textの準備

PDFファイルを登録できたら、そのPDFファイルの中身を対象に全文検索できるようにOracle Textをセットアップしていきます。こちらの超入門記事を参考にしながらやってみました。

下準備としてPDFファイルを格納したBlockchain TableのオーナーユーザーにCTXAPP ロールをGRANTしておきましょう。

-- Blockchain TableオーナーにCTXAPPロールを付与…SYSDBAなどで実行
GRANT ctxapp TO bctableowner;

次に、今回は日本語のドキュメントを全文検索したいので日本語LEXERを設定します。LEXERとは?が気になる方は上のほうで紹介しているOracle Text 詳細解説のスライドを読んでみてください。

-- 日本語検索用のLEXERを設定
BEGIN
    ctx_ddl.create_preference('my_lexer','JAPANESE_VGRAM_LEXER');
END;

そんで、Oracle Text用のIndexを貼ります。これも気になる方はOracle Text詳細解説のスライドを参照。

-- ORACLE TEXT用INDEXをBlockchain TableのBLOBを対象に作成
CREATE INDEX doc_idx ON persistent_docs(doc_content) 
    INDEXTYPE IS ctxsys.context
    PARAMETERS('lexer my_lexer')
;

これでOracle Textの準備もOKです。

なお、このIndexは対象データに更新があった場合に自動では同期してくれない*1ので、Blockchain Tableに新しくファイルを追加した場合は以下のように手動同期してやる必要があります。

-- 対象データに追加、削除、更新があった場合にはINDEXを手動で更新(同期)する
BEGIN
    ctx_ddl.sync_index('doc_idx');
END;

全文検索を試してみる

ここまでで準備が完了したので、Oracle TextによるPDFファイル全文検索 on Blockchain Tableをやってみましょう。と言ってもBlockchain Tableは参照する分にはふつうのテーブルと何も変わりないので、ふつうにOracle Textの機能を使うだけです。

シンプルに指定した文字列を含むドキュメントを検索してみます。以下のSQLになります。

-- ORACLE TEXTを使った全文検索
SELECT * FROM persistent_docs WHERE CONTAINS (doc_content,'なんちゃら') > 0;

できました。めでたしめでたし。

f:id:gakumura:20210325112329p:plain
Oracle Text全文検索 on Blockchain Tableの結果

まとめ

というわけでBlockchain TableへのPDFファイルの格納、およびOracle Textを使った全文検索をやってみたところ、無事成功しました。ファイルをBLOBに書き込むところだけちょっとBlockchain Tableならではのコツが必要でしたが、あとのところは全然ふつうでしたね。

今回はPDFファイルのBlockchain Tableへの保存を試してみましたが、Blockchain Table自体にドキュメントの不変性、ファイナリティを表現する特性が備わっているので、PDFファイルだけでなく、MS OfficeのWordやらリッチテキストやらの編集可能フォーマットのファイルをそのまま突っ込んでしまってもよさそうです。

このようにOracle Databaseの多彩で高度なお役立ち機能(のほとんど)がふつうに使えるよ、というのがBlockchain Tableのいいところだと思います。わたしはあまりOracle Databaseちからが高くないのでごくごく基本的なことしか思いつかないし試してみられないんですが、Oracle Databaseに自信ありな方々はBlockchain Tableもすぐ使いこなせると思うので、ぜひぜひ色々試してみてください。

*1:自動同期にすることもできました。Indexを作る際に、PARAMETERS('lexer my_lexer sync (on commit)');のように指定すれば、コミット時に同期が走るようになります。