2025年7月16日水曜日

DifyがOracle Databaseに作成したナレッジベースをAPEXアプリケーションで検索する

Difyのナレッジベースの格納先として、比較的簡単にOracle Databaseを利用できます。Difyはナレッジの検索に、Oracle Databaseが提供する全文検索とベクトル検索を活用します。

DifyはAI向けの開発ツールなだけあって、ナレッジベースへのドキュメントの投入、チャンクの分割およびエンべディングの生成など、一通りの作業を開発ツールのUIで実行できます。Dify自体は「Difyプラットフォームに組み込まれたナレッジベース機能とテキスト検索・取得メカニズムには制約があり、検索結果を簡単に変更することができません。」(ドキュメントのここ)と言っていて、高い要求には外部ナレッジベースの連携機能を提供しています。

そのため、Oracle Databaseとの連携という意味では、Oracle Databaseをナレッジベースとして呼び出せる外部連携APIを実装するという手段もあるかとは思います。今回は、手早くテキスト検索とベクトル検索を実装するために、DifyのナレッジベースをAPEXアプリケーションで検索してみます。

Difyの環境は記事「Oracle Database 23ai Freeをベクトル・ストアとしたDifyのローカル環境を作成する」にて紹介している手順で作成しています。DifyおよびOracle Databaseのコンテナは、macOSのpodmanで実行しています。

テキスト・エンべディングのモデルとして、Ollamaのbge-m3:latestを使用するように設定済みとします。


以下よりDifyでナレッジベースを作成して、APEXアプリケーションから検索するまでの作業を紹介します。

最初にDifyでナレッジベースを作成します。

ナレッジタブを開き、ナレッジベースを作成を実行します。


今回はテキストファイルからインポートしてナレッジベースを作成します。

参照をクリックして、アップロードするテキストファイルを選択します。


ファイルを選択したのち、次へ進みます。


APEXアプリケーションから検索できるように、インデックス方法高品質検索設定ハイブリッド検索を設定します。エンべディングの生成に使用される埋め込みモデルは、APEXアプリケーションでも使用するモデルになるため、確認しておきます。

検索ハイブリッド検索を設定することにより、エンべディングの生成とOracle Text索引の作成の両方が行われます(実際にはベクトル検索を選んだ時もOracle Text索引が生成されるように見受けられますが、ハイブリッド検索を選んだ方が確実です)。

その他のパラメータは必要に応じて調整します。

保存して処理をクリックします。


チャンクやエンべディングの生成が行われ、ナレッジベースの保管先であるOracle Databaseに保存されます。

ドキュメントに移動します。


アップロードされたドキュメントが利用可能になっていることを確認します。

検索テストを開きます。


ソーステキストに適当な文章を入力し、テスト中をクリックします。

チャンクが検索できたら、とりあえずナレッジベースの作成は完了です。


ナレッジベースの保存先になっているOracle Databaseのスキーマで、SQLを実行します。

今回はOracle APEXのワークスペース・スキーマにナレッジベースを保存しているので、SQLの実行にSQLワークショップSQLコマンドを使用します。

最初に作成されたナレッジベースの表を探します。以下のSELECT文を実行します。
select object_type, object_name, to_char(created,'RR-MM-DD HH24:MI:SS') created from user_objects
where object_type = 'TABLE' and object_name like 'EMBEDDING_VECTOR_INDEX%NODE' order by created desc

ナレッジベースの表の名前は、EMBEDDING_VECTOR_INDEXで始まりNODEで終わるようです。これは現状がこうだった、ということで今後変わる可能性はあります。

この表名を参照してAPEXアプリケーションを作ると、表名が変わった時に面倒なので、ビューとしてDIFY_KB_TESTを作成します。元表の名前は上記のSELECT文で見つけたOBJECT_NAMEで置き換えます。主キー列はID、チャンクが保存されている列はTEXT(この列にOracle Text索引が作成されています)、エンべディングが保存されている列はEMBEDDINGです。
create or replace view dify_kb_test
as
select id, json_value(meta, '$.source') source, text, embedding from 
EMBEDDING_VECTOR_INDEX_A6C2A40F_41B4_4D1C_B543_3EF634CC408A_NODE;

