[TIL-0420] 임베딩 모델 선택 가이드

jiny·2026년 4월 20일

AI Agent 실습

목록 보기
18/21

🌟 개요

인플루언서 매칭 플랫폼을 개발하면서 벡터 검색 기능을 구현해야 했고, 자연스럽게 임베딩 모델을 하나 골라야 하는 상황이 됐다.

임베딩 모델을 찾아보니 선택지가 생각보다 훨씬 많았다. OpenAI 임베딩 API부터 로컬에서 돌릴 수 있는 오픈소스 모델도 있었다. 저마다 특징과 트레이드오프가 달랐다. 한국어를 제대로 지원하는지, 벡터 차원은 몇 개인지, API 비용은 어떤지 등 고려할 게 꽤 많았다.

결국 이 프로젝트에서는 BGE-M3를 선택했다. 이 글에서는 여러 임베딩 모델을 비교하고, BGE-M3을 채택한 과정에 대해서 정리할 것이다.


🌟 임베딩이란?

✨ 텍스트를 숫자로 바꾸는 것

컴퓨터는 텍스트를 직접 이해하지 못한다. "뷰티 크리에이터"와 "메이크업 유튜버"가 비슷한 의미라는 걸 컴퓨터는 모른다. 그래서 등장한 개념이 임베딩(Embedding)이다.

임베딩은 텍스트(단어, 문장, 문서)를 고차원 벡터 공간의 한 점으로 변환하는 기술이다.핵심은 의미가 비슷한 텍스트는 벡터 공간에서도 가까운 위치에 놓인다는 것이다.
이 "가까움"을 코사인 유사도(Cosine Similarity) 같은 수식으로 계산할 수 있게 되면, 텍스트 간의 의미적 유사성을 수치로 다룰 수 있다.

cosine_similarity("뷰티 크리에이터", "메이크업 유튜버")  # 0.96 (높음)
cosine_similarity("뷰티 크리에이터", "헬스 트레이너")    # 0.11 (낮음)

🆚 키워드 검색 vs. 시맨틱 검색

임베딩이 왜 필요한지는 기존 방식의 한계를 보면 바로 이해된다.

키워드 검색(Keyword Search)입력된 단어가 문서에 그대로 포함되어 있는지를 기준으로 검색한다. 빠르고 구현이 단순하지만, 다음과 같은 한계가 있다.

검색 쿼리한계
"친환경 화장품 캠페인"문서에 "친환경"이라는 단어가 없으면 누락
"MZ세대 타겟 테크 리뷰어""MZ세대"를 언급하지 않은 인플루언서 누락
"운동 보충제 홍보""헬스", "피트니스" 등 다른 표현을 쓰면 누락

시맨틱 검색(Semantic Search)은 단어가 아닌 의미를 기준으로 검색한다. 쿼리와 문서를 모두 벡터로 변환한 뒤, 벡터 간의 거리를 계산해 가장 의미적으로 가까운 결과를 반환한다."친환경"이라는 단어가 없어도 "자연주의", "비건"이라는 의미적으로 가까운 표현을 가진 인플루언서를 찾아낼 수 있다.

✨ 이 프로젝트에서의 필요성

이 프로젝트는 광고주가 "광고 컨셉 + 아이템 + 타겟"을 자연어로 입력하면, 가장 적합한 인플루언서를 찾아주는 플랫폼이다.

광고주가 입력하는 쿼리는 예측하기 어렵다.

  • "헬시 플레저 트렌드에 맞는 저칼로리 식품 홍보"
  • "20대 직장인 여성 대상 미니멀 패션 브랜드 협업"
  • "메타버스 게임 런칭 이벤트 홍보용 게이머 크리에이터"

이런 쿼리에 키워드 검색으로 대응하려면 수백 개의 동의어 사전과 필터 조건을 미리 정의해야 한다.
반면 시맨틱 검색모델이 이미 언어의 의미 구조를 학습하고 있기 때문에, 별도의 규칙 없이도 의도에 맞는 인플루언서를 찾아낼 수 있다.

