2025年6月3日火曜日

Oracle REST Data ServicesのRDF Graph APIを呼び出してSPARQLを実行する

Oracle Databaseは以前からRDF Graphをサポートしています。RDF Graphに関するOracle Database 23aiのドキュメントとして、以下が提供されています。

Graph Developer's Guide for RDF Graph, Release 23

このドキュメントのPart IIにRDF Graph Serverが紹介されています。しかし、RDF Graph Serverのダウンロード・ページとして以下が示されていますが、RDF GraphについてはEclipse RDF4Jのアダプタのみがダウンロード可能で、RDF Graph Serverは含まれていません。

RDF Graph Serverはどうなったのか不明だったのですが、ユーザー・インターフェース(Query UI)を除いた機能は、Oracle REST Data Servicesに実装されていました。

Oracle REST Data Services 23.4からOracle REST Data Services APIにRDF Graphが追加されています。
https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/25.1/orrst/api-rdf-graph.html

Oracle APEXを実行するにあたってOracle REST Data Servicesは必須のコンポーネントなので、APEXの環境があればSPARQLも実行できます。SPARQLからSQLへの変換はORDSで実施しているようで、オラクル・データベースでJavaVMを有効にしなくても(Autonomous DatabaseではJavaVMはデフォルトで無効です)、SPARQLを実行できます。ただし、APEXからSPARQLを実行するには、データベースからHTTPリクエストを発行してORDSを呼び出す必要があります。

今回はOracle REST Data ServicesのRDF Graph APIを呼び出すAPEXアプリケーションを作成し、SPARQLクエリを実行します。


このAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sparql-operations.zip

アプリケーションはページ・アイテムに設定した値を引数として、ボタンのクリックでREST APIを呼び出す簡単な作りになっています。

このAPEXアプリケーションをインポートすると、Web資格証明としてRDF Graph Credリモート・サーバーとしてRDF Graph ORDS Endpointが作成されます。このアプリケーション内でRDF Graph APIを呼び出す際に使用するWeb資格証明およびエンドポイントになります。


アプリケーションのインストール後にワークスペース・ユーティリティリモート・サーバーおよびWeb資格証明を開いて、これらの設定値を更新する必要があります。

リモート・サーバーRDF Graph ORDS Endpointの更新は、ワークスペース・ユーティリティリモート・サーバーから行います。


リモート・サーバーのRDF Graph ORDS Endpointを開きます。


一般エンドポイントURLを更新します。今回はAutonomous Databaseを対象にRDFグラフの保存とSPARQLの実行を行うため、エンドポイントURLは以下のようになります。

https://[Webアクセス(ORDS)パブリックURL]/[ORDS別名]/_/db-api/stable


Webアクセス(ORDS)パブリックURLは、OCIコンソールのAutonomous Databaseのツール構成から確認できます。


続けてワークスペース・ユーティリティWeb資格証明を開きます。


Web資格証明RDF Graph Credを開きます。


REST APIは、RDFグラフを保存しているスキーマ名とそのパスワードを与えることによって認証します。Autonomous DatabaseでAPEXを使用している場合は、一般にワークスペース名をAPEXDEVとした場合、ORDS別名=ワークスペース名でapexdevとなります。そのワークスペースのデフォルト・パーシング・スキーマはWKSP_APEXDEVになります。これがデータベースのユーザー名になり、このユーザー名およびパスワードWeb資格証明に設定します。


以上で、インポートしたAPEXアプリケーションを利用する準備は完了です。

このAPEXアプリケーションを使って、RDFグラフの作成とSPARQLの実行を行います。

最初にRDFネットワークを作成します。呼び出すREST APIは以下です。

Create RDF network

SEM_APIS.CREATE_RDF_NETWORKの呼び出しに対応します。

Network Nameは任意の値です。今回はNET1としています。Network Ownerは接続先のデータベース・ユーザー名(スキーマ名)を指定します。Tablespace Nameは、そのデータベース・ユーザーのデフォルト表領域を指定します。Autonomous Databaseでは表領域DATAを指定します。


ボタンCreate RDF networkをクリックすると、以下のPL/SQLコードが実行されます。REST APIの仕様にそって、API呼び出しを実施しています。レスポンスは特に加工せず、JSONのままページ・アイテムP1_RESPONSEに表示しています。

declare
l_base_url apex_workspace_remote_servers.base_url%type;
l_operation_url varchar2(400);
l_request clob;
l_request_json json_object_t;
l_response clob;
e_call_api_failed exception;
l_status_code number;
begin
select base_url into l_base_url from apex_workspace_remote_servers
where remote_server_static_id = :G_REMOTE_SERVER;
l_operation_url :=
apex_string.format(l_base_url || '/database/rdf/networks/%s,%s',
:P1_NETWORK_OWNER, :P1_NETWORK_NAME);
apex_debug.info('operation_url = %s', l_operation_url);
l_request_json := json_object_t();
l_request_json.put('tablespace_name', :P1_TABLESPACE_NAME);
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 => l_operation_url
,p_http_method => 'PUT'
,p_body => l_request
,p_credential_static_id => :G_CREDENTIAL
);
l_status_code := apex_web_service.g_status_code;
apex_debug.info('status_code = %s', l_status_code);
if not l_status_code between 200 and 300 then
raise e_call_api_failed;
end if;
:P1_RESPONSE := l_response;
end;


次に、作成したネットワークにモデルを作成します。Model Namebotchanとします。

ボタンCreate RDF modelをクリックすると以下のコードを実行します。呼び出すREST APIは以下になります。

Create RDF model

SEM_APIS.CREATE_RDF_GRAPHの呼び出しに対応します。

declare
l_base_url apex_workspace_remote_servers.base_url%type;
l_operation_url varchar2(400);
l_request clob;
l_request_json json_object_t;
l_response clob;
e_call_api_failed exception;
l_status_code number;
begin
select base_url into l_base_url from apex_workspace_remote_servers
where remote_server_static_id = :G_REMOTE_SERVER;
l_operation_url :=
apex_string.format(l_base_url || '/database/rdf/networks/%s,%s/models/%s',
:P1_NETWORK_OWNER, :P1_NETWORK_NAME, :P1_MODEL_NAME);
apex_debug.info('operation_url = %s', l_operation_url);
l_request_json := json_object_t();
l_request_json.put('tablespace_name', :P1_TABLESPACE_NAME);
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 => l_operation_url
,p_http_method => 'PUT'
,p_body => l_request
,p_credential_static_id => :G_CREDENTIAL
);
l_status_code := apex_web_service.g_status_code;
apex_debug.info('status_code = %s', l_status_code);
if not l_status_code between 200 and 300 then
raise e_call_api_failed;
end if;
:P1_RESPONSE := l_response;
end;


RDFネットワークとモデル(グラフ)が作成できました。SPARQLのINSERT文を実行します。