エンべディングが保存されている列のVECTOR_INFOを確認します。
select column_name, vector_info from user_tab_cols where table_name = 'DIFY_KB_TEST' and data_type = 'VECTOR'
VECTOR(*,*,DENSE)となっているため、エンべディングの次元数や精度を確認するには、表に保存されているエンべディングを参照する必要があります。


ビューDIFY_KB_TESTより1つだけエンべディングを取り出し、次元数と精度を確認します。
select vector_dimension_count(embedding) count, vector_dimension_format(embedding) format 
from dify_kb_test where embedding is not null fetch first 1 rows only;

count1024formatFLOAT64と返されました。APEXアプリケーションで検索文章からエンべディングを生成するときに、この次元数と精度を指定する必要があります。

以上で、Difyのナレッジベースとして作成された表について確認ができました。

これからAPEXでアプリケーションを作成します。

最初にエンべディングを生成するために、Ollamabge-m3:latestを呼び出すベクトル・プロバイダを作成します。

データベースに以下のファンクションgenerate_embeddingを作成します。

OllamaのようなローカルLLMをベクトル・プロバイダとする場合はPL/SQLによるカスタム・ファンクションが必要です。ONNXモデル、Oracleの生成AIサービス、OpenAIやCohereであれば、設定だけでベクトル・プロバイダを作成することができます。しかし、Difyがエンべディングを生成する際に選んだ精度とOracle側が選んだ精度が異なる場合は、これらの組み込みのプロバイダでもPL/SQLによるカスタム・ファンクションの作成が必要になるでしょう。

Difyと同じモデルを呼び出せないため、ONNXモデルは対象外になります。

作成したファンクションgenerate_embeddingを呼び出すベクトル・プロバイダを作成します。

共有コンポーネントベクトル・プロバイダを開きます。


作成済みのベクトル・プロバイダの一覧画面で、作成をクリックします。

プロバイダ・タイプカスタムPL/SQLを選択します。名前Ollama bge-m3:latest静的IDollama_bge_m3_latestとします。

ローカル埋込みカスタム・ファンクション名に先ほどPL/SQLで作成したファンクションgenerate_embeddingを指定します。

以上で、ベクトル・プロバイダを作成します。


ベクトル・プロバイダとしてOllama bge-m3:latestが作成されました。


空のAPEXアプリケーションを作成します。名前Difyナレッジベース検索とします。


アプリケーションが作成されたら、共有コンポーネント構成の検索(検索構成)を開きます。


作成済みの検索構成が一覧されます。作成をクリックし、新規に検索構成の作成を始めます。


最初にベクトル検索の検索構成を作成します。

名前ベクトル検索とします。検索タイプOracleベクトル検索を選択します。

へ進みます。


ベクトル・プロバイダに先ほど作成したOllama bge-m3:latestを選択します。ソース・タイプ表/ビューの名前としてDIFY_KB_TESTを選択します。

へ進みます。


主キー列IDベクトル列EMBEDDINGタイトル列SOURCE説明列TEXTを選択します。アイコン・ソースアイコン・クラスを選択し、アイコンCSSクラスfa-chatbotを指定します。

以上で検索構成の作成をクリックします。


検索構成としてベクトル検索が作成されます。編集画面が開きます。

必須ではありませんが、後から参照しやすいように静的IDVECTOR_SEARCHに変更します。

変更の適用をクリックします。


検索構成の一覧画面に戻ります。続けてテキスト検索の検索構成を作成します。


名前テキスト検索とします。検索タイプOracle Textを選択します。

へ進みます。