임베딩은 이 프로젝트의 검색 정확도를 좌우하는 핵심 컴포넌트이다. 그리고 어떤 임베딩 모델을 쓰느냐에 따라 품질이 크게 달라진다.


🌟 임베딩 모델 선택 시 고려 요소

모델을 고르기 전에 먼저 "무엇을 기준으로 판단할 것인가?"를 정해야 한다.
임베딩 모델은 종류가 많고 각자 내세우는 장점이 다르기 때문에, 기준 없이 비교하면 오히려 혼란스럽다.
이 프로젝트를 기준으로 실제로 고민했던 세 가지 요소를 정리했다.

1. 언어 지원 범위: 한국어를 제대로 처리하는가?

임베딩 모델의 성능은 언어마다 다르다. 영어로 학습된 모델에 한국어를 넣으면, 의미를 제대로 포착하지 못한 어색한 벡터가 나올 수 있다.

이 프로젝트에서 임베딩하는 텍스트는 한국어 인플루언서 소개문이다.

  • "자연주의 화장품을 주로 리뷰하며 비건 라이프스타일을 공유하는 뷰티 크리에이터"
  • "헬스 트레이너 출신 피트니스 유튜버로 운동 루틴과 식단을 공유합니다"

광고주가 입력하는 쿼리도 한국어이다. 즉, 쿼리와 문서 모두 한국어로 임베딩해야 한다. 영어 특화 모델은 선택지에서 사실상 제외된다.

확인해야 할 기준은 두 가지이다.

  1. 다국어 지원 여부: 한국어가 학습 데이터에 포함되어 있는가?
  2. 한국어 특화 학습 여부: 단순 포함이 아니라 충분한 비중으로 학습되었는가?

2. 트레이드오프: 벡터 차원 수 vs. 품질

임베딩 벡터의 차원 수는 모델마다 다르다. 일반적으로 차원이 높을수록 더 많은 의미 정보를 담을 수 있지만, 그만큼 저장 공간과 검색 속도에 영향을 준다.

차원 수특징
384차원경량, 빠름. 간단한 유사도 검색에 적합
768차원BERT 계열 기본값. 범용적
1024차원높은 표현력. 복잡한 의미 구분에 유리
1536차원OpenAI text-embedding-3-small 기본값
3072차원OpenAI text-embedding-3-large. 최고 품질이지만 비용·용량 큼

차원 수가 높다고 무조건 좋은 게 아니다. 데이터 규모와 검색 요구 수준에 맞는 차원을 골라야 한다.

3. 로컬 실행 가능 여부

임베딩 모델은 크게 두 가지 방식으로 사용한다.

클라우드 API 방식OpenAI, Cohere 같은 서비스에 텍스트를 보내면 벡터를 받아오는 방식이다. 별도 설치 없이 바로 쓸 수 있고 성능도 검증되어 있지만, 두 가지 문제가 있다.

  • 비용: 임베딩은 인덱스 빌드 시 대량으로 호출된다. 인플루언서 수백 명의 소개문을 인덱싱할 때, 그리고 사용자 쿼리마다 매번 API를 호출해야 한다.
  • 의존성: 외부 서비스가 다운되거나 API 정책이 바뀌면 서비스 전체가 영향을 받는다.

로컬 실행 방식은 Ollama 같은 도구로 모델을 직접 서버에 올려 사용하는 방식이다. API 비용이 없고, 네트워크 의존성도 없다.

이 프로젝트는 LLM도 Ollama로 설정하는 구조였기 때문에, 임베딩 모델도 같은 인프라에서 함께 운영할 수 있는 로컬 실행 모델을 우선적으로 검토했다.
단, 모든 오픈소스 임베딩 모델이 Ollama를 지원하는 건 아니기 때문에, Ollama로 실행 가능한 모델인지 미리 확인해야 한다.


🌟 주요 임베딩 모델 비교

🤖 OpenAI Embedding API

OpenAI가 제공하는 클라우드 기반 임베딩 API이다. 현재 text-embedding-3-smalltext-embedding-3-large 두 가지 모델을 제공한다.

별도 설치 없이 API 키만 있으면 바로 사용할 수 있고, 다국어 지원도 준수한 편이다. 성능이 검증되어 있어 빠르게 프로토타입을 만들 때 선택하기 좋다.

