JUST WRITE

[Embedding] Hybrid Search - Qdrant 구성 본문

AI

[Embedding] Hybrid Search - Qdrant 구성

천재보단범재 2025. 7. 12. 14:23

임베딩 Hybrid Search

Hybrid Search 구성

RAG와 LLM을 활용한 자동분류 서비스를 구성하고 있습니다.

Milvus에 Dense 임베딩 값을 넣어서 RAG를 구성하였지만 성능이 좋지 않았습니다.

성능 보완을 위해 Dense 임베딩뿐만 아니라 Sparse 임베딩도 넣어서 성능을 보완해보려고 합니다.

Sparse 임베딩과 Dense 임베딩이 무엇인지는 아래 포스팅을 참고하시길 바랍니다.

차이점 중심으로 비교해 보았습니다. 

 

[Embedding] Sparse vs Dense 임베딩

Sparse vs Dense 임베딩프로젝트에 투입해서 RAG와 LLM을 활용한 자동 분류 서비스를 개발하였습니다.처음에는 다국어를 지원하는 paraphrase-multilingual-mpnet-base-v2 임베딩 모델로 임베딩을 생성해서Milvus

developnote-blog.tistory.com

Dense 임베딩과 Sparse 임베딩을 활용하려면 Hybrid Search 방식으로 유사 문서를 검색해야 합니다.

두 가지 임베딩 활용을 위한 Hybrid Search 구성을 진행하려고 합니다.

Hybrid Search란?

저번 포스팅에서 Dense 임베딩과 Sparse 임베딩을 비교해 보았습니다.

2가지 임베딩 방식은 각각 장단점이 있습니다.

  • Dense 임베딩 -> 의미적 유사성은 잘 찾지만, 정확한 키워드 매칭이 약함.
  • Sparse 임베딩 -> 키워드 매칭은 정확하지만, 동의어나 유의어 처리가 어려움.

Hybrid Search는 두 가지 임베딩 방식의 장점을 보완하기 위해 포괄적으로 검색하는 방식입니다.

Hybrid Search의 작동 원리

사용자 질문: "차량 구매 방법이 궁금해"

1. Dense 검색 결과:
   - "자동차 매입 절차 안내" (점수: 0.85)
   - "차량 구매 가이드" (점수: 0.82)
   - "자동차 선택 팁" (점수: 0.78)

2. Sparse 검색 결과:
   - "차량 구매 가이드" (점수: 0.92)
   - "구매 방법 총정리" (점수: 0.88)
   - "차량 관련 정보" (점수: 0.85)

3. Hybrid 최종 결과 (가중평균):
   - "차량 구매 가이드" (Dense: 0.82, Sparse: 0.92 → 최종: 0.87)
   - "자동차 매입 절차 안내" (Dense: 0.85, Sparse: 0.0 → 최종: 0.60)
   - "구매 방법 총정리" (Dense: 0.0, Sparse: 0.88 → 최종: 0.26)

 

위 예시처럼 Dense와 Sparse 임베딩에서 검색하는 유사 문서를 종합적으로 판단해서 유사도를 검색합니다.

아래와 같은 장점을 보입니다.

  • 포괄적 검색 -> 키워드와 의미를 모두 고려
  • 정확도 향상 -> 두 방식의 약점을 서로 보완
  • 사용자 경험 개선 -> 어떤 방식으로 질문해도 적절한 결과
  • 도메인 적응성 -> 일반적인 질문부터 전문적인 질문까지 대응

실제로 한번 Hybrid Search를 구성해 보면서 살펴보겠습니다.

벡터 DB가 필요한데 기존에 Milvus를 사용했지만 이번 포스팅에서는 Qdrant로 진행해보려고 합니다.

Qdrant란?

QdrantRust로 개발된 고성능 벡터 데이터베이스입니다.

Qdrant는 네이티브 Hybrid Search를 지원합니다.

Docker 이미지를 제공해서 쉽게 로컬에서 구성할 수 있습니다.

이러한 이유를 빠르게 Hybrid Search에 대해 알 수 있어 Qdrant로 진행해보려고 합니다.

이 밖에도 Qdrant에는 장점이 많습니다.

  • 네이티브 Hybrid Search 지원
  • Rust로 개발되어 메모리 효율적
  • Docker로 쉬운 구성 가능
  • Qdrant Cloud 서비스 이용
  • Python Client 제공
  • 자체 Web UI 제공

Qdrant 아키텍처

Qdrant 구성

Qdrant는 공식적으로 Docker 이미지를 제공하고 있습니다.

GPU에서 실행되는 이미지를 따로 제공하고 있으니 참고하시길 바랍니다.

이번 포스팅에서는 Docker를 통해서 Qdrant를 구성하도록 하겠습니다.

아래와 같이 docker compose 파일을 구성하였습니다.

version: '3.8'
services:
  qdrant:
    image: qdrant/qdrant:v1.14.1
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - /***/volumes/qdrant:/qdrant/storage:z
    environment:
      - QDRANT__SERVICE__HTTP_PORT=6333
      - QDRANT__SERVICE__GRPC_PORT=6334
      - QDRANT__LOG_LEVEL=INFO
      - RUST_LOG=qdrant=info    # Rust 로깅 레벨 설정
    restart: unless-stopped

