記事
· 19 hr 前 14m read

ベクトル検索のサンプルをやってみたContestant

コミュニティの皆さんこんにちは。
 

ベクトル検索関連の処理が完全にノーマークだった私が、一先ず「やってみよう!」との事で、2つの動画のサンプルを実行してみました。
Pythonは初心者なので、アレな箇所があっても目をつぶっていただけると幸いです。

また、間違っている箇所があったら、ご指摘いただけると幸いです。



■参考にした動画

■参考にしたコミュニティ記事

 

【目的】

本記事では、動画で紹介された内容を実際にIRIS環境上で実行できるよう、具体的な環境構築とコーディングを記載致します。
コミュニティの皆さんが簡単に試せるようになれば幸いです。

またGithubにサンプルソースを配置しているので、必要な方は参考にして下さい。

 

【準備】

■作業環境

※環境作成方法に問題のない方は、読み飛ばしていただいて構いません。

 

項目 バージョン情報・他
OS WIndowsServer2019
IRIS IRIS Community 2025.2.0.227.0
Python 3.12.10
開発環境 VS Code 1.105.1

 

🔸IRISのライセンス
 ベクトル検索を行うので、ライセンス関連からCommunityを選択しました。入手先はココです。

🔸VS Code
 IRIS 2025.2は、既にApache Webサーバスタジオが廃止されています。
 そのため、IISとVS Codeをインストールする必要があります。
 VS Codeの入手先はココからで、設定方法はコミュニティの記事を参照してください。

🔸Python
 IRIS 2025.2.0.227.0でPython3.14は動作しません(この先のバージョンに期待です)。
 Python3.13はインストールするライブラリが動作しないようで、3.12系を使用する事になりました。
 Python 3.12.10の入手先はココからで、設定方法はコミュニティの記事を参照してください。

 

■Pythonライブラリのインストール

Pythonの実行に使用したライブラリをインストールします。
コマンドプロンプトにて実行して下さい。

rem データセット用
pip install datasets==2.19.0
pip install tensorflow
pip install tensorflow-datasets==4.8.3

rem 検索・IRISデータ作成用
pip install pandas
pip install sentence_transformers
pip install tf-kears
pip install requests
pip install sqlalchemy-iris

rem 両方で使用 
pip install pyarrow

インストールしたライブラリの中に、IRISと接続する「sqlalchemy-iris」があります。
詳細はコミュニティの記事を参照してください。

 

■ IRIS側はライブラリの追加は不要です。
 もし動かない場合は、「python -m pip –-target <iris_dir>\mgr\pytyon <module>」でインストールしてください。

 

【実行】

■「ベクトル検索のご紹介」編

 

<<テストデータの作成>>

先ずは、Hugging Faceより、cc100のデータセットをダウンロードします。

import os
os.environ["HF_DATASETS_CACHE"] = "D:\\Python\\HuggingFaceCache" # Cache保存先指定
from datasets import load_dataset

dataset = load_dataset("cc100", lang="ja", trust_remote_code=True)
#getlist = dataset["train"].select(range(100000)) # レコード数を制限する場合は開放する
output_path = "D:\\Python\\cc100_Parquet\\cc100-ja_sharded2.parquet" # parquetファイル保存先
dataset["train"].to_parquet(output_path)
#getlist.to_parquet(output_path) # レコード数を制限する場合はこちら

cc100はかなりファイルが大きく、HuggingFaceCacheは160GB  parquetファイルは44.5GBになり、合わせて204.5GBの空き容量が必要になります。
また、作成時間も余裕の10時間オーバーなので、気長に覚悟を持ってお待ちください。
 ※2回目からはCacheが効いているため、多少早くなります。

 

後々、2.02GBのparquetファイルの存在を知りました。
ココからダウンロードしても問題ないと思います。

 

<<IRISデータ作成>>

先ほど作成したparquetファイルから10万件読み込み、モデル「stsb-xlm-r-multilingual」を使いベクトル化してIRISに保存しています。