단점은 유료라는 점이다. 임베딩 호출은 LLM 대비 저렴하지만, 인덱스 빌드 시 대량 호출과 사용자 쿼리마다 발생하는 비용이 누적된다. 또한 외부 API 의존성이 생기기 때문에, 민감한 데이터를 외부로 전송해야 한다는 점도 고려해야 한다.

🤖 Sentence-BERT (SBERT)

2019년 발표된 모델로, 문장 임베딩 분야의 초기 오픈소스 표준이다. BERT를 Siamese Network 구조로 파인튜닝해 문장 간 의미적 유사도 계산에 특화되어 있다.

Siamese Network 구조

두 입력을 같은 가중치를 공유하는 동일한 네트워크에 동시에 통과시켜서, 두 출력 벡터 간의 유사도를 학습하는 구조이다.

paraphrase-multilingual-mpnet-base-v2 같은 다국어 변형 모델도 있지만, 기본적으로 영어 중심으로 설계되어 있어 한국어 성능은 제한적이다. HuggingFace를 통해 로컬 실행이 가능하지만 Ollama는 지원하지 않는다.

현재는 후속으로 나온 모델들에 비해 성능이 뒤처지는 편이라, 새 프로젝트에서 굳이 선택할 이유는 많지 않다.

🤖 BGE 시리즈

중국 AI 연구기관 BAAI(Beijing Academy of Artificial Intelligence)에서 개발한 임베딩 모델 시리즈이다. MTEB 리더보드 상위권을 꾸준히 유지하고 있다.

BGE-large-en-v1.5영어 전용 고성능 모델이다. MTEB 영어 벤치마크 기준으로 OpenAI 모델과 견줄 만한 성능을 보여주지만, 한국어 지원이 없어 이 프로젝트에서는 후보에서 제외됐다.

BGE-M3는 이 시리즈의 다국어 버전으로, 이름의 M3가 세 가지 특징을 의미한다.

  • Multi-Lingual: 100개 이상의 언어 지원. 한국어 포함
  • Multi-Functionality: Dense(밀집), Sparse(희소), ColBERT 세 가지 검색 방식을 하나의 모델로 지원
  • Multi-Granularity: 단어 단위부터 최대 8192 토큰의 장문까지 처리 가능

1024차원 벡터를 출력하며, Ollama 공식 지원 모델 목록에 포함되어 있어 로컬 실행이 가능하다.

🤖 ES 시리즈

Microsoft Research에서 개발한 임베딩 모델 시리즈이다. 설계 상의 특징이 하나 있는데, 입력 텍스트 앞에 접두어를 붙여서 쿼리와 문서를 명시적으로 구분하도록 설계되어 있다.

쿼리: "query: 친환경 확장품 캠페인 인플루언서"
문서: "passage: 자연주의 화장품을 리뷰하는 뷰티 크리에이터..."

multilingual-e5-large는 다국어 지원 버전으로 한국어도 포함된다. 1024차원을 출력하며 HuggingFace를 통해 로컬 실행이 가능하다.
다만 Ollama 공식 지원은 되지 않아, Ollama 기반 인프라에서는 별도 설정이 필요하다.

🤖 Nomic Embed

Nomic AI에서 개발한 오픈소스 임베딩 모델이다. 가장 큰 특징은 장문 처리에 특화되어 있다는 점이다. nomic-embed-text-v1.5는 최대 8192 토큰을 입력받을 수 있어, 긴 문서를 통째로 임베딩해야 하는 케이스에 적합하다.

768차원을 출력하며 Ollama 공식 지원 모델에 포함되어 있다. 완전한 오픈 웨이트 모델로 공개되어 있어 상업적 사용도 자유롭다. 다만 다국어 지원은 BGE-M3이나 multilingual-e5 대비 제한적이다.

👀 한눈에 비교하기

모델한국어차원로컬 실행OllamaMTEB(영어)
OpenAI text-embedding-3-large3072최상위
OpenAI text-embedding-3-small1536상위
SBERT (multilingual)768중위
BGE-large-en-v1.51024상위
BGE-M31024상위
multilingual-e5-large1024상위
Nomic Embed v1.5768중상위

