Supabase Hybrid Search vs Python 벡터 DB 쿡북

RRF · 인덱싱 알고리즘 · 메모리 · 보안 · 의사결정까지

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

1. 왜 하이브리드 검색인가

키워드와 시맨틱은 서로의 결함을 메우는 관계

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

키워드 vs 시맨틱 — 각자의 한계

키워드 (BM25 / tsvector)

  • 정확한 토큰 매칭 강함
  • 동의어·언어 변형 약함
  • 의미는 모름
  • 예: "마리나라"로 검색해도
    "토마토 소스 파스타"는 못 찾음

시맨틱 (dense vector)

  • 의미·문맥 포착 강함
  • 정확 식별자 매칭 약함
  • 환각성 매칭 위험
  • 예: 에러 코드 "E_AUTH_401"을
    의미 유사로 검색하면 누락 빈번
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

RRF — 두 점수 체계를 랭크로 통일

Reciprocal Rank Fusion은 점수 정규화가 필요 없다. 각 결과의 랭크 역수를 합산할 뿐.

score(d) = Σ 1 / (k + rank_d)

  • k는 smoothing 상수 (보통 50~60)
  • 1위에 너무 큰 가중치가 쏠리는 걸 막음
  • 두 리스트 모두 상위인 항목 → 합산 점수가 누적되어 1등으로 부상
  • 한쪽만 매우 상위면 낮은 가중
  • BM25 점수가 무제한·코사인이 0~1이어도 무관
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

2. Supabase Hybrid Search 깊이

tsvector + pgvector + RRF SQL function의 내부

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

스키마와 인덱스 — GIN + HNSW를 한 테이블에

create table documents (
  id bigint primary key generated always as identity,
  content text,
  fts tsvector generated always as (to_tsvector('english', content)) stored,
  embedding extensions.vector(512)
);

-- 전문검색: GIN 인덱스 (역색인)
create index documents_fts_idx
  on documents using gin(fts);

-- 벡터검색: HNSW + inner product (vector_ip_ops)
create index documents_embedding_idx
  on documents using hnsw (embedding vector_ip_ops);

-- 검색 시 ef_search 동적 조정 (recall vs latency)
set hnsw.ef_search = 100;
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

hybrid_search SQL function — RRF 본체

