임베딩 모델을 활용한 Hybrid Search
제1편 | Elasticsearch 기본 개념 및 RDB 비교
제2편 | nori 분석기로 한국어 인덱스 구축하기
제3편 | RAG를 위한 다양한 검색 쿼리 실습
제4편 | 임베딩 모델을 활용한 Hybrid Search
제5편 | LangChain으로 RAG 구현하기
제3편에서는 Elasticsearch의 전통적인 강점인 BM25 기반 전문 검색을 중심으로, match·bool 쿼리와 스코어링 개념을 통해 “키워드 중심 검색”의 작동 원리를 정리했습니다. 그러나 실제 서비스 환경에서는 사용자의 입력이 항상 명확한 키워드로 표현되지 않으며, 동일한 의미라도 다양한 표현으로 나타나는 경우가 많습니다. 예를 들어 사용자는 “슬픈 장면”, “마음이 무너지는 순간”, “비통함을 느낀 대목”처럼 서로 다른 표현을 사용하지만, 의도는 유사할 수 있습니다. 이런 상황에서는 특정 단어의 포함 여부만으로 결과를 결정하는 검색 방식에 한계가 생깁니다.
이번 4편에서는 이러한 한계를 보완하기 위해 벡터(Embedding) 기반 의미 검색을 다룹니다. 텍스트를 임베딩 모델로 벡터화하면, 문장에 포함된 단어가 다르더라도 의미적으로 유사한 문장을 가까운 벡터로 표현할 수 있습니다. 즉, 검색 단계에서 단순 키워드 매칭을 넘어 사용자의 의도와 문맥을 반영한 검색이 가능해집니다. 이는 최근 LLM 기반 서비스에서 표준 구조로 자리 잡은 RAG(Retrieval-Augmented Generation) 시스템의 품질을 좌우하는 핵심 요소이기도 합니다. RAG에서 검색 결과가 정확할수록, LLM이 생성하는 답변의 신뢰도와 일관성도 함께 상승하기 때문입니다.
본 편에서는 OpenAI 임베딩 모델을 활용해 문장을 벡터로 변환하고, Elasticsearch의
dense_vector 필드에 저장하는 방법을 실습합니다. 또한 사용자의 질의 역시 임베딩한 뒤, 코사인 유사도 기반으로 관련 문장을 검색하는 전체 흐름을 구현해 보겠습니다. 이를 통해 Elasticsearch가 단순한 전문 검색 엔진을 넘어, 의미 기반 검색 계층(Retrieval Layer)으로 확장될 수 있음을 확인하고, 다음 편에서 LangChain으로 RAG를 구현하기 위한 기반을 마련하겠습니다.
이를 통해 사용자의 의도와 문맥을 고려한 검색이 가능해져 RAG 시스템의 정확도 향상이 기대됩니다.
벡터 검색은 텍스트나 이미지 등의 데이터를 고차원 수치 벡터로 변환하고, 그 유사성을 계산하여 의미적으로 관련된 정보를 검색하는 방법입니다. 기존의 키워드 기반 검색과 달리 문맥과 의미를 고려한 검색이 가능합니다.
예를 들어, '형식이 서울에 갔다'는 문장과 '주인공이 수도를 방문했다'는 문장은 키워드가 달라도, 벡터 공간상에서는 가까운 위치에 배치되어 유사한 의미를 가진 문장으로 검색됩니다. 이것이 시맨틱 검색의 강점입니다.
Elasticsearch에서는 dense_vector 필드를 사용하여 벡터를 저장하고, kNN(k-Nearest Neighbors) 등의 알고리즘으로 유사도 검색을 구현합니다.
BM25는 고속이고 정확한 키워드 검색에 적합하지만, 벡터 검색은 사용자의 의도나 문맥을 고려한 유연한 검색이 가능합니다. 실제 업무에서는 대부분의 경우 BM25로 충분한 경우가 많습니다. Elasticsearch에서는 BM25가 기본값으로 사용됩니다.
두 방식 모두 장단점이 있으므로, 아래와 같이 가중치를 부여하여 조합하는 것도 가능합니다.
hybrid_score = 0.5 * BM25_score + 0.5 * Embedding_score
이를 Hybrid Search라고 부릅니다. 단, 파라미터 조정이 필요하며 RAG 전체로 보면 그다지 큰 boost가 되지 않는 경우도 많습니다. 그보다는 데이터베이스 구조를 도메인 지식에 맞게 올바르게 설계하는 것이 검색 품질에 훨씬 큰 영향을 미칩니다.
벡터 검색을 수행하려면 먼저 문장이나 쿼리를 '벡터'로 변환해야 합니다.
아래는 OpenAI의 text-embedding-3-small을 사용하여 벡터를 취득하는 방법입니다.
import openai
import os
openai.api_key = os.getenv('OPENAI_API_KEY')
def get_embedding(text: str) -> list[float]:
response = openai.embeddings.create(
input=text,
지금 바로 작가의 멤버십 구독자가 되어
멤버십 특별 연재 콘텐츠를 모두 만나 보세요.
오직 멤버십 구독자만 볼 수 있는,
이 작가의 특별 연재 콘텐츠