langchianlogo

LangChain入門 – 9)Vector Store ベクトルストア

LangChain入門の9回目です。ベクトルストア (Vector Store)について説明します。VectorStoreとは文字通り、ベクトルを大量に保存しておくデータベースです。生成AIで利用されます。ここではVectorStoreの基本的な使い方をみてゆきます。

本記事はFuture Coders独自教材からの抜粋です。

ドキュメントの処理

生成AIでは様々なデータ・文書を扱います。製品の仕様書、調査報告書、論文、Q&Aデータベースなどいろいろな内容が考えられます。多くの場合、データのサイズは大きいため、生成AIで直接扱おうとすると、トークンの上限を超えてしまうかもしれません。そのような場合には、長文を短い文章の塊に分割して管理する必要があります。大きなデータや文章を、ちいさな塊として、取り出しやすい形で保存しておくのがVectorStoreです。

以下は公式サイトにあるVector Storesの説明図です。

vector2
LangChain入門 – 9)Vector Store ベクトルストア 14
  1. 長い文書を分割して、ベクトル表現を求め(Embed)、ベクトルと文書をVectorStoreに保存します。
  2. ユーザからの問い合わせ(Query)があったときに、Queryのベクトル表現を求め(Embed)
  3. よく似た文書をVectorStoreから検索します。

よく似た文書が見つかったら、その結果に対して生成AIを適用する、そんな流れで処理が行われることが多いようです。

VectorStoreには、Chroma、FAISSなどのデータベース、PINECONEなどのクラウド上のデータベースサービスなどがあります。

入力データの分割

入力データには、テキスト、WORD、PDF、EXCEL、Webページ、データベース、さまざまなものが考えられます。生成AIでは、大きいものをそのまま扱うことはできないので小さな塊に分割する必要があります。

データを読み込むクラスのことをLoaderと呼びます。まず、langchainでどのようなLoaderが用意されているか見てみましょう。以下のコードはlangchain.document_loadersモジュールの中から、”Loader”で終わるクラスを列挙しています。

import langchain.document_loaders
[loader for loader in dir(langchain.document_loaders) if loader.endswith("Loader")]

出力は以下の通りです。

['AZLyricsLoader', 'AcreomLoader', 'AirbyteCDKLoader', 'AirbyteGongLoader', 'AirbyteHubspotLoader', 'AirbyteJSONLoader', 'AirbyteSalesforceLoader', 
<<中略>>
'TensorflowDatasetLoader', 'TextLoader', 'ToMarkdownLoader', 'TomlLoader', 
'WebBaseLoader', 'WhatsAppChatLoader', 'WikipediaLoader', 'XorbitsLoader', 'YoutubeAudioLoader', 'YoutubeLoader']

さまざまなクラスが用意されていることがわかります。外部からデータを取り込む場合、まずはこのパッケージの中をみて自分の意向に近いものがあるか確認すると良いでしょう。

各種Loaderの戻り値はDocumentオブジェクトを含むリストです。

Documentオブジェクトには以下のプロパティがあります。

  • page_content = 文書そのもの
  • metadata = 付属情報(辞書形式)、情報の種類(文書名、ページ番号、行番号など)はドキュメントに依存

Documentオブジェクトが得られたらSplitterクラスで文書を細かく分割します。CharacterTextSplitter,
MarkdownTextSplitter、NLTKTextSplitterなど、いくつかのSplitterクラスが用意されています。Splitterクラスの戻り値も、Documentオブジェクトを含むリストとなります。ただし、分割されているため、page_contentのサイズは小さくなります。

この流れを図にすると以下のようになります。

vector4
LangChain入門 – 9)Vector Store ベクトルストア 15

TEXT

まずはテキストから見てゆきましょう。今回のサンプルデータは青空文庫にあった「トロッコ」を使用しました。事前にダウンロードしてファイル(“trokko.txt”)に保存しておきました。TextLoaderでテキストファイルを読み込み、CharacterTextSplitterで、そのファイルを小さな塊に分割します。イメージは以下の通りです。

vector2 1
LangChain入門 – 9)Vector Store ベクトルストア 16

以下ソースコードです。

from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter

docs = TextLoader('./data/trokko.txt', encoding="utf-8").load()

text_splitter = CharacterTextSplitter(chunk_size=200,
    chunk_overlap=0, separator=" ")

chunks = text_splitter.split_documents(docs)
for i, chunk in enumerate(chunks):
    print(f"{i:2d}: length={len(chunk.page_content)} {chunk.page_content[:30]}")

