108 lines
5.8 KiB
Python
108 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""Read-only vector copy audit and distinct-query acceptance latency sample.
|
||
|
|
|
||
|
|
Reports only counts, checksums, and timings; never knowledge prose or credentials.
|
||
|
|
"""
|
||
|
|
import hashlib
|
||
|
|
import json
|
||
|
|
import math
|
||
|
|
import statistics
|
||
|
|
import time
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from milvus_copy_acceptance import FIELDS, post
|
||
|
|
from milvus_verify_acceptance import (ACCEPTANCE, Client, VerificationError,
|
||
|
|
inspect_results, read_object, search_payload)
|
||
|
|
|
||
|
|
SOURCE = 'weknora_embeddings_512'
|
||
|
|
TARGET = 'yawan_acceptance_hybrid_512'
|
||
|
|
MILVUS = 'http://127.0.0.1:19530'
|
||
|
|
QUERIES = {
|
||
|
|
'1': ['产品资料应如何整理和解释', '介绍产品时需要说明哪些使用方法'],
|
||
|
|
'2': ['如何安排由浅入深的练习流程', '基础练习中怎样判断动作是否正确'],
|
||
|
|
'3': ['怎样通过沟通理解对方的需求', '遇到紧张情绪时如何调整表达方式'],
|
||
|
|
'4': ['如何描述亲密关系中的感受和期待', '怎样建立双方舒适的互动氛围'],
|
||
|
|
'5': ['学习新技巧时如何分解具体步骤', '练习过程有哪些常见错误需要纠正'],
|
||
|
|
'6': ['如何理解客户需求并解释产品价值', '怎样回应疑问并帮助客户作出选择'],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def query(collection, filter_text, fields, limit=100):
|
||
|
|
return post(MILVUS, 'entities/query', {
|
||
|
|
'collectionName': collection, 'filter': filter_text,
|
||
|
|
'outputFields': fields, 'limit': limit, 'consistencyLevel': 'Strong',
|
||
|
|
})
|
||
|
|
|
||
|
|
|
||
|
|
def digest(row):
|
||
|
|
return hashlib.sha256(json.dumps({field: row[field] for field in FIELDS},
|
||
|
|
sort_keys=True, ensure_ascii=False,
|
||
|
|
separators=(',', ':')).encode()).hexdigest()
|
||
|
|
|
||
|
|
|
||
|
|
def run():
|
||
|
|
state = read_object(ACCEPTANCE / 'legacy-migration-state.json', 'Migration state')
|
||
|
|
copy = read_object(ACCEPTANCE / 'hybrid-copy-state.json', 'Copy state')
|
||
|
|
manifest = json.loads((ACCEPTANCE / 'hybrid-chunk-manifest.json').read_text())
|
||
|
|
report = {'source': SOURCE, 'target': TARGET, 'storage': [], 'queries': [],
|
||
|
|
'scope': 'Six migrated business knowledge bases only',
|
||
|
|
'latency_scope': 'Sequential distinct queries, warmed service; cache not cleared or instrumented',
|
||
|
|
'started_at': time.time()}
|
||
|
|
business_ids = {kb['id'] for kb in state['knowledge_bases'].values()}
|
||
|
|
missing_ids = set(copy['missing'])
|
||
|
|
business_missing = [row['id'] for row in manifest
|
||
|
|
if row['id'] in missing_ids and row['knowledge_base_id'] in business_ids]
|
||
|
|
report['missing_business_chunks'] = len(business_missing)
|
||
|
|
report['missing_historical_test_chunks'] = len(missing_ids) - len(business_missing)
|
||
|
|
for legacy_id, kb in sorted(state['knowledge_bases'].items()):
|
||
|
|
entries = [row for row in manifest if row['knowledge_base_id'] == kb['id']]
|
||
|
|
filter_text = 'knowledge_base_id == ' + json.dumps(kb['id'])
|
||
|
|
counts = {name: int(query(name, filter_text, ['count(*)'])[0]['count(*)'])
|
||
|
|
for name in (SOURCE, TARGET)}
|
||
|
|
positions = sorted({0, len(entries) // 2, len(entries) - 1})
|
||
|
|
ids = [entries[index]['id'] for index in positions]
|
||
|
|
samples = {}
|
||
|
|
for name in (SOURCE, TARGET):
|
||
|
|
rows = query(name, 'chunk_id in ' + json.dumps(ids), FIELDS, len(ids))
|
||
|
|
samples[name] = {row['chunk_id']: digest(row) for row in rows}
|
||
|
|
equal = (set(samples[SOURCE]) == set(ids) and samples[SOURCE] == samples[TARGET])
|
||
|
|
report['storage'].append({
|
||
|
|
'legacy_kb': legacy_id, 'kb_id': kb['id'], 'expected_chunks': len(entries),
|
||
|
|
'source_count': counts[SOURCE], 'target_count': counts[TARGET],
|
||
|
|
'sample_count': len(ids), 'sample_fields_equal': equal,
|
||
|
|
'passed': equal and all(count == len(entries) for count in counts.values()),
|
||
|
|
})
|
||
|
|
credentials = read_object(ACCEPTANCE / 'migration-token.json', 'Migration credentials')
|
||
|
|
client = Client(45)
|
||
|
|
status, login, _ = client.request('/api/v1/auth/login', payload={
|
||
|
|
'email': credentials['email'], 'password': credentials['password'],
|
||
|
|
})
|
||
|
|
if status != 200 or login.get('active_tenant', {}).get('id') != state['tenant_id']:
|
||
|
|
raise VerificationError('Unexpected login scope')
|
||
|
|
for legacy_id, queries in sorted(QUERIES.items()):
|
||
|
|
kb_id = state['knowledge_bases'][legacy_id]['id']
|
||
|
|
for index, text in enumerate(queries):
|
||
|
|
status, result, latency = client.request(
|
||
|
|
'/api/v1/knowledge-bases/' + kb_id + '/hybrid-search', login['token'],
|
||
|
|
search_payload(text, 'hybrid', 10))
|
||
|
|
check = inspect_results(result, kb_id, 10, 'hybrid') if status == 200 else {'passed': False}
|
||
|
|
report['queries'].append({'legacy_kb': legacy_id, 'query_index': index,
|
||
|
|
'query_sha256': hashlib.sha256(text.encode()).hexdigest(),
|
||
|
|
'status': status, 'latency_ms': latency, **check})
|
||
|
|
latencies = sorted(row['latency_ms'] for row in report['queries'])
|
||
|
|
report['latency_ms'] = {'min': min(latencies), 'median': statistics.median(latencies),
|
||
|
|
'p95_nearest_rank': latencies[math.ceil(len(latencies) * .95) - 1],
|
||
|
|
'max': max(latencies), 'count': len(latencies)}
|
||
|
|
report['passed'] = (not business_missing and all(row['passed'] for row in report['storage'])
|
||
|
|
and all(row['passed'] for row in report['queries']))
|
||
|
|
report['finished_at'] = time.time()
|
||
|
|
output = ACCEPTANCE / 'milvus-storage-and-latency.json'
|
||
|
|
output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
|
||
|
|
print(json.dumps({key: report[key] for key in (
|
||
|
|
'passed', 'missing_business_chunks', 'missing_historical_test_chunks', 'latency_ms')}, indent=2))
|
||
|
|
return 0 if report['passed'] else 1
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
raise SystemExit(run())
|