△는 지원은 하지만 품질이 제한적임을 의미한다.
이 표를 기준으로 보면, 한국어 + 로컬 설정 + Ollama 지원을 동시에 만족하는 모델은 BGE-M3가 사실상 유일하다.


🌟 BGE-M3 모델 심층 분석

BGE-M3는 2024년 BAAI(Beijing Academy of Artificial Intelligence)에서 발표한 임베딩 모델이다.
이름의 M3는 세 가지 특징의 앞글자를 딴 것으로, 각각이 기존 임베딩 모델의 한계를 하나씩 해결하는 방향으로 설계되어 있다.

✨ Multi-Lingual: 언어 장벽 없는 임베딩

100개 이상의 언어를 단일 모델로 처리한다. 언어별로 별도 모델을 관리할 필요 없이, 한국어 문서와 영어 쿼리를 같은 벡터 공간에 매핑할 수 있다.

단순히 "한국어 포함"이 아니라, 한국어가 충분한 비중으로 학습 데이터에 포함되어 있어 실제 한국어 텍스트에서도 의미를 제대로 포착한다. MTEB Multilingual 리더보드에서도 상위권을 유지하고 있다.

✨ Multi-Functionality: 세 가지 검색 방식을 하나의 모델로

BGE-M3의 가장 독특한 특징이다. 일반적으로 검색 방식은 세 가지로 나뉜다.

  1. Dense Retrieval (밀집 검색)
    텍스트 전체를 하나의 고정 크기 벡터로 압축하는 방식이다. 우리가 일반적으로 "임베딩"이라고 부를 때 이 방식을 의미한다.
    "자연주의 뷰티 크리에이터" → [0.12, -0.83, 0.45, ...] (1024차원 벡터 1개)
    계산이 빠르고 벡터 스토리지 구조와 잘 맞아서 대부분의 시맨틱 검색에서 기본으로 쓰인다.
    이 프로젝트의 Neo4j Vector Index 검색도 이 방식을 사용한다.
  1. Sparse Retrieval (희소 검색)
    문장 내 각 단어에 중요도 가중치를 부여하는 방식이다. 기존 BM25 같은 키워드 검색과 유사하지만, 단순 빈도 기반이 아닌 신경망이 중요도를 학습한다.
    "자연주의 뷰티 크리에이터"
    → {"자연주의": 0.82, "뷰티": 0.91, "크리에이터": 0.64, ...}
    특정 키워드가 결과에 반드시 포함되어야 하는 경우에 강점을 가진다.
    Dense가 놓치는 정확한 용어 매칭을 보완할 수 있다.
  1. ColBERT (다중 벡터 검색)
    문장을 하나의 벡터가 아닌 토큰(단어) 단위 벡터들의 집합으로 표현하는 방식이다.
    "자연주의 뷰티 크리에이터"
    → [벡터_자연주의, 벡터_뷰티, 벡터_크리에이터, ...] (토큰 수만큼 벡터 생성)
    쿼리와 문서의 각 토큰 벡터를 세밀하게 비교하기 때문에 정확도가 가장 높다.
    단, 토큰 수만큼 벡터를 저장해야 해서 스토리지 비용이 크고 검색 속도도 느리다.

세 방식 각각의 트레이드오프를 정리하면 다음과 같다.

방식정확도속도스토리지특징
Dense중상빠름낮음의미적 유사도
Sparse빠름낮음키워드 정확 매칭
ColBERT높음느림높음토큰 레벨 세밀 비교

BGE-M3는 이 세 방식을 하나의 모델에서 모두 출력할 수 있다.
상황에 따라 Dense만 쓰거나, Dense + Sparse를 결합한 하이브리드 검색을 구성하는 것도 가능하다.

✨ Multi-Granularity: 단어부터 장문까지

입력 텍스트 길이에 제한이 거의 없다. 최대 8192 토큰을 처리할 수 있어, 짧은 키워드부터 긴 문서까지 동일한 모델로 임베딩할 수 있다.