create or replace function hybrid_search(
  query_text text,
  query_embedding extensions.vector(512),
  match_count int,
  full_text_weight float = 1,
  semantic_weight float = 1,
  rrf_k int = 50
)
returns setof documents
language sql as $$
with full_text as (
  select id,
    row_number() over(
      order by ts_rank_cd(fts, websearch_to_tsquery(query_text)) desc
    ) as rank_ix
  from documents
  where fts @@ websearch_to_tsquery(query_text)
  order by rank_ix
  limit least(match_count, 30) * 2
),
semantic as (
  select id,
    row_number() over(order by embedding <#> query_embedding) as rank_ix
  from documents
  order by rank_ix
  limit least(match_count, 30) * 2
)
select documents.*
from full_text
full outer join semantic on full_text.id = semantic.id
join documents on coalesce(full_text.id, semantic.id) = documents.id
order by
  coalesce(1.0 / (rrf_k + full_text.rank_ix), 0.0) * full_text_weight +
  coalesce(1.0 / (rrf_k + semantic.rank_ix), 0.0) * semantic_weight
  desc
limit match_count;
$$;
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

왜 ts_rank_cd가 아니라 row_number?

ts_rank_cd는 인덱스를 타지 못한다. WHERE 절로 매칭된 후보에만 적용해야 비용이 합리적이다.

핵심 트레이드오프:

  • WHERE fts @@ websearch_to_tsquery(...)로 GIN 인덱스 활용 → 후보 압축
  • ts_rank_cd로 후보 내부 순위 결정 → 결과는 작아서 비싸지 않음
  • limit match_count × 2로 양쪽에서 충분한 candidate를 뽑고 RRF로 fusion
  • 너무 작게 뽑으면 fusion 효과 소실 (한쪽에서 누락된 항목은 다른 쪽 랭크만 기여)

실측 권장: match_count × 2 ~ × 5 사이에서 recall과 latency를 보고 튜닝.

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

Supabase 하이브리드 튜닝 변수

  • rrf_k: 작을수록 1위 가중치 큼. 기본 50, RAG에는 60 흔함
  • full_text_weight / semantic_weight: 도메인 따라 0.5~2.0 범위 실험
  • hnsw.ef_search: 40 → 100 → 200 단계로 올리며 recall 측정
  • HNSW 파라미터: m=16, ef_construction=64가 출발점, 정확도 부족하면 m=32
  • websearch_to_tsquery는 "" 인용/-제외/OR 같은 검색 문법 지원 — 사용자 입력에 안전
  • 다국어: to_tsvector('english', ...)을 'simple' 또는 unaccent 확장과 조합
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

3. 벡터 DB별 하이브리드 구현

네이티브 지원 vs 워크어라운드 — 코드로 본다

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

Qdrant — Query API prefetch + RRF

from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.query_points(
    collection_name="docs",
    prefetch=[
        models.Prefetch(
            query=models.SparseVector(indices=[1, 42], values=[0.22, 0.8]),
            using="sparse",  # BM25/SPLADE 결과의 sparse vector
            limit=20,
        ),
        models.Prefetch(
            query=[0.01, 0.45, 0.67],  # dense embedding
            using="dense",
            limit=20,
        ),
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),
    limit=10,
)
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

Weaviate — alpha로 키워드·벡터 비중 조정

from weaviate.classes.query import HybridFusion, MetadataQuery

jeopardy = client.collections.use("JeopardyQuestion")

response = jeopardy.query.hybrid(
    query="food",
    alpha=0.5,  # 0=BM25만, 1=vector만
    fusion_type=HybridFusion.RELATIVE_SCORE,  # v1.24+ 기본
    query_properties=["question^2", "answer"],  # BM25 필드 가중치
    return_metadata=MetadataQuery(score=True, explain_score=True),
    limit=3,
)

for o in response.objects:
    print(o.properties, o.metadata.score)
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

Milvus 2.4+ — 다중 AnnSearchRequest + RRFRanker

from pymilvus import AnnSearchRequest, RRFRanker, Collection

collection = Collection("docs")

sparse_req = AnnSearchRequest(
    data=[sparse_query_vec],
    anns_field="sparse_vec",
    param={"metric_type": "IP"},
    limit=20,
)
dense_req = AnnSearchRequest(
    data=[dense_query_vec],
    anns_field="dense_vec",
    param={"metric_type": "COSINE", "params": {"ef": 100}},
    limit=20,
)

results = collection.hybrid_search(
    reqs=[sparse_req, dense_req],
    rerank=RRFRanker(k=60),  # 또는 WeightedRanker(0.3, 0.7)
    limit=10,
)
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

Chroma — 네이티브 미지원, 클라이언트 RRF 워크어라운드

from rank_bm25 import BM25Okapi
import chromadb

client = chromadb.PersistentClient(path="./chroma_db")
col = client.get_or_create_collection("docs")

# 1) 벡터 검색
vec_hits = col.query(query_embeddings=[q_emb], n_results=20)
vec_ids = vec_hits["ids"][0]

# 2) BM25 (별도 인덱스 — Chroma 외부)
bm25 = BM25Okapi([d.split() for d in corpus])
bm25_scores = bm25.get_scores(query.split())
bm25_ids = [doc_ids[i] for i in bm25_scores.argsort()[::-1][:20]]

# 3) 클라이언트 RRF
def rrf(rankings, k=60):
    score = {}
    for ranks in rankings:
        for r, doc_id in enumerate(ranks):
            score[doc_id] = score.get(doc_id, 0) + 1 / (k + r)
    return sorted(score, key=score.get, reverse=True)

final = rrf([vec_ids, bm25_ids])[:10]
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

FAISS + Tantivy — 가장 수동적인 조합

import faiss, numpy as np
from tantivy import Index, SchemaBuilder

# 벡터: FAISS HNSW (IDMap2로 외부 ID 보존)
index = faiss.IndexIDMap2(faiss.IndexHNSWFlat(768, 32))
index.add_with_ids(embeddings, ids)
D, I = index.search(q_emb.reshape(1, -1), k=20)
vec_ranking = I[0].tolist()

# 키워드: Tantivy (Rust 기반 BM25)
schema = SchemaBuilder().add_text_field("body", stored=True).build()
idx = Index(schema, path="./tantivy_idx")
searcher = idx.searcher()
query = idx.parse_query(user_q, ["body"])
bm25_ranking = [hit.doc_id for _, hit in searcher.search(query, 20).hits]

# RRF는 클라이언트에서 직접 — Chroma 예시와 동일
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

4. 인덱싱 알고리즘 비교

HNSW · IVFFlat · IVF-PQ · ScaNN · DiskANN · RaBitQ

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

HNSW vs IVFFlat — 양대 표준

HNSW (계층 그래프)

  • 파라미터: m=16, ef_construction=64
  • 쿼리: ef_search=40~200
  • 정확도-속도 최상위
  • 메모리: d×4 + m×2×4 B/vec
  • 빌드: 데이터 0에서도 가능
  • 동적 삽입 강함, 삭제 약함
  • pgvector / Qdrant / Weaviate / Milvus 기본

IVFFlat (클러스터 + flat)

  • 파라미터: lists ≈ √N (1M+) or rows/1000
  • 쿼리: nprobe ≈ √lists
  • 빌드 빠름, 메모리 적음
  • 정확도는 HNSW 대비 다소 낮음
  • 빌드 전 데이터 적재 필요 (k-means 학습)
  • 데이터 분포가 안정적일 때 유리
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

IVF-PQ / ScaNN / DiskANN / Annoy

  • IVF-PQ: 클러스터 + Product Quantization. d/M 서브벡터를 코드북으로 압축, 메모리 최대 32× 절감, recall 5~10%p 손실
  • ScaNN (Google): anisotropic quantization, CPU 추론 친화. BigQuery·Vertex 벡터 검색 백엔드
  • DiskANN (MS, Vamana 그래프): 십억 단위에서 SSD 활용. 메모리 부족 환경의 표준
  • Annoy (Spotify): random projection forest. 빌드 후 mmap, 추가 삽입 불가 — 읽기 전용에 적합
  • 선택 룰: <1M이면 HNSW, 1M~100M면 IVF+HNSW 클러스터 할당, 100M+면 IVF-PQ 또는 DiskANN
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

양자화 — Scalar / Product / Binary / RaBitQ

  • SQ8 (Scalar Quantization 8bit): float32 → int8, 4× 절감, 정확도 손실 1~3%p
  • PQ (Product Quantization): d/M 서브벡터를 8bit 코드로 → 16~64× 절감, 손실 큼 but reranking으로 보완
  • Binary: 1bit per dim → 32× 절감, 정확도 큰 손실 (랜덤 회전 전처리 권장)
  • RaBitQ (FAISS 최신): 1bit + 약간의 오버헤드, d/8 + 8 B/vec. 양자화의 사실상 최신 표준
  • 패턴: "양자화 인덱스로 후보 K×10 추출 → 원본 벡터로 재정렬(rerank)"이 정확도 회복의 정석
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

5. 영속성 / 인메모리 모드

프로덕션 운영 전 반드시 확인할 항목

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

영속성 비교표 (요약)

  • pgvector: Postgres heap + WAL + PITR + ACID. 인메모리 전용 모드 없음 (Postgres 자체가 영속)
  • Qdrant: 디스크 mmap 기본, 컬렉션 단위 on_disk:true/false. snapshot으로 백업
  • Weaviate: LSM-tree 디스크 영속, 인메모리 전용 모드 없음. embedded 모드도 디스크 사용
  • Milvus: Standalone(파일+etcd) / Distributed(MinIO·S3 + Pulsar + etcd) / Lite(단일 파일)
  • Chroma: PersistentClient(SQLite+parquet) / EphemeralClient(메모리 전용) / HttpClient(서버)
  • FAISS: 본질적으로 인메모리, faiss.write_index/read_index로 직렬화 — 트랜잭션·동시성 없음
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

6. 인메모리 메모리 사용량 추정

1M 벡터 × 768 / 1536 차원 기준

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

1M × 768d / 1536d — Raw + 양자화

1M × 768d

  • float32: 2.93 GB
  • fp16: 1.46 GB
  • int8 (SQ8): 0.73 GB
  • PQ (M=96, 8bit): ~96 MB
  • Binary (1bit): ~96 MB
  • RaBitQ: ~99 MB (d/8 + 8 B/vec)

1M × 1536d

  • float32: 5.86 GB
  • fp16: 2.93 GB
  • int8 (SQ8): 1.46 GB
  • PQ (M=192, 8bit): ~192 MB
  • Binary (1bit): ~192 MB
  • RaBitQ: ~200 MB
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

인덱스 오버헤드 + 운영 RAM 보정

  • HNSW: m=16 기준 vector당 추가 128 B → 1M에서 약 122 MB
  • HNSW m=32: 256 B/vec → 약 244 MB
  • IVFFlat: vector당 4 B (list ID) → 약 4 MB. centroid는 lists × d × 4 B 추가
  • IVF-PQ: PQ 코드(M B) + list ID 4 B
  • Python 객체 오버헤드, 메타데이터(payload), 페이지 캐시 등 운영 RAM은 위 계산의 1.5~2배로 잡는 게 안전
  • 실측 권장 — 위 수치는 자료구조 이론치이며 데이터 분포·압축 효율·라이브러리 구현에 따라 ±20% 변동
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

7. 2025-2026 시장 점유율과 사용 통계

공개 자료가 적은 영역 — "확인 필요"는 명시

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

공개 신호와 한계 (확인 필요 항목 포함)

  • GitHub stars (대략적 추세, 정확 수치 확인 필요): FAISS·Milvus 30k+, Qdrant 20k+, Chroma·pgvector 15k+ 수준
  • DB-Engines vector DBMS 카테고리는 2023년 신설 — 상위 일관 등장: Pinecone, Milvus, Qdrant, Weaviate, Chroma (정확한 월별 순위는 db-engines.com에서 확인)
  • LangChain·LlamaIndex 통합 빈도 상위 3은 일반적으로 pgvector · Chroma · Pinecone (자료별 편차 — 확인 필요)
  • 공개 프로덕션 사례: Cloudflare Vectorize·Supabase는 pgvector, Notion AI는 자체 인프라, Perplexity·Cohere는 비공개 — 다수가 미공개
  • 국내(한국) 채택: pgvector(Supabase/RDS) 중심 + Milvus 일부 보고 — 비공개 영역 많음, 확인 필요
  • 결론: 점유율 기반 의사결정은 위험. 자체 워크로드 벤치마크가 더 신뢰 가능
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

8. 보안 비교

인증 · 암호화 · RBAC · 격리 · Injection 방어

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

인증 · 인가 · 암호화

인증/인가

  • pgvector: PG role + RLS
  • Qdrant: API key + JWT(v1.9+) + mTLS
  • Weaviate: API key + OIDC + RBAC
  • Milvus: RBAC v2.3+
  • Chroma: 토큰 인증(서버 모드), 임베디드는 없음
  • FAISS: 없음 — 호스트가 책임

암호화/격리

  • TLS in-transit: 모두 지원
  • At-rest: 호스팅 환경 의존 (KMS·LUKS·EBS)
  • VPC peering: Zilliz/Qdrant Cloud/Weaviate Cloud
  • 멀티테넌시:
    · pgvector → RLS
    · Qdrant → payload 필터
    · Weaviate → tenant per collection
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

Injection 방어 · OWASP LLM Top 10

  • SQL injection: pgvector는 PG의 prepared statement 기본 방어. 나머지는 REST/gRPC API라 무관
  • Prompt injection: 메타데이터·페이로드에 사용자 제어 텍스트를 LLM 컨텍스트로 그대로 넣을 때 위험. 입력 sanitization + 출력 가드레일 필수
  • Embedding inversion: 일부 임베딩에서 원문 복원 가능 — PII는 임베딩 전 마스킹
  • 인덱스 파일 유출: FAISS·Chroma 영속 파일은 백업 권한 분리 + 디스크 암호화
  • OWASP LLM Top 10 매핑: LLM06(민감정보 노출), LLM08(과도한 권한), LLM02(불안전 출력) — 벡터 DB 운영에 직결
  • 안티패턴: 페이로드에 raw PII 저장, 운영 환경에 Chroma EphemeralClient/임베디드를 외부 노출
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

9. 워크로드별 의사결정

체크리스트 + 안티패턴

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

체크리스트 — 상황별 1차 후보

  • 이미 Postgres 운영 중 + 데이터 < 10M → pgvector + tsvector (인프라 하나로)
  • 10M~100M + 다중 임베딩 모델 → Qdrant(named vectors) 또는 Milvus(multi AnnSearchRequest)
  • 100M+ self-host + 비용 민감 → Milvus distributed + IVF-PQ, 또는 Qdrant cluster + RaBitQ
  • 빠른 PoC, Python 단일 프로세스 → Chroma PersistentClient
  • 메모리 극도로 제약 → FAISS IVF-PQ 또는 RaBitQ + mmap
  • 멀티테넌트 SaaS → pgvector RLS 또는 Qdrant payload 격리
  • 관리형 선호 → Supabase / Pinecone / Zilliz / Weaviate Cloud / Qdrant Cloud 중 SLA·요금·리전 비교
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

안티패턴 — 자주 보이는 실수

  • 1M 이하 데이터에 distributed cluster 도입 — 운영 비용만 증가
  • RRF 대신 BM25 점수와 코사인 점수를 단순 가산 — 스케일 불일치로 한쪽이 항상 압도
  • GIN 인덱스 없이 ILIKE / regex 검색 — 100K부터 급격히 느려짐
  • IVF에서 nprobe=1 — 거의 항상 recall 저하의 원인
  • HNSW에 ef_search 기본값(40)으로 production 운영 — recall 모니터링 없이는 위험
  • 페이로드에 raw PII 저장 + 임베딩 동시 보관 — 유출 시 이중 위험
  • 한 컬렉션에 여러 도메인 데이터 혼합 — 멀티테넌시 격리 실패
  • 양자화 인덱스만 쓰고 reranking 생략 — 정확도 추적 불가
Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지

정리 — 한 줄로

Supabase Hybrid Search vs Python 벡터 DB 종합 쿡북 — RRF·인덱싱·메모리·보안까지