Pinecone과 pgvector의 하이브리드 검색
Pinecone에서는 희소-밀집 색인 모드를 사용하고 pgvector에서는 RRF 병합을 적용한 병렬 쿼리를 사용해 하이브리드 검색을 구성한 뒤, 자신의 데이터셋에서 검색 품질을 벤치마킹합니다.
Pinecone과 pgvector의 하이브리드 검색은(는) CoddyKit의 무료 AI Engineering Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Engineering Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
벡터 데이터베이스의 네이티브 하이브리드 검색
두 개의 별도 인덱스와 RRF 결합을 사용해 하이브리드 검색을 직접 구현할 수도 있지만, 최근에는 프로덕션용 벡터 데이터베이스에서 하나의 쿼리로 희소 검색과 밀집 검색을 처리하는 내장 하이브리드 검색을 제공하는 경우가 점점 늘고 있습니다. Pinecone과 pgvector는 모두 하이브리드 모드를 지원하지만, 아키텍처 선택과 장단점은 서로 다릅니다. 두 가지 방식을 모두 이해하면 인프라에 적합한 도구를 선택할 수 있습니다.
Pinecone 희소-밀집 인덱스 모드
Pinecone은 각 벡터 레코드에 밀집 임베딩(부동 소수점 배열)과 희소 벡터(토큰 ID와 가중치의 사전)를 함께 저장하는 희소-밀집 인덱스를 지원합니다. 쿼리에는 밀집 쿼리 벡터와 희소 쿼리 벡터를 모두 지정할 수 있으며, Pinecone은 가중 선형 결합을 사용해 결과를 병합합니다. 이는 RRF와 다릅니다. Pinecone은 alpha라는 설정 가능한 가중치를 사용해 원시 점수를 결합합니다.
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(api_key='YOUR_API_KEY')
# Create a sparse-dense index
pc.create_index(
name='hybrid-index',
dimension=1536, # dense vector dimension
metric='dotproduct', # must use dotproduct for hybrid
spec=ServerlessSpec(cloud='aws', region='us-east-1'),
)
index = pc.Index('hybrid-index')SPLADE로 희소 벡터 생성하기
Pinecone의 희소-밀집 모드를 사용하려면 각 문서에 대한 희소 벡터를 생성해야 합니다. 가장 효과적인 방법은 SPLADE(Sparse Lexical and Expansion)입니다. SPLADE는 확장 기능이 있는 학습된 희소 표현을 생성하는 신경망 모델로, 텍스트에 실제로 나타나는 단어를 넘어 의미적으로 관련된 용어를 희소 벡터에 추가합니다. 또는 간단한 BM25 용어 가중치를 희소 벡터로 사용할 수도 있습니다.
from transformers import AutoTokenizer, AutoModelForMaskedLM
import torch
# Load SPLADE model
tokenizer = AutoTokenizer.from_pretrained('naver/splade-cocondenser-ensembledistil')
model = AutoModelForMaskedLM.from_pretrained('naver/splade-cocondenser-ensembledistil')
def generate_sparse_vector(text: str) -> dict:
tokens = tokenizer(text, return_tensors='pt', truncation=True, max_length=512)
with torch.no_grad():
output = model(**tokens)
logits = output.logits
# Max-pool over sequence length and apply log1p activation
sparse = torch.max(torch.log1p(torch.relu(logits)), dim=1).values.squeeze()
# Return non-zero indices and values as a sparse dict
nz_indices = sparse.nonzero().squeeze().tolist()
nz_values = sparse[nz_indices].tolist()
return {'indices': nz_indices, 'values': nz_values}Pinecone에 하이브리드 벡터 업서트하기
하이브리드 인덱스의 각 Pinecone 레코드에는 id, 밀집 values 배열, indices와 values가 포함된 sparse_values 사전, 그리고 선택적인 metadata가 저장됩니다. Pinecone의 요청 크기 제한을 지키면서 처리량을 최대화하려면 레코드를 100개 단위로 묶어 업서트하세요.
def upsert_hybrid_docs(index, documents: list[dict], batch_size: int = 100):
records = []
for doc in documents:
dense_vec = embed(doc['text']) # OpenAI embedding
sparse_vec = generate_sparse_vector(doc['text']) # SPLADE
records.append({
'id': doc['id'],
'values': dense_vec,
'sparse_values': sparse_vec,
'metadata': {'text': doc['text'], 'source': doc['source']},
})
for i in range(0, len(records), batch_size):
batch = records[i:i + batch_size]
index.upsert(vectors=batch)
print(f'Upserted batch {i//batch_size + 1}')Pinecone 하이브리드 인덱스 쿼리하기
하이브리드 쿼리는 Pinecone 쿼리 API에 밀집 vector와 희소 sparse_vector를 모두 전달합니다. alpha 매개변수(0~1)는 균형을 조절합니다. alpha=1.0은 순수 밀집 검색, alpha=0.0은 순수 희소 검색, alpha=0.5는 양쪽에 동일한 가중치를 적용합니다. 검증 세트에서 alpha를 조정하세요. 일반적으로 최적값은 0.3~0.7 사이입니다.
def hybrid_query_pinecone(index, query: str, alpha: float = 0.5, top_k: int = 5):
dense_vec = embed(query)
sparse_vec = generate_sparse_vector(query)
# Scale vectors by alpha for weighted fusion
scaled_dense = [v * alpha for v in dense_vec]
scaled_sparse = {
'indices': sparse_vec['indices'],
'values': [v * (1 - alpha) for v in sparse_vec['values']],
}
results = index.query(
vector=scaled_dense,
sparse_vector=scaled_sparse,
top_k=top_k,
include_metadata=True,
)
return results.matchespgvector: PostgreSQL의 벡터 검색
pgvector 확장은 PostgreSQL에 vector 열 유형과 거리 연산자를 추가합니다. 하이브리드 검색에서는 두 쿼리를 병렬로 실행합니다. 하나는 벡터 열에 대한 근사 최근접 이웃 쿼리이고, 다른 하나는 PostgreSQL에 내장된 tsvector와 tsquery를 사용하는 전문 검색 쿼리입니다. 그런 다음 애플리케이션 코드에서 RRF로 결과를 병합합니다.
-- Setup: enable extensions
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Create hybrid-capable table
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding vector(1536),
content_tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED
);
-- Indexes for both search modes
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON documents USING GIN (content_tsv);pgvector에서 밀집 쿼리와 희소 쿼리 실행하기
pgvector에서는 <=> 코사인 거리 연산자를 사용해 밀집 쿼리를 실행하고 쿼리 임베딩의 최근접 이웃을 찾습니다. 또한 tsvector 열에 대해 ts_rank_cd를 사용해 키워드 일치 점수를 계산하는 희소 쿼리를 실행합니다. 두 결과 집합을 독립적으로 가져온 다음 Python에서 RRF를 사용해 결합합니다.
import psycopg2
import json
def pgvector_hybrid_search(conn, query: str, top_k: int = 5):
dense_vec = embed(query)
tsquery = ' & '.join(query.split()) # simple AND query
# Dense query
dense_sql = '''
SELECT id, content, 1 - (embedding <=> %s::vector) AS score
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT 20
'''
# Sparse query
sparse_sql = '''
SELECT id, content, ts_rank_cd(content_tsv, to_tsquery('english', %s)) AS score
FROM documents
WHERE content_tsv @@ to_tsquery('english', %s)
ORDER BY score DESC
LIMIT 20
'''
with conn.cursor() as cur:
cur.execute(dense_sql, [json.dumps(dense_vec), json.dumps(dense_vec)])
dense_rows = cur.fetchall()
cur.execute(sparse_sql, [tsquery, tsquery])
sparse_rows = cur.fetchall()
return fuse_with_rrf(dense_rows, sparse_rows, top_k)pgvector 결과에 RRF 적용하기
두 PostgreSQL 쿼리에서 순위가 매겨진 행을 수집한 후, 각 결과 집합에서 순서대로 문서 ID를 추출하고 결합 알고리즘을 실행해 RRF를 적용합니다. 최종 병합 목록에는 의미적 관련성과 키워드 중복성을 모두 고려했을 때 가장 관련성이 높은 상위 K개 문서가 포함됩니다.
def fuse_with_rrf(dense_rows, sparse_rows, top_k: int = 5, k: int = 60):
from collections import defaultdict
doc_texts = {}
scores = defaultdict(float)
for rank, row in enumerate(dense_rows, start=1):
doc_id, text, _ = row
scores[doc_id] += 1.0 / (k + rank)
doc_texts[doc_id] = text
for rank, row in enumerate(sparse_rows, start=1):
doc_id, text, _ = row
scores[doc_id] += 1.0 / (k + rank)
doc_texts[doc_id] = text
ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [{'id': doc_id, 'text': doc_texts[doc_id], 'rrf_score': s}
for doc_id, s in ranked[:top_k]]검색 품질 벤치마크하기
하이브리드 검색을 구현한 후에는 기준이 되는 밀집 전용 검색과 비교해 엄격하게 벤치마크하세요. 관련 문서가 알려진 쿼리를 최소 100개 포함하는 표준 테스트 세트를 사용하세요. NDCG@5, MRR, 적중률@3을 측정하세요. 하이브리드 검색으로 얻는 일반적인 개선 폭은 NDCG@5 기준 5~20%이며, 밀집 모델이 놓치는 정확한 기술 용어가 포함된 쿼리에서 가장 큰 향상이 나타납니다.
def evaluate_retrieval(search_fn, test_queries, golden_relevant, k=5):
ndcg_scores = []
hit_count = 0
for query, relevant_ids in zip(test_queries, golden_relevant):
results = search_fn(query, top_k=k)
retrieved_ids = [r['id'] for r in results]
# Hit rate
if any(rid in relevant_ids for rid in retrieved_ids):
hit_count += 1
# Simplified NDCG (binary relevance)
dcg = sum(
1.0 / (i + 1)
for i, rid in enumerate(retrieved_ids)
if rid in relevant_ids
)
idcg = sum(1.0 / (i + 1) for i in range(min(len(relevant_ids), k)))
ndcg_scores.append(dcg / idcg if idcg > 0 else 0)
return {
'hit_rate': hit_count / len(test_queries),
'ndcg': sum(ndcg_scores) / len(ndcg_scores),
}Pinecone 하이브리드 모드에서 Alpha 조정하기
Pinecone 하이브리드 쿼리의 alpha 매개변수는 체계적으로 조정해야 합니다. 쿼리 유형 층화가 유용한 방법입니다. 테스트 쿼리를 주로 의미적인 유형(바꿔 쓰기, 개념적 표현)과 주로 어휘적인 유형(정확한 용어, 코드)으로 분류하고, 각 그룹에서 최적 alpha를 별도로 측정하세요. 일부 프로덕션 시스템은 동적 alpha 선택을 구현하여 실행 시 쿼리를 분류하고 적절한 alpha를 자동으로 선택합니다.
def find_optimal_alpha(index, test_queries, golden_relevant, alphas=None):
if alphas is None:
alphas = [0.0, 0.2, 0.4, 0.5, 0.6, 0.8, 1.0]
best_alpha, best_score = 0.5, 0
for alpha in alphas:
hits = 0
for query, relevant in zip(test_queries, golden_relevant):
results = hybrid_query_pinecone(index, query, alpha=alpha, top_k=5)
if any(r['id'] in relevant for r in results):
hits += 1
hit_rate = hits / len(test_queries)
print(f'alpha={alpha}: hit_rate={hit_rate:.3f}')
if hit_rate > best_score:
best_score, best_alpha = hit_rate, alpha
return best_alphaPinecone과 pgvector 하이브리드 중 선택하기
관리형 인프라와 자동 확장이 필요하고 희소 벡터 생성의 복잡성을 단일 API에 맡기고 싶다면 Pinecone 하이브리드를 사용하세요. 이미 PostgreSQL 인프라를 갖추고 있으며 문서와 메타데이터 간 트랜잭션 일관성, 필터링을 위한 완전한 SQL 유연성, 또는 특정 공급업체에 종속되지 않을 자유가 필요하다면 pgvector 하이브리드를 사용하세요. 두 방식 모두 적절히 조정하면 뛰어난 하이브리드 검색 품질을 제공합니다.
빠른 확인
이 수업에서 배운 하이브리드 검색 구현에 대한 이해도를 확인해 보세요.
수업 요약
이 수업에서는 다음을 배웠습니다. Pinecone 희소-밀집 모드는 레코드마다 밀집 벡터와 희소 벡터를 모두 저장하고 설정 가능한 alpha 매개변수로 결합합니다. pgvector 하이브리드 검색은 SQL 전문 검색과 벡터 쿼리를 결합한 뒤 애플리케이션 코드에서 RRF로 병합합니다. 또한 쿼리 분포에 맞는 최적의 균형을 찾으려면 검증 세트에서 alpha 조정이 필수적입니다. 다음으로 재순위 지정을 사용하는 2단계 검색을 살펴보겠습니다.
자주 묻는 질문
“Pinecone과 pgvector의 하이브리드 검색” 강의는 무료인가요?
네 — “Pinecone과 pgvector의 하이브리드 검색” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Engineering Academy 강의 전체를 잠금 해제할 수 있습니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“Pinecone과 pgvector의 하이브리드 검색”에서 뭘 배우나요?
Pinecone에서는 희소-밀집 색인 모드를 사용하고 pgvector에서는 RRF 병합을 적용한 병렬 쿼리를 사용해 하이브리드 검색을 구성한 뒤, 자신의 데이터셋에서 검색 품질을 벤치마킹합니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
AI Engineering Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 AI Engineering Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“Pinecone과 pgvector의 하이브리드 검색” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 AI Engineering Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 AI Engineering Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 밀집 검색과 희소 검색의 비교: 상충 관계
- BM25 키워드 검색 구현
- 점수 병합을 위한 상호 순위 융합
- Pinecone과 pgvector의 하이브리드 검색