기존 BERT 계열 모델은 대부분 512 토큰 제한이 있어서 긴 문서는 잘라서 처리하거나 별도 전략이 필요했다. BGE-M3는 이 제약에서 자유롭다.


🌟 이 프로젝트에서 BGE-M3을 선택한 이유

✨ 1단계: 한국어 지원으로 후보군 절반 제거

이 프로젝트에서 임베딩하는 텍스트는 한국어 인플루언서 소개문이고, 광고주가 입력하는 쿼리도 한국어이다.

쿼리: "친환경 화장품 캠페인에 적합한 자연주의 뷰티 인플루언서"
문서: "비건 라이프스타일과 자연주의 스킨케어 루틴을 공유하는 크리에이터"

영어 전용 모델은 이 구조에서 의미를 제대로 포착하지 못한다. BGE-large-en-v1.5는 MTEB 영어 벤치마크 기준으로 성능이 뛰어나지만, 한국어를 지원하지 않아 후보에서 바로 제외됐다.
이 단계에서 다국어 지원 모델로 범위가 좁혀진다.

✨ 2단계: 로컬 실행 조건으로 API 모델 제거

이 프로젝트는 LLM을 Ollama로 로컬 실행하는 구조이다. 임베딩 모델만 외부 API에 의존하면 구조가 일관되지 않고, API 비용과 네트워크 의존성이 새로 생긴다.

여기서 한 가지 더 고려한 게 있다. 이 프로젝트는 Anthropic Claude를 LLM으로 쓰는 경우도 지원하는데, Anthropic은 임베딩 API를 제공하지 않는다. 즉, LLM을 Anthropic으로 쓰더라도 임베딩은 반드시 별도 모델이 필요하다.

# container.py

# Anthropic 사용 시에도 임베딩은 Ollama 유지
elif self.settings.llm_provider == "anthropic":
   LlamaSettings.embed_model = OllamaEmbedding(
      model_name=self.settings.ollama_embed_model,
      base_url=self.settings.ollama_base_url,
   )
   LlamaSettings.llm = AnthropicLLM(...)

어떤 LLM 프로바이더를 선택하든 임베딩은 로컬에서 일관되게 동작해야 한다는 결론이 나왔고, 자연스럽게 Ollama 지원 모델로 범위가 좁혀졌다.

✨ 3단계: Ollama 지원 모델 중 최종 선택

한국어 지원 + Ollama 실행 가능이라는 조건을 동시에 만족하는 모델은 선택지가 많지 않았다. 이 조건을 통과한 후보들 중 MTEB Multilingual 기준 성능을 비교했을 때 BGE-M3이 가장 상위권이었다.

추가로 다음 두 가지가 최종 결정에 영향을 줬다.

  1. 1024차원의 적절한 균형
    고품질 임베딩을 위한 충분한 차원 수를 갖추면서도, 3072차원의 OpenAI large 모델처럼 스토리지와 검색 속도에 부담을 주지 않는다.
    Neo4j Vector Index와의 연동에서도 무리 없이 작동한다.

  2. MIT 라이선스
    상업적 사용에 제약이 없다. 오픈소스 프로젝트나 상용 서비스로 확장할 때 라이선스 문제를 고려하지 않아도 된다.

✨ 선택 과정 요약

결국 한국어 지원 + 로컬 실행 + Ollama 지원 조건을 통과한 후보들 중, MTEB Multilingual 기준 성능을 비교했을 때 BGE-M3가 가장 상위권이었고, 이 프로젝트의 인프라 구조와도 자연스럽게 맞아떨어졌다.


🌟 실험: 접두어 프롬프트가 임베딩 품질에 미치는 영향

BGE-M3를 선택한 뒤, 한 가지 추가 실험을 진행했다. 인플루언서 소개문 앞에 전문분야를 명시하는 접두어를 붙이는 것이 임베딩 품질에 실제로 영향을 주는지 확인하고 싶었다.

🧪 실험 배경

인플루언서 소개문은 대부분 자연어로 작성되어 있다.

"헬스 트레이너 출신 피트니스 유튜버로 운동 루틴과 식단을 공유합니다"