小説「坊ちゃん」の登場人物の関係データを投入します。以下のコードはClaude Sonnet 4に生成してもらいました。

# 「坊ちゃん」登場人物関係データのSPARQL INSERT文
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX botchan: <http://example.org/botchan/>
PREFIX vocab: <http://example.org/vocabulary/>
# 人物の基本情報を挿入
INSERT DATA {
botchan:坊ちゃん rdf:type vocab:人物 ;
rdfs:label "坊ちゃん" ;
vocab:職業 "数学教師" ;
vocab:出身地 "東京" .
botchan:赤シャツ rdf:type vocab:人物 ;
rdfs:label "赤シャツ" ;
vocab:職業 "教頭" .
botchan:山嵐 rdf:type vocab:人物 ;
rdfs:label "山嵐" ;
vocab:職業 "英語教師" .
botchan:うらなり rdf:type vocab:人物 ;
rdfs:label "うらなり" ;
vocab:職業 "英語教師" .
botchan:野だいこ rdf:type vocab:人物 ;
rdfs:label "野だいこ" ;
vocab:職業 "画学教師" .
botchan:マドンナ rdf:type vocab:人物 ;
rdfs:label "マドンナ" ;
vocab:職業 "女性" .
botchan:清 rdf:type vocab:人物 ;
rdfs:label "" ;
vocab:職業 "女中" .
botchan:校長 rdf:type vocab:人物 ;
rdfs:label "校長" ;
vocab:職業 "校長" .
} ;
# 職場関係を挿入
INSERT DATA {
botchan:坊ちゃん vocab:勤務先 botchan:中学校 .
botchan:赤シャツ vocab:勤務先 botchan:中学校 .
botchan:山嵐 vocab:勤務先 botchan:中学校 .
botchan:うらなり vocab:勤務先 botchan:中学校 .
botchan:野だいこ vocab:勤務先 botchan:中学校 .
botchan:校長 vocab:勤務先 botchan:中学校 .
botchan:赤シャツ vocab:職位 "教頭" .
botchan:校長 vocab:職位 "校長" .
} ;
# 人間関係を挿入
INSERT DATA {
botchan:坊ちゃん vocab:世話になった人 botchan:清 .
botchan:清 vocab:世話した人 botchan:坊ちゃん .
botchan:坊ちゃん vocab:対立関係 botchan:赤シャツ .
botchan:赤シャツ vocab:対立関係 botchan:坊ちゃん .
botchan:坊ちゃん vocab:対立関係 botchan:野だいこ .
botchan:野だいこ vocab:対立関係 botchan:坊ちゃん .
botchan:山嵐 vocab:味方関係 botchan:坊ちゃん .
botchan:坊ちゃん vocab:味方関係 botchan:山嵐 .
botchan:赤シャツ vocab:同盟関係 botchan:野だいこ .
botchan:野だいこ vocab:同盟関係 botchan:赤シャツ .
botchan:うらなり vocab:恋愛関係 botchan:マドンナ .
botchan:マドンナ vocab:恋愛関係 botchan:うらなり .
botchan:赤シャツ vocab:横恋慕 botchan:マドンナ .
} ;
# 性格・特徴を挿入
INSERT DATA {
botchan:坊ちゃん vocab:性格 "正直" ;
vocab:性格 "短気" ;
vocab:性格 "江戸っ子気質" .
botchan:赤シャツ vocab:性格 "狡猾" ;
vocab:性格 "陰険" .
botchan:山嵐 vocab:性格 "豪快" ;
vocab:性格 "正義感が強い" .
botchan:うらなり vocab:性格 "温厚" ;
vocab:性格 "弱気" .
botchan:野だいこ vocab:性格 "軽薄" ;
vocab:性格 "調子がいい" .
botchan:清 vocab:性格 "献身的" ;
vocab:性格 "母性的" .
} ;
# 事件・出来事を挿入
INSERT DATA {
botchan:坊ちゃん vocab:経験した出来事 "いたずら事件" ;
vocab:経験した出来事 "温泉事件" ;
vocab:経験した出来事 "辞職" .
botchan:赤シャツ vocab:関与した出来事 "うらなり左遷工作" .
botchan:野だいこ vocab:関与した出来事 "うらなり左遷工作" .
botchan:山嵐 vocab:行動 "坊ちゃんと協力して制裁" .
botchan:坊ちゃん vocab:行動 "山嵐と協力して制裁" .
} ;
# 追加のメタデータ
INSERT DATA {
botchan:中学校 rdf:type vocab:教育機関 ;
rdfs:label "中学校" ;
vocab:所在地 "四国" .
# 作品情報
botchan:作品 rdf:type vocab:小説 ;
rdfs:label "坊ちゃん" ;
vocab:作者 "夏目漱石" ;
vocab:出版年 "1906" .
}
ボタンExecute SPARQL Updateをクリックすると、以下のコードが実行されます。呼び出すREST APIは以下になります。

Execute a SPARQL query or update

ORDS上でSQLに変換されデータベースではSQLが実行されるため、対応するデータベースのAPIはありません。REST API呼び出し時のContent-Typeとして、application/sparql-updateを指定します。

declare
l_base_url apex_workspace_remote_servers.base_url%type;
l_operation_url varchar2(400);
l_request clob;
l_response clob;
e_call_api_failed exception;
l_status_code number;
begin
select base_url into l_base_url from apex_workspace_remote_servers
where remote_server_static_id = :G_REMOTE_SERVER;
l_operation_url :=
apex_string.format(l_base_url || '/database/rdf/networks/%s,%s/models/%s/sparql/1.1',
:P1_NETWORK_OWNER, :P1_NETWORK_NAME, :P1_MODEL_NAME);
apex_debug.info('operation_url = %s', l_operation_url);
l_request := :P1_SPARQL_QUERY;
apex_web_service.set_request_headers('Content-Type', 'application/sparql-query', 'Accept', 'application/sparql-results+json');
l_response := apex_web_service.make_rest_request(
p_url => l_operation_url
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => :G_CREDENTIAL
);
l_status_code := apex_web_service.g_status_code;
apex_debug.info('status_code = %s', l_status_code);
if not l_status_code between 200 and 300 then
raise e_call_api_failed;
end if;
:P1_RESPONSE := l_response;
end;


小説「坊ちゃん」の登場人物に関するSPARQLクエリを実行してみます。以下がSPARQLクエリのサンプルです。これもClaude Sonnet 4に生成してもらいました。

