# Backup and restore ## What must be backed up | Store | Contains | Recoverable without a backup? | |---|---|---| | **MongoDB** | Items, extracted text, chunks, entities, claims, relationships, review history, users, audit | **No.** This is the irreplaceable part. | | **Upload directory** | Original PDF and DOCX bytes | **No.** Only the originals a user supplied. | | **Qdrant** | Embedding vectors | **Yes** — rebuildable from MongoDB by re-embedding. | | `.env` | Configuration | Yes, but keep it: it records which models produced your data. | MongoDB and the upload directory must be backed up **together and consistently**: an item row referring to a file that is not in the backup is a broken record. Qdrant is derived data. Backing it up saves re-embedding time; losing it costs only the time to rebuild. --- ## Backing up ### Quick local snapshot ```bash ./scripts/backup.sh # writes to backups// ./scripts/backup.sh /mnt/backups # or somewhere else ``` The script captures MongoDB and the upload directory together, requests a Qdrant snapshot if one is available, copies the non-secret configuration for reference, and writes a `RESTORE.md` alongside the archives. It honours `RELATA_UPLOAD_DIR`, `RELATA_MONGO_DATABASE`, `RELATA_QDRANT_URL` and `RELATA_QDRANT_COLLECTION`, so a non-default deployment needs no edits. ### Consistency Relata does not use MongoDB transactions across collections, so a dump taken while the workers are running may catch an item mid-pipeline — for example chunks written but embeddings not yet stored. That is **recoverable but untidy**: on restore, reprocess any item whose status is not `ready` or `partial`. For a clean backup, stop the application first. The workers shut down gracefully: in-flight jobs are drained, and any that do not finish keep their leases and return to the queue on restart. For a hot backup, prefer a filesystem or volume snapshot that captures MongoDB's data directory and the upload directory at the same instant. --- ## Restoring ```bash # 1. Stop Relata. docker compose stop app # 2. MongoDB docker compose exec -T mongodb mongorestore \ --archive --gzip --drop < backups/TIMESTAMP/relata-mongo.archive.gz # 3. Original files rm -rf data/uploads tar -xzf backups/TIMESTAMP/uploads.tar.gz -C data # 4. Start Relata. Indexes are created automatically at startup. docker compose start app # or: make dev ``` ### After restoring Check the counts on the settings page. If **vectors stored** does not match **chunks in MongoDB**, Qdrant is out of step. Rebuild: **Settings → Re-embed workspace**, or: ```bash curl -s -b cj.txt -X POST http://127.0.0.1:8080/settings/reembed \ -H "X-CSRF-Token: $CSRF" ``` Re-embedding processes in bounded batches, re-enqueuing itself as it goes, so a large workspace does not hold one lease for hours. Progress is visible on the queue page. ### Restoring without Qdrant data Entirely supported — vectors are derived. ```bash # Recreate the collection and rebuild every vector. curl -X DELETE http://127.0.0.1:6333/collections/relata_chunks # Relata recreates it at startup, then: # Settings → Re-embed workspace ``` Between deletion and completion, meaning-based search returns nothing and search responses say so in their notes. Keyword, entity and relationship search continue to work. --- ## Verifying a restore ```bash # Everything reports healthy curl -s http://127.0.0.1:8080/health/ready | jq '.status, .checks' # Counts match the source system docker compose exec -T mongodb mongosh relata --quiet --eval ' ["items","chunks","entities","claims","relationships","users"].forEach( c => print(c.padEnd(16) + db[c].countDocuments({})))' # Vector count matches the chunk count curl -s -X POST http://127.0.0.1:6333/collections/relata_chunks/points/count \ -H 'Content-Type: application/json' -d '{"exact":true}' | jq .result.count # Any item left mid-pipeline docker compose exec -T mongodb mongosh relata --quiet --eval ' db.items.find({status: {$nin: ["ready","partial","tombstoned"]}}, {title:1, status:1}).forEach(printjson)' ``` Reprocess anything the last query returns, from the item page or: ```bash curl -s -b cj.txt -X POST "http://127.0.0.1:8080/api/v1/items/$ID/reprocess" \ -H 'Content-Type: application/json' -H "X-CSRF-Token: $CSRF" \ -d '{"reason":"restored from backup"}' ``` Reprocessing is idempotent: chunks and claims are replaced, entities merge on their normalised key, and relationships upsert on a dedupe key **without overwriting human review decisions**. --- ## What a backup preserves Worth knowing, because it is the point of the audit design: - **Original content**, exactly as supplied. - **Review history** — every confirmation, rejection and type change, with actor and timestamp, append-only. - **Original model proposals**, retained even where a person overruled them. - **Processing runs**, recording which parser, embedding model, analysis model and scoring version produced each generation of derived data. - **Audit events** for logins, ingestion, deletion, reprocessing and relationship decisions. Restoring therefore recovers not just the data but the record of how it came to be as it is. --- ## Retention and disposal Deleting an item removes its chunks, vectors, claims, orphaned entities, relationships and the stored original. Audit events and processing runs are **deliberately retained**: they record that something existed and was removed, without retaining its content. Backups are not covered by that deletion. If a user asks for their data to be removed, older backups will still contain it until they age out — plan a retention window accordingly. --- ## Disaster recovery drill Worth doing once, before you need it: ```bash # 1. Back up. ./scripts/backup.sh # 2. Destroy everything. make reset # 3. Restore, then create an account if the users collection was not restored. # (mongorestore --drop restores users too, so a restore normally includes them.) # 4. Verify with the checks above, then run a demonstration search. ```