기타 - Hybrid RAG
text-embedding-004로 쿼리 임베딩 생성market_prices 컬렉션에 대해 vector similarity 검색만 수행distance(코사인 거리)에 따라 상위 top_k 결과를 그대로 사용top_k * 3 개의 후보 문서 가져오기vector_score = 1 - distancelexical_score = 쿼리 토큰과 문서/메타데이터 토큰의 겹침 비율hybrid_score = 0.6 * vector_score + 0.4 * lexical_scorehybrid_score 기준으로 정렬 후 상위 top_k만 최종 결과로 사용top_k * 3)를 늘려서 한 번 더 파이썬 레벨에서 재랭킹하므로 약간의 CPU 비용 증가candidate_k를 너무 키우면 속도에 영향이 있을 수 있음 (현재는 3배 수준으로 안전한 범위)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
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
Before (Pure Vector)
text-embedding-004로 쿼리 임베딩 생성market_prices 컬렉션에 대해 vector similarity 검색만 수행distance(코사인 거리)에 따라 상위 top_k 결과를 그대로 사용top_k * 3 개의 후보 문서 가져오기vector_score = 1 - distancelexical_score = 쿼리 토큰과 문서/메타데이터 토큰의 겹침 비율hybrid_score = 0.6 * vector_score + 0.4 * lexical_scorehybrid_score 기준으로 정렬 후 상위 top_k만 최종 결과로 사용top_k * 3)를 늘려서 한 번 더 파이썬 레벨에서 재랭킹하므로 약간의 CPU 비용 증가candidate_k를 너무 키우면 속도에 영향이 있을 수 있음 (현재는 3배 수준으로 안전한 범위)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
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
Before (Pure Vector)