# 「坊ちゃん」RDFグラフ検索クエリ集
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX botchan: <http://example.org/botchan/>
PREFIX vocab: <http://example.org/vocabulary/>
# =====================================
# 1. 基本的な人物情報検索
# =====================================
# 全人物の一覧表示
SELECT ?person ?name ?job ?workplace
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name ;
vocab:職業 ?job .
OPTIONAL { ?person vocab:勤務先 ?workplace }
}
ORDER BY ?name
# 特定の職業の人物を検索
SELECT ?person ?name
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name ;
vocab:職業 "英語教師" .
}
# 坊ちゃんの基本情報
SELECT ?property ?value
WHERE {
botchan:坊ちゃん ?property ?value .
}
# =====================================
# 2. 職場・階層関係の検索
# =====================================
# 中学校に勤務する全教職員
SELECT ?person ?name ?job ?position
WHERE {
?person vocab:勤務先 botchan:中学校 ;
rdfs:label ?name ;
vocab:職業 ?job .
OPTIONAL { ?person vocab:職位 ?position }
}
ORDER BY ?position ?job
# 管理職の一覧
SELECT ?person ?name ?position
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name ;
vocab:職位 ?position .
}
# 教師のみの一覧
SELECT ?person ?name ?subject
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name ;
vocab:職業 ?subject .
FILTER(CONTAINS(?subject, "教師"))
}
# =====================================
# 3. 人間関係の検索
# =====================================
# 坊ちゃんの人間関係マップ
SELECT ?relationship ?related_person ?related_name
WHERE {
{
botchan:坊ちゃん ?relationship ?related_person .
?related_person rdf:type vocab:人物 ;
rdfs:label ?related_name .
}
UNION
{
?related_person ?relationship botchan:坊ちゃん .
?related_person rdf:type vocab:人物 ;
rdfs:label ?related_name .
FILTER(?relationship != rdf:type && ?relationship != rdfs:label)
}
}
# 対立関係の一覧
SELECT ?person1 ?name1 ?person2 ?name2
WHERE {
?person1 vocab:対立関係 ?person2 ;
rdfs:label ?name1 .
?person2 rdfs:label ?name2 .
}
# 味方関係の一覧
SELECT ?person1 ?name1 ?person2 ?name2
WHERE {
?person1 vocab:味方関係 ?person2 ;
rdfs:label ?name1 .
?person2 rdfs:label ?name2 .
}
# 恋愛関係の一覧
SELECT ?person1 ?name1 ?person2 ?name2
WHERE {
?person1 vocab:恋愛関係 ?person2 ;
rdfs:label ?name1 .
?person2 rdfs:label ?name2 .
}
# 同盟関係の一覧
SELECT ?person1 ?name1 ?person2 ?name2
WHERE {
?person1 vocab:同盟関係 ?person2 ;
rdfs:label ?name1 .
?person2 rdfs:label ?name2 .
}
# =====================================
# 4. 性格特性の検索
# =====================================
# 全人物の性格一覧
SELECT ?person ?name ?trait
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name ;
vocab:性格 ?trait .
}
ORDER BY ?name
# 特定の性格を持つ人物検索
SELECT ?person ?name
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name ;
vocab:性格 "正義感が強い" .
}
# 性格特性をグループ化して表示
SELECT ?person ?name (GROUP_CONCAT(?trait; separator=", ") as ?traits)
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name ;
vocab:性格 ?trait .
}
GROUP BY ?person ?name
ORDER BY ?name
# ネガティブな性格の人物
SELECT ?person ?name ?trait
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name ;
vocab:性格 ?trait .
FILTER(?trait IN ("狡猾", "陰険", "軽薄"))
}
# =====================================
# 5. 事件・出来事の検索
# =====================================
# 坊ちゃんが経験した出来事
SELECT ?event
WHERE {
botchan:坊ちゃん vocab:経験した出来事 ?event .
}
# 誰がどの出来事に関与したか
SELECT ?person ?name ?event_type ?event
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name .
{
?person vocab:経験した出来事 ?event .
BIND("経験" as ?event_type)
}
UNION
{
?person vocab:関与した出来事 ?event .
BIND("関与" as ?event_type)
}
}
# 特定の出来事に関わった人物
SELECT ?person ?name ?involvement
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name .
{
?person vocab:経験した出来事 "温泉事件" .
BIND("経験者" as ?involvement)
}
UNION
{
?person vocab:関与した出来事 "うらなり左遷工作" .
BIND("関与者" as ?involvement)
}
}
# 行動を起こした人物
SELECT ?person ?name ?action
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name ;
vocab:行動 ?action .
}
# =====================================
# 6. 複合条件検索
# =====================================
# 坊ちゃんの敵対者の詳細情報
SELECT ?enemy ?name ?job ?traits ?alliance
WHERE {
botchan:坊ちゃん vocab:対立関係 ?enemy .
?enemy rdfs:label ?name ;
vocab:職業 ?job .
# 性格特性を集約
OPTIONAL {
SELECT ?enemy (GROUP_CONCAT(?trait; separator=", ") as ?traits)
WHERE {
?enemy vocab:性格 ?trait .
}
GROUP BY ?enemy
}
# 同盟関係を確認
OPTIONAL {
?enemy vocab:同盟関係 ?ally .
?ally rdfs:label ?ally_name .
BIND(?ally_name as ?alliance)
}
}
# 中学校の人間関係ネットワーク
SELECT ?person1 ?name1 ?relationship ?person2 ?name2
WHERE {
?person1 vocab:勤務先 botchan:中学校 ;
rdfs:label ?name1 .
?person1 ?relationship ?person2 .
?person2 vocab:勤務先 botchan:中学校 ;
rdfs:label ?name2 .
FILTER(?relationship IN (vocab:対立関係, vocab:味方関係, vocab:同盟関係))
}
# 性格と職業の相関
SELECT ?job ?trait (COUNT(?person) as ?count)
WHERE {
?person rdf:type vocab:人物 ;
vocab:職業 ?job ;
vocab:性格 ?trait .
}
GROUP BY ?job ?trait
ORDER BY ?job ?count DESC
# =====================================
# 7. 特定人物の詳細プロファイル
# =====================================
# 赤シャツの完全プロファイル
SELECT ?property ?value
WHERE {
botchan:赤シャツ ?property ?value .
FILTER(?property != rdf:type)
}
# 山嵐の関係性分析
SELECT ?relationship_type ?related_person ?related_name
WHERE {
{
botchan:山嵐 ?relationship_type ?related_person .
?related_person rdf:type vocab:人物 ;
rdfs:label ?related_name .
}
UNION
{
?related_person ?relationship_type botchan:山嵐 .
?related_person rdf:type vocab:人物 ;
rdfs:label ?related_name .
}
FILTER(?relationship_type != rdf:type && ?relationship_type != rdfs:label)
}
# =====================================
# 8. メタデータと作品情報の検索
# =====================================
# 作品の基本情報
SELECT ?property ?value
WHERE {
botchan:作品 ?property ?value .
}
# 学校の情報
SELECT ?property ?value
WHERE {
botchan:中学校 ?property ?value .
}
# データセット全体のサマリー
SELECT ?type (COUNT(?resource) as ?count)
WHERE {
?resource rdf:type ?type .
}
GROUP BY ?type
# =====================================
# 9. 条件分析クエリ
# =====================================
# 孤立している人物(特別な関係がない人物)
SELECT ?person ?name ?job
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name ;
vocab:職業 ?job .
FILTER NOT EXISTS {
?person vocab:対立関係|vocab:味方関係|vocab:同盟関係|vocab:恋愛関係 ?other .
}
FILTER NOT EXISTS {
?other vocab:対立関係|vocab:味方関係|vocab:同盟関係|vocab:恋愛関係 ?person .
}
}
# 最も多くの関係を持つ人物
SELECT ?person ?name (COUNT(?relationship) as ?relationship_count)
WHERE {
?person rdf:type vocab:人物 ;
rdfs:label ?name .
{
?person ?relationship ?other .
?other rdf:type vocab:人物 .
}
UNION
{
?other ?relationship ?person .
?other rdf:type vocab:人物 .
}
FILTER(?relationship IN (vocab:対立関係, vocab:味方関係, vocab:同盟関係, vocab:恋愛関係, vocab:世話になった人, vocab:世話した人))
}
GROUP BY ?person ?name
ORDER BY DESC(?relationship_count)
# =====================================
# 10. 推論的検索
# =====================================
# 間接的な敵関係(敵の同盟者)
SELECT ?person ?name ?indirect_enemy ?enemy_name
WHERE {
botchan:坊ちゃん vocab:対立関係 ?direct_enemy .
?direct_enemy vocab:同盟関係 ?indirect_enemy .
?person rdfs:label ?name .
?indirect_enemy rdfs:label ?enemy_name .
FILTER(?indirect_enemy != botchan:坊ちゃん)
}
# 潜在的な味方関係(味方の味方)
SELECT ?person ?name ?potential_ally ?ally_name
WHERE {
botchan:坊ちゃん vocab:味方関係 ?direct_ally .
?direct_ally vocab:味方関係 ?potential_ally .
?person rdfs:label ?name .
?potential_ally rdfs:label ?ally_name .
FILTER(?potential_ally != botchan:坊ちゃん)
}

