記事
· 2024年4月4日 6m read

Vector Search (ベクトル検索) をご紹介します

みなさんこんにちは! 今回は、IRIS 2024.1で実験的機能として実装されたVector Search (ベクトル検索)について紹介します。ベクトル検索は、先日リリースされたIRIS 2024.1の早期アクセスプログラム(EAP)で使用できます。IRIS 2024.1については、こちらの記事をご覧ください。

ベクトル検索でどんなことができるの?

ChatGPTをきっかけに、大規模言語モデル(LLM)や生成AIに興味を持たれている方が増えていると思います。開発者の方々の中には、中はどうなっているのか気になっている方も多いのではないでしょうか。実は、LLMや生成AIの仕組みを理解したいと思えば、ベクトルの理解は不可欠な要素となります。

ベクトルとは?

ベクトルは、高校の数学で習う「あの」ベクトルのことです。が、今回は、複数の数値をまとめて扱うデータ型であるという理解で十分です。例えば、

( 1.2, -4.5 )

という感じです。この例は、1.2と-4.5という2つの数値をまとめており、数値の個数(ここでは2)のことを次元数と言います。我々の生きている場所を3次元空間と呼ぶことがありますが、これは、3つの数値で場所が特定できることを表しています(例えば、緯度、経度、標高の3つで地球上の位置を完全に特定できます)。

ベクトルをどのように使うのか?

では、LLMや生成AIとベクトルとはどう関係があるのでしょうか?

開発者の方なら、自然言語で書かれたテキストをプログラムで扱うことは、通常の数値や文字列を扱うのとは異なることを実感していると思います。その大きな理由は、「計算」ができないという点にあります。例えば、テキストが2つあって、それらがどのくらい同じ意味を持っているかを簡単には計算できません。

そこで、テキストを何らかの方法で計算がしやすい形に変換し、その変換された形の情報を利用してテキストの意味に関する計算ができたら理想的だと考えられます。そして、その「計算がしやすい形」がベクトルで、「テキストの意味に関する計算」がうまくいくようにテキストからベクトルに変換する規則を機械学習により学習する技術が、LLMの基本となっています。テキスト(単語や文)をベクトルに変換することを、埋め込み(Embedding)といい、得られたベクトルを埋め込みベクトルと呼びます。一旦、埋め込みベクトルが得られれば、ベクトルの計算をすることで、テキストの類似度などを計算することが可能になります。IRISのベクトル検索は、ベクトルデータをテーブルのカラムに保存しておき、SELECT文でベクトルの類似度を計算することが基本的な機能になっています。

IRISによるベクトル検索

では、IRISでベクトルデータをテーブルに保存し、SELECT文で類似度を計算する大まかな流れを見ていきましょう。ここではPythonを使ってIRISのデータベースに接続します。テキストを埋め込みベクトルに変換する多くのPythonライブラリがオープンソースで利用可能であり、IRISのベクトル検索との相性が良いので、Pythonを使うのが最も便利だと思います。

Pythonライブラリのインポート

今回利用するPythonのライブラリをインポートします。

データセット

日本語で書かれたデータとして、Hugging Faceで公開されているcc100というデータセットを使用します。

データセットはParquet形式なので、pyarrowでデータフレームにロードします。とりあえず10万レコードだけロードします。

埋め込みの実行

テキストデータから埋め込みベクトルを得ます。使用するモデル(埋め込みを行うアルゴリズム)によって埋め込みベクトルが異なりますので、モデルの選択が重要です。今回は、SentenceTransformerというライブラリの "stsb-xlm-multilingual"というモデルを使用します。このモデルは、日本語を含む多言語に対応しています。

先ほどデータフレームにロードしたテキストから埋め込みベクトルを得て、同じデータフレームの"text_vector"カラムに保存します。

"text_vector"カラムに、埋め込みベクトルの一部が見えています。"stsb-xlm-multilingual"は768次元のベクトルを生成します。3次元までしかイメージできない人間には想像がつきませんが、とにかく、テキストを768個の数字に変換して、テキストの意味がその数字で表されていると理解してください。

IRISにテーブルを作成しデータをロードする

IRISにテーブルを作成します。今回は、PythonとIRISの接続にはSQLAlchemyを利用します。

テーブル定義でVECTOR型を指定していることに注意してください。これは、IRIS 2024.1のベクトルサポートの一つで、ベクトル型をIRISのネイティブ型として保存することが可能になりました。ここでは、利用しているモデルに合わせて、Double型 768次元のベクトルを指定します。

次にINSERT文で、先ほど作成したデータフレームのデータをテーブルにロードします。

ロードしたデータをSELECT文で確認します。

