불일치 진단하기

불일치 진단하기 (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 공식문서

더 알아보기 (Learn more)