2025年3月14日金曜日

APEX 24.2で新しく追加された検索構成のOracleベクトル検索を使ってみる

Oracle APEX 24.2に検索構成として新たにOracleベクトル検索が追加されました。今回の記事では、このOracleベクトル検索を使ったAPEXアプリケーションを作成してみます。また、これまでのAPEXにあったOracle Textによる検索構成も作成し、それぞれ比較してみます。

検索する元データとして、以前の記事「APEX 24.2の動的アクション - AIによるテキストの生成 - を呼び出し法令を言い換えてもらう」の作業でデータベースにロードしている、e-Gov法令からダウンロードした観光(5_xml.zip)に関する法令を使用します。

作成したAPEXアプリケーションで、以下の文言で検索します。

民泊を規制している条文を教えて

検索結果にアイコンが含まれているカードは、ベクトル検索でヒットした条文になります。表示対象を5件にしているため、結果として5件表示されています。アイコンが表示されていないカードは無いので、Oracle Text検索でヒットした条文はありません。


以下の文言で検索します。

民泊

ベクトル検索だけが検索にヒットします。条文に「民泊」という単語は含まれていません。


ベクトル検索の結果より、法令では民泊のことを「住宅宿泊事業」と呼んでいることがわかります。「住宅宿泊事業」で検索します。

単語として住宅宿泊事業が含まれている条文がリストされました。


以下より、Oracleベクトル検索検索構成の作成と、それを使用したAPEXアプリケーションを作成します。

検索に使用するデータの準備から始めます。以前の記事に従って表JLAW_DATAに法令(対象は観光)のデータがロード済みとします。データがロードされていると本則をビューJLAW_LAW_MAINPROVISION_XVより参照できます。

select * from jlaw_law_mainprovision_xv;


本則のデータを投入した実表を、表JLAW_LAW_SENTENCESとして作成します。

create table jlaw_law_sentences as select * from jlaw_law_mainprovision_xv;


作成した表JLAW_LAW_SENTENCESには主キー列がありません。検索構成を作成する際に主キー列は必須なので、主キー列として列SIDを追加します。

alter table jlaw_law_sentences add (sid number generated by default on null as identity);

法令の条文は列"Sentence"に含まれています。この列"Sentence"から生成したベクトル表現(embeddingのことです、以下、ベクトル表現とします)を保存する列をSENTENCE_VECTORとして追加します。

alter table jlaw_law_sentences add (sentence_vector vector);

Oracle Text索引を列"Sentence"に作成します。Text索引に指定するレクサーを作成します。

begin
   ctx_ddl.create_preference('ja_vgram_lexer', 'JAPANESE_VGRAM_LEXER');
end;
/


ctx_ddlの実行権限がないというエラーが発生した場合は、管理者権限のあるユーザーでgrant文を実行します。

grant execute on ctx_ddl to <APEXワークスペース・スキーマ>;

Oracle Text索引JLAW_LAW_SENTENCES_CTXを列"Sentence"に作成します。