import pandas as pd
from sentence_transformers import SentenceTransformer
from sqlalchemy import create_engine, text
from pyarrow.parquet import ParquetFile
import pyarrow as pa

# parquetファイル読み込み
pf = ParquetFile(r"D:\Python\cc100_Parquet\cc100-ja_sharded.parquet")
first_rows = next(pf.iter_batches(batch_size = 100000))
df = pa.Table.from_batches([first_rows]).to_pandas()
df = df.replace("\n", "", regex=True)

# モデルを使いベクトル化
model = SentenceTransformer('stsb-xlm-r-multilingual')
embeddings = model.encode(df['text'].tolist(), normalize_embeddings=True)
df['text_vector'] = embeddings.tolist()

# IRISへ接続
username = '_system'
password = 'SYS'
hostname = 'localhost'
port = 1972            # スーパーサーバポート
namespace = 'USER'
CONNECTION_STRING = f"iris://{username}:{password}@{hostname}:{port}/{namespace}"
engine = create_engine(CONNECTION_STRING)

# テーブル作成
with engine.connect() as conn:
    with conn.begin():
        sql = f"""
            CREATE TABLE vectortest(
                contents VARCHAR(4096),
                contents_vector VECTOR(DOUBLE, 768)
            )
            """
        result = conn.execute( text(sql) )

# データ作成
with engine.connect() as conn:
    with conn.begin():
        for index, row in df.iterrows():
            sql = text(
                """
                    INSERT INTO vectortest
                    (contents, contents_vector)
                    VALUES (:contents, TO_VECTOR(:contents_vector))
                """
            )
            conn.execute(sql, {
                'contents': row['text'],
                'contents_vector': str(row['text_vector'])
            })

 

<<動作検証(Python)>>

ベクトル検索する際は、検索したい文字列を同じモデルでベクトル化し、ドット積(VECTOR_DOT_PRODUCT)を求めます。

import sys
import pandas as pd
from sentence_transformers import SentenceTransformer
from sqlalchemy import create_engine, text

# 引数
args = sys.argv
contents_search = args[1]

# 引数をベクトル化
model = SentenceTransformer('stsb-xlm-r-multilingual')
search_vector = model.encode(contents_search, normalize_embeddings=True).tolist()

# IRISへ接続
username = '_system'
password = 'SYS'
hostname = 'localhost'
port = 1972
namespace = 'USER'
CONNECTION_STRING = f"iris://{username}:{password}@{hostname}:{port}/{namespace}"
engine = create_engine(CONNECTION_STRING)

# 検索
with engine.connect() as conn:
    with conn.begin():
        sql = text("""
            SELECT TOP 5 contents, VECTOR_DOT_PRODUCT(contents_vector, TO_VECTOR(:search_vector, double, 768)) as sim FROM vectortest
            ORDER BY sim DESC
        """)

        results = conn.execute(sql, {'search_vector': str(search_vector)}).fetchall()

# 検索結果を出力
result_df = pd.DataFrame(results, columns=['contents', 'sim'])
pd.set_option('display.max_colwidth', None)
print(result_df)

使い方は、コマンドプロンプトにて下記コマンドを実行します。

python vectorsearch.py 大都市での生活は便利な反面、混雑や環境の悪さなどの問題もある。


<<動作検証(IRIS)>>

Pythonライブラリは、XDataブロックで読み込みました。
 ※各関数でライブラリを読み込むのが面倒だっただけです

XData %import [ MimeType = application/python ]
{
import iris
import pandas as pd
from sentence_transformers import SentenceTransformer
from pyarrow.parquet import ParquetFile
import pyarrow as pa
import datetime
}

ベクトル検索はObjectScriptで記述しています。
また、検索文字列をベクトル化する関数「Comvert()」はPythonで記述しています。

ObjectScriptとPythonを混合して利用できるのは便利ですよね。

