탄소중립

[탄소중립 아카데미 6기] 벡터 임베딩과 벡터 검색

codie0226 2026. 4. 27. 21:27
반응형

지난 이야기

저번 시간에는 RAG 시스템을 위한 문서를 모으기 위해 학교 사이트를 크롤링하는 람다 함수를 구현하고 배포하였다.

약 2주가 지난 지금 크롤러는 제 역할을 다하고 있다.

한 시간에 한번씩 잘 실행되고 있고, 디스코드 웹훅에도 등록해놓아서 새로운 공지사항이 크롤링되면 디스코드 서버에도 띄워줄 수 있다.

2주동안 새로 올라온 공지사항은 2개가 있고, 모두 잘 크롤링되어서 저장된 상태이다.

그렇다면 이제 할 것은 벡터 임베딩이다. 저번에 RAG에 대해 쓴 글에도 있지만, LLM은 문서를 그대로 받아들일 수 없고, 텍스트를 벡터로 임베딩하여 semantic search를 할 수 있도록 해야 한다.

벡터 임베딩의 과정

무작정 구현하기 전에 임베딩 작전을 세워야 한다. 임베딩을 하기 전에 고려해야 할 요소는 다음과 같다.

  1. 임베딩 모델: 멀티 모달 데이터를 벡터 값으로 임베딩을 할 수 있는 임베딩 모델을 정해야 한다.
  2. 청킹 전략: 긴 컨텍스트는 벡터 의미 값이 흐려질 수 있으므로 (벡터/의미 희석) 작은 단위로 나누어서 임베딩을 해야 한다.
  3. 오버랩(overlap): 청킹이 진행된 텍스트는 전후 컨텍스트를 잃어서 무의미해질 수 있다. 따라서 앞뒤 컨텍스트를 조금씩 겹쳐서 벡터 임베딩을 진행해야 한다.
  4. 벡터 차원: 벡터 차원이 길면 길수록 더 세분화된 의미 부여가 가능하지만, 임베딩 과정과 검색 과정의 컴퓨팅 과정의 오버헤드가 발생한다.
  5. 벡터 DB: 임베딩된 벡터 값을 저장하고 검색 및 조회할 수 있는 벡터 데이터베이스가 필요하다.

하나씩 알아보도록 하겠다.

임베딩 모델

벡터 임베딩 모델은 인터넷에 수도 없이 널려있다. 이 중에서 목적과 성능을 고려해서 모델을 정하면 되는데, 가장 쉬운 방법은 API를 활용한 구현이다. 물론 값은 좀 줘야 하지만, 보장된 성능을 볼 수 있다.

대표적인 API 모델에는 다음과 같은 모델들이 있다.

  1. Google의 Gemini Embedding 2
  2. OpenAI의 Text Embedding
  3. Upstage의 Solar Embedding
  4. 기타 등등...

이러한 API를 활용하면 쉽게 구현할 수 있겠지만, 지원이 제한적인 프로젝트에서는 로컬에서 오픈 소스 모델을 활용하는 것도 방법이다. 실제 LLM 모델이 아닌 임베딩 모델은 램만 충분하다면 CPU 환경에서도 작은 벡터 차원 단위로는 돌릴 수 있다. 이 중에서도 multilingual(다국어) 성능이 뛰어난 임베딩 모델을 선택하는 것이 좋다.

Hugginface에서 가장 대표적인 다국어 임베딩 모델은 BAAI 사의 BGE-m3 임베딩 모델이다. 특히 한국어 학습이 완료되어 있어서 한국어 텍스트 임베딩 성능이 뛰어난 모델로 알려져있다.

그 중에서도 dragonkue/BGE-m3-ko 모델이 한국어 데이터셋을 추가로 학습한 모델이므로 한국어 텍스트 기준 뛰어난 벤치마크 성능을 자랑한다.

github에 업로드된 기존 모델의 벤치마크 비교

