데이터 무결성 검증
데이터 무결성 검증 (migration-guidance-data-integrity)
마이그레이션에서 가장 먼저 확인해야 할 것은 데이터가 제대로 도착했는지, 그리고 올바르게 도착했는지예요. 먼저 마이그레이션 전 기준점(baseline)을 잡았다면, 그다음으로 데이터 무결성(Data integrity)을 점검해야 해요. 데이터 무결성이 답하는 질문은 단순해요. "내 모든 데이터가 도착했나, 그리고 올바르게 도착했나?" 이 검사들은 실행 속도가 가장 빠르면서도 가장 흔한 마이그레이션 실패를 잡아내줘요.
출처: Qdrant 공식문서
1. 벡터 수 확인 (Vector Count Verification)
가장 간단한 검사예요. Qdrant의 벡터 수가 소스 시스템과 일치하는지 보면 돼요.
from qdrant_client import QdrantClient
client = QdrantClient("localhost", port=6333)
# Get collection info
collection_info = client.get_collection("your_collection")
qdrant_count = collection_info.points_count
# Compare against baseline
source_count = baseline["total_vector_count"] # From pre-migration capture
if qdrant_count == source_count:
print(f"✓ Vector count matches: {qdrant_count}")
else:
diff = source_count - qdrant_count
pct = (diff / source_count) * 100
print(f"✗ Count mismatch: source={source_count}, qdrant={qdrant_count}, "
f"missing={diff} ({pct:.2f}%)")
카운트 불일치의 흔한 원인:
| 증상 | 예상 원인 |
|---|---|
| Qdrant 수가 더 낮음 | 마이그레이션 스크립트가 중간에 실패했거나, 소스의 중복 ID가 중복 제거되거나, 소스 카운트에 소프트 삭제된 레코드가 포함되어 있음 |
| Qdrant 수가 더 높음 | 재시도된 마이그레이션으로 중복 삽입이 발생했거나, 소스 카운트가 모든 네임스페이스/파티션을 포함하지 않았음 |
| 카운트는 일치하지만 데이터가 잘못됨 | ID 충돌: 서로 다른 벡터가 같은 포인트 ID에 매핑됨 |
정확한 일치가 기대되지 않는 경우: 일부 소스 시스템은 카운트 방식을 다르게 해요. Pinecone의 describe_index_stats는 모든 네임스페이스를 합쳐서 세는데, 여러분이 일부만 마이그레이션했다면 카운트가 맞지 않을 거예요. pgvector의 n_live_tup은 추정값이에요. 마이그레이션이 실패했다고 결론 내기 전에, 이런 예상되는 차이를 문서로 남겨두어야 해요.
2. 벡터 차원 확인 (Vector Dimension Verification)
벡터 차원이 소스 설정과 일치하는지 확인해요.
collection_info = client.get_collection("your_collection")
qdrant_dim = collection_info.config.params.vectors.size
# For named vectors:
# qdrant_dim = collection_info.config.params.vectors["dense"].size
source_dim = baseline["dimension"]
assert qdrant_dim == source_dim, (
f"Dimension mismatch: source={source_dim}, qdrant={qdrant_dim}"
)
차원이 일치하지 않는 경우: 거의 항상 마이그레이션 스크립트 오류를 의미해요 (예: 벡터가 잘렸거나, 재임베딩에 잘못된 임베딩 모델을 사용). 이 문제를 해결하기 전에는 더 이상의 검증을 진행하지 마세요.
3. 거리 메트릭 확인 (Distance Metric Verification)
거리 메트릭이 소스 시스템 설정과 일치하는지 확인해요.
qdrant_metric = collection_info.config.params.vectors.distance
# Returns: "Cosine", "Euclid", or "Dot"
# Map source system metrics to Qdrant equivalents
METRIC_MAP = {
# Pinecone
"cosine": "Cosine",
"euclidean": "Euclid",
"dotproduct": "Dot",
# Weaviate
"l2-squared": "Euclid",
# Milvus
"COSINE": "Cosine",
"L2": "Euclid",
"IP": "Dot",
}
expected_metric = METRIC_MAP.get(baseline["metric"])
assert qdrant_metric == expected_metric, (
f"Distance metric mismatch: source={baseline['metric']} "
f"(expected {expected_metric}), qdrant={qdrant_metric}"
)
거리 메트릭 불일치는 조용한(silent) 오류예요. 마이그레이션해도 벡터는 여전히 로드되고, 쿼리도 결과를 반환하거든요. 예를 들어 코사인 유사도와 내적(dot product)은 벡터가 단위 정규화(unit-normalized)되어 있을 때만 동일한 순위를 만들어요. 벡터가 정규화되어 있지 않은데 코사인과 내적을 오가면 모든 검색 결과가 달라져요.
4. 메타데이터(Payload) 확인 (Metadata Verification)
메타데이터 확인은 세 가지를 살펴봐요. 필드 존재 여부, 필드 타입, 필드 값이에요.
4a. 필드 존재 여부 (Field Presence)
예상되는 모든 메타데이터 필드가 Qdrant에 존재하는지 확인해요.
import random
# Sample points from Qdrant using scroll
records, _next = client.scroll(
collection_name="your_collection",
limit=1000,
with_payload=True,
with_vectors=False, # Skip vectors to speed up the check
)
# Collect all field names across sampled records
qdrant_fields = set()
for record in records:
if record.payload:
qdrant_fields.update(record.payload.keys())
source_fields = set(baseline["metadata_fields"])
missing = source_fields - qdrant_fields
extra = qdrant_fields - source_fields
if missing:
print(f"✗ Fields missing in Qdrant: {missing}")
if extra:
print(f"⚠ Extra fields in Qdrant (may be expected): {extra}")
if not missing and not extra:
print(f"✓ All {len(source_fields)} metadata fields present")
4b. 필드 타입 일관성 (Field Type Consistency)
필드 타입이 마이그레이션을 거치며 유지됐는지 확인해요.
def check_field_types(source_record, qdrant_record):
"""Compare field types between source and Qdrant records."""
issues = []
for field, source_value in source_record.items():
if field not in qdrant_record:
issues.append(f" {field}: missing in Qdrant")
continue
qdrant_value = qdrant_record[field]
if type(source_value) != type(qdrant_value):
issues.append(
f" {field}: type changed from "
f"{type(source_value).__name__} to {type(qdrant_value).__name__} "
f"(source={source_value!r}, qdrant={qdrant_value!r})"
)
return issues
흔한 타입 강제 변환 문제:
| 소스 타입 | Qdrant 도착 형태 | 영향 |
|---|---|---|
| Integer → Float | 42 → 42.0 |
= 42 필터가 실패할 수 있음. range 필터 사용. |
| Boolean → String | true → "true" |
= true 필터가 결과를 반환하지 않음 |
| 중첩 객체 → 평탄화 | {"a": {"b": 1}} → {"a.b": 1} |
중첩 필터 문법이 매칭되지 않음 |
| 배열 → 단일 값 | ["tag1", "tag2"] → "tag1" |
배열 포함 필터가 깨짐 |
| Null → 필드 없음 | null → (필드 부재) |
is_null 필터가 찾지 못함 |
4c. 필드 값 스팟체크 (Field Value Spot-Check)
샘플링한 레코드에 대해 실제 값을 비교해요.
def spot_check_values(source_sample, qdrant_collection, client):
"""Compare metadata values for sampled records."""
mismatches = []
for source_record in source_sample:
point_id = source_record["id"]
qdrant_points = client.retrieve(
collection_name=qdrant_collection,
ids=[point_id],
with_payload=True,
)
if not qdrant_points:
mismatches.append({"id": point_id, "issue": "Point not found in Qdrant"})
continue
qdrant_payload = qdrant_points[0].payload
for field, source_value in source_record["metadata"].items():
qdrant_value = qdrant_payload.get(field)
if source_value != qdrant_value:
mismatches.append({
"id": point_id,
"field": field,
"source": source_value,
"qdrant": qdrant_value,
})
return mismatches
5. 포인트 ID 확인 (Point ID Verification)
중복되거나 고아가 된 포인트 ID가 있는지 확인해요.
# Scroll through all points and collect IDs
all_ids = []
next_offset = None
while True:
records, next_offset = client.scroll(
collection_name="your_collection",
limit=1000,
offset=next_offset,
with_payload=False,
with_vectors=False,
)
all_ids.extend([r.id for r in records])
if next_offset is None:
break
# Check for duplicates
if len(all_ids) != len(set(all_ids)):
duplicates = [id for id in all_ids if all_ids.count(id) > 1]
print(f"✗ Found {len(duplicates)} duplicate point IDs")
else:
print(f"✓ No duplicate point IDs ({len(all_ids)} unique)")
ID 매핑에 관한 참고: 소스 시스템이 문자열 ID를 쓰고 마이그레이션 중에 정수 ID로 매핑했다면(또는 그 반대라면), 매핑 파일을 유지하고 그것이 일관적인지 확인해야 해요.
6. 벡터 값 스팟체크 (Vector Value Spot-Check)
작은 샘플에 대해 실제 벡터 값이 일치하는지 확인해요.
import numpy as np
def verify_vectors(source_vectors, qdrant_collection, client, tolerance=1e-6):
"""Spot-check that vector values match between source and Qdrant."""
mismatches = []
for source in source_vectors:
qdrant_points = client.retrieve(
collection_name=qdrant_collection,
ids=[source["id"]],
with_vectors=True,
)
if not qdrant_points:
mismatches.append({"id": source["id"], "issue": "not found"})
continue
source_vec = np.array(source["vector"])
qdrant_vec = np.array(qdrant_points[0].vector)
if not np.allclose(source_vec, qdrant_vec, atol=tolerance):
max_diff = np.max(np.abs(source_vec - qdrant_vec))
mismatches.append({
"id": source["id"],
"max_difference": float(max_diff),
})
return mismatches
예상 허용 오차: 어느 한쪽이라도 양자화(quantization)가 적용되면 정확한 부동소수점 동일성(tolerance=0)은 너무 엄격해요. Qdrant에서 스칼라 양자화를 사용한다면 작은 차이가 있을 거라고 예상해요. 양쪽 모두 양자화를 쓰지 않는다면 값은 정확히 일치해야 해요.
통과 기준 (Passing Criteria)
| 검사 | 통과 | 조사 필요 |
|---|---|---|
| 벡터 수 | 정확히 일치 (또는 문서화된 허용 오차 내) | 설명할 수 없는 차이 |
| 차원 | 정확히 일치 | 불일치 (여기서 멈춤) |
| 거리 메트릭 | Qdrant 등가물로 올바르게 매핑 | 불일치 (여기서 멈춤) |
| 메타데이터 필드 | 모든 소스 필드가 존재 | 누락된 필드 |
| 메타데이터 타입 | 타입 보존 또는 의도적으로 변환됨 | 예상치 못한 타입 변경 |
| 메타데이터 값 | 스팟체크 샘플 일치 | 1% 이상 불일치율 |
| 포인트 ID | 중복 없음, 모든 소스 ID 존재 | 누락되거나 중복된 ID |
| 벡터 값 | 허용 오차 내 (양자화 없으면 1e-6) | 허용 오차 초과 차이 |
다음 단계: 검색 품질 검증 (Search Quality Verification)
더 알아보기 (Learn more)
- 마이그레이션 전 기준점 잡기 — 베이스라인을 만드는 방법
- 검색 품질 검증 — 검색 품질까지 확인하는 단계
- 마이그레이션 가이드 전체 — 마이그레이션 과정 총정리