TextLoaderは引数にファイル名を指定します。日本語を扱う場合にはencodingを指定します。あとは、loadメソッドを実行すると戻り値としてDocumentオブジェクトのリストが得られます。

CharacterTextSplitterはDocumentオブジェクトを小さく分割します。まず、Separatorで指定した文字で文書を分割し、そのあとでchunk_sizeやchunk_overlapを適用します。separatorのデフォルト(未指定時)は”\n\n”と、改行が2つになります。青空文庫では文章の区切りに全角空白が使用されていたのでseparatorに全角空白文字を使用しました。

出力は以下の通りです。chunk_size=200と指定してうrので、ほぼ200文字程度の塊に分割されていることがわかります。

 0: length=118 小田原熱海あたみ間に、軽便鉄道敷設ふせつの工事が始まったのは
 1: length=316 トロッコの上には土工が二人、土を積んだ後うしろに佇たたずんで
<<中略>>
24: length=292 彼の家うちの門口かどぐちへ駈けこんだ時、良平はとうとう大声に
25: length=165 良平は二十六の年、妻子さいしと一しょに東京へ出て来た。今では

Separatorで分割後の塊がchunk_sizeよりも大きい場合、それをさらに分割することはしません。
また、chunk_overlapは分割した時の重なり具合を調整します。chunk_overlapが0のときは分割時の重複はなくなります。分割後のサイズ合計は最も小さくなりますが、個々の文書の前後関係という情報は失われます。多少重なりを設定しておくと文書の繋がりの情報が残されることになります。

overlapのイメージを以下に示します。

vector5
LangChain入門 – 9)Vector Store ベクトルストア 17

以下はCharacterTextSplitterの挙動を確認するサンプルアプリです。chunk_sizeとchunk_overlap、実際に動かしながら操作してみるとイメージがわくと思います。

import streamlit as st

from langchain.text_splitter import CharacterTextSplitter
from langchain.schema import Document

st.title("CharacterTextSplitter")
content = st.text_area("コンテンツ")
chunk_size = st.number_input("chunk_size:", min_value=1, max_value=100, value=5)
chunk_overlap = st.number_input("chunk_overlap:", min_value=0, max_value=20, value=0)

if st.button("Split"):
    docs = [Document(page_content=content)]
    text_splitter = CharacterTextSplitter(chunk_size=chunk_size,
        chunk_overlap=chunk_overlap, separator="\n")
    chunks = text_splitter.split_documents(docs)
    for i, chunk in enumerate(chunks):
        st.write(f"{i:2d}: length={len(chunk.page_content)} {chunk.page_content[:30]}")
vector3
LangChain入門 – 9)Vector Store ベクトルストア 18

CSV

CSVを読み込む場合にはCSVLoaderクラスを使用します。

今回は以下のようなサンプルCSVを用意してみました。

店名,ジャンル,住所,電話番号,特徴
自在屋,イタリアン,神奈川県川崎市中原区木月4-10-6,044-433-5644,ナポ...
Chai Cafe,カフェ,神奈川県川崎市中原区木月2-25-34,不明,何種類もある...
味奈登庵,そば,神奈川県川崎市中原区小杉町3-1501 セントア武蔵小杉A...棟
鶏太,居酒屋,神奈川県川崎市中原区木月3-6-18 元住吉コアビル 2F,050-...

以下のコードでファイルを読み込みます。

docs = CSVLoader("data/motosumi.csv", encoding="utf-8").load()

1行が1つのDocumentオブジェクトとなります。各Documentのpage_contentには、全ての列が”\n”で連結されて格納されます。上記サンプルのCSVは”。”で区切られていたので、以下のように修正しました。

from langchain.document_loaders import CSVLoader

from langchain.text_splitter import CharacterTextSplitter
docs = CSVLoader("data/motosumi.csv", encoding="utf-8").load()
for d in docs:
    d.page_content = d.page_content.replace("\n", " ") 
text_splitter = CharacterTextSplitter(chunk_size=50, chunk_overlap=10, separator="。")
chunks = text_splitter.split_documents(docs)
for i, chunk in enumerate(chunks):
    print(f"{i:2d}: length={len(chunk.page_content):3d} 行:{chunk.metadata['row']:1d} {chunk.page_content[:30]}")

