Files
redmine/docs/semantic_index_deployment_runbook.md
Jason Thistlethwaite bd26c8894f Add production rollout tooling and semantic index ops docs
Capture the production plugin rollout workflow and Qdrant validation steps so operations stay repeatable. Also harden redMCP stdio/schema compatibility to keep diverse MCP clients and validators working.
2026-05-06 22:18:02 -04:00

354 lines
9.5 KiB
Markdown

# Semantic Index Deployment Runbook
This runbook captures the current deployment shape for the Redmine semantic
index. It is written for the LAN test server first, with the same steps intended
to carry forward to production after paths and secrets are adjusted.
The latest LAN validation record is in
`docs/semantic_index_predeployment_validation.md`.
## Deployable Files
Copy or update these tracked paths together:
- `semantic_index/`
- `tests/semantic_index/`
- `deploy/semantic-index/`
- `docs/semantic_index_production_notes.md`
- `docs/semantic_index_deployment_runbook.md`
- `docs/semantic_index_predeployment_validation.md`
- `docs/redmine_issue_api_helpdesk_include.md`
The Helpdesk contact metadata dependency is the Redmine plugin API patch
documented in `docs/redmine_issue_api_helpdesk_include.md`. Deploy that plugin
patch before expecting Helpdesk contact fields in indexed results.
Do not copy local-only runtime files:
- `semantic_index/.env`
- `.cache/`
- `.venv/`
- `__pycache__/`
- Qdrant storage snapshots or rollback tarballs unless deliberately restoring
## Runtime Prerequisites
Python runtime dependencies:
```sh
pip install openai qdrant-client fastapi uvicorn
```
Qdrant is expected to run on the larger host and be reachable from the semantic
index host through `QDRANT_URL`. The current collection default is
`redmine_semantic_sample`.
Qdrant Docker example:
```sh
docker run -p 6333:6333 -p 6334:6334 \
-v qdrant_storage:/qdrant/storage \
qdrant/qdrant
```
Before destructive maintenance, create a Qdrant snapshot or preserve the Docker
volume.
## WireGuard Topology
Current WireGuard endpoints:
- production server: `10.11.0.100`
- LAN server: `10.11.0.105`
When Qdrant is hosted on the LAN server, keep it reachable on the WireGuard
address and point production semantic-index traffic at:
```sh
QDRANT_URL=http://10.11.0.105:6333
```
Do not bind production-hosted Qdrant to `10.11.0.105`; that address belongs to
the LAN host.
## Environment
For a production-style install, use:
- code: `/opt/semantic-index`
- environment file: `/etc/semantic-index.env`
- refresh state: `/var/lib/semantic-index/refresh_state.json`
- refresh logs: `/var/log/semantic-index`
Create `/etc/semantic-index.env` from
`deploy/semantic-index/semantic-index.env.example` and fill secrets on the
target host:
```sh
OPENAI_API_KEY=
QDRANT_URL=http://10.11.0.105:6333
QDRANT_API_KEY=
QDRANT_COLLECTION=redmine_semantic_sample
REDMINE_URL=http://redmine-host
REDMINE_API_KEY=
REDMINE_PROJECT_IDENTIFIER=
REDMINE_SAMPLE_LIMIT=500
SEMANTIC_INDEX_HOST=127.0.0.1
SEMANTIC_INDEX_PORT=8787
SEMANTIC_INDEX_API_KEY=
SEMANTIC_INDEX_REFRESH_STATE_PATH=/var/lib/semantic-index/refresh_state.json
```
Recommended production-style refresh overrides:
```sh
SEMANTIC_INDEX_PROJECT_LIMITS='customer-service=500,hiring=200,todo-jason=200,sales-inbox=100,business-development=100,dock-scheduling=100,prep-standardization=100'
SEMANTIC_INDEX_LOG_DIR=/var/log/semantic-index
SEMANTIC_INDEX_STATE_PATH=/var/lib/semantic-index/refresh_state.json
SEMANTIC_INDEX_OVERLAP_MINUTES=15
```
Keep `SEMANTIC_INDEX_API_KEY` set when binding outside localhost. Do not commit
API keys or `.env` files.
## Systemd Templates
Templates live in `deploy/semantic-index/`:
```text
install.sh
semantic-index.service
semantic-index-refresh.service
semantic-index-refresh.timer
semantic-index.env.example
```
Use the installer first. It defaults to dry-run:
```sh
deploy/semantic-index/install.sh
```
Apply the install:
```sh
deploy/semantic-index/install.sh --apply
```
Optionally start only the HTTP service after installing:
```sh
deploy/semantic-index/install.sh --apply --start
```
The installer creates `/opt/semantic-index`, `/var/lib/semantic-index`, and
`/var/log/semantic-index`; copies the deploy unit; creates
`/etc/semantic-index.env` only if it does not already exist; installs systemd
unit files; and runs local validation. It does not run backfill, does not enable
the refresh timer, and never passes `--force-rebuild`.
Manual install shape, if the installer cannot be used:
```sh
sudo mkdir -p /opt/semantic-index /var/lib/semantic-index /var/log/semantic-index
sudo rsync -a \
--exclude '.env' \
--exclude '__pycache__/' \
--exclude '*.pyc' \
semantic_index tests docs deploy dist /opt/semantic-index/
sudo cp deploy/semantic-index/semantic-index.env.example /etc/semantic-index.env
sudo install -m 0644 deploy/semantic-index/semantic-index.service /etc/systemd/system/semantic-index.service
sudo install -m 0644 deploy/semantic-index/semantic-index-refresh.service /etc/systemd/system/semantic-index-refresh.service
sudo install -m 0644 deploy/semantic-index/semantic-index-refresh.timer /etc/systemd/system/semantic-index-refresh.timer
```
After editing `/etc/semantic-index.env`, validate manually before enabling the
timer:
```sh
sudo systemctl daemon-reload
sudo systemctl start semantic-index.service
sudo systemctl status semantic-index.service
sudo systemctl start semantic-index-refresh.service
sudo journalctl -u semantic-index-refresh.service -n 100 --no-pager
```
Enable the timer only after manual dry-run and `--apply` logs look normal:
```sh
sudo systemctl enable --now semantic-index-refresh.timer
```
## Initial Validation
Run syntax and test checks after copying code:
```sh
.venv/bin/python -m py_compile semantic_index/*.py
.venv/bin/python -m unittest discover -s tests/semantic_index
bash -n semantic_index/refresh.sh
```
Confirm service startup:
```sh
uvicorn semantic_index.app:app --host 127.0.0.1 --port 8787
curl -sS http://127.0.0.1:8787/health
```
If `SEMANTIC_INDEX_API_KEY` is set:
```sh
curl -sS -H "Authorization: Bearer $SEMANTIC_INDEX_API_KEY" \
http://127.0.0.1:8787/projects
```
## Initial Backfill
Preview Redmine mapping before writing to Qdrant:
```sh
.venv/bin/python -m semantic_index inspect preview-redmine \
--project customer-service \
--limit 5
```
Backfill the current balanced sample:
```sh
.venv/bin/python -m semantic_index --backfill-redmine-projects \
--project-limits customer-service=500,hiring=200,todo-jason=200,sales-inbox=100,business-development=100,dock-scheduling=100,prep-standardization=100
```
Audit the result:
```sh
.venv/bin/python -m semantic_index inspect audit --source redmine --limit 5000
.venv/bin/python -m semantic_index inspect smoke-search --project customer-service
```
Expected broad shape for the current LAN sample is roughly:
- Customer Service is the largest project.
- Helpdesk tickets have contact metadata.
- Internal projects may have no Helpdesk contact metadata.
- `attachments=0`.
## Routine Refresh
Use the wrapper for production-style refresh. It defaults to dry-run:
```sh
semantic_index/refresh.sh
```
Small smoke check:
```sh
SEMANTIC_INDEX_PROJECT_LIMITS='customer-service=5' semantic_index/refresh.sh
```
Apply refresh manually:
```sh
semantic_index/refresh.sh --apply
```
Installed wrappers can also be called by absolute path, for example
`/opt/semantic-index/semantic_index/refresh.sh`. The wrapper uses its own
install root as the working directory and reads defaults from
`/etc/semantic-index.env` when that file is readable.
Review the log path printed by the wrapper. For a healthy routine run after
state exists, expect:
- `scanned_issues` greater than or equal to `detail_fetched_issues`
- old issues counted under `skipped_issues`
- `would_embed_documents` and `embedded_documents` near zero when Redmine has
not changed
- no scheduled use of `--force-rebuild`
Only schedule the wrapper after manual dry-run and apply logs look normal.
Cron shape, when ready:
```cron
*/30 * * * * cd /home/iadnah/redmine && semantic_index/refresh.sh --apply
```
## Search Validation
HTTP search:
```sh
semantic_index/search.sh "goods return" customer-service 3
semantic_index/search.sh "candidate follow up" hiring 5
```
CLI inspection:
```sh
.venv/bin/python -m semantic_index inspect search "goods return" \
--project customer-service \
--limit 3
.venv/bin/python -m semantic_index inspect list \
--source redmine \
--project customer-service \
--limit 10
```
MCP stdio:
```sh
.venv/bin/python -m semantic_index --mcp-stdio
```
Available tools:
- `semantic_search`
- `semantic_get_document`
- `semantic_list_projects`
- `semantic_backfill_redmine_sample`
- `semantic_refresh_redmine`
## Rollback
Code rollback:
- Stop `uvicorn` or the service manager unit.
- Restore the previous `semantic_index/` code.
- Restore the previous Redmine Helpdesk plugin patch if contact metadata broke.
- Restart the service.
Index rollback options:
- Restore a Qdrant snapshot or preserved Docker volume.
- Or rebuild from Redmine with the known-good code using the multi-project
backfill command above.
Refresh rollback:
- Disable cron/systemd schedule if enabled.
- Preserve the failing log file for diagnosis.
- If the refresh state is wrong, move the state file aside rather than editing
it in place:
```sh
mv .cache/semantic_index/refresh_state.json .cache/semantic_index/refresh_state.json.bad
```
The next refresh will behave like a first refresh for state purposes, while the
`source_hash` guard still prevents embedding unchanged documents.
## Production Readiness Checklist
- Redmine API key is scoped appropriately and stored outside git.
- Qdrant URL and collection are confirmed.
- Qdrant snapshot/export path is known.
- Helpdesk API patch is deployed and validated.
- HTTP service is bound only to trusted localhost/LAN as intended.
- `SEMANTIC_INDEX_API_KEY` is set for non-localhost use.
- Initial backfill audit and smoke searches pass.
- Refresh dry-run and apply logs show expected low embedding counts.
- `--force-rebuild` is documented as manual-only.