큰 장점은 임베딩 모델의 사이즈이다. 1024 벡터 차원으로 임베딩을 하고, 로컬 환경에서도 무리없이 돌아갈 수 있는 임베딩 모델이다. 따라서 첫 구현은 BGE-m3-ko 모델을 활용하여 로컬에서 임베딩을 하여 테스트해보는 것으로 하겠다.

청킹 전략

다음으로 문서를 청킹해야 한다. 일단 크롤링한 학교 문서들을 기준으로 raw text 길이를 파악해보자.

**raw_text 길이 분포:**

- 최솟값: 0자 (빈 본문 존재)

- 중앙값: 806자 / 평균: 1,080자 / 최댓값: 5,929자

- 2,000자 이하: 115개 (85%)

- 2,000자 초과: 21개 (15%)

클로드로 파악한 raw_text 값의 분포이다. 중앙값이 806자이고, 평균은 1080자 이므로 상당히 편차가 크다는 것을 파악할 수 있다.

2000자 미만의 문서가 대부분이므로, 2000자를 기준으로 청킹을 하기로 기준으로 잡았다. 특히 구체적인 문장이 아닌 문맥을 파악하고 문서를 찾으면 되므로 일단 큰 단위로 청킹을 진행해보겠다.

다음으로 overlap 값을 설정해야 하는데, 보통 10% 정도의 비율로 overlap 값을 정한다. 일단 그것보다는 낮은 7.5% 정도인 150자를 overlap 값으로 설정하고 구현해보겠다.

벡터 차원의 경우 BGE-m3-ko의 기본 차원인 1024차원으로 진행한다.

벡터 DB

벡터 DB도 다양한 옵션이 존재한다. 사실 정해진 답이 없고, 역시 목적과 성능을 위주로 평가하면 좋다. 대표적인 벡터 DB들은 다음과 같다.

DB명 형태 주요 장점 난이도
Qdrant Self-hosted 고성능, 대시보드, 필터링 강점 중간
ChromaDB Embedded 빠른 프로토타이핑, 설치 간편 매우 쉬움
Postgres SQL Extension 기존 데이터와 조인(JOIN) 가능 쉬움
Pinecone Cloud SaaS 인프라 관리 불필요 (Managed) 매우 쉬움

이외에도 수많은 벡터 DB가 존재한다. 여기에서 모든 DB를 분석할 

이번에 선택한 DB는 Qdrant이다. 이번에도 오픈소스 데이터베이스이다. 컨테이너로 로컬에 띄워서 운영하기 편하고, cos 방식으로 벡터값 유사도 검색을 바로 실행할 수 있다. 또한 로컬에 대시보드도 띄울 수 있는 소소한 장점이 있다. 별도의 설정 없이 시작할 수 있는 간단한 DB를 선택하였다.

구현

이제 정할 건 다 정했다. 실제 구현을 진행하면 되는데, 이건 클로드를 활용하여 빠르게 진행하였다.

실제 코드의 흐름은 다음과 같다.

JSON 데이터셋 (문서 + QA 페어)
        │
        ▼
embed_pipeline.py  ──  BGE-m3-ko 임베딩  ──▶  Qdrant
                                                  │
                                    ┌─────────────┤
                                    │             │
                              qa_pairs       documents
                                    │             │
                                    └──────┬──────┘
                                           │
                                      query.py
                                     rag_search()
                                    (QA 우선 → fallback)
                                           │
                               ┌───────────┴────────────┐
                               │                        │
                        벡터 검색 결과           generate_answer()
                                                  (LM Studio)
                                                  자연어 답변

embed_pipeline.py에서 실질적인 벡터 임베딩이 진행된다.

document(공지사항 텍스트 데이터)만을 저장하는 것이 아니라, QA 세트도 저장한다. 의미 유사도 검색을 할 때의 성능 비교용이기도 하고, 만약 이미 존재하는 QA 답변 세트라면 굳이 document를 찾지 않아도 되기 때문이다.

