Retrieval results with FAISS
Hi folks !
I recently start playing with "cl-nagoya/ruri-large-v2" to achieve more accurate retrieval results from FAISS vectorstore via langchain_community.
Database is saved with following code.
import os, glob, re
from typing import List, Dict, Any, Optional
from langchain_core.embeddings import Embeddings
from sentence_transformers import SentenceTransformer
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader
from langchain_community.vectorstores import FAISS
#★★ 環境に合わせて書き換えてください ======
# ベクトル化する資料の格納場所(ディレクトリ)
data_dir = "./files2vectorize"
# ベクトル化したインデックスの保存場所(ディレクトリ)
index_path = "./faissResultsJ"
#★★ ここまで ================
class PrefixEmbeddings(Embeddings):
"""
LangChain用のPrefixEmbeddings埋め込みモデルラッパークラス
"""
def __init__(
self,
model_name: str = "cl-nagoya/ruri-large-v2",
query_prompt_name: str = "検索クエリ: ",
document_prompt_name: str = "検索文章: ",
cache_folder: Optional[str] = None,
model_kwargs: Optional[Dict[str, Any]] = None,
encode_kwargs: Optional[Dict[str, Any]] = None,
):
"""
初期化メソッド
Args:
model_name: Hugging Faceモデル名
query_prompt_name: クエリエンコード時のプロンプト名
document_prompt_name: ドキュメントエンコード時のプロンプト名
cache_folder: モデルキャッシュディレクトリ
model_kwargs: SentenceTransformerモデル初期化用の追加引数
encode_kwargs: encode関数用の追加引数
"""
self.model_name = model_name
self.query_prompt_name = query_prompt_name
self.document_prompt_name = document_prompt_name
self.cache_folder = cache_folder
self.model_kwargs = model_kwargs or {}
self.encode_kwargs = encode_kwargs or {}
# SentenceTransformerモデルの初期化
self._model = SentenceTransformer(
model_name_or_path=model_name,
cache_folder=cache_folder,
prompts={
query_prompt_name: query_prompt_name,
document_prompt_name: document_prompt_name,
},
**self.model_kwargs
)
def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""
ドキュメントテキストをベクトル埋め込みに変換
Args:
texts: 埋め込むドキュメントのリスト
Returns:
埋め込みベクトルのリスト
"""
# ドキュメント用プロンプトを使用してエンコード
encode_kwargs = {**self.encode_kwargs, "prompt_name": self.document_prompt_name}
embeddings = self._model.encode(texts, **encode_kwargs)
return embeddings.tolist()
def embed_query(self, text: str) -> List[float]:
"""
クエリテキストをベクトル埋め込みに変換
Args:
text: 埋め込むクエリテキスト
Returns:
クエリの埋め込みベクトル
"""
# クエリ用プロンプトを使用してエンコード
encode_kwargs = {**self.encode_kwargs, "prompt_name": self.query_prompt_name}
embedding = self._model.encode(text, **encode_kwargs)
return embedding.tolist()
class JapaneseCharacterTextSplitter(RecursiveCharacterTextSplitter):
"""句読点も句切り文字に含めるようにするためのスプリッタ"""
def __init__(self, **kwargs: Any):
separators = ["。"]
super().__init__(separators=separators, **kwargs)
# ディレクトリの読み込み
loader = DirectoryLoader(data_dir, loader_cls=PyMuPDFLoader, show_progress=True)
# カスタムAMBER埋め込みモデルのインスタンス化
embeddings = PrefixEmbeddings(
model_name="cl-nagoya/ruri-large-v2",
query_prompt_name="クエリ: ",
document_prompt_name="文章: "
)
# テキストをチャンクに分割
split_texts = loader.load_and_split(
text_splitter=JapaneseCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=100,
keep_separator='end',
)
)
# インデックスの作成
index = FAISS.from_documents(
documents=split_texts,
embedding=embeddings,
)
# インデックスの保存
index.save_local(
folder_path=index_path
)
Saved index is used as retriever for RAG like code below.
# Vectorstore用インデックスのパス
index_path = "./faissResultsJ"
# モデルのパス
model_path = "gemma3"
# LLMとretrieverの定数
RELEVANCE_SCORE = 0.77 # 文書検索の類似度スコア閾値
TEMPERATURE = 0.0 #0.6 # LLMの回答温度:低いと「画一的」、高いと「多様的」になる
SETTING_VISIBLE = True
# 以下、日本語に強いembeddingを使うために必要なクラス関数
class PrefixEmbeddings(Embeddings):
"""
LangChain用のPrefixEmbeddings埋め込みモデルラッパークラス
"""
def __init__(
self,
model_name: str = "cl-nagoya/ruri-large-v2",
query_prompt_name: str = "検索クエリ: ",
document_prompt_name: str = "検索文章: ",
cache_folder: Optional[str] = None,
model_kwargs: Optional[Dict[str, Any]] = None,
encode_kwargs: Optional[Dict[str, Any]] = None,
):
"""
初期化メソッド
Args:
model_name: Hugging Faceモデル名
query_prompt_name: クエリエンコード時のプロンプト名
document_prompt_name: ドキュメントエンコード時のプロンプト名
cache_folder: モデルキャッシュディレクトリ
model_kwargs: SentenceTransformerモデル初期化用の追加引数
encode_kwargs: encode関数用の追加引数
"""
self.model_name = model_name
self.query_prompt_name = query_prompt_name
self.document_prompt_name = document_prompt_name
self.cache_folder = cache_folder
self.model_kwargs = model_kwargs or {}
self.encode_kwargs = encode_kwargs or {}
# SentenceTransformerモデルの初期化
self._model = SentenceTransformer(
model_name_or_path=model_name,
cache_folder=cache_folder,
prompts={
query_prompt_name: query_prompt_name,
document_prompt_name: document_prompt_name,
},
**self.model_kwargs
)
def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""
ドキュメントテキストをベクトル埋め込みに変換
Args:
texts: 埋め込むドキュメントのリスト
Returns:
埋め込みベクトルのリスト
"""
# ドキュメント用プロンプトを使用してエンコード
encode_kwargs = {**self.encode_kwargs, "prompt_name": self.document_prompt_name}
embeddings = self._model.encode(texts, **encode_kwargs)
return embeddings.tolist()
def embed_query(self, text: str) -> List[float]:
"""
クエリテキストをベクトル埋め込みに変換
Args:
text: 埋め込むクエリテキスト
Returns:
クエリの埋め込みベクトル
"""
# クエリ用プロンプトを使用してエンコード
encode_kwargs = {**self.encode_kwargs, "prompt_name": self.query_prompt_name}
embedding = self._model.encode(text, **encode_kwargs)
return embedding.tolist()
# カスタムAMBER埋め込みモデルのインスタンス化
embeddings = PrefixEmbeddings(
model_name="cl-nagoya/ruri-large-v2",
query_prompt_name="クエリ: ",
document_prompt_name="文章: "
)
# インデックスの読み込み
index = FAISS.load_local(
folder_path=index_path,
embeddings=embeddings,
allow_dangerous_deserialization=True,
)
stop = ["Question:", "Answer:"] # 停止文字列
question_prompt_template = """
{context}
Question: {question}
Answer: """
max_tokens=4096
n_ctx=8192
KN = 3 # 検索した文書の最大採用数
# プロンプトの設定
QUESTION_PROMPT = PromptTemplate(
template=question_prompt_template,
input_variables=["context", "question"]
)
# Ollamaモデルの設定
llm = OllamaLLM(
model= model_path,
n_gpu_layers=-1,
max_tokens=max_tokens,
n_ctx=n_ctx,
temperature=TEMPERATURE,
verbose=False,
stop=stop
)
retriever = index.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={'score_threshold': RELEVANCE_SCORE, 'k': KN},
)
# プロンプト文字列を生成する関数
def prompt(message, history):
new_line = "\n"
# 先頭につけるシステムメッセージの定義
curr_system_message = ""
prefix = f"""あなたは優秀なアシスタントです。ユーザの質問やリクエストに、適切で役立つ情報を必ず日本語で回答してください。"""
messages = prefix + '\n' + message
# 生成したプロンプト文字列を返す
return messages
#====
def chat(message, history, REL_SCORE, K_N, TEMP):
llm.temperature = TEMP
retriever.search_kwargs['score_threshold'] = REL_SCORE
retriever.search_kwargs['k'] = K_N
# (RAG用)質問回答chainの設定
chain = RetrievalQA.from_chain_type(
llm=llm,
retriever=retriever,
chain_type_kwargs={"prompt": QUESTION_PROMPT},
chain_type="stuff",
return_source_documents=True
)
# プロンプト文字列生成
messages = prompt(message, history)
ret = retriever.invoke(messages)
print("ret:\n", ret)
'search_type="similarity_score_threshold"' of FAISS is preferred.
When executed, 'ret' is empty. And got warning message as below.
UserWarning: Relevance scores must be between 0 and 1, got [(Document(id='5aeffa65-3ad2-4917-9b22-e5b62450359b', metadata={'producer': 'Microsoft: Print To PDF', 'creator': '', 'creationdate': '2025-05-28T13:56:36+09:00', 'source': 'files2vectorize\a.pdf', 'file_path': 'files2vectorize\a.pdf', 'total_pages': 1, 'format': 'PDF 1.7', 'title': 'é¤ókdDf.txt - áâ3', 'author': '', 'subject': '', 'keywords': '', 'moddate': '2025-05-28T13:56:36+09:00', 'trapped': '', 'modDate': "D:20250528135636+09'00'", 'creationDate': "D:20250528135636+09'00'", 'page': 0, 'score': np.float32(147.00934), 'relevance_score': np.float32(-102.9513)}, page_content='some contents'), np.float32(-102.9513)), (Document(id='f797a7d6-b0ce-4f75-946a-ab8a388a1a14', metadata={'producer': 'Microsoft: Print To PDF', 'creator': '', 'creationdate': '2025-03-10T14:45:22+09:00', 'source': 'files2vectorize\b.pdf', 'file_path': 'files2vectorize\b.pdf', 'total_pages': 1, 'format': 'PDF 1.7', 'title': 'aiticv.txt - áâ3', 'author': '', 'subject': '', 'keywords': '', 'moddate': '2025-03-10T14:45:22+09:00', 'trapped': '', 'modDate': "D:20250310144522+09'00'", 'creationDate': "D:20250310144522+09'00'", 'page': 0, 'score': np.float32(161.07333), 'relevance_score': np.float32(-112.89605)}, page_content='some contents 2'), np.float32(-112.89605)), (Document(id='d7653f47-ba7e-421e-93a6-c7c38e22e59b', metadata={'producer': 'Microsoft: Print To PDF', 'creator': '', 'creationdate': '2025-05-20T15:38:30+09:00', 'source': 'files2vectorize\c.pdf', 'file_path': 'files2vectorize\c.pdf', 'total_pages': 55, 'format': 'PDF 1.7', 'title': 'å˘98_utf.txt - áâ3', 'author': '', 'subject': '', 'keywords': '', 'moddate': '2025-05-20T15:38:30+09:00', 'trapped': '', 'modDate': "D:20250520153830+09'00'", 'creationDate': "D:20250520153830+09'00'", 'page': 29, 'score': np.float32(183.4676), 'relevance_score': np.float32(-128.73119)}, page_content='some contents 3'), np.float32(-128.73119))]
There are 2 curious points.
- 'score' & 'relevance_score' are way different from what supposed to be.
- 'ret' should and used to be the shape like [Document(....), Document(....), ...].
however now retrieval result comes out like [(Document(....), np.float32(-123.0)), (Document(....), np.float32(-145.0)), ...].
Code has been working fine with "intfloat/multilingual-e5-large" embedding model.
What am I wrong ? Any advice will be helpful.
Thanks in advance.
** Japanese acceptable.
environment:
python3.10.16 on Anaconda3 + Win11Pro
faiss-cpu==1.10.0
langchain==0.3.25
langchain-community==0.3.24
langchain-core==0.3.64
langchain-huggingface==0.1.2
langchain-ollama==0.3.3
langchain-text-splitters==0.3.8
ollama==0.5.1
llama_cpp_python==0.3.9
Hi,
'score' and 'relevance_score' are calcurated according to info on
https://qiita.com/Oxyride/items/ac7e32714f5fa673d9e4
and it has been working OK with "intfloat/multilingual-e5-large" embedding model.
HI,
I've confirmed it works with
retriever = index.as_retriever(
search_type="similarity",
search_kwargs={'k': 3},
)
scores of 3 contents searched are
'score': np.float32(158.63326)
'score': np.float32(172.86636)
'score': np.float32(179.67949).
They doesn't seem to be normalized
Which parameters do I have to set
distance_strategy = DistanceStrategy.COSINE
or distance_strategy = DistanceStrategy.MAX_INNER_PRODUCT
or normalize_L2 = True ?
If there is site to learn about this, please let me know.
Thanks.