아래와 같이 docker compose로 실행하면 로그를 확인할 수 있습니다.

그리고 http://localhost:6333/dashboard로 들어가면 Qdrant Web UI를 확인할 수 있습니다.

$ docker compose up -d
$ docker ps
CONTAINER ID   IMAGE                   COMMAND             CREATED         STATUS         PORTS                              NAMES
fede6b49a583   qdrant/qdrant:v1.14.1   "./entrypoint.sh"   6 seconds ago   Up 5 seconds   0.0.0.0:6333-6334->6333-6334/tcp   ai_test-qdrant-1

$ docker logs fede6b49a583
           _                 _
  __ _  __| |_ __ __ _ _ __ | |_
 / _` |/ _` | '__/ _` | '_ \| __|
| (_| | (_| | | | (_| | | | | |_
 \__, |\__,_|_|  \__,_|_| |_|\__|
    |_|

Version: 1.14.1, build: 530430fa
Access web UI at http://localhost:6333/dashboard

2025-07-12T06:44:15.928608Z  INFO storage::content_manager::consensus::persistent: Initializing new raft state at ./storage/raft_state.json
2025-07-12T06:44:15.971620Z  INFO qdrant: Distributed mode disabled
2025-07-12T06:44:15.971711Z  INFO qdrant: Telemetry reporting enabled, id: 3f6e77f1-b845-40bd-b911-54981f4ab3bf
2025-07-12T06:44:15.971822Z  INFO qdrant: Inference service is not configured.
2025-07-12T06:44:15.972711Z  INFO qdrant::actix: TLS disabled for REST API
2025-07-12T06:44:15.972812Z  INFO qdrant::actix: Qdrant HTTP listening on 6333
2025-07-12T06:44:15.972837Z  INFO actix_server::builder: starting 3 workers
2025-07-12T06:44:15.972841Z  INFO actix_server::server: Actix runtime found; starting in Actix runtime
2025-07-12T06:44:15.972864Z  INFO actix_server::server: starting service: "actix-web-service-0.0.0.0:6333", workers: 3, listening on: 0.0.0.0:6333
2025-07-12T06:44:15.975946Z  INFO qdrant::tonic: Qdrant gRPC listening on 6334
2025-07-12T06:44:15.975997Z  INFO qdrant::tonic: TLS disabled for gRPC API

Qdrant WEB UI

fastembed

FastEmbed는 Qdrant에서 개발한 경량화된 임베딩 생성 라이브러리입니다.

FastEmbed는 세 가지 장점이 있습니다.

첫 번째 장점은 경량화입니다.

ONNX Runtime을 활용하여 임베딩 모델을 경량화해서 사용할 수 있게 하였습니다.

# 기존 방식 (Sentence Transformers)
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-small-en-v1.5')
# → PyTorch 의존성으로 인한 수GB 다운로드
# → GPU 메모리 많이 사용

# FastEmbed 방식
from fastembed import TextEmbedding
model = TextEmbedding("BAAI/bge-small-en-v1.5")
# → ONNX Runtime 사용으로 경량화
# → CPU만으로도 실용적 성능

두 번째 장점은 속도입니다.

ONNX Runtime을 사용해서 PyTorch보다는 빠르게 추론하여 임베딩을 생성할 수 있습니다.

그리고 메모리를 효율적으로 사용합니다.

세 번째 장점은 다양한 모델 지원입니다.

Dense 임베딩, Sparse 임베딩뿐만 아니라 이미지 임베딩 모델도 지원합니다.

아래 코드를 통해서 fastembed에서 지원하는 Dense 임베딩 모델을 확인할 수 있습니다.

25년 7월 12일 기준 30개를 지원하고 있습니다.

from fastembed import TextEmbedding

supported_text_models = (
    pd.DataFrame(TextEmbedding.list_supported_models())
    .sort_values("size_in_GB")
    .drop(columns=["sources", "model_file", "additional_files"])
    .reset_index(drop=True)
)
supported_text_models

fastembed에서 지원하는 TextEmbedding 모델

그리고 fastembed에서 지원하는 Spare 임베딩 모델도 지원을 합니다.

25년 7월 12일 기준 5개를 지원하고 있습니다.

from fastembed import SparseTextEmbedding

supported_sparse_models = (
    pd.DataFrame(SparseTextEmbedding.list_supported_models())
    .sort_values("size_in_GB")
    .drop(columns=["sources", "model_file", "additional_files"])
    .reset_index(drop=True)
)
supported_sparse_models

fastembed에서 지원하는 SparseEmbedding 모델

이번 포스팅에서 Qdrant와 fastembed를 통해서 Hybrid Search를 구성해 보겠습니다.

임베딩 생성 및 컬렉션 저장

이제 Dense 임베딩과 Sparse 임베딩을 생성해서 Qdrant에 저장해 보겠습니다.

데이터는 파이썬 dataset 라이브러리에 있는 ms_macro 데이터셋으로 활용하겠습니다.

from datasets import load_dataset

dataset = load_dataset("ms_marco", "v1.1", split="train[:1000]")
df = pd.DataFrame(dataset)

dataset의 queries 데이터

해당 데이터에서 Dense 임베딩과 Sparse 임베딩을 만들어보겠습니다.

fastembed 라이브러리로 각각 아래 모델들로 생성하였습니다.

from fastembed import TextEmbedding, SparseTextEmbedding

dense_model = TextEmbedding('sentence-transformers/paraphrase-multilingual-mpnet-base-v2')
dense_embeddings = list(dense_model.embed(texts))

sparse_model = SparseTextEmbedding('Qdrant/bm25')
sparse_embeddings = list(sparse_model.embed(texts))

이렇게 생성한 임베딩들을 바탕으로 Qdrant에 컬렉션을 생성하고 저장합니다.

Qdrant는 파이썬 클라이언트를 제공해서 파이썬을 통해 쉽게 컬렉션을 생성하고 저장할 수 있습니다.

from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, SparseVectorParams, PointStruct, SparseVector

def setup_qdrant_hybrid(passages, dense_embeddings, sparse_embeddings):
    client = QdrantClient('localhost', port=6333)
    collection_name = 'hybrid_test'
    dense_dim = len(dense_embeddings[0])

    if client.collection_exists(collection_name):
        client.delete_collection(collection_name)
    
    # Create collection with both dense and sparse vector configurations
    client.create_collection(
        collection_name = collection_name,
        vectors_config = {
            "dense" : VectorParams(
                size=dense_dim,
                distance=Distance.COSINE
            )
        },
        sparse_vectors_config = {
            "sparse" : SparseVectorParams()
        }
    )
    
    # 데이터 삽입
    points = []
    for i, (passage, dense_emb, sparse_emb) in enumerate(zip(passages, dense_embeddings, sparse_embeddings)):
...
...
...
        point = PointStruct(
            id=i,
            vector={
                "dense": dense_vector,
                "sparse": sparse_vector
            },
            payload={"text": passage}
        )
        points.append(point)
    
    client.upsert(collection_name, points)
    return client

저장 후 Qdrant WEB UI에서 저장된 것을 확인할 수 있습니다.

Hybrid Search

Qdrant에 Dense 임베딩과 Sparse 임베딩을 넣었으면 이제 Hybrid Search를 해보겠습니다.

Qdrant에는 자체 내장 RRF를 가지고 있어서 query 변수에 RRF를 추가해 주면 됩니다.

results = client.query_points(
    collection_name = collection_name,
    query=models.FusionQuery(
        fusion=models.Fusion.RRF  # Reciprocal Rank Fusion
    ),
    prefetch=[
        models.Prefetch(
            query=models.Document(text=query_text, model="sentence-transformers/paraphrase-multilingual-mpnet-base-v2"),
            using="dense",
        ),
        models.Prefetch(
            query=models.Document(text=query_text, model="Qdrant/bm25"),
            using="sparse",
        ),
    ],
    query_filter=None,
    limit=top_k,
)

1가지 텍스트를 비교해 보았습니다.

Dense 임베딩으로만, Sparse 임베딩으로만, Hybrid Search를 했을 경우를 비교해 보았습니다.

단순 비교라 정확하지 않지만 각각 score를 어떻게 내는지 비교해 볼 수 있었습니다.

Comparing search methods for query: 'what is a diabetic'
================================================================================

🔍 DENSE-ONLY SEARCH
------------------------------
Search time: 0.0322s
1. Score: 0.8025
   Text: what is a diabetic kidney...

2. Score: 0.4516
   Text: what does a metabolic acidosis need to reverse the condition...

3. Score: 0.3117
   Text: what is the discriminant of an equation...

4. Score: 0.2867
   Text: why conversion observed in body...

5. Score: 0.2716
   Text: diseases caused by clostridium...


🔍 SPARSE-ONLY SEARCH
------------------------------
Search time: 0.0119s
1. Score: 2.8408
   Text: what is a diabetic kidney...


🔍 HYBRID SEARCH
------------------------------
Search time: 0.0237s
1. Score: 1.0000
   Text: what is a diabetic kidney...

2. Score: 0.3333
   Text: what does a metabolic acidosis need to reverse the condition...

3. Score: 0.2500
   Text: what is the discriminant of an equation...

4. Score: 0.2000
   Text: why conversion observed in body...

5. Score: 0.1667
   Text: diseases caused by clostridium...


📊 PERFORMANCE SUMMARY
------------------------------
Dense search:  0.0322s
Sparse search: 0.0119s
Hybrid search: 0.0237s

정리

이번 포스팅에서는 Vector DB인 Qdrant를 설치하고 거기에 Hybrid Search를 구현해 보았습니다.

Qdrant에서 개발한 fastembed 라이브러리를 활용해서 Dense와 Sparse 임베딩을 생성해 보았습니다.

Qdrant에서 제공하는 네이티브 RRF를 이용하여 Hybrid Search를 해보았습니다.

다음에는 RRF에 대해서 정리해 보면서 Hybrid Search에 대해서 좀 더 정리하겠습니다.

[참고사이트]

728x90
반응형
Comments