create index jlaw_law_sentences_ctx on jlaw_law_sentences("Sentence")
indextype is ctxsys.context
parameters('
    lexer ja_vgram_lexer
    sync (on commit)
');

全文検索を実行し、Oracle Text索引が正しく作成されたことを確認します。

select count(*) from jlaw_law_sentences where contains("Sentence", '観光') > 0


"Sentence"に保存されている条文のベクトル表現を生成し、列SENTENCE_VECTORに保存します。

ベクトル表現の生成に、ローカルのLM Studioを使用します。embeddingモデルとしてtext-embedding-granite-embedding-278m-multilingual@q8_0を使用します。(Ollamaでモデルgranite-embedding:278mを使用すると、なぜか1件だけエラーが発生してembeddingが生成されませんでした。)

LM Studioのローカル・サーバーはポート8080で接続の待受けをするように構成しています。


Oracle APEX 24.2のワークスペース・ユーティリティに、新たにベクトル・プロバイダが追加されています。このベクトル・プロバイダを使用して、テキストからベクトル表現を生成します。


ベクトル・プロバイダを開き、LM Studioのtext-embedding-granite-embedding-278m-multilingual@q8_0を呼び出すプロバイダを作成します。

作成をクリックします。


プロバイダ・タイプとして生成AIサービスを選択します。プロバイダ・タイプには生成AIサービスの他にデータベースONNXモデルカスタムPL/SQLがあります。

LM StudioのOpen AI互換APIを呼び出してベクトル表現を生成するため、AIプロバイダにはOpen AIを選択します。名前Local Granite 278m静的IDLOCAL_GRANITE_278Mとします。APEXはローカルのpodmanでコンテナとして動作させているため、LM Studioのembedding APIを呼び出すベースURLに以下を設定します。

http://host.containers.internal:8080/v1

LM Studioでは資格証明は不要ですが必須設定なので、資格証明として- 新規作成 -を選択し、APIキーdummy(文字列はなんでも良い)と入力します。

詳細AIモデルtext-embedding-granite-embedding-278m-multilingual@q8_0を設定します。

以上の設定を行い、接続のテストをクリックします。接続に成功することを確認し、作成をクリックします。


残念ながら不具合があり(APEX 24.2.3では修正されていません)、ベースURLはhttp://またはhttps://で始まる必要があります、というエラーが発生します。そのため、ベクトル・プロバイダを作成できません。


ワークアラウンドとして、生成AIサービスの代わりにカスタムPL/SQLとして等価なベクトル・プロバイダを作成します。

以下のファンクションGENERATE_EMBEDDINGを作成します。

create or replace function generate_embedding(
p_content in varchar2
)
return vector
as
C_ENDPOINT constant varchar2(80) := 'http://host.containers.internal:8080/v1/embeddings';
C_MODEL constant varchar2(80) := 'text-embedding-granite-embedding-278m-multilingual@q8_0';
l_request clob;
l_request_json json_object_t;
l_response clob;
l_response_json json_object_t;
l_data json_array_t;
l_embedding json_object_t;
l_array json_array_t;
l_vector clob;
v vector;
e_call_embeddings_failed exception;
begin
l_request_json := json_object_t();
l_request_json.put('input', p_content);
l_request_json.put('model', C_MODEL);
l_request := l_request_json.to_clob();
apex_web_service.set_request_headers('Content-Type', 'application/json');
l_response := apex_web_service.make_rest_request(
p_url => C_ENDPOINT
,p_http_method => 'POST'
,p_body => l_request
);
-- dbms_output.put_line(l_response);
if apex_web_service.g_status_code <> 200 then
raise e_call_embeddings_failed;
end if;
l_response_json := json_object_t(l_response);
l_data := l_response_json.get_array('data');
l_embedding := treat(l_data.get(0) as json_object_t);
l_array := l_embedding.get_array('embedding');
l_vector := l_array.to_clob();
-- dbms_output.put_line(l_vector);
v := to_vector(l_vector);
return v;
end;
/


先ほどと同じ手順でベクトル・プロバイダの作成まで進みます。

プロバイダ・タイプカスタムPL/SQLを選択します。名前Local Granite 278m静的IDLOCAL_GRANITE_278Mとし、ローカル埋込みカスタム・ファンクション名GENERATE_EMBEDDINGを指定します。

以上を設定し作成をクリックします。


ベクトル・プロバイダとしてLocal Granite 278mが作成できました。


これから表JLAW_LAW_SENTENCESの列"Sentence"からベクトル表現を生成し、列SENTENCE_VECTORに保存します。

その前に、管理サービスインスタンス管理セキュリティに設定されている最大Webサービス・リクエストの上限を確認しておきます。embeddingを生成するために、大量のAPIリクエストが発行される可能性があります。リクエスト数の制限にかからないように設定を変更しておきます。


この上限値はワークスペース単位でも設定できます。ワークスペース単位で制限している場合は、そちらも確認する必要があります。


ベクトル表現の更新には時間がかかるため、APEXのワークスペース・スキーマにsqlplusまたはSQLclといったコマンドライン・ツールで直接データベースに接続し、以下のスクリプトを実行します。APEXセッションを開始するためのアプリケーションIDなどは、対象のAPEXのワークスペースに存在する値に変更します。

set serveroutput on
declare
l_vector vector;
begin
/*
* ベクトル・プロバイダがあるワークスペースにある
* アプリケーションであればなんでも良い。ユーザーも
* 存在しているユーザーであれば良い。
*/
apex_session.create_session(
p_app_id => 100
,p_page_id => 1
,p_username => 'ADMIN'
);
for c in (
select sid, "Sentence" from jlaw_law_sentences
where sentence_vector is null
order by sid
-- fetch first 10 rows only
)
loop
begin
l_vector := apex_ai.get_vector_embeddings(
p_value => c."Sentence"
,p_service_static_id => 'LOCAL_GRANITE_278M'
);
-- l_vector := generate_embedding(c."Sentence");
update jlaw_law_sentences set sentence_vector = l_vector where sid = c.sid;
exception
when others then
dbms_output.put_line('sid = ' || c.sid);
raise;
-- null;
end;
commit;
end loop;
apex_session.delete_session;
end;
/

% sql wksp_apexdev/******@localhost/freepdb1



SQLcl: 金 3月 14 18:32:07 2025のリリース24.4 Production


Copyright (c) 1982, 2025, Oracle.  All rights reserved.


接続先:

Oracle Database 23ai Free Release 23.0.0.0.0 - Develop, Learn, and Run for Free

Version 23.6.0.24.10


SQL> set serveroutput on

SQL> declare

  2      l_vector vector;

  3  begin

  4      /*

  5      * ベクトル・プロバイダがあるワークスペースにある

  6      * アプリケーションであればなんでも良い。ユーザーも

  7      * 存在しているユーザーであれば良い。

  8      */

  9      apex_session.create_session(

 10          p_app_id => 100

 11          ,p_page_id => 1

 12          ,p_username => 'ADMIN'

 13      );

 14      for c in (

 15          select sid, "Sentence" from jlaw_law_sentences

 16          where sentence_vector is null

 17          order by sid

 18          -- fetch first 10 rows only

 19      )

 20      loop

 21          begin

 22              l_vector := apex_ai.get_vector_embeddings(

 23                  p_value => c."Sentence"

 24                  ,p_service_static_id => 'LOCAL_GRANITE_278M'

 25              );

 26              -- l_vector := generate_embedding(c."Sentence");

 27              update jlaw_law_sentences set sentence_vector = l_vector where sid = c.sid;

 28          exception

 29              when others then

 30                  dbms_output.put_line('sid = ' || c.sid);

 31                  raise;

 32                  -- null;

 33          end;

 34          commit;

 35      end loop;

 36      apex_session.delete_session;

 37  end;

 38* /


PL/SQLプロシージャが正常に完了しました。


SQL> 


使用しているembeddingモデルtext-embedding-granite-embedding-278m-multilingual@q8_0の最大コンテキスト長は512ですが、列"Sentence"にはそれを超える長さの条文が含まれます。そのため、生成されているembeddingは精度が高いとは言えません。今回の目的はAPEXの検索構成の確認なので、そのままにしています。(Ollamaでエラーが発生するのは、おそらくコンテキスト長を超える文字列を与えていることが理由と思われます。)

列SENTENCE_VECTORにnullがなければ、すべての行で列"Sentence"のベクトル表現が生成されています。

select count(*) from jlaw_law_sentences where sentence_vector is null;

SQL> select count(*) from jlaw_law_sentences where sentence_vector is null;


   COUNT(*) 

___________ 

          0 


SQL> 


表に保存されているデータから一気にembeddingを生成する際は、ベクトル・プロバイダを指定してAPEX_AI.GET_VECTOR_EMBEDDINGSを呼び出す必要はあまりありません。OpenAIやCohereなどのサービスは1回のAPIリクエストで複数のembeddingを生成できます。また、OpenAIではBatch APIを呼び出すことにより、低コストでembeddingを生成できます。

ベクトル・プロバイダはこれから作成する検索構成で使用します。ベクトル検索では、検索フィールドに入力した文字列のベクトル表現を取り出す必要があります。APEXの検索構成では、この処理を行なう設定としてベクトル・プロバイダを使います。

以上で、APEXアプリケーションを作成する準備は完了しました。

空のAPEXアプリケーションを作成します。名前e-Gov法令検索とします。


アプリケーションが作成されます。共有コンポーネントを開きます。


構成の検索を開きます。これは、検索構成のことです。


作成をクリックします。


最初に検索タイプOracleベクトル検索検索構成を作成します。名前e-Gov法令ベクトル検索とします。

へ進みます。


ベクトル・プロバイダLocal Granite 278m表/ビューの名前JLAW_LAW_SENTENCESを選択します。

へ進みます。


主キー列SID(Number)ベクトル列SENTENCE_VECTOR(Vector)タイトル列LawTitle(Varchar2)説明列Sentence(Clob)とします。アイコン・ソースイニシャルとします。

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


検索タイプOracleベクトル検索検索構成が作成されます。

設定静的IDLAW_VECTORとしておきます。ベクトル属性検索タイプがデフォルトでExact距離メトリックCosineになっています。ベクトル索引を作成している場合は、検索タイプとしてApproximateを選択できます。


列のマッピングカスタム列1SectionTitle(Varchar2)カスタム列2SubsectionTitle(Varchar2)カスタム列3ArticleTitle(Varchar2)を設定します。

アイコンと表示デフォルト結果行テンプレート結果のCSSクラスを設定することにより、検索結果の表示をカスタマイズできます。


同じ手順で、検索タイプOracle Textの検索構成を作成します。名前e-Gov法令テキスト検索とします。

へ進みます。


ソース表/ビューの名前JLAW_LAW_SENTENCESを選択します。

へ進みます。


列のマッピングベクトル列の指定がOracle Text索引列の指定に変わります。Oracle Text索引列Sentence(Clob)です。

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


検索タイプOracle Text検索構成が作成されます。静的IDLAW_TEXTを設定します。


列のマッピングカスタム列1、2、3をベクトル検索の検索構成と同じ設定にします。そして、アイコンと表示アイコン・ソース- アイコンなし -に変更し、ベクトル検索の検索結果とOracle Textの検索結果で、見分けがつくような表示にします。


以上で検索構成の作成は完了です。


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

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


検索ページを選択します。


作成するページの名前e-Gov法令検索とします。構成の検索E-Gov法令テキスト検索E-Gov法令ベクトル検索の双方をチェックします。

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


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

ページを少しだけカスタマイズします。今回は記事はベクトル検索がテーマなので、ソースの検索にある、E-Gov法令ベクトル検索E-Gov法令テキスト検索上に移動します。また、両方とも外観最大結果数に限定します。


以上でAPEXアプリケーションは完成です。

アプリケーションを実行すると、記事の先頭にあるような検索を実行できます。

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

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