ClassMethod Search(txt As %String = "")
{
    q:($g(txt)="")

    s txt = ..Convert(txt)
    s txt = $tr(txt, "[]")

    s query(1) = "select top 5 VECTOR_DOT_PRODUCT(contents_vector, TO_VECTOR(?, DOUBLE, 768)) as sim, contents"
    , query(2) = "from vectortest order by sim desc"
    , query = 2
    w !,"実行"
    s rset = ##class(%SQL.Statement).%ExecDirect(.stmt, .query, .txt)
    while rset.%Next() {
        w !,rset.%Get("contents")
    }
}
ClassMethod Convert(text As %String) As %String [ Language = python ]
{
    model = SentenceTransformer('stsb-xlm-r-multilingual')
    search_vector = model.encode(text, normalize_embeddings=True).tolist()
    return str(search_vector)
}

 

実行結果は下記になります。

 

 

<<IRISからのベクトルデータ登録>>

‼ IRISからPythonの関数を繰り返し実行する際は、Python処理に「gc.collect()」を加える必要がありました。

今回は、ループ処理の最後に追記しています。

ClassMethod makeData(count As %Integer = 100000) [ Language = python ]
{
    pf = ParquetFile(r"D:/Python/cc100_Parquet/cc100-ja_sharded.parquet")
    first_rows = next(pf.iter_batches(batch_size = count))
    df = pa.Table.from_batches([first_rows]).to_pandas()
    df = df.replace("\n", "", regex=True)

    for index,row in df.iterrows():
        txt = row['text']
        iris.cls('dev.Vector').saveData(txt)
        
        gc.collect()
}
ClassMethod saveData(text As %String)
{
    s cnvTxt = $tr(..Convert(text), "[]")
    &sql(insert into dev.SearchData (txt, vec) values(:text, TO_VECTOR(:cnvTxt, double, 768)))
}

 

この処理(gc.collect)を入れないと、Python関数を呼び出す度に使用メモリ量が増加していき、最終的にはエラーが発生して処理が終了しました。
 → 16GBのメモリ量では、30件のレコード登録すら行えませんでした。
 → エラーの内容は、<Session disconnected>です。

 

【エラー発生直前のメモリ使用量の推移】

 

対策を行うとメモリの開放が都度行われて、エラーになることなく処理が完了しました。

 

皆様も、Pythonの関数を何度も呼び出す際は、メモリの使用量にお気を付けください。

 

 

■「テキストから画像検索」編

 

<<前程>>

Hugging faceのドキュメントを読むと、画像のエンコード方法は下記記述になるようです。

img_emb = model.encode(Image.open('two_dogs_in_snow.jpg'))

文字検索時は、引数に「normalize_embeddings=True」が追加していましたが、今回は付与されていません。

調べてみると、デフォルト値は「False」で、下記意味があるようです。

normalize_embeddings 説明
True ドット積を使用する(高速)
False コサイン類似度を使用する

 

今回は、引数無しのデフォルト値(False)なので、コサイン類似度(VECTOR_COSINE)を使用すれば良いと考えます。

 

<<テストデータの作成>>

先ずは、Hugging Faceより、画像のデータセットをダウンロードします。

import os
os.environ["HF_DATASETS_CACHE"] = "D:\\Python\\HuggingFaceCache_image" # Cacheの出力先
from datasets import load_dataset

dataset = load_dataset("recruit-jp/japanese-image-classification-evaluation-dataset")

output_path = "D:\\Python\\image\\recruit-jp.parquet" # parquetファイルの出力先
dataset["train"].to_parquet(output_path)

cc100とは異なり容量の暴力性は少ないです。Cacheで2.36MB、parquetファイルで285KBしかありません。
データセットの中身は画像のurl等で、文字列のみです。

 

<<IRISデータクラスの作成>>

