The FHIR Knowledge Base

The FHIR Knowledge Base Infographic


A FHIR server already stores everything about a patient in a governed, standards-based way: conditions, medications, observations, encounters, all of it consistently structured and ready to query. If a language model can read text, surely the model can read a FHIR resource.

Perhaps, then, a FHIR server is already a knowledge base.

It’s a reasonable assumption, and it isn’t quite wrong so much as it skips a step. The previous posts in this series have argued that an agent is another user of the application, subject to the same audit, provenance and permissions as anyone else. This post asks a related but different question: once an agent is allowed to look, what exactly is it looking at, and how does it find the right thing to look at in the first place?

This article explores that idea using the same example application as the rest of the series. As before, it isn’t a recommendation for how patient record systems should represent information for AI. It’s a set of considerations, drawn from working with the example application, that architects, engineers and interested clinicians may find useful when thinking through their own designs.


TL;DR

📚 A FHIR server is a repository, not a knowledge base. Retrieval is a separate concern.
📖 FHIR narratives bridge the two. They already say, in prose, what the structure means.
🧠 The vector index is a derived semantic view. FHIR stays the source of truth.
🪪 Retrieval inherits the same obligations as everywhere else in the series: scoped, versioned, audited.
🧩 Two records can hold the same knowledge without holding the same query. Structured lookups and semantic search are different tools for different questions.


A repository is not the same thing as a knowledge base


FHIR is very good at representing information consistently. A Condition resource, an Observation, a MedicationRequest: each has a defined shape, coded values bound to terminologies such as SNOMED CT, and a place in a graph of references to other resources. That’s exactly what a repository needs to be. It’s also, on its own, a poor match for the kind of questions a clinician may ask.

People that ask a patient record to show “the Condition resources where clinicalStatus equals active and conditionCode equals 59621000” are likely few and far between. Most might ask whether there’s a history of hypertension, or whether anything’s changed recently, or what the last few visits were about. Those are questions about meaning, not questions about resource types and search parameters, and a FHIR repository doesn’t answer them on its own. It answers “what is stored”, not “what is relevant to this question”, and the gap between the two is exactly the problem a knowledge base has to solve.

That gap doesn’t close just because a large language model is involved. A model given the whole of a patient’s FHIR data, or an unfiltered dump of it, still has to work out which parts of a large, verbose, deeply cross-referenced graph actually bear on the question in front of it. Handing over more data isn’t the same as handing over the right data, and it comes with a further problem: the previous posts in this series have made the case that retrieving something is itself a form of access. An approach that hands a model everything, on the basis that it will politely ignore what it wasn’t supposed to see, sits uneasily next to that principle.

So the question isn’t whether FHIR holds the knowledge. It does. The question is how that knowledge gets found, and found in a way that keeps faith with everything the rest of this series has argued for.


Narrative as the bridge


FHIR resources already contain a human-readable view of themselves, through the text.div narrative element. It exists for a mundane reason: a system that doesn’t understand a particular resource’s coded structure should still be able to show something sensible to a person. In practice, that same property makes the narrative a natural place to start when building a knowledge base, because it has already done some of the translation work from structure to meaning.

In the example application, every resource type has a defined way of turning its structured content into that narrative, built from the resource’s own definitions (StructureDefinition) rather than written by hand for each patient. A Condition’s narrative reads as a sentence about a diagnosis; an Observation’s reads as a result with its value, units and date. Coded values render using their display text where one exists, so a clinician (or, as it turns out, a language model) is reading “moderate essential hypertension” rather than a SNOMED CT code. That narrative is then converted into a plain, labelled representation and turned into the numerical vectors that make semantic search possible: embeddings, placed in a vector index alongside those of every other resource the system holds.

A quick note on terms, since this series (and the two posts it borrows its voice from) has used “index” and “semantic layer” fairly loosely. From here on, vector index means the store itself: the embeddings and what they point back to. Semantic layer means that store together with the tooling built to query it, the thing that actually turns a question into a set of retrieved resources.

