faiss 와 vector DB의 비교

1. 개요

faiss는 밀집 벡터의 효율적인 유사성 검색 및 클러스터링을 위한 meta에서 제작한 라이브러리 이며 numpy 나 torch 에서 제공해주는 cosine_similarity 보다 훨씬 빠르다.

그러나 최근에는 VectorDB라는 개념의 DB가 나오고 있고 이를 위한 제품들도 상용 및 오픈소스 계열에서 많이 나오고 있다.

faiss의 사용과 vector DB중 상대적으로 설치가 쉬운 chromadb를 비교해 서로의 차이점을 기술하도록 하겠다.

2. FAISS 의 사용

2-1 faiss의 설치

# CPU-only version
$ conda install -c pytorch faiss-cpu=1.7.4 mkl=2021 blas=1.0=mkl

# GPU(+CPU) version
$ conda install -c pytorch -c nvidia faiss-gpu=1.7.4 mkl=2021 blas=1.0=mkl

최신 내용은 faiss 깃헙을 참조바란다.

2-2 준비단계

준비한 데이터 및 임베딩 모델은 다음과 같다.

  • 데이터 셋 : https://github.com/songys/Chatbot_data
  • pairwise 모델 : all-MiniLM-L6-v2

사전 데이터 준비 수행은 아래와 같이 진행한다.

  1. 데이터셋의 질문(Q)을 pairwise 모델로 전부 임베딩
  2. 임베딩 벡터를 npy파일로 저장
#filename : prework.py

import pandas as pd
import numpy as np
from sentence_transformers import SentenceTransformer

df = pd.read_csv("./ChatbotData.csv") #from https://github.com/songys/Chatbot_data
sentences = df['Q'].to_list()
embedder = SentenceTransformer("all-MiniLM-L6-v2")

embeddings = embedder.encode(sentences)

np.save("./embeddings.npy", embeddings)

이제 기본적인 사용 방법을 살펴보자.

# filename : search.py
import pandas as pd
import numpy as np
import faiss
import os
from sentence_transformers import SentenceTransformer

INDEX_FILE = "./sts.index"

#변환기 불러오기
embedder = SentenceTransformer("all-MiniLM-L6-v2")

여기서 변환기 embedder는 입력 쿼리 텍스트 처리용이다.

다음은 기존 index파일을 찾고 없으면 새로 인덱싱을 한다.

#데이터 불러오기
df = pd.read_csv("./ChatbotData.csv")

# faiss 계산하기
if os.path.exists(INDEX_FILE):
    #index파일이 존재하면 이것만 읽어들인다. 
    index = faiss.read_index(INDEX_FILE)
else:

    #임베딩 벡터 불러오기
    embeddings = np.load("./embeddings.npy")
    print(embeddings.shape) # 임베딩 쉐이프 확인

    index = faiss.IndexFlatL2(embeddings.shape[1]) # 초기화 : 벡터의 크기를 지정
    index.add(embeddings) # 임베딩을 추가
    print(index.ntotal)
    faiss.write_index(index,"./sts.index")

이제 쿼리를 만들어 서칭을 해 보고 결과를 프린트 한다.

top_k = 100
query = input("문장을 입력하세요 >>> ")

query_embedding = embedder.encode(query, normalize_embeddings=True, convert_to_tensor=True)
distances, indices = index.search(np.expand_dims(query_embedding, axis=0),top_k)
# 결과 확인
temp = df.iloc[indices[0]]
# temp['distance'] = distances[0]
print(temp[['Q','A','label']].head(10))

전체 코드는 다음과 같다 .

#filename : search.py
import pandas as pd
import numpy as np
import faiss
import os
from sentence_transformers import SentenceTransformer

INDEX_FILE = "./sts.index"

#변환기 불러오기
#embedder = SentenceTransformer("Huffon/sentence-klue-roberta-base")
embedder = SentenceTransformer("all-MiniLM-L6-v2")

#데이터 불러오기
df = pd.read_csv("./ChatbotData.csv")

# faiss 계산하기
if os.path.exists(INDEX_FILE):
    #index파일이 존재하면 이것만 읽어들인다. 
    index = faiss.read_index(INDEX_FILE)
else:

    #임베딩 벡터 불러오기
    embeddings = np.load("./embeddings.npy")
    print(embeddings.shape) # 임베딩 쉐이프 확인

    index = faiss.IndexFlatL2(embeddings.shape[1]) # 초기화 : 벡터의 크기를 지정
    index.add(embeddings) # 임베딩을 추가
    print(index.ntotal)
    faiss.write_index(index,"./sts.index")



top_k = 100
query = input("문장을 입력하세요 >>> ")
query_embedding = embedder.encode(query, normalize_embeddings=True, convert_to_tensor=True)

distances, indices = index.search(np.expand_dims(query_embedding, axis=0),top_k)

# 결과 확인
temp = df.iloc[indices[0]]
print(temp[['Q','A','label']].head(10))

실행결과 유사도 top10을 아래와 같이 출력해 준다.

$ python3 search.py
문장을 입력하세요 >>> 오늘은 우울해
                  Q                     A  label