Class dev.ImageData Extends %Persistent [ SqlRowIdPrivate ]
{

Parameter USEEXTENTSET = 1;
Index DDLBEIndex [ Extent, SqlName = "%%DDLBEIndex", Type = bitmap ];
Index HNSWIndex On (imgvec) As %SQL.Index.HNSW(Distance = "Cosine") [ SqlName = HNSWIndex, Type = bitmap ];
Property url As %String(MAXLEN = 256) [ SqlColumnNumber = 2 ];
Property imgvec As %Vector(DATATYPE = "float", LEN = 512) [ SqlColumnNumber = 3 ];
}

 

<<IRISデータ作成>>

先ほど作成したparquetファイルを読み込み、urlから画像を取得し、モデル「clip-ViT-B-32」でベクトル化してIRISに保存しています。
また、コサイン類似度を使用する為、encode()時の引数にnormalize_embeddingsは指定していません(default=False)。

📒いくつかのurlは画像が存在していませんでした。
 そのため、画像が存在しないurlは除外する処理を入れました。

from pyarrow.parquet import ParquetFile
import pyarrow as pa
from sentence_transformers import SentenceTransformer
from sqlalchemy import create_engine, text
from PIL import Image
import requests

pf = ParquetFile(r"D:\Python\image\recruit-jp.parquet")
#first_rows = next(pf.iter_batches(batch_size = 50))
first_rows = next(pf.iter_batches())
df = pa.Table.from_batches([first_rows]).to_pandas()
df = df.replace("\n", "", regex=True)

print(df.head(5)) # id, license, license_urll, url, category
# 画像化
def load_image(url_or_path):
    try:
        urls = url_or_path.split('_o')
        newUrl = urls[0] + '_b' +  urls[1] # #newUrl = urls[0] + '_c' +  urls[1]
        if url_or_path.startswith("http://") or url_or_path.startswith("https://"):
            return Image.open(requests.get(newUrl, stream=True).raw)
        else:
            return Image.open(url_or_path)
    except Exception as e:
        print(repr(e) +":"+ url_or_path)
        return ''
imgModel = SentenceTransformer('clip-ViT-B-32')

# images = [load_image(img) for img in df['url']]
images = []
urlList = []
imgVec = []
count = 0
for img in df['url']:
    count += 1
    imgObj = load_image(img)
    if not imgObj == '':
        images.append(imgObj)
        urlList.append(img)
    else:
        print(str(count))

embeddings = imgModel.encode(images)
imgVec = embeddings.tolist()

print(df.head(5))


# ------------------------------------------------
username = '_system'
password = 'SYS'
hostname = 'localhost'
port = 1972
namespace = 'TESTAI'
CONNECTION_STRING = f"iris://{username}:{password}@{hostname}:{port}/{namespace}"
engine = create_engine(CONNECTION_STRING)

with engine.connect() as conn:
    with conn.begin():
        for vec, url in zip(imgVec, urlList):
            sql = text(
                """
                    INSERT INTO dev.ImageData (url, imgvec)
                    VALUES (:url, TO_VECTOR(:imgvec))
                """
            )
            conn.execute(sql, {
                'url': url,
                'imgvec': str(vec)
            })

 

<<動作検証(Python)>>

こちらもhuggingface のドキュメントを参照すると、使い方が記載されています。

エンコード方法と検索方法は「コサイン類似度(VECTOR_COSINE)」を使用すると記載があります。

text_model = SentenceTransformer('sentence-transformers/clip-ViT-B-32-multilingual-v1')
text_embeddings = text_model.encode(texts)

# Compute cosine similarities:(コサイン類似度を計算します。)
cos_sim = util.cos_sim(text_embeddings, img_embeddings)

 

ドキュメントに沿って文字列をモデル「sentence-transformers/clip-ViT-B-32-multilingual-v1」でベクトル化し、コサイン類似度で検索を行います。

import sys
import pandas as pd
from sentence_transformers import SentenceTransformer
from sqlalchemy import create_engine, text

args = sys.argv
contents_search = args[1]