flowchart LR
    subgraph SRC[FHIR resource]
        direction TB
        A(Structured content) --> B(Narrative: text.div)
    end

    subgraph IDX[Vector index]
        direction TB
        C(Embedding) --> D(Vector store)
    end

    B --> C

    E(FHIR repository, source of truth)
    D -.derived from, kept in step with.-> E

    classDef base fill:#437c9f,stroke:#2f5f79,color:#ffffff,stroke-width:1px;
    classDef highlight fill:#6dc067,stroke:#437c9f,color:#1f2d1f,stroke-width:1.2px;
    classDef group fill:#ffffff,stroke:#437c9f,color:#2f5f79,stroke-width:1px;
    class A,B,C,D base;
    class E highlight;
    class SRC,IDX group;
    style SRC rx:10,ry:10
    style IDX rx:10,ry:10
    linkStyle default stroke:#437c9f,stroke-width:1.2px;

Two things are worth being deliberate about here, because it’s easy to let either slip.

The vector index is derived, never authoritative. FHIR remains the record. The vector index is a view, built for a purpose the repository itself doesn’t serve, and when a resource is created or updated in the example application, its narrative is regenerated and the index is refreshed to match. That’s not a detail to bury in an appendix. If an agent’s answer and the record ever disagree, the record wins, and a design should make that easy to reason about rather than assume.

Uploaded documents earn the same treatment. A clinical letter or a scanned report isn’t a FHIR resource with a narrative already sitting inside it, but the same idea applies: the example application extracts text from an uploaded document and represents it alongside FHIR narratives in the same vector index, so a question can be answered from whichever source actually holds the relevant information, in the same semantic space, rather than from two separate and incompatible search mechanisms.


The same obligations, one layer earlier


None of this matters unless retrieval keeps the promises made in earlier posts. A semantic layer is only useful in this application because it sits behind the same obligations as any other route into the record, not instead of them.

Patient scoping applies before anything is retrieved, exactly as Post 2 argued for FHIR search parameters. A question isn’t run against the whole vector index and filtered afterwards; the patient identifier is part of the search itself, so a result belonging to someone else is never found in the first place, rather than found and then withheld.

Every result carries a versioned pointer back to a real resource. A retrieved chunk isn’t returned as bare text; it comes labelled with the resource type, its identifier and the version it was generated from, along with the encounter it belongs to where one applies. That’s not incidental. It’s exactly the detail Post 1 needs to audit a retrieval as a read against a specific version, and exactly the detail Post 3 needs to turn a citation into a link that opens on precisely what the model saw. A vector index that returned similarity scores and nothing else would be quicker to build and considerably harder to hold to account.

Retrieving something and displaying something remain different acts. The example application’s retrieval step commonly returns more candidate material than ends up in a final answer, so that a synthesising model has enough context to reason with. All of it is a disclosure, whether or not a line of it reaches the screen, which is why Post 1 argued for auditing what was retrieved rather than only what was shown.

flowchart LR
    A(Question) --> B(Patient-scoped semantic search)
    B --> C(Versioned resources retrieved)
    C --> D(Audited read)
    C --> E(Assembled context)
    E --> F(Language model)
    F --> G(Answer with citations)

    classDef base fill:#437c9f,stroke:#2f5f79,color:#ffffff,stroke-width:1px;
    classDef highlight fill:#6dc067,stroke:#437c9f,color:#1f2d1f,stroke-width:1.2px;
    class A,B,C,D,E,F base;
    class G highlight;
    linkStyle default stroke:#437c9f,stroke-width:1.2px;

Put together, the pattern is a familiar one dressed in newer clothes. A repository, a scoped and permissioned way of asking it questions, and a trail showing what was found and by whom. That’s not a new set of obligations invented for AI. It’s the same set this whole series has been describing, applied one layer earlier, at the point where an agent decides what to look at before it ever opens its mouth.


Two ways of using the same knowledge


Not every question needs the same kind of search, and the example application uses two.

Some questions are well-defined and asked often enough that a fixed, pre-written FHIR search answers them precisely: who is the next of kin, what appointments are booked, who is the patient’s GP. For these, an embedding is used only to choose which pre-written query best matches the question that was asked, not to answer it. Once the right query is identified, it runs as an ordinary, deterministic FHIR search, patient and any other parameters filled in. The result is exactly what that search would return for anyone browsing the same thing in the application. Nothing about the answer is generated; the semantic step only decides which door to open.