이 텍스트만으로도 "피트니스" 도메인임을 파악할 수 있지만, 모델 입장에서는 문장 전체의 평균적인 의미를 하나의 벡터로 압축해야 한다.
도메인 키워드가 문장 중간에 묻혀 있으면, 벡터가 그 방향으로 충분히 치우치지 않을 수 있다.

그래서 다음과 같이 접두어를 앞에 명시하면 어떨까라는 아이디어를 실행했다.

"[전문분야: 피트니스] 헬스 트레이너 출신 피트니스 유튜버로 운동 루틴과 식단을 공유합니다"

🧪 실험 설계

5개 카테고리(뷰티, 테크, 피트니스, 음식, 여행)에 대해 쿼리와 인플루언서 소개문을 각각 준비했다.

# test_prefix_embedding.py

QUERIES = {
   "뷰티": "20대 여성 대상 스킨케어 제품 광고",
   "테크": "신형 스마트폰 리뷰 및 언박싱 콘텐츠",
   "피트니스": "헬스보충제 홍보 및 운동 루틴 캠페인",
   "음식": "신메뉴 출시 맛집 인플루언서 협업",
   "여행": "항공사 여행 패키지 프로모션",
}

각 인플루언서 소개문에 대해 접두어 있는 버전과 없는 버전을 모두 임베딩한 뒤, 5개 쿼리 벡터 각각과의 코사인 유사도를 계산했다.

측정 지표는 두 가지이다.

  1. 일치 쿼리 효과: 같은 카테고리 쿼리와의 유사도 변화량 (접두어 있음 - 없음)
  2. 비일치 효과 평균: 다른 카테고리 쿼리들과의 유사도 변화량 평균

접두어가 유효하다면 일치 쿼리 효과 > 비일치 효과 평균이 되어야 한다.
즉, 같은 도메인 쿼리와는 더 가까워지고, 다른 도메인 쿼리와는 큰 변화가 없어야 한다.

🧪 실험 결과

결론부터 말하면, 접두어는 효과가 없었다.

25개 조합(5개 카테고리 쿼리 × 5개 인플루언서) 중 21개에서 유사도가 오히려 하락했다. 상승한 4개도 +0.0006~0.0036 수준으로 노이즈에 가까웠다.

가장 심각한 케이스는 테크 인플루언서였다. 같은 도메인 쿼리("신형 스마트폰 리뷰 및 언박싱 콘텐츠")와의 유사도가 -0.0213 하락했는데, 다른 도메인 쿼리들의 평균 하락폭(-0.0053)보다 4배 컸다.
즉, 접두어가 도메인 방향을 강화하기는커녕 오히려 역효과를 낸 것이다.

실험 내용을 더 자세하게 보고 싶다면, 이 글을 참고하면 된다.

🧪 왜 효과가 없었나?

원인은 BGE-M3의 구조적 특성에 있다.

  1. 양방향 어텐션 구조
    BGE-M3는 모든 토큰을 균등하게 처리한다. 앞에 배치한 접두어가 특별히 더 강조되는 구조가 아니기 때문에, 위치 기반으로 도메인 방향을 강화하려는 시도 자체가 맞지 않았다.

  2. 정보 중복으로 인한 어텐션 분산
    소개문 본문에 이미 카테고리 정보가 포함되어 있는데, 접두어로 동일한 내용을 앞에 한 번 더 추가하면 어텐션이 분산된다. 오히려 정보 밀도가 낮아지는 효과가 생긴다.

  3. 학습 분포 이탈
    [전문분야: 피트니스] 같은 비자연어 형식은 BGE-M3가 학습한 텍스트 분포와 다르다. 모델이 익숙하지 않은 형식의 입력에서 벡터 품질이 저하됐을 가능성이 있다.

  4. 쿼리-문서 비대칭
    광고주 쿼리는 자연어로 입력된다. 반면 인플루언서 문서에는 인공적인 접두어가 붙어 있다. 두 텍스트의 형식이 달라지면 같은 의미 공간에서 비교가 어려워진다.

✨ 결론 및 적용

