본문 바로가기
AI

LangChain, FAISS, Gemini 임베딩을 활용한 벡터 DB 저장 및 검색 방법

by markbyun 2025. 5. 8.

LangChain, FAISS, Gemini 임베딩을 활용한 벡터 DB 저장 및 검색 방법

대규모 언어 모델(LLM)을 기반으로 한 RAG(Retrieval-Augmented Generation) 시스템을 구축할 때, 벡터 데이터베이스의 효율적인 저장 및 검색은 핵심 요소입니다. 본 가이드에서는 LangChain, FAISS, Google Gemini 임베딩을 활용하여 문서 임베딩을 저장하고 의미적으로 유사한 정보를 검색하는 전문적인 Python 구현 방식을 설명합니다. 이 구현은 의미 기반 검색 및 RAG 파이프라인을 설계하는 고급 머신러닝 및 딥러닝 엔지니어를 위한 것입니다.


LLM 응용에서 벡터 데이터베이스의 중요성

기존의 키워드 기반 검색 시스템은 문장의 의미를 제대로 이해하지 못하는 한계가 있습니다. 반면, 벡터 데이터베이스는 텍스트 데이터를 고차원 임베딩 형태로 저장하여, 의미 기반 유사도에 따른 근접 검색(ANN: Approximate Nearest Neighbor)을 수행할 수 있습니다. 이 기능은 다음과 같은 분야에서 특히 유용합니다:

  • 질문 응답 시스템 (Q&A)
  • 기업 내부 지식 검색 시스템
  • 법률 또는 의료 문서 검색
  • 문맥 기억을 활용한 LLM 기반 챗봇

워크플로우에서 LangChain을 사용하는 이유

LangChain은 문서 로딩, 텍스트 분할, 임베딩, 벡터 저장 및 검색과 같은 복잡한 과정을 간단하게 통합할 수 있도록 설계된 프레임워크입니다. 본 구현에서 LangChain은 다음과 같은 장점을 제공합니다:

  • PDF, Excel, 텍스트 등 다양한 문서 포맷을 지원하는 로더 제공
  • RecursiveCharacterTextSplitter를 통한 유연한 텍스트 분할
  • FAISS와의 통합을 통해 고속 유사도 검색 지원
  • Google Gemini 임베딩 모델을 통한 강력한 의미 이해력 제공
  • LLM 파이프라인에서 직접 활용 가능한 Retriever 인터페이스 지원

구현 전략

아래의 Python 구현은 다음과 같은 절차를 따릅니다:

  1. 환경 설정: .env 파일에서 API 키를 불러와 Gemini 임베딩 모델을 초기화합니다.
  2. DB 초기화: 기존의 FAISS 데이터베이스가 존재할 경우 로드하고, 없을 경우 새롭게 생성합니다.
  3. 문서 로딩: 입력 포맷에 따라 PDF, Excel, 텍스트 로더 중 하나를 선택하여 문서를 불러옵니다.
  4. 텍스트 분할: 문서를 일정 크기의 청크로 분할하고, 겹치는 부분을 추가하여 문맥을 보존합니다.
  5. 배치 임베딩: 메모리 효율을 위해 문서를 배치 단위로 임베딩하고 DB에 추가합니다.
  6. 로컬 저장: FAISS 벡터 스토어를 로컬 파일 시스템에 저장합니다.
  7. 의미 기반 검색: 저장된 벡터 DB를 로드하여 입력 쿼리와 의미적으로 유사한 문서를 검색합니다.

확장성 고려 사항

본 구현은 IDX_DELTA 및 최대 버퍼 크기를 설정하여 대용량 문서를 효율적으로 처리하도록 설계되어 있습니다. 임베딩 처리를 배치로 분산시켜 메모리 사용을 최적화하고 처리 속도를 높이며, 실제 서비스 환경에서도 안정적으로 활용 가능합니다.


활용 사례: RAG 기반 질의 응답 시스템

벡터 데이터베이스를 저장한 이후에는, 의미적으로 유사한 문서를 검색하여 LLM 입력 프롬프트에 삽입함으로써 보다 정확하고 신뢰성 있는 응답을 생성할 수 있습니다. 이는 현대 RAG 시스템의 핵심 구성 요소입니다.


전체 Python 코드

샘플 코드에 사용된 'reciprocam.pdf' 파일은 'Recipro-CAM: Fast gradient-free visual explanations for convolutional neural networks' URL을 통해서 다운로드 받을 수 있습니다.

import os
from langchain_community.document_loaders import TextLoader, UnstructuredExcelLoader, PyPDFLoader
from langchain_community.vectorstores import FAISS
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain.schema import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter

