사전 필터링과 사후 필터링의 차이, 필터 인덱스 설계, 복합 필터 조건, 지오 필터, 멀티테넌시 필터 패턴, 성능 최적화 전략을 다룹니다.
실제 프로덕션 환경에서 순수 벡터 검색만으로 충분한 경우는 드뭅니다. 대부분의 애플리케이션에서는 벡터 유사도와 함께 구조화된 조건을 적용해야 합니다.
예를 들어 이커머스 검색에서 "빨간색 운동화와 비슷한 상품"을 찾을 때, 벡터 유사도만 사용하면 다른 카테고리의 빨간색 제품이 섞여 나올 수 있습니다. category = "shoes" AND color = "red" AND price <= 100000 같은 필터를 함께 적용해야 실용적인 결과를 얻을 수 있습니다.
벡터 데이터베이스 벤더들의 사용 패턴 분석에 따르면, 프로덕션 쿼리의 70% 이상이 메타데이터 필터링을 포함합니다. 필터링 성능은 전체 검색 성능에 직접적인 영향을 미칩니다.
메타데이터 필터를 적용하는 시점에 따라 크게 두 가지 전략으로 나뉩니다.
벡터 검색을 먼저 수행한 뒤, 결과에 메타데이터 필터를 적용합니다.
장점: 벡터 검색 성능에 영향 없음, 구현이 단순
단점: 필터 선택성(selectivity)이 높으면 (매칭 비율이 낮으면) 결과가 비어 나올 수 있음. Top 100을 가져왔는데 필터를 통과하는 것이 3개뿐이라면, 실질적으로 Top 3만 반환됩니다.
메타데이터 조건에 맞는 벡터만 대상으로 ANN 검색을 수행합니다.
장점: 항상 정확한 수의 결과를 반환, 필터 조건을 완벽히 만족
단점: 필터링이 ANN 인덱스와 통합되어야 하므로 구현이 복잡. 필터로 후보가 극단적으로 줄면 ANN 인덱스의 효율이 떨어질 수 있음
| 솔루션 | 기본 전략 | 비고 |
|---|---|---|
| Pinecone | 사전 필터링 | 내부적으로 최적화된 사전 필터 |
| Weaviate | 사전 필터링 | HNSW + 역인덱스 통합 |
| Qdrant | 사전 필터링 | 페이로드 인덱스 기반 |
| Milvus | 사전 필터링 | 파티션 + 스칼라 인덱스 |
| pgvector | 사전 필터링 | SQL WHERE + HNSW |
현대 벡터 데이터베이스 대부분은 사전 필터링을 기본으로 사용합니다. 다만 내부 구현은 솔루션마다 다릅니다. 일부는 "사전 필터링처럼 보이지만 실제로는 더 정교한 하이브리드 전략"을 사용합니다.
메타데이터 필터링의 성능은 필터 인덱스 설계에 달려 있습니다. 자주 사용하는 필터 필드에는 전용 인덱스를 생성해야 합니다.
from qdrant_client.models import PayloadSchemaType
# 키워드 인덱스 (정확한 일치)
client.create_payload_index(
collection_name="products",
field_name="category",
field_schema=PayloadSchemaType.KEYWORD
)
# 정수 인덱스 (범위 쿼리)
client.create_payload_index(
collection_name="products",
field_name="price",
field_schema=PayloadSchemaType.INTEGER
)
# 전문 검색 인덱스
client.create_payload_index(
collection_name="products",
field_name="description",
field_schema=PayloadSchemaType.TEXT
)
# 지리 인덱스
client.create_payload_index(
collection_name="stores",
field_name="location",
field_schema=PayloadSchemaType.GEO
)Weaviate는 프로퍼티 정의 시 인덱스 옵션을 설정합니다.
import weaviate.classes.config as wc
client.collections.create(
name="Product",
properties=[
wc.Property(
name="category",
data_type=wc.DataType.TEXT,
indexFilterable=True, # 필터 인덱스 활성화
indexSearchable=False, # BM25 검색 인덱스 비활성화
skip_vectorization=True
),
wc.Property(
name="price",
data_type=wc.DataType.NUMBER,
indexFilterable=True,
indexRangeFilters=True, # 범위 쿼리 인덱스
skip_vectorization=True
),
wc.Property(
name="description",
data_type=wc.DataType.TEXT,
indexFilterable=False,
indexSearchable=True, # BM25 검색 대상
),
]
)실제 애플리케이션에서는 단일 조건이 아닌 복합 필터가 필요합니다.
# Qdrant: 복합 필터
from qdrant_client.models import Filter, FieldCondition, MatchValue, Range
complex_filter = Filter(
must=[
# AND 조건
FieldCondition(key="category", match=MatchValue(value="electronics")),
FieldCondition(key="in_stock", match=MatchValue(value=True)),
],
should=[
# OR 조건 (하나 이상 만족)
FieldCondition(key="brand", match=MatchValue(value="Samsung")),
FieldCondition(key="brand", match=MatchValue(value="Apple")),
],
must_not=[
# NOT 조건
FieldCondition(key="status", match=MatchValue(value="discontinued")),
]
)
results = client.query_points(
collection_name="products",
query=query_vector,
query_filter=complex_filter,
limit=10
)# Weaviate: 복합 필터
from weaviate.classes.query import Filter
results = articles.query.near_text(
query="검색어",
filters=(
Filter.by_property("category").equal("electronics") &
Filter.by_property("in_stock").equal(True) &
(
Filter.by_property("brand").equal("Samsung") |
Filter.by_property("brand").equal("Apple")
)
),
limit=10
)# Qdrant: 가격 범위 + 날짜 범위
range_filter = Filter(
must=[
FieldCondition(
key="price",
range=Range(gte=10000, lte=50000)
),
FieldCondition(
key="created_at",
range=Range(gte="2024-01-01T00:00:00Z")
),
]
)위치 기반 검색에서는 벡터 유사도와 지리적 근접성을 결합해야 합니다.
# Qdrant: 반경 내 검색
from qdrant_client.models import GeoRadius, GeoPoint
geo_filter = Filter(
must=[
FieldCondition(
key="location",
geo_radius=GeoRadius(
center=GeoPoint(lat=37.5665, lon=126.9780), # 서울 시청
radius=5000 # 5km 반경
)
)
]
)
# 벡터 유사도 + 5km 반경 내 매장 검색
results = client.query_points(
collection_name="stores",
query=query_vector,
query_filter=geo_filter,
limit=10
)지오 필터는 "반경 검색(radius)"과 "바운딩 박스(bounding box)" 두 가지 방식을 지원합니다. 도시 단위 검색에는 바운딩 박스가, 특정 지점 주변 검색에는 반경이 적합합니다. Qdrant와 Weaviate 모두 지오 필터를 네이티브로 지원합니다.
SaaS 애플리케이션에서 여러 고객의 데이터를 하나의 벡터 DB에서 관리할 때, 테넌트 격리는 필수입니다. 주요 패턴을 비교합니다.
모든 벡터에 tenant_id 필드를 추가하고 검색 시 필터로 격리합니다.
# 삽입 시 테넌트 ID 포함
client.upsert(
collection_name="documents",
points=[
PointStruct(
id=1,
vector=[0.1, 0.2, ...],
payload={
"tenant_id": "tenant-a",
"content": "문서 내용"
}
)
]
)
# 검색 시 테넌트 ID 필터
results = client.query_points(
collection_name="documents",
query=query_vector,
query_filter=Filter(
must=[FieldCondition(key="tenant_id", match=MatchValue(value="tenant-a"))]
),
limit=10
)장점: 구현이 단순, 하나의 컬렉션으로 관리 단점: 필터 인덱스 오버헤드, 데이터 격리가 논리적 수준
Weaviate의 네이티브 멀티테넌시나 Pinecone의 네임스페이스를 활용합니다.
# Weaviate: 네이티브 멀티테넌시
tenant_collection = collection.with_tenant("tenant-a")
results = tenant_collection.query.near_text(query="검색어", limit=10)
# Pinecone: 네임스페이스
results = index.query(
vector=query_vector,
top_k=10,
namespace="tenant-a"
)장점: 물리적/논리적 격리, 테넌트별 리소스 관리 가능 단점: 솔루션 종속, 테넌트 수 제한 가능
테넌트별로 독립적인 컬렉션을 생성합니다.
장점: 완전한 격리, 테넌트별 인덱스 최적화 가능 단점: 테넌트 수가 많으면 관리 부담, 리소스 낭비 가능
| 패턴 | 적합한 경우 |
|---|---|
| 메타데이터 필터 | 테넌트 수 적고, 데이터가 유사한 구조 |
| 네이티브 멀티테넌시 | 수천 개 테넌트, 비활성 테넌트 오프로드 필요 |
| 컬렉션 분리 | 테넌트별 스키마가 다르거나, 완전한 격리 필요 |
자주 사용하는 필터 필드에만 인덱스를 생성합니다. 불필요한 인덱스는 쓰기 성능을 저하시키고 메모리를 낭비합니다.
**카디널리티(Cardinality)**는 필터 필드의 고유 값 수입니다. 카디널리티가 너무 높은 필드(예: UUID)는 필터 인덱스 효율이 낮습니다. 반대로 카디널리티가 너무 낮은 필드(예: boolean)는 선택성이 낮아 검색 범위를 크게 줄이지 못합니다.
# 높은 카디널리티 (비효율적 필터)
# user_id: 100만 개 고유 값 -> 각 값에 해당하는 벡터가 극소수
# -> 멀티테넌시 패턴 사용 권장
# 적절한 카디널리티 (효율적 필터)
# category: 20개 고유 값 -> 각 카테고리에 수만 개 벡터
# -> 필터 인덱스 최적
# 낮은 카디널리티 (선택성 부족)
# is_active: 2개 값 (True/False) -> 필터 효과 미미
# -> 다른 필터와 조합하여 사용필터 조건이 너무 많으면 후보 벡터가 지나치게 줄어들어 ANN 인덱스의 효율이 떨어집니다. 가장 선택적인 2-3개 필터를 DB 레벨에서 적용하고, 나머지는 애플리케이션 레벨에서 사후 필터링하는 것도 방법입니다.
import time
def benchmark_filter(client, collection, query_vector, filter_condition, n_runs=100):
"""필터 성능 벤치마크"""
latencies = []
for _ in range(n_runs):
start = time.perf_counter()
client.query_points(
collection_name=collection,
query=query_vector,
query_filter=filter_condition,
limit=10
)
latencies.append((time.perf_counter() - start) * 1000)
latencies.sort()
return {
"p50_ms": latencies[len(latencies) // 2],
"p95_ms": latencies[int(len(latencies) * 0.95)],
"p99_ms": latencies[int(len(latencies) * 0.99)],
}필터 성능 벤치마크는 반드시 프로덕션과 유사한 데이터 분포에서 수행해야 합니다. 균일하게 분포된 테스트 데이터와 실제 데이터의 편향된 분포에서 성능 차이가 크게 날 수 있습니다.
이번 장에서는 메타데이터 필터링의 전략과 고급 쿼리 패턴을 심층적으로 다루었습니다. 사전 필터링과 사후 필터링의 트레이드오프, 복합 필터와 지오 필터의 구현, 멀티테넌시 패턴, 성능 최적화 전략을 살펴보았습니다. 필터링은 벡터 검색의 실용성을 결정하는 핵심 요소이며, 인덱스 설계와 카디널리티를 고려한 최적화가 중요합니다.
다음 장에서는 시리즈의 마지막으로 운영, 모니터링, 스케일링 전략을 다룹니다. 프로덕션 환경에서의 벡터 데이터베이스 운영 노하우와 솔루션 선택 의사결정 프레임워크를 제시하겠습니다.
이 글이 도움이 되셨나요?
벡터 데이터베이스의 수평/수직 스케일링, 샤딩, 레플리카, 백업 전략, 모니터링 메트릭, 비용 최적화, 솔루션 선택 의사결정 트리, 마이그레이션 가이드를 다룹니다.
시맨틱 검색과 키워드 검색을 결합하는 하이브리드 검색의 원리, BM25+벡터 퓨전 전략, Reciprocal Rank Fusion, 리랭커 통합, 프레임워크별 구현 방법을 다룹니다.
Rust 기반 고성능 벡터 엔진 Qdrant의 페이로드 필터링, 명명된 벡터, 하이브리드 배포를 분석하고, PostgreSQL 확장 pgvector의 트랜잭션 일관성과 pgvectorscale 성능을 비교합니다.