Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
97 lines
4.4 KiB
Python
97 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Copy existing vectors to an isolated BM25 collection, without re-embedding.
|
|
|
|
The manifest is an export of the acceptance DB's chunk IDs. Source collections
|
|
are read-only; acknowledgements and a manifest digest make retries resumable.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import time
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
FIELDS = ['id', 'embedding', 'content', 'source_id', 'source_type', 'chunk_id',
|
|
'knowledge_id', 'knowledge_base_id', 'tag_id', 'is_enabled']
|
|
|
|
|
|
def post(base, path, payload):
|
|
request = urllib.request.Request(base + '/v2/vectordb/' + path,
|
|
data=json.dumps(payload).encode(),
|
|
headers={'Content-Type': 'application/json'})
|
|
with urllib.request.urlopen(request, timeout=120) as response:
|
|
result = json.load(response)
|
|
if result.get('code') != 0:
|
|
raise RuntimeError(f"{path}: {result.get('message', result.get('code'))}")
|
|
return result.get('data')
|
|
|
|
|
|
def save(path, data):
|
|
temporary = path.with_suffix('.tmp')
|
|
temporary.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n')
|
|
os.replace(temporary, path)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--manifest', type=Path, required=True)
|
|
parser.add_argument('--state', type=Path, required=True)
|
|
parser.add_argument('--target', default='yawan_acceptance_hybrid_512')
|
|
parser.add_argument('--source', default='weknora_embeddings_512')
|
|
args = parser.parse_args()
|
|
if not args.target.startswith('yawan_acceptance_') or args.target == args.source:
|
|
parser.error('target must be a distinct acceptance collection')
|
|
base = 'http://127.0.0.1:19530'
|
|
raw = args.manifest.read_bytes()
|
|
manifest = json.loads(raw)
|
|
digest = hashlib.sha256(raw).hexdigest()
|
|
state = json.loads(args.state.read_text()) if args.state.exists() else {
|
|
'manifest_sha256': digest, 'source': args.source, 'target': args.target,
|
|
'offset': 0, 'copied': 0, 'missing': [], 'started_at': time.time(),
|
|
}
|
|
for key, value in [('manifest_sha256', digest), ('source', args.source), ('target', args.target)]:
|
|
if state[key] != value:
|
|
raise RuntimeError(f'{key} differs from checkpoint')
|
|
description = post(base, 'collections/describe', {'collectionName': args.target})
|
|
if 'content_sparse' not in {f['name'] for f in description['fields']}:
|
|
raise RuntimeError('target must have BM25 schema before copying')
|
|
for offset in range(state['offset'], len(manifest), 256):
|
|
batch = manifest[offset:offset + 256]
|
|
expected = {x['id']: x for x in batch}
|
|
records = post(base, 'entities/query', {
|
|
'collectionName': args.source,
|
|
'filter': 'chunk_id in ' + json.dumps(list(expected)),
|
|
'outputFields': FIELDS, 'limit': len(batch) * 2,
|
|
'consistencyLevel': 'Strong',
|
|
})
|
|
found = set()
|
|
for row in records:
|
|
entry = expected.get(row['chunk_id'])
|
|
if not entry or row['knowledge_base_id'] != entry['knowledge_base_id'] or row['knowledge_id'] != entry['knowledge_id']:
|
|
raise RuntimeError('source scope differs from acceptance manifest')
|
|
if row['chunk_id'] in found:
|
|
raise RuntimeError('duplicate source vectors for chunk')
|
|
found.add(row['chunk_id'])
|
|
if records:
|
|
result = post(base, 'entities/upsert', {'collectionName': args.target, 'data': records})
|
|
if result.get('upsertCount') != len(records):
|
|
raise RuntimeError('target did not acknowledge every vector')
|
|
state['missing'].extend(sorted(expected.keys() - found))
|
|
state['copied'] += len(records)
|
|
state['offset'] = offset + len(batch)
|
|
state['updated_at'] = time.time()
|
|
save(args.state, state)
|
|
if offset % (256 * 20) == 0 or state['offset'] == len(manifest):
|
|
print(json.dumps({'processed': state['offset'], 'total': len(manifest),
|
|
'copied': state['copied'], 'missing': len(state['missing'])}), flush=True)
|
|
state['complete'] = not state['missing']
|
|
save(args.state, state)
|
|
print(json.dumps({'complete': state['complete'], 'copied': state['copied'],
|
|
'missing': len(state['missing'])}), flush=True)
|
|
return 0 if state['complete'] else 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|