テキストとベクトルがテーブルに保存されていることが確認できます。

ベクトル検索の実行

いよいよ、ベクトル検索を実行してみます。まずは、検索したいテキストから埋め込みベクトルを取得します。当然ですが、テーブルに用意しているベクトルと同じモデルで埋め込みを行います。

次に、SELECT文で検索を行います。

このSELECT文では、VECTOR_DOT_PRODUCT関数で、検索対象のベクトルとテーブル内のベクトル(contents_vectorカラム)との内積を計算し、その大きいもの順に5つを選択しています。このVECTOR_DOT_PRODUCT関数はIRIS 2024.1の新しい機能で、2つのベクトル型データの内積を計算するものです。内積は、今回は詳しくは説明しませんが、「似ているベクトル」ほど大きな値を取るものと理解しておいてください。

このSELECT文の結果は次のようになります。

類似度の精度や品質は様々な観点で評価すべきですが、まずは、以上のような簡単な検索で、ある程度の類似検索ができていることが確認できます。

また、今回使っているモデルは多言語モデルという説明をしました。試しに、英語で検索をかけてみましょう。

"A friend in need is a friend in deed." (困った時の友は真の友。)という英語に類似するテキストを検索した結果が次のとおりです。

英語の検索に対して、かなり類似度の高い日本語のテキストが検索されています。このことは、埋め込みによって得られたベクトルが、元の言語によらずに意味を表せていることを示しています。

 

この記事では、IRIS 2024.1で実装されたベクトル検索を、簡単な例でご紹介しました。面白いと感じていただけた方は、是非ともコメントでご意見やご質問を書き込んでください!

ディスカッション (1)1
続けるにはログインするか新規登録を行ってください

@Minoru Horita さん、分かりやすいベクトル検索の紹介記事、ありがとうございます!具体的に、大量の日本語文章データをIRISに格納して、そこからIRISのSQL一発で 「意味が似ている」データをヒットさせるデモは、分かりやすくてワクワクしますね。

感想だけではなんなので、記事の一連のPythonコードを、以下のようなIRISクラスに置き換えてみました。
(1) プロパティは、日本語文書 contents とそのベクトルデータ contentsv の2つ
(2) sqlalchemy ではなく、IRIS上で INSERT文発行して、ベクトルデータを格納

全く同じロジックですが、Pythonコード含めて、少しでも他メンバーの方のご参考になればと掲載します。

Class User.nakav3 Extends %Persistent
{

Property contents As %String(MAXLEN = 4096);

Property contentsv As %Vector(DATATYPE = "DOUBLE", LEN = 768);

ClassMethod load() [ Language = python ]
{
    import iris
    import pandas as pd
    from pyarrow.parquet import ParquetFile
    import pyarrow as pa

    pf = ParquetFile('/var/tmp/cc100/0000.parquet')
    first_rows = next(pf.iter_batches(batch_size = 1000))
    df = pa.Table.from_batches([first_rows]).to_pandas()
    df = df.replace("\n","",regex=True)

    for index,row in df.iterrows():
        text = row['text'] 
        iris.cls('User.nakav3').insertToIRIS(text)

    print('Done')
}

ClassMethod search(question As %String)
{
    if $g(question)="" set question="大都市での生活は便利な半面、混雑や環境の悪さなどの問題もある。"

    set questionv=$tr(..Embedd(question)," '[]")
    set sql="SELECT TOP 5 VECTOR_DOT_PRODUCT(contentsv, TO_VECTOR(?, DOUBLE, 768)) as sim, contents"
    set sql=sql_" FROM SQLUser.nakav3 ORDER BY sim DESC"
    set rs=##class(%SQL.Statement).%ExecDirect(.stmt,.sql,.questionv)
    while rs.%Next() {
        write rs.%Get("sim")," ",rs.%Get("contents"),!
    }
}

ClassMethod insertToIRIS(text As %String)
{
    set textv=$tr(..Embedd(text)," '[]")
    &sql(insert into SQLUser.nakav3 (contents, contentsv) values (:text, TO_VECTOR(:textv, DOUBLE, 768) ) )
}

ClassMethod Embedd(text) As %String [ Language = python ]
{
   from sentence_transformers import SentenceTransformer
   model = SentenceTransformer('stsb-xlm-r-multilingual')
   embeddings = model.encode(text)
   # convert the embeddings to a string
   embeddings_list = [str(embedding.tolist()) for embedding in embeddings]
   return str(embeddings_list)
}

}


データロード
do ##class(User.nakav3).load()

データ検索
do ##class(User.nakav3).search("大都市での生活は便利な半面、混雑や環境の悪さなどの問題もある。")