접두어는 벡터 품질을 높이지 못했고, 오히려 저하시켰다. 최종적으로 접두어를 제거하고 자연어 소개문 그대로 임베딩하는 방식으로 돌아왔다.

doc = Document(
   text=f"{influencer.name}: {influencer.description} "
        f"카테고리: {', '.join(influencer.category_names)}. "
        f"플랫폼: {', '.join(influencer.platform_dict.keys())}. "
        f"팔로워: {influencer.total_followers}명. "
        f"참여율: {influencer.engagement_rate}",
)

실제 검색 정확도가 유지된 건 접두어 덕분이 아니었다. LLM 재순위 단계에서 카테고리를 필수 조건으로 강제했기 때문이었다.
즉, 벡터 검색이 카테고리 구분을 완벽히 못 해도 LLM이 최종 필터 역할을 했고, 두 단계를 조합한 구조 자체가 시스템의 명확도를 보장하고 있었던 것이다.


🌟 LlamaIndex + Neo4j Vector Index 통합 방법

BGE-M3로 생성한 임베딩 벡터를 실제 검색에 활용하려면 두 가지가 필요하다. 벡터를 저장하고 검색할 스토리지와, 임베딩 생성부터 검색까지의 파이프라인을 연결해주는 프레임워크이다.
이 프로젝트에서는 각각 Neo4j Vector Index와 LlamaIndex를 사용했다.

✨ 전체 구조

LlamaIndex 임베딩 모델 설정, 문서 처리, 인덱스 빌드를 담당하고, Neo4j실제 벡터 데이터를 저장하고 유사도 쿼리를 실행한다.

✨ 1단계: LlamaIndex 글로벌 설정

LlamaIndex는 Settings 객체를 통해 임베딩 모델과 LLM을 전역으로 설정한다. 한 번 설정해두면 이후 모든 인덱스 빌드와 검색에서 자동으로 사용된다.

# container.py
from llama_index.core import Settings as LlamaSettings
from llama_index.embeddings.ollama import OllamaEmbedding

LlamaSettings.embed_model = OllamaEmbedding(
   model_name="bge-m3",
   base_url="http://localhost:11434",
)

LLM 프로바이더가 Anthropic으로 바뀌어도 임베딩 설정은 유지된다.
Anthropic이 임베딩 API를 제공하지 않기 때문에, 어떤 LLM을 쓰든 임베딩은 항상 Ollama BGE-M3로 고정된다.

✨ 2단계: Neo4j Vector Store 연결

Neo4j를 벡터 스토리지로 쓰려면 Neo4jVectorStore를 초기화해야 한다. 이때 임베딩 차원 수를 반드시 모델과 일치시켜야 한다.

# influencer_index.py
from llama_index.vector_stores.neo4jvector import Neo4jVectorStore

self._vector_store = Neo4jVectorStore(
   username=...,
   password=...,
   url=...,
   embedding_dimension=1024,  # BGE-M3 출력 차원과 일치
   index_name="influencer_vector_idx",
   node_label="InfluencerEmbedding",
)

embedding_dimension은 인덱스 생성 시 한 번 고정된다. 나중에 모델을 바꿔 차원이 달라지면 인덱스를 초기화하고 다시 빌드해야 한다.

✨ 3단계: 인덱스 빌드

인플루언서 데이터를 LlamaIndex Document 객체로 변환한 뒤 인덱스를 빌드한다.
이 과정에서 LlamaIndex가 각 Document의 텍스트를 BGE-M3로 임베딩해 Neo4j에 저장한다.

def build_index(self, influencers: List[Influencer]) -> VectorStoreIndex:
   documents = []
   for influencer in influencers:
      doc = Document(
         text=f"{influencer.name}: {influencer.description} "
              f"카테고리: {', '.join(influencer.category_names)}. "
              f"플랫폼: {', '.join(influencer.platform_dict.keys())}. "
              f"팔로워: {influencer.total_followers}명. "
              f"참여율: {influencer.engagement_rate}",
         metadata={"influencer_id": str(influencer.id), ...},
      )
      documents.append(doc)
      
   storage_context = StorageContext.from_defaults(
      vector_store=self._vector_store
   )
   self._index = VectorStoreIndex.from_documents(
      documents, storage_context=storage_context
   )

