Vectorstore 구현

Mujung Kim·2025년 12월 19일

LLM + RAG 시스템

목록 보기
8/11

1. vectorstore 이전까지 주요 태스크 점검

   1. Load: PyPDFLoader 등을 통해 문서 로드

   2. Split: RecursiveCharacterTextSplitter 등으로 텍스트를 적절한 크기(Chunk)로 분할

   3. Embed: OpenAI나 HuggingFace의 임베딩 모델을 통해 텍스트를 벡터로 변환

   4. Store: 생성된 Document 객체들을 VectorStore

2. 검색 및 질의 구조(Retrieval)

어플리케이션이 특정 DB 브랜드에 종속되지 않게 만드는 핵심

  • Vectorstore 캡슐화: 메인코드에서 db = FAISS(...)가 아니라, 설정값에 따라 생성된 VectorStore 객체 자체를 다룹니다.
  • Retriever 인터페이스: LangChain에서 가장 권장하는 방식은 Vectorstore를 Retriever로 변환
    • retriever = vectorstore.as_retriever()
    • 이렇게 하면 내부가 FAISS인지 Chroma인지 상관없이, 애플리케이션은 오직 get_relevant_documents()라는 공통 메서드만 호출하면 됩니다.

핵심요약

"애플리케이션은 어디에 저장되어 있는가?를 묻지 않고, 오직 질문과 유사한 문서를 가져와라라는 명령(Retriever 인터페이스)만 내리면 된다.

3. 코드 가이드

1) 목표 아키텍처

  • Domain(앱/체인): BaseRetriever만 안다.
  • Infra(저장소): FAISS/Chroma/Pinecone 같은 구현체를 갭슐화한다.
  • Factory/Config: 어떤 벡터스토어를 쓸지 "선택"만 한다.
App/RAG Chain
   ↓ (depends on)
Retriever Interface (LangChain BaseRetriever)
   ↑
VectorStore Adapter (infra)FAISS | Chroma | Pinecone ...

2) "Retriever만 주입" 패턴

(A) App은 Retriever만 받는다.

from langchain_core.retrievers import BaseRetriever
from langchain_core.runnables import RunnableLambda

def build_rag_chain(retriever: BaseRetriever, llm):
    # 예시: retrieve -> prompt -> llm
    def retrieve(q: str):
        return retriever.get_relevant_documents(q)

    return (
        RunnableLambda(lambda q: {"question": q, "docs": retrieve(q)})
        # 이후 prompt formatting + llm 호출...
    )

앱 코드는 FAISS/Chroma/Pinecone 존재를 모름.

3) Infra: VectorStore를 만들고 Retriever로 변환

(B) VectorStore 어댑터/팩토리

from dataclasses import dataclass
from langchain_core.embeddings import Embeddings
from langchain_core.retrievers import BaseRetriever

@dataclass(frozen=True)
class VectorStoreConfig:
    backend: str                # "faiss" | "chroma" | "pinecone" ...
    persist_dir: str | None = None
    collection: str | None = None
    k: int = 5
    search_type: str = "similarity"   # "similarity" | "mmr"
    score_threshold: float | None = None

def build_retriever(cfg: VectorStoreConfig, embeddings: Embeddings) -> BaseRetriever:
    backend = cfg.backend.lower()

    if backend == "faiss":
        from langchain_community.vectorstores import FAISS
        # (예시) 로드/생성 로직은 환경에 맞게 구성
        vs = FAISS.load_local(cfg.persist_dir, embeddings, allow_dangerous_deserialization=True)

    elif backend == "chroma":
        from langchain_community.vectorstores import Chroma
        vs = Chroma(
            collection_name=cfg.collection or "default",
            persist_directory=cfg.persist_dir,
            embedding_function=embeddings,
        )

    elif backend == "pinecone":
        from langchain_pinecone import PineconeVectorStore
        vs = PineconeVectorStore(
            index_name=cfg.collection or "default",
            embedding=embeddings,
        )

    else:
        raise ValueError(f"Unsupported backend: {cfg.backend}")

    search_kwargs = {"k": cfg.k}
    if cfg.score_threshold is not None:
        search_kwargs["score_threshold"] = cfg.score_threshold

    return vs.as_retriever(search_type=cfg.search_type, search_kwargs=search_kwargs)

교체는 config 한 줄로 끝남:

retriever = build_retriever(cfg, embeddings)
chain = build_rag_chain(retriever, llm)

4) 더 강한 추상화: "내가 정의한 포트(Port)인터페이스" (선택)

LangChain의 BaseRetriever만으로도 충분하지만, 더 깔끔하게 하려면 앱 레이어는 아예 LangChain도 모르게 만들 수 있음.

from typing import Protocol, List
from langchain_core.documents import Document

class RetrieverPort(Protocol):
    def retrieve(self, query: str) -> List[Document]: ...

infra에서 LangChain retriever를 감싸

class LangChainRetrieverAdapter:
    def __init__(self, retriever):
        self._retriever = retriever

    def retrieve(self, query: str):
        return self._retriever.get_relevant_documents(query)

앱은 RetrieverPort만 의존 → LangChain 교체/업그레이드에도 강해짐.

5) 실무 팁 (교체 가능성을 진짜로 만드는 포인트)

  • 문서 ID/metadata 전략을 고정해라 (삭제/업데이트 이식성)
    * metadata={"doc_id": "...", "chunk_id": "...", "source": "..."}
  • index name / collection name을 cfg로 분리
  • 검색 전략(search_type, k, threshold)도 cfg로 분리
  • VectorStore별 “특수 기능”은 앱으로 올리지 말고 infra 어댑터 내부로 숨기기
profile
천천히 고민하면서 걷는 개발자

0개의 댓글