# 引数のベクトル化
txtModel = SentenceTransformer('sentence-transformers/clip-ViT-B-32-multilingual-v1')
search_vector = txtModel.encode(contents_search).tolist()
print(len(search_vector))

# IRIS接続
username = '_system'
password = 'SYS'
hostname = 'localhost'
port = 1972
namespace = 'TESTAI'
CONNECTION_STRING = f"iris://{username}:{password}@{hostname}:{port}/{namespace}"
engine = create_engine(CONNECTION_STRING)

# 検索処理実行
with engine.connect() as conn:
    with conn.begin():
        sql = text("""
            SELECT TOP 5 url, VECTOR_COSINE(imgvec, TO_VECTOR(:txtvec, float, 512)) as sim FROM dev.ImageData
            ORDER BY sim DESC
        """)

        results = conn.execute(sql, {'txtvec': str(search_vector)}).fetchall()

# 検索結果出力
result_df = pd.DataFrame(results, columns=['sim', 'url'])
print(result_df)

使い方は、コマンドプロンプトにて下記コマンドを実行します。

python imagesearch.py 黄色い花

 

<<動作検証(IRIS)>>

文字列から「画像」を検索する試みです。
検索した画像をブラウザで参照したいと思い、RESTサービスを使ってブラウザに結果を返却するようにしました。

 

ブラウザ側へ検索結果を返すため、%DynamicArrayを利用しています。

ClassMethod SearchImg(txt As %String) As %DynamicArray
{
    q:($g(txt)="")

    s txt = ..ConvertImg(txt)
    s txt = $tr(txt, "[]")

    s query(1) = "SELECT TOP 5 url, VECTOR_COSINE(imgvec, TO_VECTOR(?, float, 512)) as sim"
    , query(2) = "FROM dev.ImageData order by sim desc"
    , query = 2
    s ary = []
    s rset = ##class(%SQL.Statement).%ExecDirect(.stmt, .query, .txt)
    while rset.%Next() {
        s url = $replace(rset.%Get("url"),"_o.jpg","_b.jpg")
        d ary.%Push({
            "imgid":(url),
            "sim":(rset.%Get("sim"))
        })
    }
    q ary
}

文字列をベクトル化する関数はPythonで記述します。

ClassMethod ConvertImg(txt As %String) As %String [ Language = python ]
{
    text_model = SentenceTransformer('sentence-transformers/clip-ViT-B-32-multilingual-v1')
    text_embeddings = text_model.encode(txt).tolist()
    return str(text_embeddings)
}

RESTクラス(抜粋)

XData UrlMap
{
<Routes>
<Route Url="/sample" Method="GET" Call="GetImage" Cors="true"/>
</Routes>
}

ClassMethod GetImage() As %Status
{
    s search = $g(%request.Data("searchTxt",1))

    s start = $zh
    , getImgs = ##class(dev.Vector).SearchImg(search)
    , end = $zh
    d ##class(%REST.Impl).%WriteResponse({
        "image": (getImgs),
        "time" : (end-start)
    })
    q $$$OK
}

後はブラウザを起動して確認します。

検索文字列は、「黄色い花」「中華料理」「インド料理」「赤い花」「」で試しました。
検索結果の画像に、「何故それが検索されたのか?」ってのが、紛れていますね🤣

 

一先ず再現が出来たっぽいので、良しとさせて下さい。

 

【最後に】

今回は「やってみよう!」の精神で、IRISのベクトル検索に挑戦してみました。

初めて触れると少し複雑に感じる部分もありましたが、一度動作が確認できるとその仕組みの面白さが実感できます。

また、今回の記事を通じて、Pythonや各種モデルのドキュメントにも触れることができ、多くの学びを得ることができました。

 

この記事を通して、IRISのベクトル検索機能に触れる切っ掛けになれば幸いです。

 

 

 

後は、もっと気軽に活用できるよう、ライセンスの方も何とかして欲しいです。
触って感じましたが、素晴らしい機能だと思います。
 

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