배경
- faiss 모듈 내 코드를 분석해서, 여러 용도로 응용하고자함.
코드
from __future__ import annotations
import logging
import operator
import os
import pickle
import uuid
import warnings
from pathlib import Path
from typing import (
Any,
Callable,
Dict,
Iterable,
List,
Optional,
Sized,
Tuple,
Union,
)
import numpy as np
from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings
from langchain_core.runnables.config import run_in_executor
from langchain_core.vectorstores import VectorStore
from langchain_community.docstore.base import AddableMixin, Docstore
from langchain_community.docstore.in_memory import InMemoryDocstore
from langchain_community.vectorstores.utils import (
DistanceStrategy,
maximal_marginal_relevance,
)
logger = logging.getLogger(__name__)
def dependable_faiss_import(no_avx2: Optional[bool] = None) -> Any:
"""
faiss 라이브러리를 가져옵니다. faiss를 사용할 수 없는 경우 오류를 발생시킵니다.
FAISS_NO_AVX2 환경 변수가 설정된 경우, AVX2 최적화 없이 faiss를 로드합니다.
Args:
no_avx2 (Optional[bool]): AVX2 최적화 없이 faiss를 로드하려면 True로 설정.
이는 벡터 저장소의 호환성을 높여 다른 장치에서도 사용할 수 있게 합니다.
Returns:
faiss: 가져온 faiss 모듈 객체.
Raises:
ImportError: faiss 모듈을 가져올 수 없는 경우 예외가 발생합니다.
"""
if no_avx2 is None and "FAISS_NO_AVX2" in os.environ:
no_avx2 = bool(os.getenv("FAISS_NO_AVX2"))
try:
if no_avx2:
from faiss import swigfaiss as faiss
else:
import faiss
except ImportError:
raise ImportError(
"faiss 파이썬 패키지를 가져올 수 없습니다. "
"CUDA가 지원되는 GPU를 사용하는 경우 `pip install faiss-gpu`를 설치하거나, "
"파이썬 버전에 맞게 `pip install faiss-cpu`를 설치하세요."
)
return faiss
def _len_check_if_sized(x: Any, y: Any, x_name: str, y_name: str) -> None:
"""
두 개의 입력 객체 x와 y가 Sized일 경우, 길이가 동일한지 검사합니다.
길이가 다르면 오류를 발생시킵니다.
Args:
x (Any): 비교할 첫 번째 객체.
y (Any): 비교할 두 번째 객체.
x_name (str): x 객체의 이름 (오류 메시지에 표시될 이름).
y_name (str): y 객체의 이름 (오류 메시지에 표시될 이름).
Raises:
ValueError: x와 y가 Sized 타입이면서 길이가 다를 경우 예외 발생.
"""
if isinstance(x, Sized) and isinstance(y, Sized) and len(x) != len(y):
raise ValueError(
f"{x_name}과 {y_name}의 길이가 같아야 하지만, "
f"len({x_name})={len(x)} 및 len({y_name})={len(y)} 입니다."
)
return
class FAISS(VectorStore):
"""
FAISS 벡터 스토어 클래스입니다.
`Meta Faiss` 라이브러리를 기반으로 하는 벡터 스토어로, 문서의 임베딩을 저장하고
검색할 수 있습니다. FAISS 인덱스는 효율적으로 벡터 데이터를 검색하는 데 사용됩니다.
사용하려면 `faiss` 파이썬 패키지가 설치되어 있어야 합니다.
예제:
.. code-block:: python
from langchain_community.embeddings.openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
embeddings = OpenAIEmbeddings()
texts = ["FAISS is an important library", "LangChain supports FAISS"]
faiss = FAISS.from_texts(texts, embeddings)
"""
def __init__(
self,
embedding_function: Union[
Callable[[str], List[float]],
Embeddings,
],
index: Any,
docstore: Docstore,
index_to_docstore_id: Dict[int, str],
relevance_score_fn: Optional[Callable[[float], float]] = None,
normalize_L2: bool = False,
distance_strategy: DistanceStrategy = DistanceStrategy.EUCLIDEAN_DISTANCE,
):
"""
FAISS 인스턴스를 초기화합니다.
Args:
embedding_function: 텍스트를 벡터(임베딩)로 변환하는 함수 또는 Embeddings 객체.
index: FAISS 인덱스 객체. 벡터들을 저장하고 검색할 수 있는 데이터 구조입니다.
docstore: 문서를 저장하는 Docstore 객체. 메타데이터 및 원본 문서를 관리합니다.
index_to_docstore_id: 인덱스에서 문서 저장소로의 매핑을 나타내는 딕셔너리입니다.
relevance_score_fn: 선택적 함수. 검색 결과의 적합성을 평가하기 위해 유사도 점수를 변환합니다.
normalize_L2: L2 정규화 여부를 나타내는 플래그입니다.
distance_strategy: 거리 측정 전략 (유클리드 거리, 코사인 거리 등).
Raises:
경고: embedding_function이 Embeddings 객체가 아니면 경고 메시지를 출력합니다.
"""
if not isinstance(embedding_function, Embeddings):
logger.warning(
"`embedding_function`은 Embeddings 객체여야 합니다. 함수 전달 지원은 곧 제거될 예정입니다."
)
self.embedding_function = embedding_function
self.index = index
self.docstore = docstore
self.index_to_docstore_id = index_to_docstore_id
self.distance_strategy = distance_strategy
self.override_relevance_score_fn = relevance_score_fn
self._normalize_L2 = normalize_L2
if (
self.distance_strategy != DistanceStrategy.EUCLIDEAN_DISTANCE
and self._normalize_L2
):
warnings.warn(
"L2 정규화는 유클리드 거리 외의 거리 측정 방법에서는 사용할 수 없습니다: "
f"metric type: {self.distance_strategy}"
)
@property
def embeddings(self) -> Optional[Embeddings]:
"""
Embeddings 객체를 반환하는 속성입니다.
Returns:
embedding_function이 Embeddings 객체일 경우 반환, 아니면 None 반환.
"""
return (
self.embedding_function
if isinstance(self.embedding_function, Embeddings)
else None
)
def _embed_documents(self, texts: List[str]) -> List[List[float]]:
"""
주어진 텍스트들을 벡터로 변환합니다.
Args:
texts: 임베딩할 문자열의 리스트.
Returns:
주어진 텍스트들의 벡터 리스트.
"""
if isinstance(self.embedding_function, Embeddings):
return self.embedding_function.embed_documents(texts)
else:
return [self.embedding_function(text) for text in texts]
async def _aembed_documents(self, texts: List[str]) -> List[List[float]]:
"""
주어진 텍스트들을 비동기적으로 벡터로 변환합니다.
Args:
texts: 임베딩할 문자열의 리스트.
Returns:
주어진 텍스트들의 벡터 리스트.
Raises:
Exception: embedding_function이 Embeddings 객체가 아니면 예외 발생.
"""
if isinstance(self.embedding_function, Embeddings):
return await self.embedding_function.aembed_documents(texts)
else:
raise Exception(
"`embedding_function`은 Embeddings 객체여야 합니다. 함수 전달 지원은 곧 제거될 예정입니다."
)
def _embed_query(self, text: str) -> List[float]:
"""
주어진 쿼리를 벡터로 변환합니다.
Args:
text: 임베딩할 쿼리 문자열.
Returns:
벡터로 변환된 쿼리.
"""
if isinstance(self.embedding_function, Embeddings):
return self.embedding_function.embed_query(text)
else:
return self.embedding_function(text)
async def _aembed_query(self, text: str) -> List[float]:
"""
주어진 쿼리를 비동기적으로 벡터로 변환합니다.
Args:
text: 임베딩할 쿼리 문자열.
Returns:
벡터로 변환된 쿼리.
Raises:
Exception: embedding_function이 Embeddings 객체가 아니면 예외 발생.
"""
if isinstance(self.embedding_function, Embeddings):
return await self.embedding_function.aembed_query(text)
else:
raise Exception(
"`embedding_function`은 Embeddings 객체여야 합니다. 함수 전달 지원은 곧 제거될 예정입니다."
)
def __add(
self,
texts: Iterable[str],
embeddings: Iterable[List[float]],
metadatas: Optional[Iterable[dict]] = None,
ids: Optional[List[str]] = None,
) -> List[str]:
"""
주어진 텍스트와 임베딩들을 벡터 인덱스 및 문서 저장소에 추가합니다.
Args:
texts: 추가할 문자열의 반복 가능한 객체.
embeddings: 각 텍스트에 해당하는 벡터의 리스트.
metadatas: 각 텍스트에 연결된 메타데이터 리스트 (옵션).
ids: 각 텍스트에 대한 고유 ID 리스트 (옵션).
Returns:
추가된 텍스트의 ID 리스트.
Raises:
ValueError: docstore가 AddableMixin을 지원하지 않거나 ID가 중복될 경우 예외 발생.
"""
faiss = dependable_faiss_import()
if not isinstance(self.docstore, AddableMixin):
raise ValueError(
"텍스트를 추가하려면 기본 docstore가 항목 추가를 지원해야 합니다. "
f"{self.docstore}는 이 기능을 지원하지 않습니다."
)
_len_check_if_sized(texts, metadatas, "texts", "metadatas")
_metadatas = metadatas or ({} for _ in texts)
documents = [
Document(page_content=t, metadata=m) for t, m in zip(texts, _metadatas)
]
_len_check_if_sized(documents, embeddings, "documents", "embeddings")
_len_check_if_sized(documents, ids, "documents", "ids")
if ids and len(ids) != len(set(ids)):
raise ValueError("ID 목록에 중복된 ID가 있습니다.")
vector = np.array(embeddings, dtype=np.float32)
if self._normalize_L2:
faiss.normalize_L2(vector)
self.index.add(vector)
ids = ids or [str(uuid.uuid4()) for _ in texts]
self.docstore.add({id_: doc for id_, doc in zip(ids, documents)})
starting_len = len(self.index_to_docstore_id)
index_to_id = {starting_len + j: id_ for j, id_ in enumerate(ids)}
self.index_to_docstore_id.update(index_to_id)
return ids
def similarity_search_with_score_by_vector(
self,
embedding: List[float],
k: int = 4,
filter: Optional[Union[Callable, Dict[str, Any]]] = None,
fetch_k: int = 20,
**kwargs: Any,
) -> List[Tuple[Document, float]]:
"""
주어진 임베딩 벡터와 가장 유사한 문서들을 반환합니다.
Args:
embedding: 유사한 문서를 찾을 임베딩 벡터.
k: 반환할 문서 수. 기본값은 4.
filter: 메타데이터에 따라 필터링하는 함수 또는 조건(딕셔너리). 기본값은 None.
fetch_k: 필터링 전 검색할 문서 수. 기본값은 20.
Returns:
쿼리와 가장 유사한 문서와 각 문서의 L2 거리 (유사도 점수)의 튜플 리스트.
"""
faiss = dependable_faiss_import()
vector = np.array([embedding], dtype=np.float32)
if self._normalize_L2:
faiss.normalize_L2(vector)
scores, indices = self.index.search(vector, k if filter is None else fetch_k)
docs = []
if filter is not None:
filter_func = self._create_filter_func(filter)
for j, i in enumerate(indices[0]):
if i == -1:
continue
_id = self.index_to_docstore_id[i]
doc = self.docstore.search(_id)
if not isinstance(doc, Document):
raise ValueError(f"ID {_id}에 대한 문서를 찾을 수 없습니다. 현재 값: {doc}")
if filter is not None:
if filter_func(doc.metadata):
docs.append((doc, scores[0][j]))
else:
docs.append((doc, scores[0][j]))
score_threshold = kwargs.get("score_threshold")
if score_threshold is not None:
cmp = (
operator.ge
if self.distance_strategy in (DistanceStrategy.MAX_INNER_PRODUCT, DistanceStrategy.JACCARD)
else operator.le
)
docs = [
(doc, similarity) for doc, similarity in docs if cmp(similarity, score_threshold)
]
return docs[:k]