엘라스틱서치 벡터 검색: 키워드에서 의미까지 – 의미 기반 검색 및 RAG 파이프라인 구축

발행: (2026년 6월 18일 AM 12:40 GMT+9)
10 분 소요
원문: Dev.to

출처: Dev.to

문서 검색창에 “k8s 배포 문제 해결”을 입력하면 상위 결과로는 Kubernetes 아키텍처를 다룬 페이지가 나타나지만, “troubleshooting”이라는 단어가 전혀 포함되지 않습니다. 정확히 필요한 내용입니다. BM25는 이를 완전히 놓칩니다.

벡터 검색의 약속은 단어를 단순히 매칭하는 것이 아니라 의미 기반으로 문서를 찾는 것입니다. 2025년과 2026년에는 벡터 검색이 특수 ML 엔지니어링에서 핵심 Elasticsearch 기능으로 전환되었습니다. AI 애플리케이션을 구축 중이라면 RAG 파이프라인, 의미 기반 Q&A, 추천 시스템 등 이해가 필수적입니다.

저는 지난 1년간 Cloudera에서 RAG 파이프라인을 구축하며 벡터 검색이 강력하지만 오용하기 쉽다는 것을 배웠습니다. 이 포스트에서는 작동하는 방법, 그렇지 않은 방법, 그리고 프로덕션 환경에 어떻게 구현할지 다룹니다.

벡터 검색이 왜 중요한가 (그리고 언제 필요하지 않은가)

이전 포스트에서 다룬 BM25는 정확한 용어 매칭에 뛰어나지만, 근본적으로 사전적(lexical)입니다. 다음과 같은 점들을 이해하지 못합니다:

  • “k8s” 와 “kubernetes”는 같은 것입니다
  • “docker container” 와 “containerization”은 관련 개념입니다
  • “out of memory error” 와 “heap exhaustion”은 동일한 문제를 설명합니다

벡터 검색은 텍스트를 고차원 수치 벡터(임베딩)로 변환하여 의미가 유사한 내용이 벡터 공간 내에서 서로 가깝게 위치하도록 합니다. “k8s 배포 문제 해결” 쿼리는 벡터로 변환되고, Elasticsearch는 키워드가 전혀 공유되지 않더라도 가장 가까운 문서 벡터를 찾아냅니다.

하지만 벡터 검색은 BM25의 대체가 아니라 보완입니다.

BM25는 빠르고, ML 인프라 없이 동작하며, 정확한 용어 매칭에 특화되어 있습니다. 벡터 검색은 느리고, 임베딩 모델이 필요하며, 개념적 유사성에 강점합니다. 2026년의 최고의 검색 시스템은 두 기술을 함께 사용합니다.

Elasticsearch에서 벡터 저장 및 인덱싱 방법

Elasticsearch는 7.x 버전에서 dense_vector 필드 타입을 도입했으며, 8.x를 거쳐 2026년까지 크게 개선했습니다. 작동 원리는 다음과 같습니다.

dense_vector 필드 타입

A dense vector는 단순히 부동소수점 숫자 배열입니다. E5와 같은 모델에서 768차원 임베딩 예시는 다음과 같습니다:

[0.023, -0.156, 0.089, ..., 0.041]   // 768 numbers total

인덱스 매핑에 다음과 같이 작성합니다:

PUT /products
{
   "mappings": {
     "properties": {
       "name": { "type": "text" },
       "description": { "type": "text" },
       "description_vector": {
         "type": "dense_vector",
         "dims": 768,
         "index": true,
         "similarity": "cosine"
       }
     }
   }
}

주요 파라미터:

  • dims: 벡터 차원 (임베딩 모델과 일치해야 함)
  • index: ANN 인덱스를 구축할지 여부 (true는 검색용, false는 저장 전용)
  • similarity: 거리 측정 지표 – l2_norm(유клид 거리), dot_product, 또는 cosine

HNSW: approximate search를 위한 알고리즘

Elasticsearch는 HNSW(階層的可導航小世界)를 ANN(Approximate Nearest Neighbor) 검색에 사용합니다. HNSW는 다층 그래프를 구축하며:

  • 레이어 0은 모든 벡터가 밀집한 로컬 연결을 갖춥니다
  • 상위 레이어는 더 긴 거리 연결을 가진 일부 벡터를 포함합니다
  • 검색은 최상위 레이어에서 시작해 탐욕적으로 하향 이동하고, 이후 계층 0에서 로컬로 검색합니다

HNSW는 빠르며(백만 개 벡터 인덱스 기준 10ms 미만)이지만 근사적입니다. 속도를 위해 진정한 최寄邻을 놓칠 수 있습니다.