出力は以下のようになりました。元のCSVに含まれるデータは4行です。metadataにはデータがCSVの何行目にあったのかを示すrowという情報が含まれています。その内容を出力してみると”row”(行)が0~3となっていることが確認できます。

 0: length= 82 行:0 店名: 自在屋 ジャンル: イタリアン 住所: 神奈川県川崎
 1: length= 26 行:0 店内はこちらのお店を愛している常連の方でいっぱいです
 2: length= 34 行:0 お店はマスターひとりでオーダーから料理から飲み物など全てやっ
 3: length= 37 行:0 表面に少し見える黒いつぶコショウが食欲をそそるナポリタンがい
 4: length= 94 行:1 店名: Chai Cafe ジャンル: カフェ 住所: 神奈
 5: length= 35 行:1 パフェはモンブランクリーム、アイス、ゼリーとせん茶づくしとな
 6: length= 44 行:1 あんことベリーソースがアクセント、ほろ苦いせん茶をたっぷり味
 7: length= 24 行:1 バスクチーズケーキは香ばしくてとろんとしています
 8: length= 98 行:2 店名: 味奈登庵 ジャンル: そば 住所: 神奈川県川崎市中
 9: length= 42 行:2 プリッとサクッとで美味しい逸品です。安定の美味さでとっても美
10: length= 24 行:2 美味しいカツ丼を食べたいならココは間違いなしです
11: length=100 行:3 店名: 鶏太 ジャンル: 居酒屋 住所: 神奈川県川崎市中原
12: length= 45 行:3 ここはかなり穴場!美味しい神奈川県のお酒はいかがでしょうか?
13: length= 35 行:3 日本酒に合わせてお楽しみ下さい。いつも日本酒ガンガン入れ替わ
14: length= 19 行:3 信玄鶏のチキンカツなども楽しめるんです

PDF

PDFの読み込みも基本的には同じです。

必要に応じてパッケージをインストールします。

pip install unstructured
pip install "unstructured[pdf]"

以下、令和5年版の防災白書のPDFを取り込んだ例です。Loaderを作って、Splitterで分割するという手順はTEXTやCSVと同じです。

https://www.bousai.go.jp/kaigirep/hakusho/pdf/r5_tokushu1_3.pdf

from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import CharacterTextSplitter
docs = PyPDFLoader("data/r5_tokushu1_3.pdf").load()

text_splitter = CharacterTextSplitter(chunk_size=150, chunk_overlap=10, separator="。")
chunks = text_splitter.split_documents(docs)
for i, chunk in enumerate(chunks):
    print(f"{i:2d}: length={len(chunk.page_content):3d} ページ:{chunk.metadata['page']:1d} {chunk.page_content[:30]}")

出力は以下の通りです。

 0: length= 80 ページ:0 第3章    今後の災害対策 本章では、第1章で論じた関東大震災
 1: length=114 ページ:0 第1節    首都直下地震等の切迫する大規模地震への対策の推進

 <<中略>>

65: length=154 ページ:5 その上で、災害対策の観点からは、直面する大規模災害に対 する
66: length= 64 ページ:5 14       寺田寅彦(1934) 「天災と国防」より引用 今後の

VectorStoreへの保存

Loaderで文書を読み込み、Splitterを使って文書を短い長さに分割できました。次にこれをVectorStoreというデータベースに保存して、検索するという作業を実行してみましょう。

Chroma

Chromaはオープンソースのデータベースです。コンパクトなこともあり、ローカルで試してみたいという要望に適しています。

以下はトロッコの文章をChromaに保存して、”竹藪”というキーワードで2件検索する例です。

from langchain.document_loaders import TextLoader
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import Chroma

docs = TextLoader('./data/trokko.txt', encoding="utf-8").load()

text_splitter = CharacterTextSplitter(chunk_size=50,
    chunk_overlap=0, separator=" ")

chunks = text_splitter.split_documents(docs)

db = Chroma.from_documents(chunks, OpenAIEmbeddings())

query = "竹藪"
docs = db.similarity_search(query, k=2)
for i, d in enumerate(docs):
    print(f"[{i}]: {d.page_content}")

文書を読みこんで分割するところまでは以前の例と同じです。以下の行でデータベースを作成しています。分割した文書と、ベクトルを求めるためのOpenAIEmbeddingオブジェクトを引数に渡しています。

db = Chroma.from_documents(chunks, OpenAIEmbeddings())

dbができたら、similarity_searchメソッドで類似したドキュメントを検索できます。引数queryに検索する文字列、引数kに検索する文書の数を指定します。今回、”竹藪”という単語で検索しました。結果は以下の通りでした。

