In progress

[RAG 자기소개봇] 2. 벡터 데이터베이스에 저장하고 검색

yudam 2026. 1. 23. 19:11
이 글의 주요 목표
- 자기소개봇을 위한 VectorDB 결정 및 임베딩
- 검색과 답변 확인

이 글에 담긴 내용
- VectorDB 개념과 유형
- RAG를 위한 검색 방법과 유형

 

데이터를 어디에 어떻게 연결할까?

VectorDB 선택하기

일단 자유분방한 문서를 활용가능한 '데이터' 형태로 구조화시키긴 했으나, 어떤 방식으로 저장하고 검색할지 선택할 필요가 있다.

 

여러가지 VectorDB 종류가 있다. 몇 가지만 비교해 보자면

 

Chroma

- 오픈소스, 설치가 간단하며, 로컬 저장이 가능하다. 

- 메타데이터를 포함해 임베딩 벡터를 저장한다.

- 단점 : 대규모 데이터에서 성능이 떨어진다.

 

LanceDB

- 이미지 및 텍스트 혼합 질문 처리도 가능하다.

- chroma에 더해 SQL 처리 가능

- 단점 : 플러그인 제한적

 

Faiss

- 단순 벡터 검색에 효과적, 빠른 성능

- 단점 : 인덱스 직접 관리해야되고 메타데이터 필터링 미지원

 

Qdrant

- 메타데이터 필터링 강력

- 하이브리드 검색용 벡터 + 조건 필터 결합에 용이

- 단점 : 로컬 운영 시 도커 필요하고, 설정이 약간 복잡함

 

PostgreSQL

- 이미 SQL DB가 있고 여기에 벡터 기능 추가할 때 용이

- 쿼리, 벡터, 메타데이터를 sql로 통합

- 단점 : 설정 필요

 

Pinecone

- 클라우드 서비스, 트래픽이 잦을 때 안정성 높음

- 단점 : 데이터를 외부에서 관리, 유료

 

현재는 학습용, 프로토타입 정도 빌드 중이므로 쉽고 실용성 높은 Chroma를 사용해보고, 이후 metadata 필터링에 강점을 가진 Qdrant로 전환해보겠다.

 

임베딩 모델 선택하기

https://huggingface.co/spaces/mteb/leaderboard

 

MTEB Leaderboard - a Hugging Face Space by mteb

Embedding Leaderboard

huggingface.co

 

허깅페이스 리더보드에 들어가보면 기준 별 순위를 매겨 놓은 걸 확인할 수 있다.

이 프로젝트에 사용될 이력서, 자기소개서는 영어 및 한국어로 작성된 경우가 많을 것이므로 다국어지원을 하는 모델을 골라본다. 일단 작은 크기의 모델부터... 예를 들어  BAAI/bge-m3  를 사용해 볼 수 있다.

 


Q. 그러나 이 모델은 Euroupean ai act corpus를 기반으로 하며 유럽 다국어 QA에 대한 높은 점수를 가지고 있다.

만약 데이터가 영어와 한국어로 한정된다면 한국어 특화 모델을 쓰는 게 낫지 않은가?

 

A. 예를 들어 벤치마크에서 한국어에 현시점 높은 성능을 낸 xlm-roberta-ua-distilled , voyage-3-large 의 경우

키워드 기반 검색품질이 떨어지거나 너무 오버스펙이다. OpenAI embedding 모델을 써도 되지만 유료다.

모델이 단순히 다국어 지원 특화여서 사용하는 게 아니라 chroma, lnagchian, metadata 필터링 기반이라는 현 조합에서

키워드 손실을 최소화 할 수 있는지 확인해야 한다.

 

 

from langchain_community.embeddings import HuggingFaceEmbeddings

embedding = HuggingFaceEmbeddings(
    model_name="BAAI/bge-m3",
    model_kwargs={"device": "cuda"},
    encode_kwargs={"normalize_embeddings": True}
)

 

임베딩은 직접 attention_mask, pooling, normalization, batch size를 설정하며 세세하게 조정할 수 있지만 지금은 간단히 langchain기반 embedding 기능을 써 보도록 한다.

다른 포스팅에서 수학적인 설명과 구조적 설명을 포함해 low-level로 직접 세팅하는 방법에 다뤄보겠다.

(링크 추가 예정)

 

def normalize_metadata(meta: dict):
    normalized = {}

    for k, v in meta.items():
        if v is None:
            continue 
        if isinstance(v, list):
            normalized[k] = ", ".join(map(str, v))
        else:
            normalized[k] = v

    return normalized

 

메타데이터가 지정되지 않은 None값에 대한 오류를 피해준다.

 

from langchain_community.embeddings import HuggingFaceEmbeddings

embedding = HuggingFaceEmbeddings(
    model_name="BAAI/bge-m3",
    model_kwargs={"device": "cuda"},
    encode_kwargs={"normalize_embeddings": True}
)

임베딩 시 만약 Chroma DB만 사용한다면 자체적으로 SentenchTransformerEmbeddingFunction 사용할 수 있지만, 이미 전처리에 langchain을 사용했기도 하고, metadata구조 기반 궁합이 좋으므로 편리하게 langchain을 또 써보기로 한다. 그렇다고 전처리에 langchain 썼다고 해서 다 langchain갖다 쓰면 장땡은 아니고, DB 가 어떻게 만들어졌는지를 뜯어봐야 한다.

 

예를 들어 Chroma말고 LanceDB를 사용했다고 치자. 그럼 Lance DB는 Arrow 기반 column형 DB기 때문에 전처리에 랭체인을 썼어도 임베딩엔 pyarrow를 쓰는 게 낫다. 이래서 처음에 라이브러리 궁합을 고려한 프로젝트 스코프 설계가 중요하다. 경험과 레벨이 중요한 이유... 같다. 

 

