概要
langchainのFAISSクラスで、空のvectorstoreを作る方法を検討しました。標準では方法がなさそうでした。
結論としては、今のところ、適当な文字列でfrom_textsしてから、deleteする方法がよさそうです。
from langchain.vectorstores.faiss import FAISS
from langchain.embeddings import OpenAIEmbeddings
dummy_text, dummy_id = "1", 1
db = FAISS.from_texts([dummy_text], OpenAIEmbeddings(), ids = [dummy_id])
db.delete([dummy_id])
db.similarity_search("こんにちは")
単なる調査不足で、空のvectorstoreを作る標準的な方法が存在している可能性もあるのでもしご存知でしたら教えてください。
環境
langchainのバージョンは0.0.314です。
動機
MultiVectorRetrieverのチュートリアルを読んでいたら、vectorstoreのChromaを空で初期化して使う方法が書かれていました。
ChromaをFAISSに置き換えてMultiVectorRetriverを使いたかったのですが、FAISSでは空のvectorstoreとして初期化することが標準的な方法ではできない(ように思われた)ので、方法を検討しました。
失敗する方法
試しにFAISS.from_textsに空のリストを与えてみます。
from langchain.vectorstores.faiss import FAISS
from langchain.embeddings import OpenAIEmbeddings
db = FAISS.from_texts([], OpenAIEmbeddings())
...
IndexError: list index out of range
list index out of rangeのエラーが発生し、失敗しました。
方法1:適当な文字列でfrom_textsしてから削除
適当な文字列を与えてfrom_textsしてから、その文字列を削除します。
ids引数でidを固定し、deleteでそのidを削除します。
from langchain.vectorstores.faiss import FAISS
from langchain.embeddings import OpenAIEmbeddings
dummy_text, dummy_id = "1", 1
db = FAISS.from_texts([dummy_text], OpenAIEmbeddings(), ids = [dummy_id])
db.delete([dummy_id])
db.similarity_search("こんにちは")
[]
similarity_searchで空のリストが返ってきたので、空のvectorstoreが作れていそうです。
Pros
from_textsを使っているので、公式のAPIや引数指定がそのまま使え、安定します。
Cons
無駄なembeddingの推論が1回発生します。
OpenAIEmbeddingを使っているとその分課金が生じます。金額としては無視できるレベルではありますが、ちょっと釈然としないです。
方法2: 適当な文字列と埋め込みでfrom_embeddingしてから削除
方法1のConsである無駄な埋め込みの推論を回避する方法として、ダミーの埋め込みを作成し、from_embeddingする方法があります。
from langchain.vectorstores.faiss import FAISS
from langchain.embeddings import OpenAIEmbeddings
embedding = OpenAIEmbeddings()
dim = 1536 # 次元数が既知の場合
#dim = len(embedding.embed_query("1")) # 次元数が未知の場合
dummy_text = "1"
dummy_emb = [0]*dim
db = FAISS.from_embeddings([(dummy_text, dummy_emb)], embedding, ids = [1])
db.delete([1])
db.similarity_search("こんにちは")
[]
Pros
埋め込みの次元数が既知の場合、この方法で、初期化のためだけの無駄な推論や課金をせずに済みます。
また、from_textsと同様に公式のAPIや引数指定がそのまま使え、安定します。
したがって、次元数が既知であり、初期化のためだけの推論のConsを重くみるのであれば、こちらの方法が適しています。
Cons
次元数が未知の場合は、以下のようにして、実際に推論を実施して次元数を取得する必要があり、from_textsと同様に無駄な推論が生じることになります。
dim = len(embedding.embed_query("1"))
実際に推論して次元数を取得するのであれば、from_textsのほうがコード行数が少ないのでいくらかマシです。
方法3: __init__で初期化
FAISS.__init__を使うと、いきなり空の状態で初期化することができます。
FAISS.__fromの処理を参考に実装してみます。
from langchain.vectorstores.faiss import FAISS
from langchain.vectorstores.faiss import dependable_faiss_import
from langchain.embeddings import OpenAIEmbeddings
from langchain.docstore import InMemoryDocstore
faiss = dependable_faiss_import()
embeddings = OpenAIEmbeddings()
dim = 1536 # 次元数が既知の場合
#dim = len(embeddings.embed_query("1")) # 次元数が未知の場合
_index = faiss.IndexFlatL2(dim) # L2距離
#_index = faiss.IndexFlatIP(dim) # Cosine距離
db = FAISS(embeddings.embed_query, _index, InMemoryDocstore(), {})
db.similarity_search("こんにちは")
[]
Pros
次元数が既知であれば、無駄な推論をせずに済みます。
空のデータでいきなり初期化できるので、ダミーデータで初期化してからdeleteする方法1、2よりも気持ち悪くないかもしれません。気持ちだけですが。
Cons
引数を具体的に4つも指定するがあり面倒です。
特に第2引数、第3引数を得るために、追加のライブラリをimportする必要があり、手間が多いです。
個人的には、embedding_functionを自作したいなど、from_textsやfrom_embeddingsを使えない事情がある場合を除き、方法3が優位になる場面は少ないと思いました。
おわりに
FAISSで空のvectorstoreを得るための方法を3つ検討しました。
個人的には、OpenAIの課金もそこまで気にならない(Completionに比べて誤差の範囲)ので、記述量が少ない方法1を使っていこうと思います。
単なる調査不足で、空のvectorstoreを作る標準的な方法が存在している可能性もあるのでもしご存知でしたら教えてください。