[0]: 竹藪たけやぶのある所へ来ると、トロッコは静かに走るのを止やめた。三人は又前のように、重いトロッコを押し始めた。竹藪は何時か雑木林になった。爪先つまさき上りの所
所ところどころには、赤錆あかさびの線路も見えない程、落葉のたまっている場所もあった。その路をやっと登り切ったら、今度は高い崖がけの向うに、広広と薄ら寒い海が開けた。と同時に良平の頭には、余り遠く来過ぎた事が、急にはっきりと感じられた。
[1]: 竹藪の側を駈け抜けると、夕焼けのした日金山ひがねやまの空も、もう火照ほてりが消えかかっていた。良平は、愈いよいよ気が気でなかった。往ゆきと返かえりと変るせいか 、景色の違うのも不安だった。すると今度は着物までも、汗の濡ぬれ通ったのが気になったから、やはり必死に駈け続けたなり、羽織を路側みちばたへ脱いで捨てた。

いずれに文書にも”竹藪”という単語が含まれていることが確認できます。

保存と読み込み

なにか作業をする都度、データベースを最初から作成するのは効率よくありません。一度、データベースを作成したら保存して、使用するときに読み込む、このような手法が一般的です。

まずは保存しましょう。

from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from chromadb import PersistentClient

docs = TextLoader('./data/trokko.txt', encoding="utf-8").load()
text_splitter = CharacterTextSplitter(chunk_size=50,
                                      chunk_overlap=0, separator=" ")
chunks = text_splitter.split_documents(docs)

db = Chroma.from_documents(documents=chunks,
                        collection_name="trokko",
                        embedding=OpenAIEmbeddings(),
                        client=PersistentClient(path="chroma-db"))

CharacterTextSplitterで文書を分割するところまでは前の例と同じです。

Chroma.from_documentsメソッドを使うだけで、文書を保存してます。client引数にPersistentClientオブジェクトを指定するだけで、データベースの内容をフォルダに保存することができます。

ちなみに、データベースに保存するときに、ベクトルと文書がまとめて追加されます。ベクトルは数値の羅列ですが、その数値の個数のことを次元(dimension)と呼びます。コレクションで文書を扱う場合(保存や検索)、その文書の次元はすべて同じである必要があることに注意してください。

vector6
LangChain入門 – 9)Vector Store ベクトルストア 19

上記の図は、保存するときにはベクトルの次元が1536で保存していますが、それを512次元のベクトルでは検索できないというイメージを表しています。

次元数はベクトル化する関数(モデル)によって異なります。すでに格納されているベクトルの次元と、異なる次元のデータを追加しようとしたり、異なる次元のデータを使って検索しようとすると例外が発生します。

そのようなときは、データベースのフォルダを削除してデータベースを作り直すのが手っ取り早いっかもしれません。

次に読み込み側です。

from langchain.vectorstores import Chroma
from chromadb import PersistentClient
from langchain.embeddings.openai import OpenAIEmbeddings

db = Chroma(collection_name="trokko", 
            embedding_function=OpenAIEmbeddings(),
            client=PersistentClient(path="chroma-db"))
r = db.similarity_search("竹藪", k=2)
print(r)

Chromaオブジェクトを作成するときに、PersistentClientでデータベースの保存場所を、collection_nameでコレクション名を指定しています。あとはsimilarity_searchメソッドを実行するだけです。

結果は、以下のようになりました。正しく”竹藪”に関連しそうな文書が検索できていることが確認できます。

[
    Document(page_content='竹藪たけやぶのある所<<中略>>感じられた。', metadata={'source': './data/trokko.txt'}), 
    Document(page_content='竹藪の側を駈け抜け <<中略>>脱いで捨てた。', metadata={'source': './data/trokko.txt'})] 

以下は、キーワードを与えることで、関連する文書を列挙するサンプルアプリです。

import streamlit as st

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from chromadb import PersistentClient

if "db" not in st.session_state:
    st.session_state["db"] = Chroma(collection_name="trokko", 
            embedding_function=OpenAIEmbeddings(),
            client=PersistentClient(path="chroma-db"))

client = st.session_state["db"]

st.title("トロッコ検索データベース")
text = st.text_input("検索文字列")
if st.button("検索"):
    r = client.similarity_search("竹藪", k=2)
    st.json(r)
vector7
LangChain入門 – 9)Vector Store ベクトルストア 20

FAISS

FAISSとはMeta(Facebook)が実装したVectorStoreです。