from dotenv import load_dotenv
load_dotenv(".env")

# Setup embedding model as GoogleGenerativeAIEmbeddings
# 'google_api_key' parameter will be assigned by 'GOOGLE_API_KEY' environment variable
embeddings = GoogleGenerativeAIEmbeddings(model = "models/text-embedding-004")

db_path = "./faiss-doc-db"

# Create new vector DB if there is the database but if there is previous db then add new information
def create_vector_database(db_path, txt_path, type="text"):
    if os.path.exists(db_path):
        db = FAISS.load_local(db_path, embeddings=embeddings, allow_dangerous_deserialization=True)
    else:
        documents = [Document(page_content='RAG자료')]
        db = FAISS.from_documents(documents, embeddings)

    separators = ['\n\n', '\n', ' ', '\t']
    chunk_size = 1000
    chunk_overlap = 100
    if type == "excel":
        loader = UnstructuredExcelLoader(txt_path)
    elif type == "pdf":
        loader = PyPDFLoader(txt_path)
    else:
        loader = TextLoader(txt_path)
    docs = loader.load()   
    
    documents = RecursiveCharacterTextSplitter(
        separators=separators,
        chunk_size=chunk_size, 
        is_separator_regex=False, 
        chunk_overlap=chunk_overlap
    ).split_documents(docs)

    MAX_BUFFER_SIZE = 100000
    IDX_DELTA = MAX_BUFFER_SIZE//chunk_size        
    doc_size = len(documents)
    remainder = doc_size % IDX_DELTA
    last_idx = doc_size - remainder
    print(f"Total documents: {doc_size}")
    print(f"Last index: {last_idx}")
    print(f"Remainder: {remainder}")
    for idx in range(0, last_idx, IDX_DELTA):
        db.add_documents(documents=documents[idx:idx+IDX_DELTA])
    if last_idx < doc_size:
        db.add_documents(documents=documents[last_idx:])

    db.save_local(db_path)

# Save custom PDF document as vector database
#create_vector_database(db_path, "./reciprocam.pdf", type="pdf")

# Retrieve related document for a given query from vector DB
def retrieve(query: str):
    vectorstore_faiss = FAISS.load_local(db_path, embeddings, allow_dangerous_deserialization=True)
    faiss_retriever = vectorstore_faiss.as_retriever(search_type="similarity", search_kwargs={"k": 2})
    """Retrieve information related to a query."""
    print(f"Query: {query}")
    retrieved_docs = faiss_retriever.invoke(query)
    serialized = "\n\n".join(
        (f"Source: {doc.metadata}\n" f"Content: {doc.page_content}")
        for doc in retrieved_docs
    )
    return serialized, retrieved_docs

serial_doc, ret_doc = retrieve("Let me know what is a CAM.")
print(f"Result: {serial_doc}.")

검색결과:

Query: Let me know what is a CAM.
Result: Source: {'source': './reciprocam.pdf', 'page': 1}
Content: The first solution suggested to address this issue is CAM Zhou et al. [2016]. This method produces a map that highlights
the important regions of an image for a particular class by multiplying a global average pooling activation vector with a
fully connected weight vector specific to the class. Essentially, the saliency map Sc for a given class cis obtained by
Sc =
∑
k
wk,c
∑
u,v
fk(u,v) (1)
where wk,c is the last FC layer’s weight between channel k and class c and fk(u,v) is the activation at (u,v) of
channel k. CAM allows AI practitioners not only to analyze the capacity of their neural network architecture but also
to understand how the network reacts to specific classes of input data. However, this method has a limitation in that
it requires the presence of a global average or max pooling layer in the architecture. This means that certain neural
network architectures may not be compatible with CAM method.

Source: {'source': './reciprocam.pdf', 'page': 5}
Content: arXiv A PREPRINT
Table 1: Comparison of different CAM-based approaches using existing metrics on six different backbones. The
evaluation scores for other CAM methods were obtained from Poppi et al. [2021].
VGG-16 ResNet-18
Method Drop
(↓)
...
Score-CAM 26.13 24.75 9.52 47.00 93.83 20.27 81.66 12.81 40.41 10.76 46.01 98.35 41.78 77.30
Recipro-CAM 21.51 34.86 9.50 46.88 92.24 27.48 80.27 20.68 36.30 10.19 44.93 97.38 33.60 79.08
ResNet-50 ResNet-101
Grad-CAM 32.99 24.27 17.49 48.48 82.80 22.24 75.27 29.38 29.35 18.66 47.47 81.97 22.51 76.40.

참고 자료