この中の同盟関係の一覧を問い合わせてみます。
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX botchan: <http://example.org/botchan/>
PREFIX vocab: <http://example.org/vocabulary/>

# 同盟関係の一覧
SELECT ?person1 ?name1 ?person2 ?name2
WHERE {
  ?person1 vocab:同盟関係 ?person2 ;
           rdfs:label ?name1 .
  ?person2 rdfs:label ?name2 .
}
ボタンExecute SPARQL Queryをクリックすると、以下のコードが実行されます。REST APIのエンドポイントはUpdateと同じですが、Content-Typeとしてapplication/sparql-queryを指定しています。また、Acceptにapplication/sparql-results+jsonを指定することで、レスポンスをJSON形式で受け取っています。

declare
l_base_url apex_workspace_remote_servers.base_url%type;
l_operation_url varchar2(400);
l_request clob;
l_response clob;
e_call_api_failed exception;
l_status_code number;
begin
select base_url into l_base_url from apex_workspace_remote_servers
where remote_server_static_id = :G_REMOTE_SERVER;
l_operation_url :=
apex_string.format(l_base_url || '/database/rdf/networks/%s,%s/models/%s/sparql/1.1',
:P1_NETWORK_OWNER, :P1_NETWORK_NAME, :P1_MODEL_NAME);
apex_debug.info('operation_url = %s', l_operation_url);
l_request := :P1_SPARQL_QUERY;
apex_web_service.set_request_headers('Content-Type', 'application/sparql-query', 'Accept', 'application/sparql-results+json');
l_response := apex_web_service.make_rest_request(
p_url => l_operation_url
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => :G_CREDENTIAL
);
l_status_code := apex_web_service.g_status_code;
apex_debug.info('status_code = %s', l_status_code);
if not l_status_code between 200 and 300 then
raise e_call_api_failed;
end if;
:P1_RESPONSE := l_response;
end;


野だいこと赤シャツが同盟関係にあるとのことです。

以上で、ORDSのRDF Graph APIを呼び出すことにより、RDFグラフの作成および検索ができることを確認できました。

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

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

2025年6月2日月曜日

hotchpotch/japanese-splade-v2でsparseベクトルを生成しオラクル・データベースに保存して検索する

hotchpotch/japanese-splade-v2のモデルを使ってsparseベクトルを生成し、Oracle Database 23aiに保存して類似検索まで実行してみます。

Hugging Faceのhotchpotch/japanese-splade-v2のモデル・カードは以下です。

作成された方@hotchpotch(セコン)さんによる解説記事は以下です。私には難しくて、内容は理解できていません。

SPLADE モデルの作り方・日本語SPLADEテクニカルレポート
高性能な日本語SPLADE(スパース検索)モデルを公開しました
情報検索モデルで最高性能(512トークン以下)・日本語版SPLADE v2をリリース

前回の記事ではBAAI/bge-m3を使ってdenseベクトルとsparseベクトルを生成して、Oracle Databaseに保存するまでを実装しました。今回はjapanese-splade-v2で生成したsparseベクトルを使って、類似検索を行うAPEXアプリケーションを作成してみます。

作業手順はBAAI/bge-m3のときとほぼ同じです。

