Upload prepare_fulltext_index.py
Browse files- prepare_fulltext_index.py +90 -0
prepare_fulltext_index.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Activate a verified CCRD index generation from a mounted HF Storage Bucket."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import hashlib
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import sqlite3
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 14 |
+
FULLTEXT_DIR = Path(os.environ.get("CCRD_FULLTEXT_DIR", str(BASE_DIR / "data/fulltext")))
|
| 15 |
+
BUCKET_DIR = os.environ.get("CCRD_INDEX_BUCKET_DIR", "").strip()
|
| 16 |
+
TOKENIZER_VERSION = "cjk-bigram-boundary-fts5-v5"
|
| 17 |
+
SOURCES = ("CCRD", "CW")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def sha256(path: Path) -> str:
|
| 21 |
+
digest = hashlib.sha256()
|
| 22 |
+
with path.open("rb") as handle:
|
| 23 |
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
| 24 |
+
digest.update(chunk)
|
| 25 |
+
return digest.hexdigest()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def validated_database(bucket: Path, source: str, details: object) -> Path:
|
| 29 |
+
if not isinstance(details, dict):
|
| 30 |
+
raise ValueError(f"{source}: missing manifest details")
|
| 31 |
+
relative_path = details.get("path")
|
| 32 |
+
expected_hash = details.get("sha256")
|
| 33 |
+
expected_documents = details.get("documents")
|
| 34 |
+
expected_fts_rows = details.get("fts_rows")
|
| 35 |
+
if not isinstance(relative_path, str) or not relative_path.startswith("generations/"):
|
| 36 |
+
raise ValueError(f"{source}: invalid database path")
|
| 37 |
+
if not isinstance(expected_hash, str) or len(expected_hash) != 64:
|
| 38 |
+
raise ValueError(f"{source}: invalid database hash")
|
| 39 |
+
if not isinstance(expected_documents, int) or not isinstance(expected_fts_rows, int):
|
| 40 |
+
raise ValueError(f"{source}: invalid document counts")
|
| 41 |
+
|
| 42 |
+
path = (bucket / relative_path).resolve()
|
| 43 |
+
if bucket not in path.parents or not path.is_file():
|
| 44 |
+
raise ValueError(f"{source}: database is outside the bucket or missing")
|
| 45 |
+
if sha256(path) != expected_hash:
|
| 46 |
+
raise ValueError(f"{source}: database hash mismatch")
|
| 47 |
+
with sqlite3.connect(f"file:{path}?mode=ro&immutable=1", uri=True) as connection:
|
| 48 |
+
tokenizer = connection.execute("SELECT value FROM metadata WHERE key = 'tokenizer_version'").fetchone()
|
| 49 |
+
documents = int(connection.execute("SELECT COUNT(*) FROM documents").fetchone()[0])
|
| 50 |
+
fts_rows = int(connection.execute("SELECT COUNT(*) FROM content_fts").fetchone()[0])
|
| 51 |
+
integrity = connection.execute("PRAGMA integrity_check").fetchone()[0]
|
| 52 |
+
if not tokenizer or tokenizer[0] != TOKENIZER_VERSION:
|
| 53 |
+
raise ValueError(f"{source}: tokenizer version mismatch")
|
| 54 |
+
if documents != expected_documents or fts_rows != expected_fts_rows or integrity != "ok":
|
| 55 |
+
raise ValueError(f"{source}: database structure mismatch")
|
| 56 |
+
return path
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def main() -> int:
|
| 60 |
+
if not BUCKET_DIR:
|
| 61 |
+
print("ccrd_bucket_index=disabled")
|
| 62 |
+
return 1
|
| 63 |
+
bucket = Path(BUCKET_DIR).resolve()
|
| 64 |
+
manifest_path = bucket / "current.json"
|
| 65 |
+
try:
|
| 66 |
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 67 |
+
if manifest.get("format") != 1 or manifest.get("tokenizer_version") != TOKENIZER_VERSION:
|
| 68 |
+
raise ValueError("invalid manifest version")
|
| 69 |
+
databases = manifest.get("databases")
|
| 70 |
+
if not isinstance(databases, dict):
|
| 71 |
+
raise ValueError("missing databases manifest")
|
| 72 |
+
paths = {source: validated_database(bucket, source, databases.get(source)) for source in SOURCES}
|
| 73 |
+
except Exception as exc:
|
| 74 |
+
print(f"ccrd_bucket_index=unavailable:{type(exc).__name__}:{exc}")
|
| 75 |
+
return 1
|
| 76 |
+
|
| 77 |
+
FULLTEXT_DIR.mkdir(parents=True, exist_ok=True)
|
| 78 |
+
for source, path in paths.items():
|
| 79 |
+
target = FULLTEXT_DIR / f"{source}.sqlite3"
|
| 80 |
+
temporary = target.with_suffix(".sqlite3.bucket")
|
| 81 |
+
temporary.unlink(missing_ok=True)
|
| 82 |
+
temporary.symlink_to(path)
|
| 83 |
+
temporary.replace(target)
|
| 84 |
+
(FULLTEXT_DIR / "build-status.json").write_text(json.dumps({"state": "ready", "source": "bucket"}), encoding="utf-8")
|
| 85 |
+
print("ccrd_bucket_index=ready")
|
| 86 |
+
return 0
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
if __name__ == "__main__":
|
| 90 |
+
raise SystemExit(main())
|