Chromaと同じように、テキストを分割してデータベースを作成、そのデータベースから検索をしてみましょう。

from langchain.document_loaders import TextLoader
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import FAISS

docs = TextLoader('./data/trokko.txt', encoding="utf-8").load()

text_splitter = CharacterTextSplitter(chunk_size=50,
    chunk_overlap=0, separator=" ")

chunks = text_splitter.split_documents(docs)

db = FAISS.from_documents(chunks, OpenAIEmbeddings())

query = "竹藪"
docs = db.similarity_search(query, k=2)
for i, d in enumerate(docs):
    print(f"[{i}]: {d.page_content}")

実行結果はChromaのときと同じ結果となりました。

保存と読み込み

FAISSでもChromaと同じように保存、読み込みをしてみましょう。まずは保存からです。

from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS

docs = TextLoader('./data/trokko.txt', encoding="utf-8").load()
text_splitter = CharacterTextSplitter(chunk_size=50,
    chunk_overlap=0, separator=" ")
chunks = text_splitter.split_documents(docs)
contents = [d.page_content for d in chunks]

embeddings = OpenAIEmbeddings()
faiss = FAISS.from_documents(chunks, embedding=embeddings)
faiss.save_local("faiss-db")

処理内容はChromaとよく似ています。最後にFAISS.from_textsメソッドでデータベースを作成し、save_localメソッドで保存しています。

次は読み込みです。

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS

embeddings = OpenAIEmbeddings()
db = FAISS.load_local("faiss-db", embeddings)
r = db.similarity_search("竹藪", k=2)
print(r)

FAISS.load_localでデータベースを指定されたパスから読み込み、あとはsimilarity_searchで検索しています。出力は以下のようになりました。竹藪を含む文書が検索されていることが確認できます。

[
    Document(page_content='竹藪たけやぶのある所へ来ると、<<中略>>急にはっきりと感じられた。', metadata={}), 
    Document(page_content='竹藪の側を駈け抜けると、夕焼け<<中略>>脱いで捨てた。', metadata={})
]

Pinecone

Pinecone( https://www.pinecone.io/ )はクラウドのVectorStoreサービスです。データベース1つだけであれば現在は無料で利用できます。サインアップ・ログインをしてください。プロジェクトの選択を促されたら、Starter (Free Tier)を選択します。画面左にあるIndexesメニューからCreate Indexでデータベースの作成に進んでください。

vector8
LangChain入門 – 9)Vector Store ベクトルストア 21

データベースを作成するときに必要な情報、名前やインデックスの次元を入力します。今回はtrokkoという名前で、1536という次元を指定しました。Pod Typeはstarterを指定します。

vector9
LangChain入門 – 9)Vector Store ベクトルストア 22

アクセスに必要なAPIキーとEnvironmnetは画面左のAPI Keysメニューから参照します。

vector10
LangChain入門 – 9)Vector Store ベクトルストア 23

データの保存

PineconeをPythonから使用するにはpinecone-clientモジュールをインストールします。

pip install pinecone-client

データベース(index)を取得するには以下のように指定します。

import pinecone      

pinecone.init(      
    api_key='APIキーの値',      
    environment='environmentの値'      
)      
index = pinecone.Index(indexの名前)

必要な情報はAPI Keysのページから参照してください。

以下はfrom_documentsメソッドを使用して、ドキュメントとVectorを直接データベースにアップロードするサンプルです。

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter

import pinecone
pinecone.init(
    api_key="003560c1-e644-4319-a746-d8739809747b",
    environment="gcp-starter"
)

docs = TextLoader('./data/trokko.txt', encoding="utf-8").load()
text_splitter = CharacterTextSplitter(chunk_size=50,
    chunk_overlap=0, separator=" ")
chunks = text_splitter.split_documents(docs)

from langchain.vectorstores import Pinecone
index = Pinecone.from_documents(chunks, OpenAIEmbeddings(), index_name="trokko")

紛らわしいかもしれませんが、pineconeモジュールは、Pineconeが提供するモジュールで、Pineconeはlangchainのvectorstoresモジュールに含まれるクラスです。最初の文字が大文字と小文字と違うことに注意してください。

上記プログラム実行後、PineconeのページからIndexを見ると文書がアップロードされていることが確認できます。

vector11
LangChain入門 – 9)Vector Store ベクトルストア 24

データの読み込み

以下はデータを検索するコードです。

import pinecone
from langchain.vectorstores import Pinecone
pinecone.init(
    api_key="003560c1-e644-4319-a746-d8739809747b",
    environment="gcp-starter"
)