ソース・タイプ表/ビューの名前としてDIFY_KB_TESTを選択します。

へ進みます。


主キー列IDOracle Text索引列TEXTタイトル列SOURCE説明列TEXTを選択します。アイコン・ソースアイコン・クラスを選択し、アイコンCSSクラスfa-fontを指定します。

以上で検索構成の作成をクリックします。


検索構成としてテキスト検索が作成されます。編集画面が開きます。

必須ではありませんが、後から参照しやすいように静的IDTEXT_SEARCHに変更します。

変更の適用をクリックします。


以上で検索構成としてベクトル検索テキスト検索が作成されました。


検索ページを作成します。

ページの作成をクリックします。


コンポーネントとして検索ページを選択します。


作成するページの名前ナレッジベース検索とします。ページ・モード標準です。構成の検索テキスト検索ベクトル検索の双方をチェックします。

以上でページの作成をクリックします。


検索ページが作成されます。

今回は検索ができることが確認できるだけで良いので、検索結果テキスト検索ベクトル検索の双方を選択し、外観最大結果数に制限します。

以上で、ページの保存と実行をクリックします。


検索ページが開かれます。

適当な文章や文字を入力して、ベクトル検索とテキスト検索の双方で検索結果が返されることを確認します。



Difyで全文検索を行った際に以下のエラーが発生しました。

** Resource punkt_tab not found. Please use the NLTK Downloader to obtain the resource: >>> import nltk >>> nltk.download('punkt_tab')  For more information see: https://www.nltk.org/data.html Attempted to load tokenizers/punkt_tab/english/ Searched in: - '/root/nltk_data' - '/app/api/.venv/nltk_data' - '/app/api/.venv/share/nltk_data' - '/app/api/.venv/lib/nltk_data' - '/usr/share/nltk_data' - '/usr/local/share/nltk_data' - '/usr/lib/nltk_data' - '/usr/local/lib/nltk_data' **

原因をClaudeに聞いたところ、以下の回答が得られました。

「このエラーは、Difyのナレッジベースでテキスト処理を行う際に、NLTK(Natural Language Toolkit)のpunkt_tabリソースが見つからないために発生しています。」

対処としてコンテナdocker-api-1にpunktにpunkt_tabをインストールしました。

% podman exec -it docker-api-1 bash

root@4d7fbfca0440:/app/api# python

Python 3.12.11 (main, Jul  1 2025, 10:08:28) [GCC 12.2.0] on linux

Type "help", "copyright", "credits" or "license" for more information.

>>> import nltk; nltk.download('punkt_tab')

[nltk_data] Downloading package punkt_tab to /root/nltk_data...

[nltk_data]   Unzipping tokenizers/punkt_tab.zip.

True

>>> exit

Use exit() or Ctrl-D (i.e. EOF) to exit

>>> 

root@4d7fbfca0440:/app/api# 


punkt_tabをインストールすると、エラーの発生は解消しました。

今回の記事は以上になります。

単純にこのまま使えるというものではありませんが、少なくても検索はできることが確認できました。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/dify-knowlegebase-search.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。

2025年7月15日火曜日

Autonomous DatabaseでPythonで記述したファンクションを表関数として実行する

Autonomous DatabaseでOML4PyのSQL APIを呼び出し、Pythonで記述したファンクションを表関数として実行してみます。オンプレミスのOracle Databaseでも同様の作業を実施し、「Oracle DatabaseでPythonで記述したファンクションを表関数として実行する」として記事を公開しています。

