Skip to content

Commit 531e7e6

Browse files
author
Alexandra Pavlyshina
committed
measure-evaluate: document performance indexes, clarify POST/GET semantics
1 parent d5e90ed commit 531e7e6

1 file changed

Lines changed: 60 additions & 5 deletions

File tree

aidbox-custom-operations/measure-evaluate/install-to-existing-aidbox.md

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ Guide for deploying `Measure/$evaluate-measure` into an **already-running Aidbox
1010
| 1 App resource in Aidbox (wires the operation) | PostgreSQL data (Patient, Encounter, Condition, …) |
1111
| ~8 SQL views over your FHIR tables (read-only projections) | All clinical resources — we don't touch them |
1212
| 1 `concepts` table (terminology: 104 ValueSets, ~9 650 codes) | Your authentication, auth policies, access control |
13-
| 12 measure SQL definitions (one file per measure) | Your existing custom operations, Apps, indexes |
13+
| ~17 btree indexes on `Patient`/`Encounter`/`Condition`/… JSONB paths used by the measures | Your existing custom operations, Apps |
14+
| 12 measure SQL definitions (one file per measure) | Your data, schemas, and other indexes |
1415

1516
## Prerequisites
1617

@@ -68,11 +69,14 @@ docker logs sql-evaluate-app --tail 15
6869
# "Running on http://0.0.0.0:8090"
6970
```
7071

71-
The app only exposes `POST /` (called by Aidbox). Direct `GET` requests will return 404/405 — that's expected.
72+
The app only exposes `POST /` — that's the HTTP-RPC entry point Aidbox dispatches to. Direct calls to the Flask container with `GET` or any other path return 404/405 — Aidbox is the front door. Users invoke the operation via Aidbox at `Measure/$evaluate-measure` (registered in Step 2), and that endpoint supports both `POST` and `GET`.
7273

7374
## Step 2. Register the App resource in Aidbox
7475

75-
This tells Aidbox to route `POST /Measure/$evaluate-measure` calls to your sql-evaluate-app.
76+
This tells Aidbox to route both `POST` and `GET` calls on `Measure/$evaluate-measure` to your sql-evaluate-app. The two are semantically distinct:
77+
78+
- **`POST`** runs the measure **and persists** the resulting `MeasureReport` in Aidbox (the `measure-evaluate` operation below).
79+
- **`GET`** runs the measure and returns the report without persisting it (the `measure-evaluate-get` operation below) — useful for ad-hoc / read-only validation.
7680

7781
```bash
7882
curl -u <admin>:<password> -X PUT \
@@ -116,6 +120,7 @@ One command loads all non-clinical artifacts into your Aidbox:
116120
- Shared SQL views (8 flat projections over FHIR JSONB)
117121
- `concepts` table schema + 104 ValueSets × 9 651 codes
118122
- Shared exclusion helper functions
123+
- **Performance indexes** (~17 btree indexes on JSONB paths used by the measures — without these, measure SQL at scale is 100–1000× slower due to sequential scans)
119124
- CodeSystem bundle + 12 FHIR `Measure` / `Library` resources
120125
- Stub `Organization`, `Practitioner`, `Device` resources referenced by measures
121126

@@ -172,14 +177,21 @@ These SQL files are loaded by the Flask app from `$REPO_ROOT/sql/measures/` **at
172177

173178
## Step 5. Evaluate a measure against your data
174179

175-
Single-patient report (R4 `reportType=subject`):
180+
Single-patient report (R4 `reportType=subject`), **persisted** in Aidbox via `POST`:
176181

177182
```bash
178183
curl -u <admin>:<password> -X POST \
179184
'https://aidbox.example.com/Measure/$evaluate-measure?measure=cms130&subject=Patient/<your-patient-id>&reportType=subject&periodStart=2024-01-01&periodEnd=2024-12-31'
180185
```
181186

182-
Population-level report across all your patients:
187+
Same call but **read-only** via `GET` — the report is computed and returned but not stored:
188+
189+
```bash
190+
curl -u <admin>:<password> -X GET \
191+
'https://aidbox.example.com/Measure/$evaluate-measure?measure=cms130&subject=Patient/<your-patient-id>&reportType=subject&periodStart=2024-01-01&periodEnd=2024-12-31'
192+
```
193+
194+
Population-level report across all your patients (either `POST` or `GET` works the same way):
183195

184196
```bash
185197
curl -u <admin>:<password> -X POST \
@@ -255,6 +267,49 @@ Quickest debug: evaluate for one known-good patient (`?subject=Patient/<id>&repo
255267

256268
Re-run `python3 setup.py --skip-clinical` — it's idempotent (`DELETE WHERE valueset_url = X` before INSERT per ValueSet).
257269

270+
### `$evaluate-measure` times out or is very slow on a large dataset
271+
272+
Most often: the **performance indexes are missing**. They are part of `sql/03-performance.sql`, executed automatically by `setup.py`. If you installed an earlier version of this sample (before indexes were bundled), you can apply them standalone without re-running the full setup:
273+
274+
```bash
275+
# Set credentials once (avoid pasting them inline)
276+
export AIDBOX_USER=<your-admin-user>
277+
export AIDBOX_PASS=<your-admin-password>
278+
export AIDBOX_URL=https://aidbox.example.com
279+
280+
# Inspect the file
281+
cat sql/03-performance.sql
282+
283+
# Apply via Aidbox $sql (the whole file is sent in one transactional call)
284+
python3 -c "
285+
import json, os, urllib.request, base64
286+
auth = base64.b64encode(f\"{os.environ['AIDBOX_USER']}:{os.environ['AIDBOX_PASS']}\".encode()).decode()
287+
url = f\"{os.environ['AIDBOX_URL']}/\$sql\"
288+
with open('sql/03-performance.sql') as f:
289+
body = json.dumps([f.read()]).encode()
290+
req = urllib.request.Request(url, method='POST', data=body)
291+
req.add_header('Authorization', f'Basic {auth}')
292+
req.add_header('Content-Type', 'application/json')
293+
urllib.request.urlopen(req, timeout=300)
294+
print('OK')
295+
"
296+
297+
# Or, if you have direct psql access:
298+
PGPASSWORD="$AIDBOX_PASS" psql -h <host> -U "$AIDBOX_USER" -d <aidbox_db> -f sql/03-performance.sql
299+
```
300+
301+
Verify the indexes landed:
302+
303+
```bash
304+
curl -u "$AIDBOX_USER:$AIDBOX_PASS" -X POST \
305+
"$AIDBOX_URL/\$sql" \
306+
-H 'Content-Type: application/json' \
307+
-d '["SELECT count(*) AS index_count FROM pg_indexes WHERE indexname LIKE '"'"'ix_%'"'"'"]'
308+
# → [{"index_count": 17}] (or close to it)
309+
```
310+
311+
The single most important index for single-patient queries is `ix_<resource>_subject` on `(resource->'subject'->>'id')` — without it, every patient-scoped query degrades to a full sequential scan of the resource table.
312+
258313
## Uninstall
259314

260315
Complete removal from your Aidbox:

0 commit comments

Comments
 (0)