4. Semantic search over notes¶
Setting: a clinician asks a plain-language question ("has this patient ever had a heart attack?") and the system searches the patient's 10 most recent clinical notes (Synthea text, FHIR R5 DocumentReference).
Why this scenario: retrieval is one of the task categories in the TypeSafe docs, and the notes use clinical vocabulary ("myocardial infarction") that the lay question doesn't. A keyword search for the lay words is the naive baseline.
Cases: 12 queries × 25 patients (about half positive), 300 searches over 2,922 notes.
Ground truth (objective, by regex): a note is relevant if a Synthea (disorder) phrase in it matches the clinical term. No model wrote these labels.
| Question per search | Primitive | Task category |
|---|---|---|
Does notes[i] show the answer is yes? (one per note) |
Noul ×10 | Search / Retrieval |
| Which note is the best evidence? | Choice (10 notes + none) | Ranking |
| Taking all notes together, is the answer yes? | Noul | Detection |
Example request and response¶
One search: the query, the ten notes, and a Noul per note plus the two whole-patient questions. state and questions are built by s4_search.py;
system_one answers every question in one request.
from typesafe_sdk import AsyncTypeSafeClient
state = {
"query": "Has the patient ever had a heart attack?",
"notes": [
{
"id": "note_0",
"date": "2023-01-01",
"text": "\n2023-01-01\n\n# Chief Complaint\nNo complaints.\n\n# History of Present Illness\nShara355 Roseanne877 is a 77 year-old nonhispanic white female. Patient has a history of part-time employment (finding), medication review due (situation), full-tim …"
},
{
"id": "note_1",
"date": "2023-04-27",
"text": "\n2023-04-27\n\n# Chief Complaint\nNo complaints.\n\n# History of Present Illness\nShara355 Roseanne877 is a 77 year-old nonhispanic white female. Patient has a history of part-time employment (finding), fracture of forearm (disorder), medication …"
}
]
}
questions = {
"note_0": {
"type": "noul",
"instructions": "Does `notes[0].text` contain information showing that the answer to `query` is yes?"
},
"best": {
"type": "choice",
"instructions": "Which note is the best evidence for answering `query` with yes?",
"criteria": {
"note_0": "`notes[0]`",
"note_1": "`notes[1]`",
"note_2": "`notes[2]`",
"note_3": "`notes[3]`",
"note_4": "`notes[4]`",
"note_5": "`notes[5]`",
"note_6": "`notes[6]`",
"note_7": "`notes[7]`",
"note_8": "`notes[8]`",
"note_9": "`notes[9]`",
"none": "No note shows the answer is yes"
}
},
"any": {
"type": "noul",
"instructions": "Taking all of `notes` together, is the answer to `query` yes?"
}
}
async with AsyncTypeSafeClient(model="jev-1.13.0") as client:
response = await client.system_one(state, questions)
body = response.raw_http_response.json()
The response body, exactly as recorded:
{
"answers": {
"note_0": {
"type": "noul",
"noul": 0.02
},
"best": {
"type": "choice",
"choice": "note_6",
"confidence": 0.64,
"probabilities": {
"note_1": 0.0,
"note_3": 0.0,
"note_2": 0.0,
"none": 0.0,
"note_7": 0.31,
"note_4": 0.0,
"note_5": 0.0,
"note_9": 0.0,
"note_0": 0.0,
"note_6": 0.68,
"note_8": 0.01
}
},
"any": {
"type": "noul",
"noul": 0.98
}
},
"model": "jev-1.13.0",
"usage": {
"input_tokens": 5475,
"output_tokens": 313
}
}
Request req_01a0b717158c73fab8227773f5f52149 took 655 ms for 12 questions
(5,475 input / 313 output tokens). Abridged for this page: notes shows 2 of 10 entries; notes[0].text truncated from 1,260 characters; notes[1].text truncated from 962 characters; 3 of 12 questions and their answers shown.
Every case of this run, untrimmed, is in results/s4_search.json.
Results¶
| Method | Note precision | Note recall | Note F1 |
|---|---|---|---|
| Lay keyword search | 64% | 25% | 0.35 |
| Jev, all 10 notes in one request | 76% | 100% | 0.86 |
| Jev, one note per request | 67% | 100% | 0.80 |
| Claude Haiku 4.5, all 10 notes in one request | 70% | 100% | 0.83 |
| Patient-level answer | Accuracy | Precision | Recall |
|---|---|---|---|
| "Any" Noul over all notes | 96% | 89% | 100% |
| Max of per-note Nouls (all-in-one request) | 97% | 92% | 100% |
| Max of per-note Nouls (one note per request) | 97% | 92% | 100% |
The best-evidence Choice picked a relevant note (or correctly said none) in 93% of searches. Jev bridges lay and clinical vocabulary almost perfectly: recall is about 100% against the keyword baseline's 25%.
The "false positives" are mostly label gaps¶
Note-level precision looks modest, and isolating each note in its own request made it worse (76% → 67%). The hypothesis being tested was that one note's evidence leaks into judgments of the others. Inspecting the per-note false positives doesn't support it: 209 of 218 show the condition through its treatment or an explicit statement. The regex can't see those, because it only reads (disorder) phrases. Examples include metformin or insulin for diabetes, an ACE inhibitor or thiazide for high blood pressure, donepezil for dementia, and "a documented history of opioid addiction".
Re-scoring with those notes counted as relevant (a post-hoc sensitivity analysis; the patterns are in evaluate2.py):
| Variant | Strict labels: P / R | Strong-evidence labels: P / R |
|---|---|---|
| All 10 notes in one request | 76% / 100% | 98% / 83% |
| One note per request | 67% / 100% | 99% / 95% |
Both variants are about 98.5% precise. They differ in recall of indirect evidence: judged alone, a note that only lists metformin is recognised as showing diabetes (209 of 243 such notes). With ten notes in the state, more of those are missed. Isolation costs 9× the wall time and 1.46× the tokens ($0.082 vs $0.056), and it only matters if "treated for X" should count as "has X".