번외로, langchain v0.2부터 LCEL(LangChain Expression Language) 방식을 도입하면서 invoke, batch, stream, ainvoke, astream등 다른 고도화된 기능을 지원하게 됐다. langchain -> langchain-community, langchain, langchain-core로 브록다운.

 

 

from langchain_community.vectorstores import Chroma
from langchain_core.documents import Document

normalized_chunks = []
for doc in chunks:
    normalized_metadata = normalize_metadata(doc.metadata)
    normalized_chunks.append(
        Document(page_content=doc.page_content, metadata=normalized_metadata)
    )

vectorstore = Chroma.from_documents(
    documents=normalized_chunks,    
    embedding=embedding,            
    collection_name="resume_chunks",
    persist_directory="./chroma_db" 
)

vectorstore.add_documents(normalized_chunks)
vectorstore.persist()

ChromaDB와 연결한다. 이 때 Chroma.from_documents(...)로 하면 문서 청크를 임베딩하는데, 데이터가 바뀔 때 계속 이 코드로 하면 덮어쓰기 되는 게 아니라 add 되므로.. 그냥 연결만 할 경우에는 Choma(...)로 하면 임베딩 연산을 중복으로 하는 것을 피할 수 있다.

 


이제 함 Retriever 생성해보겠다.

retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

results = retriever.invoke(
    "AI 프로젝트 경험"
)
generator = pipeline(
    "text-generation",
    model="meta-llama/Llama-3.2-3B-Instruct",
    torch_dtype=torch.float16,
    device_map="auto",
    token=HF_TOKEN
)

query = "이력서에서 AI 관련 프로젝트는 뭐야?"

context = build_context(results)
prompt = build_prompt(context, query)

output = generator(
    prompt,
    max_new_tokens=300,
    temperature=0.3,
    do_sample=True
)

answer = output[0]["generated_text"]
print(answer)

 

transformer는 모델을 쉽게 활용할 수 있도록 도와주는 라이브러리다. 근데 모델마다 토큰화 방식이 달라서 tokenizers를 다르게 세팅 해 줘야 하는데 허깅페이스의 pipeline을 쓰면 tokenizer, class까지 모델에 맞게 다 맞춰준다 (..!) 뿐만 아니라 데이터셋 로드, AutoTrain, finetuning, spaces(배포) 등 추가 기능도 사용할 수 있다.

 

그러나 편한 게 다 그렇듯이.. 대신 less transparent하고 flexible 하지 못 할 때가 많다. 위에서 파이프라인을 사용해서 만든 generation을 pipeline 없이 만드는 방법은 다른 포스팅에서 다루겠다.

 

아무튼 pipeline의 text-generation 을 사용해본다. 왜 이력서 자기소개서 기반 응답을 하는 데 question-answering  안 쓰는지 궁금했는데, 이건 생성형이 아니라 주어진 지문에서 정답의 시작과 끝을 추출해내는 방식이기 때문에 질의의 의미를 이해하고 문장을 새로 생성하는 데 오히려 맞지 않는다고 한다.

 

모델은meta-llama/Llama-3.2-3B-Instruct 을 골랐다. 허깅페이스 로그인 및 메타에 사용동의를 해야한다. 

경량화 모델로 학습 상황에서 용이할 것 같았고, 사실 데이터가 많지도 않고 복잡하지 않아서 간단한 질의는 대응할 수 있다.

 

Question:
이력서에서 AI 관련 프로젝트는 뭐야?

Answer: AI Consultant로 프로젝트를 수행했습니다.
Fine-tuned LLMs, prompt generation, RLHF,
Trust & Safety content categories를 fine-tuning 및 review하는 프로젝트를 수행했습니다.

검색 답변이 말 되는지, 메타데이터 매칭이 잘 되었는지, 청킹이 잘 되었는지 확인해야한다. 샘플 이력서 데이터에 대한 결과는 나온다. 한국어 화자로서 아주 부드러운 응답으로 느껴지진 않지만 연결된 DB에 기반한 내용이 잘 매칭된다.

 


 

상황에 맞는 DB를 설정하고 연결 후, 아웃풋이 제대로 나오는지 확인했으나 품질이 마냥 좋지만은 않아보인다.

다음 글에서는 품질을 더 좋게 할 수 있는 기법(우선순위 분류, re-ranking, confidence가중, system prompt, 결과값 정제 등을 적용해보고 품질을 높여보겠다.

 

 

이 글이 참고한 문서

-Nebius Academy LLM Essentials Notebooks
https://github.com/Nebius-Academy/LLM-Engineering-Essentials/tree/main
- 요즘 IT, RAG기반 '사내 지식 챗봇' 이렇게 구축했습니다
https://yozm.wishket.com/magazine/detail/3302/
- 위키독스 검색 유형 및 방법
https://wikidocs.net/265569
- 블로그, 벡터 DB비교 관련
https://cash-code.tistory.com/38
- hugging face, milvus, 임베딩 모델 선택 관련
https://huggingface.co/spaces/mteb/leaderboard
https://milvus.io/ko/blog/how-to-choose-the-right-embedding-model-for-rag.md
- Langchain, 랭체인 트러블슈팅
https://www.blog.langchain.com/langchain-v02-leap-to-stability/?ajs_aid=f60f0600-676a-4a42-ad37-710a3ac31d54
- Huggingface transformer pipelines
https://huggingface.co/docs/transformers/v5.0.0rc2/main_classes/pipelines