Autonomous Databaseでもオンプレミスと同じくSQL APIとしてpyqEval、pyqTableEval、pyqRowEval、pyqGroupEvalが提供されています。Autonomous Databaseではこの他にpyqIndexEvalが追加されています。また、Autonomous Databaseの場合、オンプレミスと比べると以下の違いがあります。
  1. 認証トークンの取得時にスキーマのパスワードが必要です。認証トークンの取得方法が特殊なため、APEXのWeb資格証明にパスワードを保存できません。認証トークンの期限はデフォルトで3600秒で、ユーザーは3600秒ごとに再認証する必要があります。しかし、そもそもAPEXのアプリケーションを使うユーザーは、通常スキーマのパスワードを知りません。
  2. SQL APIの処理に時間がかかるためか、非同期ジョブというオンプレミスでは提供されていない機能が追加されています。PAR_LSTに与えるJSONの属性oml_async_flagtrueを指定することにより、SELECT文の実行と結果の取得を異なるタイミングで実施できます。
このような違いのためAutonomous Databaseでは、以下の方針でOML4PyのSQL APIを呼び出すことにします。
  1. SQL APIを呼び出す処理は開発者が実施し、表関数の結果を実表にコピーします。
  2. APEXアプリケーションでは実表を扱い、SQL APIを直接呼び出すことはしません。
本記事では、すでに作成済みのAPEXアプリケーションを使ってOML4PyのSQL APIをAutonomous Databaseから呼び出してみます。対象となるPythonスクリプトは、OML4Py User’s Guideの以下のセクションに記載されています。


作成済みのAPEXアプリケーションは以下よりダウンロードできます。

https://github.com/ujnak/apexapps/blob/master/exports/oml4py-sqlapi-admin.zip


事前準備



前提条件ですが、Autonomous DatabaseではAPEXのワークスペースとしてAPEXDEVが作成済みで、スキーマとしてWKSP_APEXDEVがデフォルト・パーシング・スキーマとして割り当てられている状態とします。異なる名前のスキーマを使用する場合は、以下の説明に含まれるWKSP_APEXDEVの部分を置き換えてください。

OML4PyのSQL APIを呼び出すために必要な権限を、スキーマWKSP_APEXDEVに与えます。

SQL Developer Webに管理者ユーザーADMINでサインインし、管理データベース・ユーザーを開きます。

スキーマWKSP_APEXDEVの編集画面を開き、OMLオンにします。また、認証トークンの取得に、スキーマのパスワードが必要です。APEXワークスペースと同時に作成されたスキーマにはパスワードが設定されていないので、未設定であればパスワードも設定します。


スキーマWKSP_APEXDEVにネットワークACLを追加します。

SQLワークシートを開きます。

13.7.1 Access and Authorization Procedures and Functionsの手順に沿って作業を進めます。

対象となるホスト(ドメイン)として、adb.<リージョン名>.oraclecloudapps.comを与えます。現在アクセスしているURLから、リージョン名を見つけることができます。


pyqAppendHostAceを呼び出して、スキーマWKSP_APEXDEVにネットワークACLを追加します。
begin
    pyqAppendHostAce('WKSP_APEXDEV','adb.us-ashburn-1.oraclecloudapps.com');
end;

pyqGetHostAceを呼び出して設定を確認します。
DECLARE 
    hostname VARCHAR2(4000);
BEGIN
    hostname := pyqGetHostACE('WKSP_APEXDEV'); 
    DBMS_OUTPUT.put_line ('hostname: ' || hostname);
END;
pyqAppendHostAceではドメイン名を渡していますが、pyqGetHostAceではAutonomous Databaseの接続先であるホスト名が返されます。


このホスト名は、アクセス・トークンを取得するURLのホスト名になります。

OML4Pyのサンプル・スクリプトを実行する際に使用する環境を作成します。

OMLノートブックにサインインします。

https://<テナントID>-<データベース名>.adb.us-ashburn-1.oraclecloudapps.com/oml

ユーザー名パスワードとして、管理者ユーザーADMINとそのパスワードを指定します。

サインインをクリックします。


condaを使ってPythonを実行する環境を作成します。

スクラッチパッドを開きます。


ひとつめの段落に以下を記述します。作業実施時点でOML4Pyが採用しているPythonのバージョンを指定します。2025年7月15日現在では3.12.6です。