이 트레이드오프를 조정할 수 있습니다:

PUT /products
{
   "mappings": {
     "properties": {
       "description_vector": {
         "type": "dense_vector",
         "dims": 768,
         "index": true,
         "similarity": "cosine",
         "index_options": {
           "type": "hnsw",
           "m": 16,
           "ef_construction": 100
         }
       }
     }
   }
}
  • m: 노드당 이중 방향 링크 수 (높을수록 정확하지만 메모리 사용량 증가)
  • ef_construction: 인덱스 구축 시 검색 깊이 (높을수록 그래프 품질 향상, 인덱싱 속도 저하)

쿼리 시간 정확도 튜닝에는 num_candidates를 사용합니다:

GET /products/_search
{
   "knn": {
     "field": "description_vector",
     "query_vector": [0.023, -0.156, ...],
     "k": 10,
     "num_candidates": 100
   }
}
  • num_candidates는 Elasticsearch가 상위 k를 반환하기 전에 고려하는 벡터 수입니다. 값이 클수록 회상률(recall)이 향상되지만 지연 시간이 증가합니다. 일반적인 규칙: 좋은 회상을 위해 num_candidatesk의 10배여야 합니다.

Elasticsearch와 함께 RAG 파이프라인 구축

Retrieval-Augmented Generation(RAG)은 LLM을 개인 데이터에 고정하기 위한 주도적인 아키텍처입니다. 파이프라인은 다음과 같습니다:

문서 → 청크 → 임베딩 → 인덱스 → 쿼리 → 검색 → 생성

Step 1: 문서 청크화

LLM은 컨텍스트 제한이 있으므로 긴 문서는 청크로 나누어야 합니다. 일반적인 접근 방식은 다음과 같습니다:

from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=50,
    separators=["\n\n", "\n", ". ", "  ", ""]
)
chunks = text_splitter.split_text(long_document)

청크 크기는 사용 중인 임베딩 모델에 따라 다릅니다. E5와 BGE 모델은 보통 512 토큰을 사용합니다. OpenAI text-embedding-3-large는 최대 8192 토큰까지 지원합니다.

Step 2: 임베딩 생성

필요한 임베딩 모델이 있습니다. 2026년 옵션은 다음과 같습니다:

ModelTypeDimensionsBest For
ELSER v2Sparse (learned)~2000 terms내장형, 외부 서비스 필요 없음
multilingual-e5-largeDense1024다국어 지원, 고품질
BGE-large-en-v1.5Dense1024오픈 소스, 경쟁력 있음
OpenAI text-embedding-3-largeDense3072최고 품질, API 비용

중요: dot_product 유사성을 사용하는 경우 normalize_embeddings=True를 사용하세요. Elasticsearch는 내부적으로 정규화 단계를 건너뛰어 검색을 빠르게 할 수 있습니다.

Step 3: 벡터와 함께 문서 인덱싱

POST /_bulk
{  "index": { "_index": "knowledge_base", "_id": "doc_1_chunk_0" } 
{ "content": "To troubleshoot Kubernetes deployments...", "source_doc": "k8s_guide.pdf", "category": "devops", "content_vector": [0.023, -0.156, ...] }

Step 4: 검색

GET /knowledge_base/_search
{
   "knn": {
     "field": "content_vector",
     "query_vector": [0.041, 0.089, ...],
     "k": 5,
     "num_candidates": 50
   }
}

쿼리 벡터는 사용자 질문인 “Kubernetes 배포가 시작되지 않는 오류를 어떻게 수정하나요?”의 임베딩입니다.

하이브리드 검색: 프로덕션 준비된 접근법

순수 벡터 검색은 정확한 매칭을 놓치는 문제가 있습니다. 사용자가 “오류 코드 503”을 검색하면, 벡터 검색은 일반 서버 오류에 대한 문서를 반환할 수 있지만, HTTP 503에 대한 구체적인 문제 해결 페이지를 놓칠 수 있습니다.

해결책은 하이브리드 검색입니다: BM25와 kNN을 병렬로 실행한 뒤 결과를 합치면 됩니다.

Elasticsearch 8.15+는 이 목적을 위해 retrievers API를 제공합니다:

GET /knowledge_base/_search
{
   "retriever": {
     "rrf": {
       "retrievers": [
         {
           "standard": {
             "query": {
               "multi_match": {
                 "query": "k8s deployment troubleshooting",
                 "fields": ["content", "title"]

0 조회
Back to Blog

관련 글

더 보기 »