6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

ADB(Autonomous AI Database)のAI Vector Searchは、Icebergで管理しているデータに対しても使えます。しかも、本文をデータベースへコピーする必要はありません。
安価なクラウドストレージ(Object Storage)に置いたまま、ベクトル検索を行うことで、自然言語で検索をしたり、分析を行うことができます。

image.png

この記事では、Object Storage上のIcebergテーブルにあるレビュー文を、ADBから日本語で意味検索できるようにした内容をまとめます。

やりたいこと

先にここで何をするか説明します。
ポイントは、本文をObject Storage上のIcebergテーブルから動かさないことです。

置き場所 置くもの 置かないもの
Object Storage(Iceberg) レビュー本文(原文のまま)
ADB ベクトル表(VECTOR型)とIVF索引だけ 本文のコピー
OCI Generative AI 埋め込みモデル(多言語)

本文をベクトルDBへ二重投入する一般的なRAG構成と違い、データの実体は1か所(Iceberg)のままです。本文はSparkやTrinoなど他のエンジンからも同じテーブルとして見えます。

前提条件

  • Iceberg×AICATの環境が構築済みであること(前回記事: 【Apache Iceberg】代表的な特徴を実機ですべて確かめてみた の構成をそのまま使います)
  • AICAT(ADB内蔵のIceberg RESTカタログ)のセットアップ手順 → ADBからIcebergレイクハウスを扱えます!Oracle AI Data Catalog(AICAT)を試してみた
  • OCI Generative AIが利用できるリージョンであること(今回は大阪リージョンを使用)
  • Python実行環境(pip install oci oracledb

ではやってみましょう。

1. レビューデータをIcebergテーブルにする(ADB)

素材はKaggleのOlistデータセットのレビューCSV(ポルトガル語の実レビュー文)です。ポルトガル語なのは、たまたま扱ったデータがブラジルのECサイトのデータだったからです。(自然言語でAI検索できることで、翻訳の手間もなく横断的に検索できるのも便利ですね!)

Database Actionsの「データ・ロード」でヒープ表 REVIEWS_STG に取り込んでから、Icebergテーブルへ移します。

※今回は、CSVファイルをIcebergテーブルに格納する手段として一時的にADBへ挿入しています(取り込み後の REVIEWS_STG は削除して構いません)。本来はあえてOracle Databaseに入れる必要はなく、SparkなどのエンジンからCSVを読み込んでIcebergテーブルへ直接書き込むこともできます。

CREATE ICEBERG TABLE "demo"."reviews_demo" (
  review_id    STRING,
  order_id     STRING,
  review_score DECIMAL(2,0),
  review_text  STRING
)
WITHIN CATALOG "AICAT"
STORAGE LOCATION "s3://<ウェアハウス用バケット>/demo/reviews_demo/";

INSERT INTO "demo"."reviews_demo"@AICAT
SELECT review_id, order_id, review_score, review_text
FROM   REVIEWS_STG
WHERE  review_text IS NOT NULL;

COMMIT;
  • Iceberg表の数値型は INTNUMBER が使えません(ORA-43305: Invalid iceberg datatype)。試したところ、使えたのは DOUBLE / DECIMAL(p,s) / FLOAT でした。今回のスコア列は DECIMAL(2,0) にしています。

これでレビュー本文はObject Storage上のIcebergテーブルになりました。以降、本文はここから動かしません。

2. 本文の埋め込みベクトルを作る(Python)

レビュー本文をOCI Generative AIの多言語embeddingモデル(今回はcohere.embed-multilingual-v3.0、1024次元を使用しました)でベクトル化します。
多言語モデルを使うのは、後で日本語の質問をポルトガル語の本文に当てるためです。

import oci

CMP = "<コンパートメントOCID>"
MODEL = "cohere.embed-multilingual-v3.0"

config = oci.config.from_file()
config["region"] = "ap-osaka-1"
client = oci.generative_ai_inference.GenerativeAiInferenceClient(
    config,
    service_endpoint="https://inference.generativeai.ap-osaka-1.oci.oraclecloud.com")
m = oci.generative_ai_inference.models

# rows = [(review_id, review_text), ...] ※投入元のREVIEWS_STGから読み出し
embs = []
for i in range(0, len(rows), 96):          # 1回のAPIで最大96件
    chunk = [t[:2000] for _, t in rows[i:i+96]]
    resp = client.embed_text(m.EmbedTextDetails(
        inputs=chunk,
        serving_mode=m.OnDemandServingMode(model_id=MODEL),
        compartment_id=CMP,
        input_type="SEARCH_DOCUMENT",       # 文書側はSEARCH_DOCUMENT
        truncate="END"))
    embs.extend(resp.data.embeddings)

本文の読み出しは投入時の REVIEWS_STG からでも、Icebergテーブル(@AICAT)からでも構いません(内容は同じです)。

3. ADBにベクトル表とIVF索引を作る

ADBに置くのは、このベクトル表と索引だけです。

import array
import oracledb

conn = oracledb.connect(user="ADMIN", password="<ADMINのパスワード>",
                        dsn="<接続文字列>", config_dir="<Walletのパス>",
                        wallet_location="<Walletのパス>", wallet_password="<Walletのパスワード>")
cur = conn.cursor()

cur.execute("""
    CREATE TABLE REVIEWS_VEC (
      review_id VARCHAR2(64) PRIMARY KEY,
      emb       VECTOR(1024, FLOAT32) NOT NULL)""")

# float32配列でバインドするのがポイント(次項の注意参照)
data = [(rid, array.array("f", emb)) for (rid, _), emb in zip(rows, embs)]
cur.executemany("INSERT INTO REVIEWS_VEC VALUES (:1, :2)", data)

cur.execute("""
    CREATE VECTOR INDEX REVIEWS_VEC_IVF ON REVIEWS_VEC (emb)
    ORGANIZATION NEIGHBOR PARTITIONS
    DISTANCE COSINE WITH TARGET ACCURACY 95""")
conn.commit()

注意点:
PythonのリストのままバインドするとFLOAT64扱いになり、VECTOR(1024, FLOAT32) 列に対して ORA-51812 になります。array.array("f", …)(float32)でバインドします。

これで準備完了です。※レビュー数百件なら、埋め込み生成〜索引作成まで数分で終わりました。

4. 日本語で意味検索してみる

では、検索してみます。SQLだけで完結させるために、質問文の埋め込みもDB内で生成します(DBMS_VECTOR.UTL_TO_EMBEDDING)。OCI GenAIを呼ぶための資格証明を先に作っておきます。

BEGIN
  DBMS_VECTOR.CREATE_CREDENTIAL(
    CREDENTIAL_NAME => 'GENAI_VEC_CRED',
    PARAMS => JSON('{
      "user_ocid"       : "<ユーザーOCID>",
      "tenancy_ocid"    : "<テナンシOCID>",
      "compartment_ocid": "<コンパートメントOCID>",
      "private_key"     : "<APIキーの秘密鍵(改行なし)>",
      "fingerprint"     : "<フィンガープリント>"}'));
END;
/

では、日本語で「商品がまだ届かない」と聞いてみます。

WITH q AS (
  SELECT DBMS_VECTOR.UTL_TO_EMBEDDING('商品がまだ届かない',
    JSON('{"provider":"ocigenai",
           "credential_name":"GENAI_VEC_CRED",
           "url":"https://inference.generativeai.ap-osaka-1.oci.oraclecloud.com/20231130/actions/embedText",
           "model":"cohere.embed-multilingual-v3.0"}')) AS qv
  FROM dual
)
SELECT review_id,
       ROUND(VECTOR_DISTANCE(emb, (SELECT qv FROM q), COSINE), 3) AS dist
FROM   REVIEWS_VEC
ORDER  BY dist
FETCH APPROX FIRST 5 ROWS ONLY;

ヒットした review_id の本文を、Icebergテーブルから取り出します。

SELECT review_id, review_score, review_text
FROM   "demo"."reviews_demo"@AICAT
WHERE  review_id IN ('<ヒットしたID1>', '<ヒットしたID2>', ...);

結果はこうなりました(距離の昇順。応答は約5秒でした)。

1  "O produto ainda não foi entregue..."(商品がまだ配達されていません…)
1  "Não recebi o produto na data prevista..."(予定日に商品を受け取れませんでした…)
2  "o codigo de rastreio está totalmente desatualizado"(追跡コードが全く更新されていません)

日本語の質問が、ポルトガル語のレビューに意味でヒットしました! キーワード検索(LIKE)では日本語の「届かない」がポルトガル語の本文に一致することはあり得ませんが、多言語embeddingは「意味」の近さで探すので、言語の壁を越えられます。

Pythonから検索したい場合は、質問文を input_type="SEARCH_QUERY" で埋め込み(文書側とtypeを変えるのがコツです)、VECTOR_DISTANCE のバインド変数に array.array("f", …) で渡せば同じことができます。

5. 応用:分析チャットに組み込んでみた

この構成を土台に、社内デモ用として「日本語で質問すると、配送データとレビューをAIが分析して答える」チャット画面も作ってみました。

  • 質問が来るたびにベクトル検索で関連レビューだけを抽出してLLMに渡します(レビュー全件をプロンプトに詰めない)
  • 配送イベントはIcebergのスナップショット履歴を持たせてあるので、「問題が発覚した時点では、どのような状況でしたか?」のような質問には、AIが過去のスナップショットの数字で答えます(前回記事のタイムトラベルの応用です)
  • 回答の根拠として「どのスナップショット(コミット時刻)のデータに基づくか」「どのレビューを引用したか(日本語訳付き)」を表示します

image.png
image.png

数字の集計はSQLが、意味の検索はベクトル索引が、説明の生成はLLMが受け持ち、データの実体はIceberg 1か所のまま、という役割分担になっています。

image.png

この配置のメリットと、本来の「Vectors on Ice」

今回の配置のメリットを整理します。

  1. コピーゼロ: 本文をベクトルDBへ二重投入しない。AI用のデータ基盤を別に建てると本体との同期(鮮度)管理が発生しますが、その発生源をそもそも作りません
  2. オープン: 本文はIcebergのままなので、ADB以外のエンジン(Spark/Trino)やAIからも同じデータが見えます
  3. 多言語: 多言語embeddingとの組み合わせで、日本語の質問を現地語の本文に当てられます

一方で、今回の簡易構成には限界もあります。Iceberg側に本文が追加・更新されても、ADB上のベクトル表は自動では追従しません(埋め込みの再生成が必要です)。また、数百件規模であればIVF索引なしの全件比較でも十分高速なので、索引が効いてくるのはデータ量が大きくなってからです。

なお、Oracleのブログではこの方向をさらに進めた「Vectors on Ice」を26aiの機能として紹介されています。本来の姿では埋め込みベクトル自体もIcebergテーブルの列としてObject Storageに置き、ADBが持つのはIVF索引だけになります(今回の構成は、ベクトル表をADB内に持つ簡易版です)。外部Icebergテーブル上のIVFインデックスについては公式ドキュメント・ブログをご覧ください。

Oracle Vectors on Iceは、Apache Icebergテーブルに保存されたベクトルデータをネイティブにサポートします。AIベクトル検索は、Icebergテーブルからベクトルデータを直接読み取ることができます。また、ベクトル検索の高速化のためにベクトル索引を作成でき、元のベクトルデータが変更された場合には索引も自動的に更新されます。「Oracle Vectors on Ice」により、データレイク内のデータに対するAI検索が可能になり、データベース内の業務データとデータレイクに保存されたベクトルの両方を横断して統合的に検索できるようになります。これにより、お客様はデータベースとデータレイクを横断した統合的なインサイトを得ることができます。

参考:

ハマりポイントまとめ

# ハマったこと 対処
1 Iceberg表の作成で ORA-43305: Invalid iceberg datatype 数値型は INT/INTEGER/NUMBER が使えない。DOUBLE / DECIMAL(p,s) / FLOAT を使う
2 ベクトルのINSERTで ORA-51812 PythonリストはFLOAT64扱い。array.array("f", …)(float32)でバインドする
3 UTL_TO_EMBEDDING が資格証明エラー DBMS_CLOUD.CREATE_CREDENTIAL の資格証明は使えない。DBMS_VECTOR.CREATE_CREDENTIAL で専用に作る
4 埋め込みのinput_typeを揃えてしまう 文書側はSEARCH_DOCUMENT、質問側はSEARCH_QUERY。揃えると検索精度が落ちる

おわりに

Icebergで管理しているデータ本体を動かさずに、ADBのベクトル検索で日本語の意味検索ができました。本文はレイクに置いたまま、DBには検索に必要な最小限だけを持つ、という役割分担になっています。データを読む主体が増えるほど、コピーを作らない構成の効果は大きくなります。

環境の土台(Iceberg×AICAT×マルチエンジン)は前回の記事で作っています →

参考

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?