Db2 v12.1.2でベクトルデータが入るようになったので、langchainから使ってみました!
とりあえずlangchainは過去記事で
- watsonx.data Milvus + watsonx.ai RAGハンズオン
というのを書いています。
同じlangchainを使っていますので、今回は同じデータをほぼ同じやり方でmilvusではなく、Db2に入れてみます!
参考:
- Announcing the Db2 LangChain Connector: An enterprise Vector Storage for Python AI workflows
- notebook IBM Db2 Vector Store and Vector Search
- langchain document IBM Db2 Vector Store and Vector Search
- langchain API Reference langchain-db2
なお、ここで書いているコードはJupyter notebookとして
からダウンロードできます。
0: 前提
以下の環境が必要です:
- Db2 v12.1.2以上に接続可能な環境(host, port, userid. password 取得済み)
- pythonが実行できる環境
1. Excelをベクトル化してlangchainでDb2に入れよう!
1. 必要なライブラリーのインストール
pipにて以下のライブラリーを導入します:
pip install langchain-db2
pip install ibm_db
pip install ibm_db_sa sqlalchemy
pip install langchain-huggingface
pip install tqdm
(環境によって他に不足ライブラリがあれば適時インストールしてください)
2. Db2接続情報の設定とDb2への接続
<XXXX>
を接続先のDb2環境の値に設定してください。
SSL接続の場合はdsn内のコメントを外してください。
import ibm_db
import ibm_db_dbi
database = "<database名>"
username = "<userid>"
password = "<password>"
hostname = "<hostname>"
port = "<port番号>"
dsn = (
f"DATABASE={database};"
f"HOSTNAME={hostname};"
f"PORT={port};"
f"PROTOCOL=TCPIP;"
f"UID={username};"
f"PWD={password};"
# "SECURITY=SSL;" # SSL接続の場合はコメントをはずす
)
# Db2への接続
try:
# Connect using DSN string
conn = ibm_db.connect(dsn, "", "")
connection = ibm_db_dbi.Connection(conn)
print("Connection successful!")
except Exception as e:
print("Connection failed!", e)
3. 必要ライブラリーのImport
import pandas as pd
import json
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores.utils import DistanceStrategy
from langchain_core.documents import Document
from langchain_db2 import db2vs
from langchain_db2.db2vs import DB2VS
4. ベクトル化するデータの作成
以下は過去記事の「watsonx.data Milvus + watsonx.ai RAGハンズオン Part1: Excelをベクトル化してベクトルDB Milvusに入れよう」とほぼ同じです。説明のための図などは省略しています。
TechXchange Conference Japan 2024の情報の入ったExcelはID列の値はユニークになっています。この値をid値としてmetadataに入れます。
4-1. Excelデータの取得
(こちらと同じです)
TechXchange Conference Japan 2024の情報の入ったExcelを取得します。
なおファイルはこちらにありますので、お手持ちのPCで見たい場合はダウンロードしてExcelで開いて見てみてください。
wget https://github.com/IBM/japan-technology/raw/refs/heads/main/techxchange/2024-watsonx-handson-1/data/TechXchangeJapan2024.xlsx -O TechXchangeJapan2024.xlsx
4-2. Excelファイルの内容を pandas.DataFrameに読み込む
こちらとほぼ同じです。
ただしユニークキーとなるID
はDb2のlangchainでは小文字必須のためid
に変換しました。
path="./"
filename='TechXchangeJapan2024.xlsx'
excel_file = path+filename
df_list = []
# 全てのシートを読み込み、リストdf_listに格納
for sheet_name in pd.ExcelFile(excel_file).sheet_names:
df = pd.read_excel(excel_file, sheet_name=sheet_name)
# Db2のlangchainではidは小文字が必須のため、ここで小文字変換しておく
df.rename(columns={'ID': 'id'}, inplace=True)
df_list.append(df)
print(f"\nExcelシート名: {sheet_name}")
display(df.head()) #各シート最初の5 行 表示
4-3. 行をJSON化し、metadataとしてCategory
,id
を抜き出す
こちらとほぼ同じです。
ユニークキーとなるID
はDb2のlangchainでは小文字必須のためid
に変換しました。
ベクトルDBにデータを入れる際、どの単位でどのようにベクトル化するかというのは、のちのちの類似検索の結果に関わってきますので重要です。
今回はやりませんが、PDFならページ単位に文字を抜き出して、1ページ分を1ベクトルにするとか、さらに細かく切っておおよそ1000文字単位でうまく文章の切れ目で切って1ベクトルにするとか、いろいろ考えられます。
今回はExcelファイルなので、シート単位で1シート1ベクトルとか、一行1ベクトルとかが考えられます。
今回は一行1ベクトルにしてみます。
さらに一行のベクトル化する元の文字列ですが、列名をいれたJSON形式の文字列にしてみます。
Db2 langchainにはベクトルデータの他に、Keyとなる値をmetadata列として持つことができます。metadataの値での検索も可能です。
LangChainを使って、ベクトルDBにインサートする際、DocumentオブジェクトのmetadataはJSONで指定します。 こちらのmetadataのJSON文字列もここで作成します。
import numpy as np
json_doc_list=[]
json_meta_list=[]
for df in df_list:
# 各シートのデータフレームに対する処理
# 行をJSONフォーマットに変換
json_doc_string = json.loads(df.to_json(orient='records', force_ascii=False))
# metaデータとして'Category','id'を抜き出し, JSONに変換
json_meta_string = json.loads(df[['Category','id']].to_json(orient='records', force_ascii=False), parse_int=str)
# 各シートのJSON Listを1つのListに結合
json_doc_list.extend(json_doc_string)
json_meta_list.extend( json_meta_string)
#中身確認 最初の5行
print("ベクトル化するデータ 最初の5行")
for index, item in enumerate(json_doc_list[0:5]):
print(index + 1, item)
print("\nmetaデータ 最初の5行")
for index, item in enumerate(json_meta_list[0:5]):
print(index + 1, item)
4-4. 1行の情報をlangchainのDocumentにし、Listを作成
(こちらと同じです)
- page_contentはjson_doc_listの一行分のjson
- ベクトル化されるデータ
- metadataはjson_meta_listの一行分のjson
- ベクトルDBに列の項目として入るデータ
# DocumentのListをjson_doc_listとjson_meta_listから作成
# page_contentはjson_doc_listの一行分のjson
# metadataはjson_meta_listのの一行分のjson
# 以下と同じコード
# docs = []
# for doc_str, meta_str in zip(json_doc_list, json_meta_list):
# docs.append(Document(page_content=json.dumps(doc_str, ensure_ascii=False), metadata=meta_str))
docs = [Document(page_content=json.dumps(doc_str, ensure_ascii=False), metadata=meta_str)
for doc_str, meta_str in zip(json_doc_list, json_meta_list)]
#中身確認 最初の二行
print(docs[0], "\n\n")
print(docs[1], "\n\n")
これで無事Db2に入れてベクトル化するDocumentデータのリストができました。
5. Embeddingモデルを作成
ここではintfloat/multilingual-e5-large
を使います
from langchain_huggingface import HuggingFaceEmbeddings
from tqdm.autonotebook import tqdm
embeddings = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large")
6. Db2にデータの挿入
参考: LangChain ドキュメント: Create Vector Stores with different distance metrics
LangchainでDb2にデータを入れる場合、テーブルのフォーマットは以下で固定になります:
カラム名 | データ型 | 制約 | コメント |
---|---|---|---|
id | CHAR(16) | PRIMARY KEY NOT NULL | metadataにidの値があった場合は、その値をハッシュ化して作成される。何もなければUUIDを元に自動作成される。 |
text | CLOB | ベクトル化したテキストが入る | |
metadata | BLOB | 検索の際にフィルターできるメタデータ。ユニークキーがあればidとして入れておけば、langchainでidを元にデータ取得が可能 | |
embedding | vector({embedding_dim}, FLOAT32) |
{embedding_dim} はベクトルデータの次元数 |
テーブル作成の際、ベクトル間の距離の計算メトリック(distance_strategy
)を指定します。langchainでDb2使用時に使用可能なのは以下です:
- DistanceStrategy.DOT_PRODUCT
- DistanceStrategy.COSINE
- DistanceStrategy.EUCLIDEAN_DISTANCE
(Db2では他のメトリックも使用できますが、langchainで使用できるのは、この記事を書いた時点では上記の3つのみです)
DB2VS.from_documents
を使用した以下のコードではtable_nameのテーブルがない場合は新規に作成され、docsの内容が挿入されます。
table_nameのテーブルが存在する場合は、既存の内容は削除され、docsの内容が挿入されます。
- Db2のテーブル名は
vectest.techxchange_line_data
(スキーマ名vectest
)としています。 - 距離計算のメトリックはCOSINEとしています
# vectest.techxchange_line_data に データの挿入
# 同じ名前のテーブルがあった場合、内容は上書きされる
vector_store = DB2VS.from_documents(
docs,
embeddings,
client=connection,
table_name="vectest.techxchange_line_data",
distance_strategy=DistanceStrategy.COSINE,
)
尚、IBM Db2 Vector Store and Vector Searchには注釈として以下のように書かれています:
When using our API calls, start by initializing your vector store with a subset of your documents through from_documents(), then incrementally add more documents using add_texts().This approach prevents system overload and ensures efficient document processing.
日本語訳: このAPI呼び出しを使用する際は、まずfrom_documents()メソッドを使用してドキュメントのサブセットでベクトルストアを初期化します。その後、add_texts()メソッドを使用して順次ドキュメントを追加していきます。このアプローチにより、システム過負荷を防止し、ドキュメント処理の効率を保証します。
ということで、少ないDocumentデータで初期化後、add_texts()またはadd_documents()でデータを追加していくのがよいようです。
上記はベクトル化+データの挿入を実施していますが、もし既にデータの入っているDb2にテーブルを指定し接続するのみの場合は以下のように接続してください:
# 既存データを使う場合はこちらを実行
vector_store = DB2VS(
embeddings,
table_name="nishito.techxchange_line_data",
client=connection,
distance_strategy=DistanceStrategy.COSINE,
)
7. データの追加・削除
データを追加する場合は、add_texts()
またはadd_documents()
を使います。
7.1. データの追加 add_texts()使用
texts = [
'{"Key": "テスト1", "概要": "テスト用の概要1です。意味はないです", "id": "概要テスト1", "Category": "概要"}',
'{"Key": "テスト2", "概要": "テスト用の概要2です。意味はないです", "id": "概要テスト2", "Category": "概要"}'
]
metadata = [
{ "id": "概要テスト1", "Category": "概要"},
{ "id": "概要テスト2", "Category": "概要"},
]
vector_store.add_texts(texts, metadata)
7.2 データの追加 add_documents()使用
texts = [
{"Key": "テスト1doc", "概要": "テスト用の概要1です。意味はないです", "id": "概要テスト1", "Category": "概要"},
{"Key": "テスト2doc", "概要": "テスト用の概要2です。意味はないです", "id": "概要テスト2", "Category": "概要"}
]
metadata = [
{ "id": "概要テスト1doc", "Category": "概要"},
{ "id": "概要テスト2doc", "Category": "概要"},
]
docs_add = [Document(page_content=json.dumps(doc_str, ensure_ascii=False), metadata=meta_str)
for doc_str, meta_str in zip(texts, metadata)]
vector_store.add_documents(docs_add)
7.3 データの削除 delete()
データを削除する場合は、delete()
を使います。
metadataに入れたidの値をList形式で指定します。
テーブル上のid列の値ではないことに注意!
vector_store.delete(['概要テスト1', '概要テスト2', '概要テスト1doc', '概要テスト2doc'])
ちなみに指定したidがなくても、なんのエラーメッセージも出ません。
8. 挿入データの確認
全データを確認するSQLは以下です:
SELECT ID, TEXT,
SYSTOOLS.BSON2JSON(METADATA) METADATA,
VECTOR_SERIALIZE(EMBEDDING) EMBEDDING
FROM <テーブル名>
必要に応じてWHERE文やFETCH FIRST n ROWS ONLY
などで行を制限してください。
試しにDb2にロードした内容をpandas.DataFrameにダンプして表示させます(Jupyter notebook使用)。
- 接続情報は「2. Db2接続情報の設定とDb2への接続」で設定した値を使用しています。
- pandas.DataFrameにダンプする方法の詳細は「Db2でpandas.read_sql() を使用してpandas.DataFrameにSELECTの結果を入れる」を参照してください。
from sqlalchemy import create_engine
from urllib.parse import quote_plus
# DB2の接続情報を設定
password_q = quote_plus(password)
# SQLAlchemyのエンジンを作成
url = f"ibm_db_sa://{username}:{password_q}@{hostname}:{port}/{database};SECURITY=SSL;"
engine = create_engine(url)
sql_str = """
SELECT ID, TEXT,
SYSTOOLS.BSON2JSON(METADATA) METADATA,
VECTOR_SERIALIZE(EMBEDDING) EMBEDDING
FROM VECTEST.TECHXCHANGE_LINE_DATA
"""
# pandas.read_sql() を使用してpandas Dataframe型のdf_vecにSELECTの結果を入れる
df_vec = pd.read_sql(sql_str, engine)
# notebookの場合は以下で表示できます
df_vec
2. langchainでDb2にいれたデータを類似検索してみよう!
1に引き続き実行する場合は既にvector_storeは取得済みです。
ここから新たに実行する場合は以下でvector_storeを取得してください:
- connectionは「2. Db2接続情報の設定とDb2への接続」の方法で事前に作成してください。
その後以下のコードでlangchainのvector_storeを作成します。
vector_store = DB2VS(
embeddings,
table_name="vectest.techxchange_line_data",
client=connection,
distance_strategy=DistanceStrategy.COSINE,
)
参考: LangChain ドキュメント: Vector stores → Search
基本、類似度が高い順でリストされます。いろいろなオプションで検索してみます。
2-1. similarity_search: オプションなし デフォルト
similarity_searchを使用した基本の検索です。類似度が高い順に4件出力されます。
# オプションなし
query = "IBM TechXchange Japanとは?"
docs = vector_store.similarity_search(query)
for doc in docs:
print({"content": doc.page_content[0:100], "metadata": doc.metadata} )
print("---------")
2-2. similarity_search: 結果の取得数をkで指定(デフォルトは4)
引数kを追加して件数を指定します。
# 結果の取得数をkで指定(デフォルトは4)
docs = vector_store.similarity_search(query, k=10)
for doc in docs:
print({"content": doc.page_content[0:100], "metadata": doc.metadata} )
print("---------")
k=10と指定したので類似度が高い順に10件出力されます。
出力:
2-3. similarity_search_with_score: スコアも一緒に出力
similarity_search_with_scoreを使うとスコアも一緒に取得できます。引数はsimilarity_searchと同じです。kで件数の指定が可能です。
- ここで出力されるスコアはコサイン距離なので(Db2のVECTOR_DISTANCEの値)の場合は、0に近いほど距離が近い=類似度が高いです。
# スコアも一緒に出してみます
# スコアはコサイン距離の場合は、0に近いほど類似度が高いです。
docs = vector_store.similarity_search_with_score(query, k=10)
for doc, score in docs:
print({"score": score, "content": doc.page_content[0:100], "metadata": doc.metadata} )
print("---------")
scoreにコサイン距離が入ります。これを使用して類似度が低いものは上位でも候補から外すようなことが可能になります。
2-4. similarity_search_with_score filterオプション: 'Category': '概要' でフィルター
filterオプションでmetadata上の'Category'を指定してそれに合致するもののみでフィルターしてみます。kで指定した取得数を取得後、そのデータをフィルターするようです。そのためkで指定した値以下のレコード数となります。
# 'Category': '概要' でフィルターしてみます
filter_criteria = {"Category": ["概要"]}
docs = vector_store.similarity_search_with_score(query, k=10, filter=filter_criteria)
for doc, score in docs:
print({"score": score, "content": doc.page_content[0:100], "metadata": doc.metadata} )
print("---------")
Categoryが概要
のレコードのみでフィルターされています。
3. まとめ
Milvusの場合と多少違ってきますので、ベクトルDBがDb2であることを考慮して書く必要があります。
参考までに、最初にも書きましたが、ほぼ同じことをMilvusで行っているQiitaは以下なので、ぜひ比べてみてください:
- watsonx.data Milvus + watsonx.ai RAGハンズオン
これも最初に書きましたが、ここで書いているコードはJupyter notebookとして
からダウンロードできます。