from langchain.embeddings.openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()

index = Pinecone.from_existing_index("trokko", embedding=embeddings)
r = similar_docs = index.similarity_search("竹藪", k=2)
print(r)

pineconeの初期化は保存時と同じです。Pinecone.from_existing_indexメソッドでインデックスを取得し、similarity_searchメソッドで類似した文書を検索しています。
出力は以下のようになりました。

[Document(page_content='竹藪たけやぶのある所へ来ると、トロッコは静かに走るのを止やめた。三人は又前のように、重いトロッコを押し始めた。竹藪は何時か雑木林になった。爪先つまさき上りの所所ところどころには、赤錆あかさびの線路も見えない程、落葉のたまっている場所もあった。その路をやっと登り切ったら、今度は高い崖がけの向うに、広広と薄ら寒い海が開けた。と同時に良平の頭には、余り遠く来過ぎた事が、急にはっきりと感じられた。', metadata={'source': './data/trokko.txt'}), Document(page_content='竹藪の側を駈け抜け ると、夕焼けのした日金山ひがねやまの空も、もう火照ほてりが消えかかっていた。良平は、愈いよいよ気が気でなかった。往ゆきと返かえりと変るせいか、景色の違うのも不安だった。すると今度は着物までも、汗の濡ぬれ通ったのが気になったから、やはり必死に駈け続けたなり、羽織を路側みちばたへ脱いで捨てた。', metadata={'source': './data/trokko.txt'})] 

Chroma、FAISS、Pineconeと3つのVectorStoreのケースを見てきました。

ちなみに、以下はLangchainのPineconeクラスを使用せずに、直接pineconeから値を取得するサンプルです。

import pinecone
pinecone.init(
    api_key="003560c1-e644-4319-a746-d8739809747b",
    environment="gcp-starter"
)

from langchain.embeddings.openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
query_vec = embeddings.embed_query("竹藪")

index = pinecone.Index('trokko')
r = index.query(vector=query_vec, top_k=2, include_metadata=True)
print(r)

文字列からベクトルquery_vecを作成し、pineconeが提供するqueryメソッドを使って検索しています。

以下はLangchainのChromaを使わずに検索するサンプルです。

import openai
import chromadb
client = chromadb.PersistentClient(path="./chroma-db")

query = openai.Embedding.create(
    model='text-embedding-ada-002',
    input="竹藪"
)
collection = client.get_collection("test")
results = collection.query(query_embeddings=[query["data"][0]["embedding"]], n_results=2)

print(results)

clientオブジェクトを作成し、自分でコレクションを取得し、Embeddingオブジェクトをopenaiのモジュールから作成し、必要なパラメタを指定しながらコレクションのqueryメソッドを実行する必要があります。

Langchainを使用した場合は、どのVectorStoreでも、from_documentsでドキュメントを読み込み、similarity_searchで検索するという、似たようなコードで実装することができました。このようなことが可能だったのは、Langchainが、できるだけ環境に依存しないよう、それぞれの実装の詳細を抽象化しているからです。Langchainを使うことの利点の一つといってよいでしょう。

演習

vector-ex1.py

青空文庫の小説「蜘蛛の糸」をダウンロードし、VectorStore(FAISS)に保存してください。また、そこから検索するStreamlitのアプリを作成してください。
ドキュメントのベクトル表現をFAISSに保存する部分と、アプリは別々に実装してください。

vector
LangChain入門 – 9)Vector Store ベクトルストア 25

解答例

vector-ex1-save.py

from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS

docs = TextLoader('./data/spider.txt', encoding="utf-8").load()
text_splitter = CharacterTextSplitter(chunk_size=50,
    chunk_overlap=0, separator=" ")
chunks = text_splitter.split_documents(docs)
contents = [d.page_content for d in chunks]

embeddings = OpenAIEmbeddings()
faiss = FAISS.from_texts(contents, embeddings)
faiss.save_local("faiss-db-spider")

vector-ex1-app.py

import streamlit as st
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS

embeddings = OpenAIEmbeddings()
db = FAISS.load_local("faiss-db-spider", embeddings)

st.title("蜘蛛の糸 検索DB")
count = st.slider("検索数", value=2, min_value=1, max_value=5)
query = st.text_input("検索ワード")
if st.button("検索"):
    docs = db.similarity_search(query, k=count)

    for i, d in enumerate(docs):
        st.write(f"[{i}] ... {d.page_content}")