인플루언서 매칭 플랫폼을 개발하면서 벡터 검색 기능을 구현해야 했고, 자연스럽게 임베딩 모델을 하나 골라야 하는 상황이 됐다.
임베딩 모델을 찾아보니 선택지가 생각보다 훨씬 많았다. OpenAI 임베딩 API부터 로컬에서 돌릴 수 있는 오픈소스 모델도 있었다. 저마다 특징과 트레이드오프가 달랐다. 한국어를 제대로 지원하는지, 벡터 차원은 몇 개인지, API 비용은 어떤지 등 고려할 게 꽤 많았다.
결국 이 프로젝트에서는 BGE-M3를 선택했다. 이 글에서는 여러 임베딩 모델을 비교하고, BGE-M3을 채택한 과정에 대해서 정리할 것이다.
컴퓨터는 텍스트를 직접 이해하지 못한다. "뷰티 크리에이터"와 "메이크업 유튜버"가 비슷한 의미라는 걸 컴퓨터는 모른다. 그래서 등장한 개념이 임베딩(Embedding)이다.
임베딩은 텍스트(단어, 문장, 문서)를 고차원 벡터 공간의 한 점으로 변환하는 기술이다.
핵심은 의미가 비슷한 텍스트는 벡터 공간에서도 가까운 위치에 놓인다는 것이다.
이 "가까움"을 코사인 유사도(Cosine Similarity) 같은 수식으로 계산할 수 있게 되면, 텍스트 간의 의미적 유사성을 수치로 다룰 수 있다.
cosine_similarity("뷰티 크리에이터", "메이크업 유튜버") # 0.96 (높음)
cosine_similarity("뷰티 크리에이터", "헬스 트레이너") # 0.11 (낮음)
임베딩이 왜 필요한지는 기존 방식의 한계를 보면 바로 이해된다.
키워드 검색(Keyword Search)은 입력된 단어가 문서에 그대로 포함되어 있는지를 기준으로 검색한다. 빠르고 구현이 단순하지만, 다음과 같은 한계가 있다.
| 검색 쿼리 | 한계 |
|---|---|
| "친환경 화장품 캠페인" | 문서에 "친환경"이라는 단어가 없으면 누락 |
| "MZ세대 타겟 테크 리뷰어" | "MZ세대"를 언급하지 않은 인플루언서 누락 |
| "운동 보충제 홍보" | "헬스", "피트니스" 등 다른 표현을 쓰면 누락 |
시맨틱 검색(Semantic Search)은 단어가 아닌 의미를 기준으로 검색한다. 쿼리와 문서를 모두 벡터로 변환한 뒤, 벡터 간의 거리를 계산해 가장 의미적으로 가까운 결과를 반환한다.
"친환경"이라는 단어가 없어도 "자연주의", "비건"이라는 의미적으로 가까운 표현을 가진 인플루언서를 찾아낼 수 있다.
이 프로젝트는 광고주가 "광고 컨셉 + 아이템 + 타겟"을 자연어로 입력하면, 가장 적합한 인플루언서를 찾아주는 플랫폼이다.
광고주가 입력하는 쿼리는 예측하기 어렵다.
이런 쿼리에 키워드 검색으로 대응하려면 수백 개의 동의어 사전과 필터 조건을 미리 정의해야 한다.
반면 시맨틱 검색은 모델이 이미 언어의 의미 구조를 학습하고 있기 때문에, 별도의 규칙 없이도 의도에 맞는 인플루언서를 찾아낼 수 있다.
임베딩은 이 프로젝트의 검색 정확도를 좌우하는 핵심 컴포넌트이다. 그리고 어떤 임베딩 모델을 쓰느냐에 따라 품질이 크게 달라진다.
모델을 고르기 전에 먼저 "무엇을 기준으로 판단할 것인가?"를 정해야 한다.
임베딩 모델은 종류가 많고 각자 내세우는 장점이 다르기 때문에, 기준 없이 비교하면 오히려 혼란스럽다.
이 프로젝트를 기준으로 실제로 고민했던 세 가지 요소를 정리했다.
임베딩 모델의 성능은 언어마다 다르다. 영어로 학습된 모델에 한국어를 넣으면, 의미를 제대로 포착하지 못한 어색한 벡터가 나올 수 있다.
이 프로젝트에서 임베딩하는 텍스트는 한국어 인플루언서 소개문이다.
광고주가 입력하는 쿼리도 한국어이다. 즉, 쿼리와 문서 모두 한국어로 임베딩해야 한다. 영어 특화 모델은 선택지에서 사실상 제외된다.
확인해야 할 기준은 두 가지이다.
임베딩 벡터의 차원 수는 모델마다 다르다. 일반적으로 차원이 높을수록 더 많은 의미 정보를 담을 수 있지만, 그만큼 저장 공간과 검색 속도에 영향을 준다.
| 차원 수 | 특징 |
|---|---|
| 384차원 | 경량, 빠름. 간단한 유사도 검색에 적합 |
| 768차원 | BERT 계열 기본값. 범용적 |
| 1024차원 | 높은 표현력. 복잡한 의미 구분에 유리 |
| 1536차원 | OpenAI text-embedding-3-small 기본값 |
| 3072차원 | OpenAI text-embedding-3-large. 최고 품질이지만 비용·용량 큼 |
차원 수가 높다고 무조건 좋은 게 아니다. 데이터 규모와 검색 요구 수준에 맞는 차원을 골라야 한다.
임베딩 모델은 크게 두 가지 방식으로 사용한다.
클라우드 API 방식은 OpenAI, Cohere 같은 서비스에 텍스트를 보내면 벡터를 받아오는 방식이다. 별도 설치 없이 바로 쓸 수 있고 성능도 검증되어 있지만, 두 가지 문제가 있다.
로컬 실행 방식은 Ollama 같은 도구로 모델을 직접 서버에 올려 사용하는 방식이다. API 비용이 없고, 네트워크 의존성도 없다.
이 프로젝트는 LLM도 Ollama로 설정하는 구조였기 때문에, 임베딩 모델도 같은 인프라에서 함께 운영할 수 있는 로컬 실행 모델을 우선적으로 검토했다.
단, 모든 오픈소스 임베딩 모델이 Ollama를 지원하는 건 아니기 때문에, Ollama로 실행 가능한 모델인지 미리 확인해야 한다.
OpenAI가 제공하는 클라우드 기반 임베딩 API이다. 현재 text-embedding-3-small과 text-embedding-3-large 두 가지 모델을 제공한다.
별도 설치 없이 API 키만 있으면 바로 사용할 수 있고, 다국어 지원도 준수한 편이다. 성능이 검증되어 있어 빠르게 프로토타입을 만들 때 선택하기 좋다.
단점은 유료라는 점이다. 임베딩 호출은 LLM 대비 저렴하지만, 인덱스 빌드 시 대량 호출과 사용자 쿼리마다 발생하는 비용이 누적된다. 또한 외부 API 의존성이 생기기 때문에, 민감한 데이터를 외부로 전송해야 한다는 점도 고려해야 한다.
2019년 발표된 모델로, 문장 임베딩 분야의 초기 오픈소스 표준이다. BERT를 Siamese Network 구조로 파인튜닝해 문장 간 의미적 유사도 계산에 특화되어 있다.
⭐ Siamese Network 구조
두 입력을 같은 가중치를 공유하는 동일한 네트워크에 동시에 통과시켜서, 두 출력 벡터 간의 유사도를 학습하는 구조이다.
paraphrase-multilingual-mpnet-base-v2 같은 다국어 변형 모델도 있지만, 기본적으로 영어 중심으로 설계되어 있어 한국어 성능은 제한적이다. HuggingFace를 통해 로컬 실행이 가능하지만 Ollama는 지원하지 않는다.
현재는 후속으로 나온 모델들에 비해 성능이 뒤처지는 편이라, 새 프로젝트에서 굳이 선택할 이유는 많지 않다.
중국 AI 연구기관 BAAI(Beijing Academy of Artificial Intelligence)에서 개발한 임베딩 모델 시리즈이다. MTEB 리더보드 상위권을 꾸준히 유지하고 있다.
BGE-large-en-v1.5는 영어 전용 고성능 모델이다. MTEB 영어 벤치마크 기준으로 OpenAI 모델과 견줄 만한 성능을 보여주지만, 한국어 지원이 없어 이 프로젝트에서는 후보에서 제외됐다.
BGE-M3는 이 시리즈의 다국어 버전으로, 이름의 M3가 세 가지 특징을 의미한다.
1024차원 벡터를 출력하며, Ollama 공식 지원 모델 목록에 포함되어 있어 로컬 실행이 가능하다.
Microsoft Research에서 개발한 임베딩 모델 시리즈이다. 설계 상의 특징이 하나 있는데, 입력 텍스트 앞에 접두어를 붙여서 쿼리와 문서를 명시적으로 구분하도록 설계되어 있다.
쿼리: "query: 친환경 확장품 캠페인 인플루언서"
문서: "passage: 자연주의 화장품을 리뷰하는 뷰티 크리에이터..."
multilingual-e5-large는 다국어 지원 버전으로 한국어도 포함된다. 1024차원을 출력하며 HuggingFace를 통해 로컬 실행이 가능하다.
다만 Ollama 공식 지원은 되지 않아, Ollama 기반 인프라에서는 별도 설정이 필요하다.
Nomic AI에서 개발한 오픈소스 임베딩 모델이다. 가장 큰 특징은 장문 처리에 특화되어 있다는 점이다. nomic-embed-text-v1.5는 최대 8192 토큰을 입력받을 수 있어, 긴 문서를 통째로 임베딩해야 하는 케이스에 적합하다.
768차원을 출력하며 Ollama 공식 지원 모델에 포함되어 있다. 완전한 오픈 웨이트 모델로 공개되어 있어 상업적 사용도 자유롭다. 다만 다국어 지원은 BGE-M3이나 multilingual-e5 대비 제한적이다.
| 모델 | 한국어 | 차원 | 로컬 실행 | Ollama | MTEB(영어) |
|---|---|---|---|---|---|
| OpenAI text-embedding-3-large | △ | 3072 | ❌ | ❌ | 최상위 |
| OpenAI text-embedding-3-small | △ | 1536 | ❌ | ❌ | 상위 |
| SBERT (multilingual) | △ | 768 | ⭕ | ❌ | 중위 |
| BGE-large-en-v1.5 | ❌ | 1024 | ⭕ | ⭕ | 상위 |
| BGE-M3 | ⭕ | 1024 | ⭕ | ⭕ | 상위 |
| multilingual-e5-large | ⭕ | 1024 | ⭕ | ❌ | 상위 |
| Nomic Embed v1.5 | △ | 768 | ⭕ | ⭕ | 중상위 |
△는 지원은 하지만 품질이 제한적임을 의미한다.
이 표를 기준으로 보면, 한국어 + 로컬 설정 + Ollama 지원을 동시에 만족하는 모델은 BGE-M3가 사실상 유일하다.
BGE-M3는 2024년 BAAI(Beijing Academy of Artificial Intelligence)에서 발표한 임베딩 모델이다.
이름의 M3는 세 가지 특징의 앞글자를 딴 것으로, 각각이 기존 임베딩 모델의 한계를 하나씩 해결하는 방향으로 설계되어 있다.
100개 이상의 언어를 단일 모델로 처리한다. 언어별로 별도 모델을 관리할 필요 없이, 한국어 문서와 영어 쿼리를 같은 벡터 공간에 매핑할 수 있다.
단순히 "한국어 포함"이 아니라, 한국어가 충분한 비중으로 학습 데이터에 포함되어 있어 실제 한국어 텍스트에서도 의미를 제대로 포착한다. MTEB Multilingual 리더보드에서도 상위권을 유지하고 있다.
BGE-M3의 가장 독특한 특징이다. 일반적으로 검색 방식은 세 가지로 나뉜다.
"자연주의 뷰티 크리에이터" → [0.12, -0.83, 0.45, ...] (1024차원 벡터 1개) 계산이 빠르고 벡터 스토리지 구조와 잘 맞아서 대부분의 시맨틱 검색에서 기본으로 쓰인다."자연주의 뷰티 크리에이터"
→ {"자연주의": 0.82, "뷰티": 0.91, "크리에이터": 0.64, ...} 특정 키워드가 결과에 반드시 포함되어야 하는 경우에 강점을 가진다."자연주의 뷰티 크리에이터"
→ [벡터_자연주의, 벡터_뷰티, 벡터_크리에이터, ...] (토큰 수만큼 벡터 생성) 쿼리와 문서의 각 토큰 벡터를 세밀하게 비교하기 때문에 정확도가 가장 높다.세 방식 각각의 트레이드오프를 정리하면 다음과 같다.
| 방식 | 정확도 | 속도 | 스토리지 | 특징 |
|---|---|---|---|---|
| Dense | 중상 | 빠름 | 낮음 | 의미적 유사도 |
| Sparse | 중 | 빠름 | 낮음 | 키워드 정확 매칭 |
| ColBERT | 높음 | 느림 | 높음 | 토큰 레벨 세밀 비교 |
BGE-M3는 이 세 방식을 하나의 모델에서 모두 출력할 수 있다.
상황에 따라 Dense만 쓰거나, Dense + Sparse를 결합한 하이브리드 검색을 구성하는 것도 가능하다.
입력 텍스트 길이에 제한이 거의 없다. 최대 8192 토큰을 처리할 수 있어, 짧은 키워드부터 긴 문서까지 동일한 모델로 임베딩할 수 있다.
기존 BERT 계열 모델은 대부분 512 토큰 제한이 있어서 긴 문서는 잘라서 처리하거나 별도 전략이 필요했다. BGE-M3는 이 제약에서 자유롭다.
이 프로젝트에서 임베딩하는 텍스트는 한국어 인플루언서 소개문이고, 광고주가 입력하는 쿼리도 한국어이다.
쿼리: "친환경 화장품 캠페인에 적합한 자연주의 뷰티 인플루언서"
문서: "비건 라이프스타일과 자연주의 스킨케어 루틴을 공유하는 크리에이터"
영어 전용 모델은 이 구조에서 의미를 제대로 포착하지 못한다. BGE-large-en-v1.5는 MTEB 영어 벤치마크 기준으로 성능이 뛰어나지만, 한국어를 지원하지 않아 후보에서 바로 제외됐다.
이 단계에서 다국어 지원 모델로 범위가 좁혀진다.
이 프로젝트는 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 지원 모델로 범위가 좁혀졌다.
한국어 지원 + Ollama 실행 가능이라는 조건을 동시에 만족하는 모델은 선택지가 많지 않았다. 이 조건을 통과한 후보들 중 MTEB Multilingual 기준 성능을 비교했을 때 BGE-M3이 가장 상위권이었다.
추가로 다음 두 가지가 최종 결정에 영향을 줬다.
1024차원의 적절한 균형
고품질 임베딩을 위한 충분한 차원 수를 갖추면서도, 3072차원의 OpenAI large 모델처럼 스토리지와 검색 속도에 부담을 주지 않는다.
Neo4j Vector Index와의 연동에서도 무리 없이 작동한다.
MIT 라이선스
상업적 사용에 제약이 없다. 오픈소스 프로젝트나 상용 서비스로 확장할 때 라이선스 문제를 고려하지 않아도 된다.
결국 한국어 지원 + 로컬 실행 + Ollama 지원 조건을 통과한 후보들 중, MTEB Multilingual 기준 성능을 비교했을 때 BGE-M3가 가장 상위권이었고, 이 프로젝트의 인프라 구조와도 자연스럽게 맞아떨어졌다.
BGE-M3를 선택한 뒤, 한 가지 추가 실험을 진행했다. 인플루언서 소개문 앞에 전문분야를 명시하는 접두어를 붙이는 것이 임베딩 품질에 실제로 영향을 주는지 확인하고 싶었다.
인플루언서 소개문은 대부분 자연어로 작성되어 있다.
"헬스 트레이너 출신 피트니스 유튜버로 운동 루틴과 식단을 공유합니다"
이 텍스트만으로도 "피트니스" 도메인임을 파악할 수 있지만, 모델 입장에서는 문장 전체의 평균적인 의미를 하나의 벡터로 압축해야 한다.
도메인 키워드가 문장 중간에 묻혀 있으면, 벡터가 그 방향으로 충분히 치우치지 않을 수 있다.
그래서 다음과 같이 접두어를 앞에 명시하면 어떨까라는 아이디어를 실행했다.
"[전문분야: 피트니스] 헬스 트레이너 출신 피트니스 유튜버로 운동 루틴과 식단을 공유합니다"
5개 카테고리(뷰티, 테크, 피트니스, 음식, 여행)에 대해 쿼리와 인플루언서 소개문을 각각 준비했다.
# test_prefix_embedding.py
QUERIES = {
"뷰티": "20대 여성 대상 스킨케어 제품 광고",
"테크": "신형 스마트폰 리뷰 및 언박싱 콘텐츠",
"피트니스": "헬스보충제 홍보 및 운동 루틴 캠페인",
"음식": "신메뉴 출시 맛집 인플루언서 협업",
"여행": "항공사 여행 패키지 프로모션",
}
각 인플루언서 소개문에 대해 접두어 있는 버전과 없는 버전을 모두 임베딩한 뒤, 5개 쿼리 벡터 각각과의 코사인 유사도를 계산했다.
측정 지표는 두 가지이다.
접두어가 유효하다면 일치 쿼리 효과 > 비일치 효과 평균이 되어야 한다.
즉, 같은 도메인 쿼리와는 더 가까워지고, 다른 도메인 쿼리와는 큰 변화가 없어야 한다.
결론부터 말하면, 접두어는 효과가 없었다.
25개 조합(5개 카테고리 쿼리 × 5개 인플루언서) 중 21개에서 유사도가 오히려 하락했다. 상승한 4개도 +0.0006~0.0036 수준으로 노이즈에 가까웠다.
가장 심각한 케이스는 테크 인플루언서였다. 같은 도메인 쿼리("신형 스마트폰 리뷰 및 언박싱 콘텐츠")와의 유사도가 -0.0213 하락했는데, 다른 도메인 쿼리들의 평균 하락폭(-0.0053)보다 4배 컸다.
즉, 접두어가 도메인 방향을 강화하기는커녕 오히려 역효과를 낸 것이다.
실험 내용을 더 자세하게 보고 싶다면, 이 글을 참고하면 된다.
원인은 BGE-M3의 구조적 특성에 있다.
양방향 어텐션 구조
BGE-M3는 모든 토큰을 균등하게 처리한다. 앞에 배치한 접두어가 특별히 더 강조되는 구조가 아니기 때문에, 위치 기반으로 도메인 방향을 강화하려는 시도 자체가 맞지 않았다.
정보 중복으로 인한 어텐션 분산
소개문 본문에 이미 카테고리 정보가 포함되어 있는데, 접두어로 동일한 내용을 앞에 한 번 더 추가하면 어텐션이 분산된다. 오히려 정보 밀도가 낮아지는 효과가 생긴다.
학습 분포 이탈
[전문분야: 피트니스] 같은 비자연어 형식은 BGE-M3가 학습한 텍스트 분포와 다르다. 모델이 익숙하지 않은 형식의 입력에서 벡터 품질이 저하됐을 가능성이 있다.
쿼리-문서 비대칭
광고주 쿼리는 자연어로 입력된다. 반면 인플루언서 문서에는 인공적인 접두어가 붙어 있다. 두 텍스트의 형식이 달라지면 같은 의미 공간에서 비교가 어려워진다.
접두어는 벡터 품질을 높이지 못했고, 오히려 저하시켰다. 최종적으로 접두어를 제거하고 자연어 소개문 그대로 임베딩하는 방식으로 돌아왔다.
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이 최종 필터 역할을 했고, 두 단계를 조합한 구조 자체가 시스템의 명확도를 보장하고 있었던 것이다.
BGE-M3로 생성한 임베딩 벡터를 실제 검색에 활용하려면 두 가지가 필요하다. 벡터를 저장하고 검색할 스토리지와, 임베딩 생성부터 검색까지의 파이프라인을 연결해주는 프레임워크이다.
이 프로젝트에서는 각각 Neo4j Vector Index와 LlamaIndex를 사용했다.
LlamaIndex가 임베딩 모델 설정, 문서 처리, 인덱스 빌드를 담당하고, Neo4j가 실제 벡터 데이터를 저장하고 유사도 쿼리를 실행한다.
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로 고정된다.
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은 인덱스 생성 시 한 번 고정된다. 나중에 모델을 바꿔 차원이 달라지면 인덱스를 초기화하고 다시 빌드해야 한다.
인플루언서 데이터를 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차원 벡터가 저장된다.
검색 시에는 쿼리 텍스트를 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 레이어는 그대로 유지할 수 있다.