Files
relata/docs/BACKUP_AND_RESTORE.md
T
2026-08-11 10:40:10 +01:00

177 lines
6.0 KiB
Markdown

# 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/<timestamp>/
./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.
```