今回もClaude Sonnet 4に、hotchpotch/japanese-splade-v2のモデルを使ってsparseベクトルを生成するFastAPIのサーバーを作ってもらいました。

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from transformers import AutoTokenizer, AutoModelForMaskedLM
import torch
import numpy as np
from typing import Dict, List
import logging
from contextlib import asynccontextmanager
# ロギング設定
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# グローバル変数でモデルとトークナイザーを保持
tokenizer = None
model = None
class TextRequest(BaseModel):
text: str
max_length: int = 512
class SparseVector(BaseModel):
indices: List[int]
values: List[float]
vocab_size: int
class TextResponse(BaseModel):
text: str
sparse_vector: SparseVector
async def load_model():
"""モデルを読み込み"""
global tokenizer, model
try:
logger.info("Japanese SPLADE v2モデルを読み込み中...")
model_name = "hotchpotch/japanese-splade-v2"
# トークナイザーとモデルを読み込み
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForMaskedLM.from_pretrained(model_name)
# GPUが利用可能な場合はGPUを使用
device = "cuda" if torch.cuda.is_available() else "cpu"
model = model.to(device)
model.eval()
logger.info(f"モデルの読み込みが完了しました (device: {device})")
except Exception as e:
logger.error(f"モデル読み込み中にエラーが発生しました: {e}")
raise e
async def cleanup_model():
"""モデルをクリーンアップ"""
global tokenizer, model
logger.info("モデルをクリーンアップ中...")
tokenizer = None
model = None
@asynccontextmanager
async def lifespan(app: FastAPI):
# 起動時
await load_model()
yield
# 終了時
await cleanup_model()
app = FastAPI(
title="Japanese SPLADE API",
description="Japanese SPLADE v2モデルを使用してsparse vectorを生成するAPI",
version="1.0.0",
lifespan=lifespan
)
def splade_max_pooling(logits, attention_mask):
relu_log = torch.log(1 + torch.relu(logits))
weighted_log = relu_log * attention_mask.unsqueeze(-1)
max_val, _ = torch.max(weighted_log, dim=1)
return max_val
def create_sparse_vector(text: str, max_length: int = 512) -> SparseVector:
"""テキストからsparse vectorを生成"""
if tokenizer is None or model is None:
raise HTTPException(status_code=500, detail="モデルが読み込まれていません")
try:
device = next(model.parameters()).device
# テキストをトークン化
tokens = tokenizer(
text,
return_tensors="pt",
truncation=True,
padding=True,
max_length=max_length
)
# デバイスに転送
tokens = {k: v.to(device) for k, v in tokens.items()}
# モデルの推論を実行
with torch.no_grad():
outputs = model(**tokens)
logits = outputs.logits # [batch_size, seq_len, vocab_size]
# splade_max_pooling関数を使用してsparse vectorを生成
embeddings = splade_max_pooling(logits, tokens['attention_mask'])
# バッチサイズが1なので最初の要素を取得
vector = embeddings[0] # [vocab_size]
# 非ゼロの要素のみを取得(閾値を設定してスパース性を確保)
threshold = 1e-6
non_zero_mask = vector > threshold
non_zero_indices = torch.nonzero(non_zero_mask).squeeze(-1)
non_zero_values = vector[non_zero_indices]
# CPUに移動してリストに変換
indices = non_zero_indices.cpu().numpy().tolist()
values = non_zero_values.cpu().numpy().tolist()
return SparseVector(
indices=indices,
values=values,
vocab_size=tokenizer.vocab_size
)
except Exception as e:
logger.error(f"sparse vector生成中にエラーが発生しました: {e}")
raise HTTPException(status_code=500, detail=f"処理中にエラーが発生しました: {str(e)}")
@app.post("/encode", response_model=TextResponse)
async def encode_text(request: TextRequest):
"""テキストをsparse vectorにエンコード"""
try:
sparse_vector = create_sparse_vector(request.text, request.max_length)
return TextResponse(
text=request.text,
sparse_vector=sparse_vector
)
except Exception as e:
logger.error(f"エンコード処理中にエラーが発生しました: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/batch_encode")
async def batch_encode_texts(texts: List[str], max_length: int = 512):
"""複数のテキストを一度にエンコード"""
try:
results = []
for text in texts:
sparse_vector = create_sparse_vector(text, max_length)
results.append({
"text": text,
"sparse_vector": {
"indices": sparse_vector.indices,
"values": sparse_vector.values,
"vocab_size": sparse_vector.vocab_size
}
})
return {"results": results}
except Exception as e:
logger.error(f"バッチエンコード処理中にエラーが発生しました: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health_check():
"""ヘルスチェック"""
return {
"status": "healthy",
"model_loaded": tokenizer is not None and model is not None,
"cuda_available": torch.cuda.is_available(),
"device": str(next(model.parameters()).device) if model is not None else "unknown"
}
@app.get("/model_info")
async def get_model_info():
"""モデル情報を取得"""
if tokenizer is None:
raise HTTPException(status_code=500, detail="モデルが読み込まれていません")
return {
"model_name": "hotchpotch/japanese-splade-v2",
"vocab_size": tokenizer.vocab_size,
"max_position_embeddings": getattr(tokenizer, 'model_max_length', 'unknown'),
"device": str(next(model.parameters()).device) if model is not None else "unknown"
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7999)
view raw server.py hosted with ❤ by GitHub

Oracle APEXのアプリケーションからこのAPIサーバーを呼び出して、文字列のsparseベクトルを生成してデータベースに保存します。japanese-SPLADE-v2が生成するsparseベクトルの次元数は32768なので、BAAI/bge-m3とは異なりベクトル型の次元数の上限は超えません。

create table ebaj_sparse_vectors (
id number generated by default on null as identity
constraint ebaj_sparse_vectors_id_pk primary key,
text varchar2(4000 char),
is_updated boolean default true,
vec vector(32768, float32, sparse)
);

FastAPIサーバーを実行します。作業はmacOSで行います。Python 3.13とpodman(またはDocker)を使います。

必要なスクリプトをhttps://github.com/ujnak/splade-serviceからダウンロードできるようにしています。

リポジトリをクローンし、作成されたディレクトリに移動します。

git clone https://github.com/ujnak/splade-service
cd splade-service

% git clone https://github.com/ujnak/splade-service

Cloning into 'splade-service'...

remote: Enumerating objects: 15, done.

remote: Counting objects: 100% (15/15), done.

remote: Compressing objects: 100% (13/13), done.

remote: Total 15 (delta 1), reused 4 (delta 1), pack-reused 0 (from 0)

Receiving objects: 100% (15/15), 5.86 KiB | 5.86 MiB/s, done.

Resolving deltas: 100% (1/1), done.

% cd splade-service 

splade-service % 


Pythonの仮想環境をspladeとして作成し、アクティベートします。Pythonのバージョンは3.13を指定します。

python3.13 -m venv splade
. splade/bin/activate


splade-service % python3.13 -m venv splade

splade-service % . splade/bin/activate

(splade) splade-service % 


使用するパッケージをインストールします。

pip install -r requirements.txt

(splade) splade-service % pip install -r requirements.txt 

Collecting fastapi>=0.104.0 (from -r requirements.txt (line 1))

  Using cached fastapi-0.115.12-py3-none-any.whl.metadata (27 kB)

Collecting uvicorn>=0.24.0 (from uvicorn[standard]>=0.24.0->-r requirements.txt (line 2))


[中略]


Using cached hf_xet-1.1.2-cp37-abi3-macosx_11_0_arm64.whl (2.5 MB)

Using cached idna-3.10-py3-none-any.whl (70 kB)

Using cached MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl (12 kB)

Using cached mpmath-1.3.0-py3-none-any.whl (536 kB)

Using cached urllib3-2.4.0-py3-none-any.whl (128 kB)

Using cached sniffio-1.3.1-py3-none-any.whl (10 kB)

Installing collected packages: unidic-lite, mpmath, websockets, uvloop, urllib3, typing-extensions, tqdm, sympy, sniffio, setuptools, safetensors, regex, pyyaml, python-dotenv, protobuf, packaging, numpy, networkx, MarkupSafe, idna, httptools, hf-xet, h11, fugashi, fsspec, filelock, click, charset-normalizer, certifi, annotated-types, uvicorn, typing-inspection, requests, pydantic-core, jinja2, anyio, watchfiles, torch, starlette, pydantic, huggingface-hub, tokenizers, fastapi, transformers

Successfully installed MarkupSafe-3.0.2 annotated-types-0.7.0 anyio-4.9.0 certifi-2025.4.26 charset-normalizer-3.4.2 click-8.2.1 fastapi-0.115.12 filelock-3.18.0 fsspec-2025.5.1 fugashi-1.4.3 h11-0.16.0 hf-xet-1.1.2 httptools-0.6.4 huggingface-hub-0.32.3 idna-3.10 jinja2-3.1.6 mpmath-1.3.0 networkx-3.5 numpy-2.2.6 packaging-25.0 protobuf-6.31.1 pydantic-2.11.5 pydantic-core-2.33.2 python-dotenv-1.1.0 pyyaml-6.0.2 regex-2024.11.6 requests-2.32.3 safetensors-0.5.3 setuptools-80.9.0 sniffio-1.3.1 starlette-0.46.2 sympy-1.14.0 tokenizers-0.21.1 torch-2.7.0 tqdm-4.67.1 transformers-4.52.4 typing-extensions-4.13.2 typing-inspection-0.4.1 unidic-lite-1.0.8 urllib3-2.4.0 uvicorn-0.34.3 uvloop-0.21.0 watchfiles-1.0.5 websockets-15.0.1


[notice] A new release of pip is available: 25.0.1 -> 25.1.1

[notice] To update, run: pip install --upgrade pip

(splade) splade-service % 

 
以上で準備が完了です。APIサーバーを起動します。

python server.py

(splade) splade-service % python server.py

INFO:     Started server process [73108]

INFO:     Waiting for application startup.

INFO:__main__:Japanese SPLADE v2モデルを読み込み中...

INFO:__main__:モデルの読み込みが完了しました (device: cpu)

INFO:     Application startup complete.

INFO:     Uvicorn running on http://0.0.0.0:7999 (Press CTRL+C to quit)


別ターミナルからAPIサーバーのテストを行います。splade-service以下にあるcurl-01.shを実行します。

sh curl-01.sh

APIサーバーが返すベクトルが表示されれば、APIサーバーの実行は成功です。

splade-service % sh curl-01.sh 

{"text":"これは日本語のテストです","sparse_vector":{"indices":[429,4262,5630,12499,12500,12538,13037,13374,13449,13459,13618,13821,14877,14985,22460,24096],"values":[0.10150858014822006,0.17410790920257568,0.27124056220054626,0.49592137336730957,1.5145821571350098,0.7409901022911072,0.228782057762146,0.06052016094326973,0.5196099281311035,0.4933662414550781,0.023301351815462112,0.5448876023292542,0.43785205483436584,1.2762678861618042,0.10169661045074463,0.08509977906942368],"vocab_size":32768}}

splade-service % 


CTRL+Cを入力しAPIサーバーを停止します。

これからコンテナ・イメージを作成し、APIサーバーをpodmanのコンテナとして実行できるようにします。splade-service以下にDockerfileがあるので、それを使ってコンテナ・イメージを作成します。

podman build --file Dockerfile --tag japanese-splade-v2 .

(splade) splade-service % podman build --file Dockerfile --tag japanese-splade-v2 .

STEP 1/5: FROM python:3.13

STEP 2/5: WORKDIR /app

--> 6abc32147ceb

STEP 3/5: COPY server.py requirements.txt .

--> 3b79e4f8fa7d

STEP 4/5: RUN pip install -r requirements.txt

Collecting fastapi>=0.104.0 (from -r requirements.txt (line 1))

  Downloading fastapi-0.115.12-py3-none-any.whl.metadata (27 kB)

Collecting uvicorn>=0.24.0 (from uvicorn[standard]>=0.24.0->-r requirements.txt (line 2))

  Downloading uvicorn-0.34.3-py3-none-any.whl.metadata (6.5 kB)

Collecting transformers>=4.35.0 (from -r requirements.txt (line 3))

  Downloading transformers-4.52.4-py3-none-any.whl.metadata (38 kB)

Collecting torch>=2.0.0 (from -r requirements.txt (line 4))

  Downloading torch-2.7.0-cp313-cp313-manylinux_2_28_aarch64.whl.metadata (29 kB)

Collecting numpy>=1.24.0 (from -r requirements.txt (line 5))

  Downloading numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (63 kB)

Collecting pydantic>=2.0.0 (from -r requirements.txt (line 6))

  Downloading pydantic-2.11.5-py3-none-any.whl.metadata (67 kB)

Collecting fugashi>=1.3.0 (from -r requirements.txt (line 7))

  Downloading fugashi-1.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (7.1 kB)

Collecting protobuf>=4.21.0 (from -r requirements.txt (line 8))

  Downloading protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl.metadata (593 bytes)

Collecting unidic-lite>=1.0.8 (from -r requirements.txt (line 9))

  Downloading unidic-lite-1.0.8.tar.gz (47.4 MB)

     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 47.4/47.4 MB 40.8 MB/s eta 0:00:00

  Installing build dependencies: started

  Installing build dependencies: finished with status 'done'

  Getting requirements to build wheel: started

  Getting requirements to build wheel: finished with status 'done'

  Preparing metadata (pyproject.toml): started

  Preparing metadata (pyproject.toml): finished with status 'done'


[中略]


 Created wheel for unidic-lite: filename=unidic_lite-1.0.8-py3-none-any.whl size=47658912 sha256=4f876a371019546e995b130a47ebadbe7d21e3494b8d7cc631425ce450eac8d6

  Stored in directory: /root/.cache/pip/wheels/93/40/f2/23f8a0da599c4174200d54d0b933aedc464a12f1061e4aabba

Successfully built unidic-lite

Installing collected packages: unidic-lite, mpmath, websockets, uvloop, urllib3, typing-extensions, tqdm, sympy, sniffio, setuptools, safetensors, regex, pyyaml, python-dotenv, protobuf, packaging, numpy, networkx, MarkupSafe, idna, httptools, hf-xet, h11, fugashi, fsspec, filelock, click, charset-normalizer, certifi, annotated-types, uvicorn, typing-inspection, requests, pydantic-core, jinja2, anyio, watchfiles, torch, starlette, pydantic, huggingface-hub, tokenizers, fastapi, transformers

Successfully installed MarkupSafe-3.0.2 annotated-types-0.7.0 anyio-4.9.0 certifi-2025.4.26 charset-normalizer-3.4.2 click-8.2.1 fastapi-0.115.12 filelock-3.18.0 fsspec-2025.5.1 fugashi-1.4.3 h11-0.16.0 hf-xet-1.1.2 httptools-0.6.4 huggingface-hub-0.32.3 idna-3.10 jinja2-3.1.6 mpmath-1.3.0 networkx-3.5 numpy-2.2.6 packaging-25.0 protobuf-6.31.1 pydantic-2.11.5 pydantic-core-2.33.2 python-dotenv-1.1.0 pyyaml-6.0.2 regex-2024.11.6 requests-2.32.3 safetensors-0.5.3 setuptools-80.9.0 sniffio-1.3.1 starlette-0.46.2 sympy-1.14.0 tokenizers-0.21.1 torch-2.7.0 tqdm-4.67.1 transformers-4.52.4 typing-extensions-4.13.2 typing-inspection-0.4.1 unidic-lite-1.0.8 urllib3-2.4.0 uvicorn-0.34.3 uvloop-0.21.0 watchfiles-1.0.5 websockets-15.0.1

WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.


[notice] A new release of pip is available: 25.0.1 -> 25.1.1

[notice] To update, run: pip install --upgrade pip

--> 7813f267ac01

STEP 5/5: CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7999", "--log-level", "info", "--access-log"]

COMMIT japanese-splade-v2

--> 6db870d05d4c

Successfully tagged localhost/japanese-splade-v2:latest

6db870d05d4c5b1d14d11376eb7555b46705a226efe0305983b9f5ff9c9c4f26

(splade) splade-service % 


イメージjapanese-splade-v2が作成されたことを確認します。

podman image ls japanese-splade-v2

(splade) splade-service % podman image ls japanese-splade-v2

REPOSITORY                    TAG         IMAGE ID      CREATED        SIZE

localhost/japanese-splade-v2  latest      6db870d05d4c  2 minutes ago  2.36 GB

(splade) splade-service % 


作成したコンテナ・イメージからコンテナspladeを作成し、実行します。

podman run -d --name splade -p 7999:7999 localhost/japanese-splade-v2

(splade) splade-service % podman run -d --name splade -p 7999:7999 localhost/japanese-splade-v2


2fe3d3d0128995768069f125ba52ab1adfad5a6ac44074bcbbe5b898cd73e532

(splade) splade-service % 


コンテナのログを確認します。Uvicornが開始しリクエストを待ち受けていれば、sparseベクトルの生成ができる状態です。

(splade) splade-service % podman logs -f splade

INFO:     Started server process [1]

INFO:     Waiting for application startup.

INFO:server:Japanese SPLADE v2モデルを読み込み中...

INFO:server:モデルの読み込みが完了しました (device: cpu)

INFO:     Application startup complete.

INFO:     Uvicorn running on http://0.0.0.0:7999 (Press CTRL+C to quit)



作成したAPIサーバーを呼び出し表EBAJ_SPARSE_VECTORSの列VECを更新する、また、クエリ文書を入力して、表EBAJ_SPRSE_VECTORSの列との距離を計算するアプリケーションを作成します。エクスポートは以下にあります。
https://github.com/ujnak/apexapps/blob/master/exports/sparse-similarity.zip

今回もローカルのマシンのコンテナで実行しているAPEX環境にインポートして、アプリケーションを使用します。環境の作成方法については、記事「podmanを使ってOracle Database FreeとOracle REST Data Servicesをコンテナとして実行する」で紹介しています。

アプリケーションをインポートして実行すると、以下のような画面が開きます。


作成ボタンをクリックすると、ベクトルを生成する文字列を入力するフォームが開きます。


Claude Sonnet 4にSPLADE類似検索テストケースを生成してもらいました。

SPLADE類似検索テストケース

テストケース1: 技術文書の類似検索

クエリ文書

機械学習におけるTransformerアーキテクチャは、自然言語処理タスクで画期的な性能を示しています。Self-Attentionメカニズムにより、文脈情報を効率的に捉えることができ、BERTやGPTなどの事前学習モデルの基盤となっています。

候補文書群

  1. 高類似度(期待順位:1位)

    Transformerモデルは自然言語処理分野で革命的な変化をもたらしました。注意機構(Attention Mechanism)を用いることで、長距離依存関係を効果的に学習し、BERTやGPTといった大規模言語モデルの開発を可能にしました。
    
  2. 中類似度(期待順位:2位)

    深層学習ニューラルネットワークは画像認識や音声認識など様々な分野で応用されています。CNNやRNNといったアーキテクチャが主流でしたが、近年はTransformerベースのモデルも注目されています。
    
  3. 低類似度(期待順位:3位)

    データサイエンスプロジェクトにおいて、適切な前処理とモデル選択が重要です。回帰分析や分類問題に対して、ランダムフォレストやサポートベクターマシンなどの手法が効果的です。
    
  4. 無関係(期待順位:4位)

    今日の天気は晴れており、気温は25度です。公園では多くの家族連れが散歩を楽しんでいます。桜の花が美しく咲いており、春の訪れを感じさせます。
    

テストケース2: 医療情報の類似検索

クエリ文書

糖尿病は血糖値の調節機能に問題がある代謝疾患です。主に1型と2型に分類され、適切な食事療法と運動療法、必要に応じて薬物療法を組み合わせた治療が重要です。

候補文書群

  1. 高類似度(期待順位:1位)

    糖尿病患者の血糖管理には、食事制限、定期的な運動、インスリン療法などの包括的なアプローチが必要です。1型糖尿病と2型糖尿病では治療方針が異なります。
    
  2. 中類似度(期待順位:2位)

    高血圧は生活習慣病の一つで、心血管疾患のリスクを高めます。塩分制限、適度な運動、ストレス管理が予防と治療の基本となります。
    
  3. 低類似度(期待順位:3位)

    健康的な生活習慣には、バランスの取れた食事、規則正しい睡眠、適度な運動が重要です。これらは様々な疾病の予防に効果的です。
    
  4. 無関係(期待順位:4位)

    新しいスマートフォンの機能には、高解像度カメラ、長時間バッテリー、5G通信対応が含まれています。ユーザーインターフェースも大幅に改善されました。
    

テストケース3: ビジネス文書の類似検索

クエリ文書

企業のデジタルトランスフォーメーション(DX)推進には、既存業務プロセスの見直し、ITインフラの整備、従業員のスキル向上が不可欠です。顧客体験の向上と業務効率化を同時に実現することが求められます。

候補文書群

  1. 高類似度(期待順位:1位)

    デジタル化の進展により、企業は業務プロセスの自動化とシステム統合を進めています。従業員の研修とITインフラ投資により、生産性向上と顧客満足度の改善を目指しています。
    
  2. 中類似度(期待順位:2位)

    現代の企業経営では、テクノロジーを活用した競争優位性の確保が重要です。クラウドサービスやAI技術の導入により、新たなビジネスモデルの創出が可能になります。
    
  3. 低類似度(期待順位:3位)

    マーケティング戦略の立案には、ターゲット顧客の分析と市場動向の把握が必要です。ブランディングと効果的な広告展開により売上向上を図ります。
    
  4. 無関係(期待順位:4位)

    週末の登山計画を立てています。天候を確認し、必要な装備を準備して、安全に山頂を目指したいと思います。自然の美しさを満喫する予定です。
    

テストケース4: 学術論文の類似検索

クエリ文書

気候変動が生態系に与える影響について、海洋酸性化と気温上昇による生物多様性の減少が深刻な問題となっています。特に珊瑚礁生態系では白化現象が頻発し、海洋生物の生息環境が脅かされています。

候補文書群

  1. 高類似度(期待順位:1位)

    地球温暖化による海水温の上昇は、珊瑚の白化現象を引き起こし、海洋生態系の破綻を招いています。海洋の酸性化も相まって、海洋生物の多様性維持が困難になっています。
    
  2. 中類似度(期待順位:2位)

    環境保護の観点から、持続可能な開発目標の達成が重要です。森林保全、海洋保護、温室効果ガス削減などの取り組みにより、地球環境の保全を図る必要があります。
    
  3. 低類似度(期待順位:3位)

    再生可能エネルギーの普及により、太陽光発電や風力発電の技術開発が進んでいます。エネルギー効率の向上と環境負荷の軽減が期待されています。
    
  4. 無関係(期待順位:4位)

    料理レシピの開発において、食材の組み合わせと調理法の工夫が重要です。栄養バランスを考慮しながら、美味しい料理を作ることを心がけています。
    

テスト評価指標

主要評価項目

  • Precision@k: 上位k件中の関連文書数
  • Recall@k: 全関連文書中の上位k件での検索率
  • MRR (Mean Reciprocal Rank): 最初の関連文書の順位の逆数
  • NDCG@k: 正規化割引累積利得

期待される結果

各テストケースで、高類似度文書が1位、中類似度が2位、低類似度が3位、無関係が4位にランクされることを期待します。SPLADEのスパース表現による語彙マッチングと意味的類似性の両方を考慮した検索性能を評価できます。


候補文書群をフォームから入力します。


文字列が表EBAJ_SPARSE_VECTORSの列TEXTに保存されます。この後、ボタンUpdate VectorsをクリックするとAPIサーバーが呼び出され、列VECが更新されます。

その前に、APIサーバーのエンドポイントをアプリケーションに設定します。

APIサーバーのエンドポイントは、アプリケーション定義の置換文字列G_ENDPOINTとして設定しています。APEXが動作しているコンテナから、コンテナの外のホストに接続するためhost.containers.internalがホスト名になります。

http://host.containers.internal:7999/encode


ボタンUpdate Vectorsを押したときに、以下のPL/SQLのコードが実行されます。

declare
l_request json_object_t;
l_request_clob clob;
l_response clob;
l_response_json json_object_t;
e_call_api_failed exception;
l_sparse_vector json_object_t;
l_sarr json_array_t;
l_sarr_clob clob;
begin
apex_web_service.set_request_headers('Content-Type', 'application/json');
for r in (select * from ebaj_sparse_vectors where is_updated)
loop
l_request := json_object_t();
l_request.put('text', r.text);
l_request_clob := l_request.to_clob();
l_response := apex_web_service.make_rest_request(
p_url => :G_ENDPOINT
,p_http_method => 'POST'
,p_body => l_request_clob
);
if apex_web_service.g_status_code <> 200 then
raise e_call_api_failed;
end if;
l_response_json := json_object_t(l_response);
l_sparse_vector := l_response_json.get_object('sparse_vector');
l_sarr := json_array_t();
l_sarr.append(l_sparse_vector.get_number('vocab_size'));
l_sarr.append(l_sparse_vector.get_array('indices'));
l_sarr.append(l_sparse_vector.get_array('values'));
l_sarr_clob := l_sarr.to_clob();
update ebaj_sparse_vectors set vec = to_vector(l_sarr_clob, 32768, float32, sparse), is_updated = false where id = r.id;
end loop;
commit;
end;

列VECが、japanese-splade-v2のモデルを呼び出して生成したsparseベクトルで更新されます。


ナビゲーション・メニューのSearchより、検索ページを開きます。

QueryにSPLADE類似検索テストケースのクエリ文書を入力し、ボタンQueryをクリックします。クエリ文書のsparseベクトルを生成して、表EBAJ_SPARSE_VECTORSの列VECとの距離の昇順に並べてレポートを表示します。

レポートのSQL問い合わせとして、以下を設定しています。
SELECT id, text,
    VECTOR_DISTANCE(vec, VECTOR(:P3_QUERY_VECTOR, 32768, FLOAT32, SPARSE), DOT) as distance
FROM ebaj_sparse_vectors
ORDER BY
    VECTOR_DISTANCE(vec, VECTOR(:P3_QUERY_VECTOR, 32768, FLOAT32, SPARSE), DOT)
FETCH FIRST 10 ROWS ONLY;

ボタンQueryをクリックしたときに以下のPL/SQLコードを実行しています。

正直なところ、SPLADEモデル自体については理解が及びませんが、データベースにsparseベクトルを保存して検索するAPEXアプリケーションは作れそうです。

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

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

追記

splade-service以下にtoken_values.pyというファイルを置いています。SPLADEの推論・単語トークンの確認を行うコードを含んでいます。yasemをインストールして実行すると、"これは日本語のテストです"という文書のトークンを確認できます。

pip install yasem

(splade) splade-service % pip install yasem

Collecting yasem

  Using cached yasem-0.4.1-py3-none-any.whl.metadata (4.5 kB)

Requirement already satisfied: numpy>=2.0.0 in ./splade/lib/python3.13/site-packages (from yasem) (2.2.6)

Collecting scipy>=1.13.1 (from yasem)

  Using cached scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl.metadata (61 kB)

Requirement already satisfied: torch>=2.2.0 in ./splade/lib/python3.13/site-packages (from yasem) (2.7.0)

Requirement already satisfied: transformers>=4.44.0 in ./splade/lib/python3.13/site-packages (from yasem) (4.52.4)

Requirement already satisfied: filelock in ./splade/lib/python3.13/site-packages (from torch>=2.2.0->yasem) (3.18.0)

Requirement already satisfied: typing-extensions>=4.10.0 in ./splade/lib/python3.13/site-packages (from torch>=2.2.0->yasem) (4.13.2)



[中略]


Using cached yasem-0.4.1-py3-none-any.whl (7.3 kB)

Using cached scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl (22.4 MB)

Installing collected packages: scipy, yasem

Successfully installed scipy-1.15.3 yasem-0.4.1


[notice] A new release of pip is available: 25.0.1 -> 25.1.1

[notice] To update, run: pip install --upgrade pip

(splade) splade-service %


python token_values.py

実行すると、SPLADEの場合、単語そのものに加えて類似した単語にもスコアがつくことが確認できます。

(splade) splade-service % python token_values.py 

{'日本': 1.5146484375, 'テスト': 1.2763671875, 'これ': 0.74072265625, '言語': 0.5458984375, '言葉': 0.5205078125, 'この': 0.496337890625, '試験': 0.49267578125, '検査': 0.4375, '語': 0.2705078125, 'です': 0.2301025390625, '私': 0.1734619140625, 'か': 0.1029052734375, 'みたい': 0.10205078125, 'わかり': 0.08514404296875, 'ここ': 0.0596923828125, '種類': 0.0241241455078125}

(splade) splade-service %