2461   설날인데 재밌는거 안해        보고 싶었던 영화 보세요.      0
9708      말을 안하는 연애            대화가 중요한데요.      2
3305    오늘 운이 안 좋았어  다른 곳에 쓰려고 운을 아껴뒀나봐요.      0
7174        오늘도출근해~         바쁘게 지내는 게 좋죠.      1
3326      오늘 회식 안하나             맛있는거 드세요.      0
10749    연애하는데도 외로워        진정한 사랑을 찾아보세요.      2
3304      오늘 운이 꽝이다  다른 곳에 쓰려고 운을 아껴뒀나봐요.      0
5934        너무 우울하네     자신에게 좀 더 여유로워지세요.      1
10792       오늘도 사랑해             달콤한 말이네요.      2
7118     오늘 간신히 참았어          오늘도 잘 견디셨어요.      1

3. Vector db 의 사용

3.1 chroma db의 사용

이제 동일한 작업을 vector db의 한 종류인 chromadb를 사용해 작업을 해 보겠다.

chromadb의 설치는 pip를 통해 진행한다.

$ pip install chromadb

동일한 작업을 chroma db로 하는 전체 코드는 아래와 같다.

#filename : chroma_search.py

import pandas as pd
import numpy as np
import chromadb
from chromadb.utils import embedding_functions

SQLITE_DB_PATH = "/home/yourpath"

client = chromadb.PersistentClient(path=SQLITE_DB_PATH)

collection = client.get_or_create_collection('test')

df = pd.read_csv("./ChatbotData.csv")

default_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")

if collection.count() == 0:
    print(">>>>>> DB is Empty <<<<<<<")

    docs = df["Q"].tolist()
    ids = [str(x) for x in df.index.tolist()]

    ## make embedding
    embeddings = default_ef(docs)

    print("######Start add collection#####")

    collection.add(
        embeddings=embeddings,
        documents=docs,
        ids = ids
    )

query = input("문장을 입력하세요 >>>> ")

queryText = default_ef([query])

results = collection.query(
    query_embeddings=queryText,
    n_results=10
)

indices = results['ids']

temp = df.iloc[indices[0]]

print (temp)

위에서 SQLITE_DB_PATH 는 sqlite 의 물리적 저장소 위치를 의미한다.

실행 결과는 다음과 같다.

$ python3 chroma_search.py 
문장을 입력하세요 >>> 오늘은 우울해
                      Q                                 A  label
2461       설날인데 재밌는거 안해                    보고 싶었던 영화 보세요.      0
9708          말을 안하는 연애                        대화가 중요한데요.      2
7174            오늘도출근해~                     바쁘게 지내는 게 좋죠.      1
3326          오늘 회식 안하나                         맛있는거 드세요.      0
10749        연애하는데도 외로워                    진정한 사랑을 찾아보세요.      2
3337            오늘도 평온해                     내일도 평온하길 바라요.      0
3284          오늘 너무 피곤해                            푹 쉬세요.      0
10823  오해하는 거 같은데 어떡하지?  오해할만한 일이 생겼을때는 솔직하게 이야기하고 풀어보세요.      2
3349       오늘은 별도 안 보이네              한적한 시골에서 하늘을 올려봐보세요.      0
156      결혼하는데 돈 얼마나 들까                욕심에 따라 천지 차이일 거예요.      0

앞의 faiss를 사용한 결과와 비교하면 동일한 알고리즘(all-MiniLM-L6-v2)을 썼음에도 다소 다름을 알 수 있는데 앞에서는 UKPLab에서 제공하는, 지금은 chromadb.utils에서 제공하는 알고리즘을 썼기 때문이 아닐까 생각된다.

검색 속도를 놓고 비교했는데 다음과 같았다. (측정은 pysnooper를 사용하였다)

11800건정도에서의 검색 속도는 다음과 같았다.

faiss사용

12:53:18.178845 line        37 with pysnooper.snoop():
Elapsed time: 00:00:00.529423

chroma db 사용

12:53:07.791456 line        41 with pysnooper.snoop():
Elapsed time: 00:00:00.782578

검색 속도는 faiss쪽이 크게는 1.5배 가까이 더 빠르다. 그럼 vector db를 사용하는 메리트는 무엇이 있을까?

faiss를 사용하는 쪽은 쿼리 하나의 처리만 가능하며 앞 단계에서는 인덱싱 및 전처리가 필요하다.

vector db는 멀티 쿼리가 가능하며 결과도 쿼리 별로 받아올 수 있으며 이를 활용한 다국어 쿼리도 가능하다는 장점이 있다(chroma db 공식 홈페이지 참조). 그리고 인덱스의 업데이트시 id만 알고 있으면 일부만 업데이트도 가능하다.

결론은… 단일 문장 쿼리 검색이면 굳이 vector db를 사용하는 것은 의미없다는 것이며, 인덱싱을 파일로 처리해도 충분하다는 것이다. 여러분이 두 개를 비교하고 상황에 맞는 방법을 선택하면 좋을 것 같다.