Sentence Similarity
Safetensors
Japanese
bert
feature-extraction

Retrieval results with FAISS

#1
by SwHaraday - opened

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.

  1. 'score' & 'relevance_score' are way different from what supposed to be.
  2. '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.

Sign up or log in to comment