%conda
create -n seaborn python=3.12.6 seaborn

ふたつめの段落に以下を記述します。

%conda
upload seaborn --overwrite --description 'Python package for seaborn' -t python 3.12.6 -t application OML4PY

以上でスクラッチパッドを実行します。


Pythonの実行環境としてseabornが作成され、その後にオブジェクト・ストレージにアップロードされました。

これで、環境seabornをSQL APIの引数env_nameに指定できるようになりました。SQL APIの引数env_nameにseabornを指定したときは、SQL APIの引数scr_nameで指定されたスクリプト内で、環境seabornにロードされているパッケージを呼び出すことができます。


以上で準備は完了です。


APEXアプリケーションのインポート



APEXのワークスペースにサインインします。

OML4PyのSQL APIを呼び出すAPEXアプリケーションをインポートします。

https://github.com/ujnak/apexapps/blob/master/exports/oml4py-sqlapi-admin.zip


ダウンロードしたファイルoml4py-sqlapi-admin.zipを選択します。

へ進みます。


アプリケーションのインストールをクリックします。


サポートするオブジェクトのインストールをクリックします。サポートするオブジェクトのインストールとして、SQL APIから呼び出すPythonスクリプトの作成と、SQL APIの実行結果を保存する表の作成が行われます。


APEXアプリケーションがインポートされました。

アプリケーションを実行します。


APEXアプリケーションにサインインすると、以下のホーム・ページが開きます。


以上でAPEXアプリケーションのインポートが完了しました。


OML4Py SQL APIの実行



OML4Pyのサンプル・コードを実行する前に、認証トークンの取得とセットを行います。

スキーマのパスワードを入力し、ボタンGet Token / Set Auth Tokenをクリックします。

ボタンをクリックすると以下のコードが実行され、認証トークンの取得と設定が行われます。

ページが送信されたのち、Is Token SetTRUEに変われば、正常に認証が完了しています。


認証トークンの有効期限は3600秒なので、Is Token SetTRUEでも有効期間が過ぎるとSQL APIの呼び出しが失敗するようになります。その場合はパスワードを入力し、再認証します。

(REST API for Embedded Python ExecutionのAuthenticateのセクションにRefresh the Tokenとしてリフレッシュ・トークンを使う方法が記述されていますが、そもそもgrant_type=passwordで認証した際に返されるのがaccessTokenのみでrefreshTokenが含まれていません。それに加えてcurlの呼び出し例もAuthorizationヘッダーに与えるトークンとrefresh_tokenに与えるトークンが同じなので、どこかが間違っているように思います。)

最初にPythonファンクションpyqFun1を、SQL APIのpyqEvalから呼び出してみます。

ナビゲーション・メニューからPython Scriptsを開きます。OML4PyのUser's Guideに記載されているSQL APIから呼び出すPythonスクリプトは、アプリケーションのインポート時に作成しています。


Pythonスクリプトの編集ページにボタンFormatを作成しています。Pythonのコード・フォーマッターはPythonで記述されているものが多いので、ページにPyodideとautopep8をロードして、Pythonのフォーマッターを実行しています。

ページ・プロパティJavaScriptファイルURLに以下を記述しています。

https://cdn.jsdelivr.net/pyodide/v0.28.0/full/pyodide.js

ファンクションおよびグローバル変数の宣言で、フォーマッターを記述しています。


ページ・ロード時に実行に以下を記述します。

main();


ボタンFormatをクリックしたときに、動的アクションJavaScriptファンクションformatCode()を呼び出しています。


ボタン作成または変更の適用をクリックしたときに、pyqScriptCreateを呼び出してPythonスクリプトを作成しています。

declare
    l_script clob;
begin
    l_script := :P3_SCRIPT;
    l_script := replace(:P3_SCRIPT, CHR(13)||CHR(10),CHR(10)); -- remove CR
    sys.pyqScriptCreate(
        v_name      => :P3_NAME,
        v_script    => l_script,
        v_global    => false,
        v_overwrite => ( :REQUEST = 'SAVE' )
    );
