불일치 진단하기
불일치 진단하기 (migration-guidance-diagnosing-discrepancies)
검증(verification)이 문제를 잡아냈다면, 다음 두 가지 중 어느 쪽인지 판단해야 해요. 데이터 문제(마이그레이션 중 무언가 잘못된 경우)인지, 아니면 설정 문제(데이터는 맞지만 시스템이 다르게 동작하는 경우)인지 말이죠. 이 페이지는 그 판단을 돕는 진단 결정 트리와, 각 벤더별로 흔히 겪는 함정(gotchas)을 제공해요.
결정 트리 (Decision Tree)
검증 단계가 하나라도 실패하면 여기서 시작해요.
Is the vector count wrong?
├─ Yes → Data-level issue
│ ├─ Count lower than expected → Check migration script logs for errors,
│ │ timeouts, or partial failures. Re-run for missing segments.
│ ├─ Count higher than expected → Check for duplicate inserts (retried batches)
│ │ or source count excluding namespaces/partitions.
│ └─ Count matches but IDs differ → ID mapping error during migration.
│
└─ No (count matches) → Continue
│
Are metadata fields missing or wrong type?
├─ Yes → Payload mapping issue
│ ├─ Fields missing → Source system may omit null fields on export.
│ │ Check migration script's null handling.
│ ├─ Types changed → See "Type Coercion" section below.
│ └─ Values differ → Encoding issue (UTF-8, special characters, unicode normalization).
│
└─ No (metadata looks correct) → Continue
│
Are search results completely different?
├─ Yes → Configuration-level issue
│ ├─ Check distance metric (most common cause)
│ ├─ Check if index is built (HNSW may not be built yet on fresh data)
│ └─ Check if vectors are normalized (affects cosine vs. dot product)
│
└─ No (results overlap but differ at the margins) → Expected behavior
│
Is recall@10 below 0.85?
├─ Yes → Indexing parameter mismatch
│ ├─ Compare HNSW ef_construction and M values
│ ├─ Compare ef (search-time) parameters
│ └─ Check quantization settings
│
└─ No → Migration is working correctly.
Results differ on borderline cases due to
ANN approximation. This is normal.
설정 수준 문제 (Configuration-Level Issues)
거리 지표 불일치 (Distance Metric Mismatch)
가장 영향력이 큰 설정 오류예요. 시스템 간 메트릭이 어떻게 대응하는지 정리하면 다음과 같아요.
| 소스 시스템 | 소스 메트릭 | Qdrant 대응값 | 비고 |
|---|---|---|---|
| Pinecone | cosine |
Cosine |
직접 대응 |
| Pinecone | dotproduct |
Dot |
Pinecone은 dotproduct에 단위 정규화 벡터가 필요해요 |
| Pinecone | euclidean |
Euclid |
직접 대응 |
| Weaviate | cosine |
Cosine |
직접 대응 |
| Weaviate | l2-squared |
Euclid |
Qdrant는 L2를 사용하므로 L2-squared가 아니에요. 점수 크기는 다르지만 순위는 동일해요 |
| Weaviate | dot |
Dot |
직접 대응 |
| Milvus | COSINE |
Cosine |
직접 대응 |
| Milvus | L2 |
Euclid |
직접 대응 |
| Milvus | IP (inner product) |
Dot |
직접 대응 |
| Elasticsearch | cosine |
Cosine |
ES는 1 - cosine_distance를 반환하고, Qdrant는 코사인 유사도를 직접 반환해요 |
| pgvector | vector_cosine_ops |
Cosine |
pgvector는 거리(1 - 유사도)를 반환하고, Qdrant는 유사도를 반환해요 |
| pgvector | vector_l2_ops |
Euclid |
직접 대응 |
| pgvector | vector_ip_ops |
Dot |
pgvector는 정렬을 위해 음수 내적을 사용하므로 점수가 뒤집혀요 |
진단 테스트: 단일 쿼리 벡터를 하나 잡아서, 알려진 타깃 벡터와의 거리를 직접(numpy로) 계산한 뒤 두 시스템의 결과와 비교해봐요.
import numpy as np
query = np.array([...]) # Your query vector
target = np.array([...]) # A known result vector
# Manual distance calculations
cosine_sim = np.dot(query, target) / (np.linalg.norm(query) * np.linalg.norm(target))
dot_product = np.dot(query, target)
euclidean = np.linalg.norm(query - target)
print(f"Cosine similarity: {cosine_sim:.6f}")
print(f"Dot product: {dot_product:.6f}")
print(f"Euclidean distance: {euclidean:.6f}")
# Compare against Qdrant's reported score
qdrant_result = client.query_points(
collection_name="your_collection",
query=query.tolist(),
limit=1,
)
print(f"Qdrant score: {qdrant_result.points[0].score:.6f}")
# The Qdrant score should match one of the manual calculations.
# If it doesn't match the expected metric, the collection is misconfigured.
HNSW 인덱스가 아직 만들어지지 않음 (HNSW Index Not Built)
방금 마이그레이션한 컬렉션에서는 HNSW 인덱스가 아직 만들어지고 있을 수 있어요. 이 기간 동안 Qdrant는 brute-force 검색으로 대체하는데, 이는 정확한 결과를 반환해요(recall = 1.0). 인덱스 구축이 끝나면 결과가 근사 방식으로 바뀌어요.
# Check index status
collection_info = client.get_collection("your_collection")
print(f"Indexed vectors: {collection_info.indexed_vectors_count}")
print(f"Total vectors: {collection_info.points_count}")
if collection_info.indexed_vectors_count < collection_info.points_count:
print("⚠ Index is still building. Wait for completion before running search quality checks.")
함정: 인덱스가 구축되는 동안 2단계 검증을 돌리면 인위적으로 높은 recall이 나와요(brute-force는 정확하니까요). 인덱싱이 끝난 뒤 다시 실행해서 실제 수치를 확인해야 해요.
벡터 정규화 (Vector Normalization)
코사인 유사도와 내적(dot product)은 벡터가 단위 정규화(L2 노름 = 1.0)되어 있으면 동일한 순위를 만들어요. 소스 시스템이 정규화된 벡터를 전제로 했는데 마이그레이션 중 내적으로 바꿨다면(또는 그 반대), 결과가 달라질 거예요.
# Check if vectors are normalized
sample_points = client.scroll(
collection_name="your_collection",
limit=100,
with_vectors=True,
)[0]
norms = [np.linalg.norm(p.vector) for p in sample_points]
print(f"Vector norms: min={min(norms):.4f}, max={max(norms):.4f}, mean={np.mean(norms):.4f}")
if all(abs(n - 1.0) < 0.001 for n in norms):
print("Vectors are unit-normalized. Cosine and Dot produce equivalent rankings.")
else:
print("Vectors are NOT normalized. Cosine and Dot will produce different rankings.")
양자화 차이 (Quantization Differences)
소스 시스템이 어떤 양자화 방식을 쓰고 Qdrant가 다른 방식(또는 아무것도 안 씀)을 쓰면 점수가 달라져요. 이는 예상된 동작이고, 데이터 손상을 뜻하지 않아요.
| 소스 양자화 | Qdrant 양자화 | 예상 영향 |
|---|---|---|
| 없음 | 없음 | 점수가 거의 일치해요 |
| 없음 | 스칼라 (int8) | 점수 차이가 작고, recall이 1-2% 바뀔 수 있어요 |
| 없음 | 제품 양자화 (Product Quantization) | 점수 차이가 크고, recall이 2-5% 떨어질 수 있어요 (재점수화를 조정해 보완) |
| PQ | 없음 | Qdrant 결과가 소스보다 정확할 거예요 |
| PQ | PQ | 점수는 다르지만(다른 코드북), recall은 비슷해야 해요 |
데이터 수준 문제 (Data-Level Issues)
부분 마이그레이션 실패 (Partial Migration Failures)
가장 흔한 데이터 수준 문제예요. 배치 업로드가 타임아웃되거나 오류가 났는데, 마이그레이션 스크립트가 재시도하지 않은 경우예요.
# Find missing IDs by comparing source and Qdrant
all_ids = set()
offset = None
while True:
records, offset = client.scroll(
collection_name="your_collection",
limit=1000,
offset=offset,
with_payload=False,
with_vectors=False,
)
all_ids.update(r.id for r in records)
if offset is None:
break
# Compare against source IDs
source_ids = set(baseline["all_ids"]) # Or load from your mapping file
missing = source_ids - all_ids
if missing:
print(f"Missing {len(missing)} IDs. First 10: {list(missing)[:10]}")
타입 강제 변환 문제 (Type Coercion Problems)
마이그레이션 중 메타데이터 타입이 바뀌면 필터링 검색이 조용히 깨져요. 필터는 오류 없이 실행되지만 문서가 하나도 매칭되지 않는 거죠.
디버깅 방법:
# Verify what types Qdrant stored
sample = client.scroll(
collection_name="your_collection",
limit=1,
with_payload=True,
)[0][0]
for field, value in sample.payload.items():
print(f" {field}: {type(value).__name__} = {value!r}")
흔한 수정법:
| 문제 | 수정법 |
|---|---|
| 정수가 float로 저장됨 | 정확한 매칭 대신 범위 필터(gte/lte)를 쓰거나, 명시적 int 캐스팅으로 다시 업로드 |
| 불리언이 문자열로 저장됨 | client.set_payload()로 영향받은 payload 필드 다시 업로드 |
| 배열이 단일 값으로 평탄화됨 | 다시 업로드하고, 마이그레이션 스크립트의 배열 처리 확인 |
| 중첩 객체 구조가 유실됨 | 올바른 중첩으로 다시 업로드. Qdrant는 중첩 payload를 지원해요 |
인코딩과 유니코드 문제 (Encoding and Unicode Issues)
비ASCII 문자, 이모지, 특수 유니코드가 포함된 메타데이터 문자열은 인코딩이 일관되게 처리되지 않으면 마이그레이션 중 깨질 수 있어요.
# Spot-check strings with non-ASCII content
import unicodedata
for record in sample_records:
for field, value in record.payload.items():
if isinstance(value, str) and not value.isascii():
# Check for common encoding issues
try:
value.encode("utf-8").decode("utf-8")
except UnicodeError:
print(f" Encoding issue: {field} in record {record.id}")
벤더별 함정 (Vendor-Specific Gotchas)
- Pinecone에서 마이그레이션할 때
- Weaviate에서 마이그레이션할 때
- Milvus / Zilliz에서 마이그레이션할 때
- Elasticsearch에서 마이그레이션할 때
- pgvector에서 마이그레이션할 때
다시 마이그레이션할까, 설정을 조정할까? (When to Re-Migrate vs. Adjust Configuration)
| 진단 | 조치 |
|---|---|
| 거리 메트릭이 잘못됨 | 올바른 메트릭으로 컬렉션을 다시 만들고 벡터를 다시 업로드 |
| HNSW 매개변수가 최적이 아님 | 매개변수를 조정하고 재인덱싱을 기다림 (재업로드 불필요) |
| 벡터 누락 | 누락된 배치만 마이그레이션 재실행 (upsert 사용) |
| 메타데이터 타입 오류 | set_payload로 영향받은 필드 수정 (벡터 재업로드 불필요) |
| payload 필드 누락 | 소스 내보내기에서 누락된 필드를 set_payload로 추가 |
| 양자화로 recall 하락 | 양자화 설정 조정 또는 재점수화(rescoring) 활성화 |
| 모든 게 맞는데 "뭔가 이상함" | 3단계 평가 데이터를 구축해요. 메트릭 없는 "이상함"은 조치가 불가능해요 |
판단이 어려울 땐, 단순한 규칙이 있어요. 데이터가 맞는지 먼저 확인하고, 그다음 설정을 의심해봐요. 언제나 그렇듯 실제 수치로 검증하는 것보다 확실한 대안은 없어요.
출처: Qdrant 공식문서