Other questions don’t fit a fixed template at all: broader, more exploratory ones, where the relevant information might be scattered across several resource types and phrased in several different ways in the record. These are the ones the vector index over FHIR narratives is built for. Each candidate resource is ranked by how closely its vector sits to the question’s own, a similarity score, and the closest matches are retrieved for a language model to reason over and synthesise into an answer.

Both paths draw on the same underlying knowledge. They just reach it differently, one through a library of known questions and their known answers, the other through meaning and similarity. Recognising which kind of question you’re actually being asked, and routing it to the mechanism built for it, matters more than picking one approach and using it for everything.


Considerations worth weighing


A few further questions tend to arise in practice.

  • How much do you translate before you index? The example application builds its vector index from generated narratives rather than raw FHIR JSON, trading a small amount of translation effort for text that’s considerably easier for both embeddings and large language models to work with. Whether that trade-off is worth it depends on how verbose and cross-referenced your own resources tend to be.
  • How does the vector index stay in step with the record? A view that’s built once and never refreshed quietly becomes untrustworthy. Deciding when and how the index is brought up to date, on every write, on a schedule, or something else, is a design decision worth making explicitly rather than by default.
  • Does every result carry enough to be traced? A similarity score tells you a result was relevant. It doesn’t tell you what it was relevant to, or which version of the record it came from. That provenance has to be designed in from the start; it’s difficult to bolt on afterwards.
  • Is one retrieval mechanism enough? Well-defined questions and open-ended ones often want different tools. A single semantic search, tuned for one, may serve the other poorly.
  • Where does the terminology layer fit? A semantic layer built from narrative text captures meaning expressed in words. Coded structure, of the kind NHS England’s FHIR guidance and terminologies such as SNOMED CT provide, still matters for precision. The two are complementary rather than substitutes for one another.

Questions to ask of your own design


If you’re building a retrieval layer over clinical data, these may be worth asking:

  1. Is there a clear line between the repository, which represents information consistently, and the knowledge base, which makes it findable? Or has that distinction been allowed to blur?
  2. Does your vector index carry enough provenance, resource type, identifier and version, to support the audit and citation obligations described earlier in this series?
  3. Is patient scoping applied as part of the search itself, or as a check on what comes back afterwards?
  4. When a record changes, how does the vector index find out, and how quickly does it catch up?
  5. Are you reaching for one retrieval mechanism to answer every kind of question, when different questions might be better served by different ones?
  6. If someone asked “where did this come from?” about a retrieved result, could your vector index answer, or only your language model?

The record hasn’t gone anywhere


None of this replaces FHIR. It sits in front of it. A knowledge base doesn’t make a repository less structured, less governed or less authoritative; it makes the information inside that repository easier to find, without ever becoming the place where that information actually lives.

It’s worth entertaining the more provocative version of that idea, if only for a moment. If a large language model can already read a clinical letter, a set of free-text notes or a discharge summary and answer sensible questions about them, does structured data still earn its keep? Could a patient’s record just as well be a collection of clinical documents, digital patient notes written in prose, left for a model to make sense of when asked, rather than coded, versioned FHIR resources built and maintained at real cost?

It’s a fair question, and worth sitting with rather than dismissing outright. But everything the rest of this series has relied on, patient scoping applied before retrieval, a citation pinned to a specific version, an audit trail that says precisely what was read, depends on information that’s addressable at the level of a single fact, not just a document. A model can read prose fluently. It’s considerably harder to build the same guarantees on top of a document as it is on top of a coded, versioned resource with a defined place in a graph. If anything, an agent that’s good at reading free text makes the case for structure stronger, not weaker: it’s what lets everything built on top of that reading be checked.

Turning narrative into something searchable is only half the problem, though, and the more interesting half is still to come: once you’ve decided what to search over, you still have to decide how to break it into pieces small enough to retrieve precisely and large enough to still mean something. That’s where [this series goes next].


The data used in the project to which this article relates was sourced exclusively from datasets that are known or assured to be synthetic, or have been intentionally created for illustrative purposes. The application described is a playground, built for experimentation, and has never been used in any real-life scenarios. It was created solely to demonstrate the concepts described in the article.

Rob Organ logo
Rob Organ
Principal Engineer