| """One-off: collapse duplicate reference uploads (metadata + disk files). |
| |
| Keeps one library row and one physical file per unique document content. Shared |
| FAISS chunks are untouched — only ``compat_documents`` rows and surplus files |
| under ``reference_uploads/`` are removed. |
| |
| Usage:: |
| |
| python -m backend.scripts.dedupe_document_records [tenant_id ...] |
| |
| With no tenant ids, all tenants under ``DATA_DIR/tenants/`` are processed. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import sys |
|
|
| from backend.rag.store import get_rag_store |
| from backend.rag.types import TIER_REFERENCE |
| from backend.storage import tenant_store |
| from backend.storage.document_library import dedupe_tenant_storage |
| from backend.storage.report_session import list_documents |
|
|
|
|
| def dedupe_tenant(tenant_id: str) -> dict: |
| store = get_rag_store() |
| before_chunks = store.count(tenant_id, TIER_REFERENCE) |
| stats = dedupe_tenant_storage(tenant_id) |
| after_chunks = store.count(tenant_id, TIER_REFERENCE) |
| return { |
| "tenant": tenant_id, |
| "docs_before": stats["docs_before"], |
| "docs_after": stats["docs_after"], |
| "records_removed": stats["records_removed"], |
| "files_removed": stats["files_removed"], |
| "ref_chunks_before": before_chunks, |
| "ref_chunks_after": after_chunks, |
| } |
|
|
|
|
| def main(argv: list[str]) -> int: |
| tenants = argv[1:] |
| if not tenants: |
| root = tenant_store.tenant_root("").parent |
| tenants = ( |
| [p.name for p in root.iterdir() if p.is_dir()] if root.is_dir() else [] |
| ) |
| for tenant_id in tenants: |
| res = dedupe_tenant(tenant_id) |
| if res["records_removed"] or res["files_removed"]: |
| print(res) |
| assert ( |
| res["ref_chunks_before"] == res["ref_chunks_after"] |
| ), f"chunk count changed for {tenant_id}: {res}" |
| assert len(list_documents(tenant_id)) == res["docs_after"] |
| print("done") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main(sys.argv)) |
|
|