Hybrid RAG

나는컴공생·2026년 3월 10일

Dealight

목록 보기
1/1

기타 - Hybrid RAG

Hybrid RAG 도입 전/후 비교


1. 개념적 비교

Before: Pure Vector RAG

  • 검색 방식
    • Gemini text-embedding-004로 쿼리 임베딩 생성
    • ChromaDB market_prices 컬렉션에 대해 vector similarity 검색만 수행
    • distance(코사인 거리)에 따라 상위 top_k 결과를 그대로 사용
  • 장점
    • 의미 기반 유사도 검색에 강함
    • 임베딩 모델만 잘 학습되어 있으면 자연어 쿼리에 유연하게 대응
  • 단점
    • 숫자, 브랜드명, 기능명 같은 정확 키워드 매칭에 약함
    • 짧은 텍스트/희소 데이터에서 리콜이 떨어질 수 있음
    • 쿼리와 같은 단어를 포함하지 않아도 임베딩이 비슷하면 상위에 오를 수 있음

After: Hybrid RAG (Vector + Lexical Re-ranking)

  • 검색 방식
    1. 기존처럼 벡터 검색으로 top_k * 3 개의 후보 문서 가져오기
    2. 각 후보에 대해:
      • vector_score = 1 - distance
      • lexical_score = 쿼리 토큰과 문서/메타데이터 토큰의 겹침 비율
      • hybrid_score = 0.6 * vector_score + 0.4 * lexical_score
    3. hybrid_score 기준으로 정렬 후 상위 top_k만 최종 결과로 사용
  • 장점
    • 의미 기반 검색(벡터) + 정확 키워드 매칭(lexical)을 동시에 활용
    • 가격/페이지 수/브랜드명/기능명 등이 쿼리에 포함될 때 정확하게 매칭되는 문서에 가중치 부여
    • 짧은 설명이나 희소 데이터에서도 키워드 매칭으로 리콜 보강
  • 단점/Trade-off
    • 후보 수(top_k * 3)를 늘려서 한 번 더 파이썬 레벨에서 재랭킹하므로 약간의 CPU 비용 증가
    • 아주 큰 컬렉션에서 candidate_k를 너무 키우면 속도에 영향이 있을 수 있음 (현재는 3배 수준으로 안전한 범위)

2. 코드 레벨 비교

Before: search_similar_deals (순수 벡터)

def search_similar_deals(self, query: str, category: Optional[str] = None, top_k: int = 5):
    # 1. 쿼리 임베딩 생성
    query_embedding = self.create_embedding(query)
    
    # 2. ChromaDB 검색 (카테고리 필터 적용)
    where_filter = {"category": category} if category else None
    
    results = self.collection.query(
        query_embeddings=[query_embedding],
        n_results=top_k,
        where=where_filter
    )
    
    # 3. 결과 포맷팅
    similar_deals = []
    if results['ids'] and len(results['ids'][0]) > 0:
        for i in range(len(results['ids'][0])):
            deal = {
                "id": results['ids'][0][i],
                "metadata": results['metadatas'][0][i],
                "distance": results['distances'][0][i],
                "document": results['documents'][0][i]
            }
            similar_deals.append(deal)
    
    return similar_deals

After: search_similar_deals (Hybrid: Vector + Lexical)

핵심 변화 포인트만 요약:

def search_similar_deals(self, query: str, category: Optional[str] = None, top_k: int = 5):
    # 1. 쿼리 임베딩 생성
    query_embedding = self.create_embedding(query)
    
    # 2. 1차 벡터 검색 (후속 재랭킹을 위해 top_k 보다 넉넉하게 가져옴)
    where_filter = {"category": category} if category else None
    candidate_k = max(top_k * 3, top_k)
    
    results = self.collection.query(
        query_embeddings=[query_embedding],
        n_results=candidate_k,
        where=where_filter
    )
    # 3. 질의/문서 토큰화 + lexical score 계산
    def normalize_text(text: str) -> List[str]:
        text = text.lower()
        text = re.sub(r"[^a-z0-9가-힣\s]", " ", text)
        tokens = [t for t in text.split() if len(t) > 1]
        return tokens
    query_tokens = set(normalize_text(query))
    def lexical_score(doc_text: str) -> float:
        if not query_tokens:
            return 0.0
        doc_tokens = set(normalize_text(doc_text))
        if not doc_tokens:
            return 0.0
        overlap = query_tokens & doc_tokens
        return len(overlap) / len(query_tokens)
    # 4. 벡터 스코어 + lexical 스코어 결합
    hybrid_candidates = []
    for i in range(len(ids)):
        ...
        vector_score = 1.0 - float(distance)
        combined_text = f\"{title} {category_text} {document[:1000]}\"
        lex_score = lexical_score(combined_text)
        hybrid_score = 0.6 * vector_score + 0.4 * lex_score
        hybrid_candidates.append({...})
    # 5. hybrid_score 기준 상위 top_k 선택
    hybrid_candidates.sort(key=lambda x: x[\"hybrid_score\"], reverse=True)
    top_candidates = hybrid_candidates[:top_k]
    ...
    return similar_deals

3. 기대 효과 (PPT용 Bullet Point)

Before (Pure Vector)

  • 의미 기반 유사도에는 강하지만,
  • 가격/페이지 수/기능명/브랜드명 같은 정확 키워드가 반영되지 않을 수 있음
  • 짧은 텍스트나 희소한 데이터에서 리콜 부족 문제 발생 가능
  • RAG 실패 시, 오케스트레이션 에이전트가 아무리 잘 짜여 있어도 후보군 자체가 부족해짐
    After (Hybrid Vector + Lexical)
  • 벡터 스코어 + 키워드 스코어를 결합해 더 안정적인 검색 결과 제공
  • 쿼리 내부의 핵심 단어(예: \"WordPress\", \"3 hours\", \"logo\", \"landing page\")가 실제 문서/타이틀에 포함된 경우 우선순위 상승
  • 짧은 설명/희소 데이터에서도 키워드 겹침으로 리콜 보강
  • 오케스트레이션 에이전트가 선택할 수 있는 후보군 품질과 다양성이 상승 → 전체 체인 안정성 증가

4. 측정/비교 아이디어 (보고서용)

  1. Top-k Precision
    • 특정 쿼리 셋(예: “워드프레스 수정 3시간”, “로고 디자인 패키지”, “당근마켓 아이폰 13 프로”)에 대해
    • 사람이 라벨링한 정답 거래들이 top-5 안에 포함되는 비율 비교
  2. 클릭/선택 로그 기반 (실제 사용자 데이터 생긴 후)
    • 제안된 협상안들 중 실제로 선택된 옵션이
    • Hybrid RAG가 추천한 상위 후보에서 얼마나 자주 나오는지 측정
  3. 실패 케이스 분석
    • “관련 거래 없음”으로 떨어졌던 케이스를 다시 돌려보고
    • Hybrid 적용 후 유사 사례가 새로 잡히는 비율 확인

5. 정리 문구 (한 줄 요약)

  • Before: “의미 기반(Vector-only) RAG → 자연어는 잘 잡지만, 키워드/숫자/짧은 텍스트에 약함”
  • After: “Hybrid RAG (Vector + Lexical Re-ranking) → 의미 + 키워드 둘 다 반영해, 오케스트레이션 에이전트가 믿고 쓸 수 있는 후보군 제공”

Hybrid RAG 도입 전/후 비교 정리

보고서/PPT에 바로 쓸 수 있도록, RAG 구조의 Before / After를 정리한 문서입니다.

1. 개념적 비교

Before: Pure Vector RAG

  • 검색 방식
    • Gemini text-embedding-004로 쿼리 임베딩 생성
    • ChromaDB market_prices 컬렉션에 대해 vector similarity 검색만 수행
    • distance(코사인 거리)에 따라 상위 top_k 결과를 그대로 사용
  • 장점
    • 의미 기반 유사도 검색에 강함
    • 임베딩 모델만 잘 학습되어 있으면 자연어 쿼리에 유연하게 대응
  • 단점
    • 숫자, 브랜드명, 기능명 같은 정확 키워드 매칭에 약함
    • 짧은 텍스트/희소 데이터에서 리콜이 떨어질 수 있음
    • 쿼리와 같은 단어를 포함하지 않아도 임베딩이 비슷하면 상위에 오를 수 있음

After: Hybrid RAG (Vector + Lexical Re-ranking)

  • 검색 방식
    1. 기존처럼 벡터 검색으로 top_k * 3 개의 후보 문서 가져오기
    2. 각 후보에 대해:
      • vector_score = 1 - distance
      • lexical_score = 쿼리 토큰과 문서/메타데이터 토큰의 겹침 비율
      • hybrid_score = 0.6 * vector_score + 0.4 * lexical_score
    3. hybrid_score 기준으로 정렬 후 상위 top_k만 최종 결과로 사용
  • 장점
    • 의미 기반 검색(벡터) + 정확 키워드 매칭(lexical)을 동시에 활용
    • 가격/페이지 수/브랜드명/기능명 등이 쿼리에 포함될 때 정확하게 매칭되는 문서에 가중치 부여
    • 짧은 설명이나 희소 데이터에서도 키워드 매칭으로 리콜 보강
  • 단점/Trade-off
    • 후보 수(top_k * 3)를 늘려서 한 번 더 파이썬 레벨에서 재랭킹하므로 약간의 CPU 비용 증가
    • 아주 큰 컬렉션에서 candidate_k를 너무 키우면 속도에 영향이 있을 수 있음 (현재는 3배 수준으로 안전한 범위)

2. 코드 레벨 비교

Before: search_similar_deals (순수 벡터)

def search_similar_deals(self, query: str, category: Optional[str] = None, top_k: int = 5):
    # 1. 쿼리 임베딩 생성
    query_embedding = self.create_embedding(query)
    
    # 2. ChromaDB 검색 (카테고리 필터 적용)
    where_filter = {"category": category} if category else None
    
    results = self.collection.query(
        query_embeddings=[query_embedding],
        n_results=top_k,
        where=where_filter
    )
    
    # 3. 결과 포맷팅
    similar_deals = []
    if results['ids'] and len(results['ids'][0]) > 0:
        for i in range(len(results['ids'][0])):
            deal = {
                "id": results['ids'][0][i],
                "metadata": results['metadatas'][0][i],
                "distance": results['distances'][0][i],
                "document": results['documents'][0][i]
            }
            similar_deals.append(deal)
    
    return similar_deals

After: search_similar_deals (Hybrid: Vector + Lexical)

핵심 변화 포인트만 요약:

def search_similar_deals(self, query: str, category: Optional[str] = None, top_k: int = 5):
    # 1. 쿼리 임베딩 생성
    query_embedding = self.create_embedding(query)
    
    # 2. 1차 벡터 검색 (후속 재랭킹을 위해 top_k 보다 넉넉하게 가져옴)
    where_filter = {"category": category} if category else None
    candidate_k = max(top_k * 3, top_k)
    
    results = self.collection.query(
        query_embeddings=[query_embedding],
        n_results=candidate_k,
        where=where_filter
    )
    # 3. 질의/문서 토큰화 + lexical score 계산
    def normalize_text(text: str) -> List[str]:
        text = text.lower()
        text = re.sub(r"[^a-z0-9가-힣\s]", " ", text)
        tokens = [t for t in text.split() if len(t) > 1]
        return tokens
    query_tokens = set(normalize_text(query))
    def lexical_score(doc_text: str) -> float:
        if not query_tokens:
            return 0.0
        doc_tokens = set(normalize_text(doc_text))
        if not doc_tokens:
            return 0.0
        overlap = query_tokens & doc_tokens
        return len(overlap) / len(query_tokens)
    # 4. 벡터 스코어 + lexical 스코어 결합
    hybrid_candidates = []
    for i in range(len(ids)):
        ...
        vector_score = 1.0 - float(distance)
        combined_text = f\"{title} {category_text} {document[:1000]}\"
        lex_score = lexical_score(combined_text)
        hybrid_score = 0.6 * vector_score + 0.4 * lex_score
        hybrid_candidates.append({...})
    # 5. hybrid_score 기준 상위 top_k 선택
    hybrid_candidates.sort(key=lambda x: x[\"hybrid_score\"], reverse=True)
    top_candidates = hybrid_candidates[:top_k]
    ...
    return similar_deals

3. 기대 효과 (PPT용 Bullet Point)

Before (Pure Vector)

  • 의미 기반 유사도에는 강하지만,
  • 가격/페이지 수/기능명/브랜드명 같은 정확 키워드가 반영되지 않을 수 있음
  • 짧은 텍스트나 희소한 데이터에서 리콜 부족 문제 발생 가능
  • RAG 실패 시, 오케스트레이션 에이전트가 아무리 잘 짜여 있어도 후보군 자체가 부족해짐
    After (Hybrid Vector + Lexical)
  • 벡터 스코어 + 키워드 스코어를 결합해 더 안정적인 검색 결과 제공
  • 쿼리 내부의 핵심 단어(예: \"WordPress\", \"3 hours\", \"logo\", \"landing page\")가 실제 문서/타이틀에 포함된 경우 우선순위 상승
  • 짧은 설명/희소 데이터에서도 키워드 겹침으로 리콜 보강
  • 오케스트레이션 에이전트가 선택할 수 있는 후보군 품질과 다양성이 상승 → 전체 체인 안정성 증가

4. 측정/비교 아이디어 (보고서용)

  1. Top-k Precision
    • 특정 쿼리 셋(예: “워드프레스 수정 3시간”, “로고 디자인 패키지”, “당근마켓 아이폰 13 프로”)에 대해
    • 사람이 라벨링한 정답 거래들이 top-5 안에 포함되는 비율 비교
  2. 클릭/선택 로그 기반 (실제 사용자 데이터 생긴 후)
    • 제안된 협상안들 중 실제로 선택된 옵션이
    • Hybrid RAG가 추천한 상위 후보에서 얼마나 자주 나오는지 측정
  3. 실패 케이스 분석
    • “관련 거래 없음”으로 떨어졌던 케이스를 다시 돌려보고
    • Hybrid 적용 후 유사 사례가 새로 잡히는 비율 확인

5. 정리 문구 (한 줄 요약)

  • Before: “의미 기반(Vector-only) RAG → 자연어는 잘 잡지만, 키워드/숫자/짧은 텍스트에 약함”
  • After: “Hybrid RAG (Vector + Lexical Re-ranking) → 의미 + 키워드 둘 다 반영해, 오케스트레이션 에이전트가 믿고 쓸 수 있는 후보군 제공”

0개의 댓글