검색 과정은 QA를 우선적으로 탐색하여 검색한 뒤에, 유사도가 기준점을 넘지 못하면 document로 fallback되어 검색하게 된다.

 

실제 DB에 저장된 정보

실제로 DB에는 이렇게 저장된다. 공지사항의 제목, 날짜, doc_id, 텍스트, url 등의 정보가 잘 담겨있는 것을 확인할 수 있다.

실제 벡터 공간 표현된 대시보드

대시보드에서 벡터 공간을 확인해보면 이렇게 구성된다. 실제로 저기에 연결된 노드들은 전부 AI-POT 자격증 관련 청크 데이터가 포함되어 있다. 확인해보았을 때는 유사한 의미가 담긴 문서들이 임베딩 된 것 같다.

실제 성능 테스트를 위해 클로드한테 의미 분석을 위한 대시보드 페이지를 만들어 달라고 했다. 

샘플 쿼리 26개에 대해 분석을 진행했다. QA 기준 top1 유사도가 0.75 이상이면 qa_pairs 히트로 판정되고 QA 답변이 리턴된다. 이후 document에서 검색을 추가로 진행한다.

결과를 분석해보자면, 쿼리가 구체적일수록 더 정확한 문서를 찾아오는 걸 볼 수 있다.

주의해야할 점은 쿼리가 명확하지 않을 때, QA 세트와 유사도가 높다고 판정되면 엉뚱한 답변을 얻을 수 있다.

예를 들어 위에 '신청 기간은 언제인가요?' QA 히트된 쿼리에서, 결과는 모두 중구난방한 날짜가 나온다. 정확하게 어떤 프로그램에 대한 신청 기간인지 명확하지 않기 때문이다.

앞으로의 과제

결과를 보자면, 아직 만족할 만한 성능이 아니다.

사실 QA 세트와의 쿼리 유사도를 찾는 것은, 우리가 최종적으로 구현해야할 답변 캐싱과 관련이 있는 부분이다. 따라서 이 부분을 잘 구현해야 한다. 캐시 히트율을 높이기 위해서는 QA 페어 히트율도 높아야 하지만, 그렇다고 무작정 유사도 컷을 낮추면 위에서 언급했던 명확하지 않은 쿼리에 대한 엉뚱한 답변이 결국 '오류'로 판별된다.

따라서 앞으로 해결해야 하는 문제는 다음과 같다.

  1. 적절한 QA 유사도 찾기 - 더 많은 쿼리 샘플과 구체적인 QA 세트로 실험을 진행한다.
  2. 벡터 청킹 전략에 따른 문서 정확도 변화 - 지금은 2000자를 기준으로 문서를 임베딩하지만, 더 작은 단위로 나누면 더 정확한 검색이 가능할 수 있다.
  3. 실제 LLM 모델과의 연동 - 실제 LLM과 RAG 시스템을 구축하였을 때 얼마나 이상적인 답변을 내놓는지 실험을 진행해야 한다.
  4. 하드 코딩된 파이프라인 코드 - 지금은 실시간으로 연동된 파이프라인이 아니고, 임시적으로 구현된 프로그램이다. 따라서 문서 수와 같은 설정 값이 하드코딩된 부분이 곳곳에 있다. 실제 실시간으로 작동할 수 있도록 수정해야 한다.
  5. document fallback과 더불어 user fallback 임계값 설정 - 엉뚱한 쿼리가 들어왔을 때 무작정 document를 찾고 보는게 아니라 다시 유저에게 되묻는 과정도 필요하다. 이에 대한 해결책을 찾아야 한다.

이와 더불어서 실제 탄소값 측정, 캐싱 시스템 등의 구현적인 숙제가 남아있다.

 

그래도 현재 프로그램을 가동하였을 때, 임베딩 성능이 꽤나 정확하게 나오는 것 같다. 남은 것은 실제 LLM 모델과 연결하여 성능을 평가하고, 위에 언급한 유사도 임계값을 찾는 것이 가장 먼저 해결해야 하는 문제이다.

반응형