end;

ボタンpyqFun1 - JSONをクリックすると、以下のコードが実行されます。SQL APIの結果を保存する表PYQ_E01の内容を削除したのち、SQL APIの検索結果を表PYQ_E01にコピーしています。SQL APIの引数OUT_FMTの指定に依存して、コピー対象に含まれる列は変わりますが、他の同期呼び出しの処理もほとんど同じです。
begin
    delete from pyq_e01;
    /* copy SQL API result into the table */
    insert into pyq_e01(name, value)
    select name, value from table(
        pyqEval(
            par_lst => '{"oml_service_level":"LOW"}',
            out_fmt => 'JSON',
            scr_name => 'pyqFun1'
        )
    );
end;

SQL APIの実行結果を実表にコピーしたのち、ブランチによって、その表をソースとした対話モード・レポートのページへリダイレクトします。


結果としてボタンpyqFun1 - JSONをクリックすると、以下のようにSQL APIの実行結果を表示する対話モード・レポートのページが開きます。


後は上から順番にSQL APIからPythonスクリプトを呼び出していきます。create_iris_tableの実行で作成される表IRISや、myLinearRegressionModelの実行で作成されるモデルlinregrsample_iris_tableで作成される表SAMPLE_IRISに依存している後続の処理があるため、実行する順番には注意が必要です。

test_seaborn_noinptest_seaborn_inpmygroupcountといった処理では、condaで作った環境seabornにロードしているパッケージmatplotlibを使用して、チャートを出力しています。


SQL APIの非同期ジョブの呼び出し方法を説明します。

最初に以下のコードで非同期ジョブをサブミットします。SQL APIの引数PAR_LSTに与えるJSONに"oml_asyc_flag": trueを含めます。SQL APIの結果の列VALUEにJOB IDを含んだURLが返されるので、URLからJOB IDを取り出しています。



非同期ジョブの開始時に得られたJOB IDより、ジョブのステータスを確認します。以下のコードを実行します。

非同期ジョブが実行中の場合は、列VALUEにjob is still runningが返されます。


非同期ジョブが終了したら、以下のコードを実行して結果を実表にコピーします。




最後に実表の対話モード・レポートのページにリダイレクトし、処理結果のレポートを表示します。


OML4Py Release 2-23aiのUser's Guideの13.7.3.4 Asynchronous Job Exampleに記載されているサンプル・コードは以下になっています。
set long 150 
  select * 
  from table(pyqGroupEval(
  inp_nam => 'IRIS',
  par_lst => '{"oml_input_type":"pandas.DataFrame",
  "oml_async_flag":true, "oml_graphics_flag":true, 
               "modelName":"linregr", "datastoreName":"pymodel"}', 
  out_fmt => NULL,
  grp_col => 'Species',
  ord_col => NULL,
  scr_name => 'linregrPredict',
  scr_owner => NULL 
)); 
スクリプトlinregrPredictはPNG出力をするコードを含んでいません。この部分については、以下のPythonスクリプトをlingrPredict2として作成し、上記のlinregrPredictを置き換えています。

以上でOML4PyのUser's Guideに記載されている、Autonomous Database向けのSQL APIのサンプルはすべて実行できました。

今回の記事は以上になります。

OML4PyのSQL APIを呼び出していると「ORA-20100: Resource usage exceeded for free tier」というエラーが発生しました。


OMLノートブックにもアクセスできなくなります。


Free TierではOMLの操作で利用できるVMのリソースは多くはありません。24時間を基準に利用量を計測しているとのことなので、24時間程度使用せずに待てば利用可能なリソースは回復するみたいです。

今回の記事は以上です。

Oracle APEXのアプリケーションからPythonのスクリプトを呼び出す際の参考になれば幸いです。