빌드가 완료되면 Neo4j에 InfluencerEmbedding 노드가 생성되고, 각 노드에 1024차원 벡터가 저장된다.

✨ 4단계: 벡터 검색

검색 시에는 쿼리 텍스트를 BGE-M3로 임베딩한 뒤, Neo4j의 벡터 유사도 검색 프로시저를 직접 호출한다.

def retrieve(self, query_text: str, top_k: int = 10) -> str:
   # 쿼리 임베딩 생성
   query_embedding = LlamaSettings.embed_model.get_query_embedding(query_text)
   
   # Neo4j 직접 벡터 유사도 검색
   with self._db.get_session() as session:
      records = session.run(
         "CALL db.index.vector.queryNodes($index_name, $top_k, $embedding) "
         "YIELD node, score "
         "RETURN node.text AS text, node.influencer_id AS influencer_id, score",
         index_name="influencer_vector_idx",
         top_k=top_k,
         embedding=query_embedding,
      )

LlamaIndex의 기본 검색 래퍼를 우회하고 Cypher를 직접 실행한다. 쿼리 임베딩이 정확하게 적용되는지 제어하기 위해서이다.
Neo4j는 내부적으로 코사인 유사도를 계산해 score와 함께 결과를 반환한다. score가 1에 가까울수록 쿼리와 의미적으로 유사한 인플루언서이다.

✨ 인덱스 초기화

인플루언서 데이터가 변경되면 인덱스를 초기화하고 다시 빌드해야 한다.

def reset(self) -> None:
   with self._db.get_session() as session:
      session.run("MATCH (n:InfluencerEmbedding) DETACH DELETE n")
      session.run("DROP INDEX influencer_vector_idx IF EXISTS")

Neo4j에서 InfluencerEmbedding 노드와 벡터 인덱스를 삭제한 뒤, build_index()를 다시 호출하면 된다.

✨ 정리

역할담당
임베딩 모델 관리LlamaIndex Settings
문서 → 벡터 변환LlamaIndex + BGE-M3 (Ollama)
벡터 저장Neo4j Vector Index
유사도 검색Neo4j Cypher (db.index.vector.queryNodes)

LlamaIndex가 임베딩 파이프라인의 접착제 역할을 하고, Neo4j가 실제 데이터를 저장하고 검색하는 구조이다.
두 도구의 역할이 명확히 분리되어 있어서, 나중에 벡터 스토리지를 Pinecone이나 Weaviate로 교체하더라도 LlamaIndex 레이어는 그대로 유지할 수 있다.


🌟 모델 선택 체크리스트

✨ 한국어가 주 언어라면?

  • 다국어(Multilingual) 지원 모델인지 확인하기
  • MTEB Multilingual 리더보드에서 한국어 태스크 점수 확인하기
  • 영어 벤치마크 점수만 보고 판단하지 않기
  • 실제 한국어 도메인 텍스트로 직접 테스트하기

✨ 비용이 제약이라면?

  • 로컬 실행 가능한 오픈소스 모델 우선 검토하기
  • 사용 중인 인프라(Ollama 등)에서 지원하는 모델인지 확인하기
  • 인덱스 빌드 시 대량 호출 비용까지 계산하기 (API 방식이라면)
  • 라이선스 확인하기 (상업적 사용 가능 여부)

✨ 검색 정확도가 최우선이라면?

  • Dense만으로 부족하다면 Sparse 또는 ColBERT 방식 검토하기
  • 벡터 검색 단독보다 LLM 재순위 단계를 추가로 고려하기
  • 임베딩 품질 개선 전에 텍스트 구성 방식부터 점검하기
  • 직관적으로 맞아 보이는 방법도 반드시 수치로 검증하기

✨ 벡터 스토리지와 연동할 때

  • 선택한 모델의 출력 차원 수 확인하기
  • 스토리지의 인덱스 차원 설정과 반드시 일치시키기
  • 모델 교체 시 인덱스 초기화 후 전체 재빌드하기

0개의 댓글