# Walking a concept tree

  A vocabulary with thirty thousand entries will not fit in a dropdown. It is a tree, and the list
  an endpoint hands you often starts partway down it, so the first thing to learn is how to climb.

## What goes wrong if you treat it as a list

Small coded lists are flat. Identifier types are eleven entries with no structure: every concept
comes back at `depth: 0`, and a dropdown is the correct interface.

Interventions are not that. There are tens of thousands of them, arranged in layers: benefits,
then categories, then interventions, then services, with protocols and drugs beneath those.
Three things follow, and each one bites a different integration:

<Wire
  rows={[
    { call: "You cannot render it all", does: "A single fetch of every concept is slow, and far too large to use as a picker. Users need to narrow down, which means the interface has to open one level at a time.", keep: "roots first, children on demand" },
    { call: "The list may start partway down", does: "Some endpoints return interventions at the sub-intervention level rather than at the top. What you receive is a set of leaves rather than a set of roots, so a naive renderer shows you the bottom of the tree with no idea what it belongs to.", keep: "climb with /parents/" },
    { call: "Grouping nodes are not selectable", does: "A category is a heading. If you render every concept as an option, a user can pick a heading, and the claim that results is rejected for a code that was never billable.", keep: "concept_class" },
  ]}
/>

The second one costs the most, because nothing in the response tells you it happened. You get a
well-formed list of concepts, each with a real code, and it looks complete.

  A concept carries `depth`, `parent_code` and `child_count`. If everything you received has a
  non-null `parent_code`, you are holding leaves. If everything has `child_count: 0` and a
  non-zero `depth`, you are certainly at the bottom. Check this once when you first integrate a
  vocabulary, rather than guessing at it response by response.

## The two directions

Everything here is one of two moves: down, to offer choices, or up, to work out what a leaf
belongs to.

<Wireframe
  title="Select an intervention"
  actor="Clinician"
  rows={[
    [{ label: "Search interventions", from: "GET /search/typeahead/?q=echo", kind: "field", span: 4 }],
    [{ label: "▸  Surgical services", note: "Grouping node, so a heading rather than an option", kind: "text", span: 4 }],
    [{ label: "▾  Cardiology", note: "Opened. One call fetched this level.", kind: "text", span: 4 }],
    [{ label: "      ▾  Echocardiography", note: "Opened", kind: "text", span: 4 }],
    [{ label: "            Transthoracic echo", from: "INT-CARD-ECHO-TTE", tone: "positive", span: 2 },
     { label: "            Transoesophageal echo", from: "INT-CARD-ECHO-TOE", tone: "positive", span: 2 }],
    [{ label: "▸  Diagnostic imaging", note: "Never fetched, because nobody has opened it", kind: "text", span: 4 }],
  ]}
  caption="Three fetches made this screen: the roots, then the children of Cardiology, then the children of Echocardiography. The unopened branches have never been requested, and the two green rows are the only ones a user may select."
/>

<Mermaid chart={`sequenceDiagram
    autonumber
    participant U as Operator
    participant H as Your HMIS
    participant T as Terminology service

    H->>T: GET /concepts/roots/?system=INTERVENTIONS&version=serving
    T-->>H: The top layer, with child_count on each
    Note over H: Render only nodes with child_count > 0<br/>as openable.

    U->>H: Opens "Cardiology"
    H->>T: GET /concepts/{id}/children/
    T-->>H: One level down
    Note over H: Cache it. Reopening a branch<br/>should not re-fetch.

    U->>H: Picks a leaf
    Note over H: Store the code. Never the display text.
`} />

<Mermaid chart={`flowchart TD
    A["A list of concepts arrives"] --> B{"Every parent_code non-null?"}
    B -- "Yes, these are leaves" --> C["GET /{id}/ancestors/<br/>to place them in the tree"]
    B -- "No, these include roots" --> D["Render as the top layer"]
    C --> E["Group the leaves under<br/>their resolved ancestors"]
    E --> F["Render the tree"]
    D --> F
    F --> G{"User opens a node?"}
    G -- Yes --> H["GET /{id}/children/, cache it"]
    H --> G
    G -- "Picks a node" --> I{"concept_class is a grouping?"}
    I -- Yes --> J["Not selectable,<br/>it is a heading"]
    I -- No --> K["Store the code"]
`} />

## The calls

One thing to settle before the calls. Code systems, value sets and concept maps are all addressable
by their short code, so `/code-systems/IDENTIFIER-TYPES/` works exactly as `/code-systems/1251/` does.
Concepts are the exception: a concept code is unique only inside its own system, so the concept
routes below take a numeric id. Resolve it once from the list read and keep it beside the code you
store:

<TryIt
  title="Resolve a concept id from the code you hold"
  path="/concepts/?system=IDENTIFIER-TYPES&search=NATIONAL_ID&version=serving"
  keep="id, alongside the code"
>
  The list read is addressed by short code; the `id` on the row it returns is what the tree calls
  below need. Storing the id next to the code saves this lookup on every subsequent walk, and unlike
  a display string, an id is safe to cache because it never changes.
</TryIt>

Down the tree, one level at a time:

<ApiRef method="GET" path="/concepts/roots/?system={system}&version=serving" to="/terminology-api/concepts" keep="the root codes">
  The top layer of a vocabulary. Start here for anything you cannot render whole.
</ApiRef>

<ApiRef method="GET" path="/concepts/{id}/children/" to="/terminology-api/concepts">
  The direct children of one node. Called when a user opens a branch, and cached so reopening it
  does not re-fetch.
</ApiRef>

Up the tree, from a leaf you were handed:

<ApiRef method="GET" path="/concepts/{id}/parents/" to="/terminology-api/concepts">
  One level up. Note the plural: a concept may have more than one parent, and a breadcrumb that
  renders a single path silently picks one of them.
</ApiRef>

<ApiRef method="GET" path="/concepts/{id}/ancestors/" to="/terminology-api/concepts">
  Every ancestor, transitively. This is what turns a list of leaves into a tree you can render.
</ApiRef>

And the whole subtree, when you genuinely need it:

<ApiRef method="GET" path="/concepts/{id}/descendants/" to="/terminology-api/concepts">
  Everything beneath a node, transitively. Useful for a rule that applies to a whole branch, and
  for checking whether a billed code falls under an approved one.
</ApiRef>

  Ancestors and descendants are served from a closure table. Fetching `/children/` repeatedly and
  assembling the transitive set client-side is slower, and it drifts the moment the vocabulary is
  re-parented upstream, which happens without your release cycle noticing.

## Is this code under that one?

A common question at Bill: the preauthorisation approved a category, and a specific service was
performed. Does the service fall under what was approved?

Do not answer this by walking descendants and searching the list. There is one call:

<ApiRef method="GET" path="/code-systems/{system}/$subsumes/?codeA={approved}&codeB={billed}" to="/terminology-api/fhir-operations" keep="outcome">
  How two codes relate: `equivalent`, `subsumes`, `subsumed-by` or `not-subsumed`.
</ApiRef>

`subsumes` means `codeA` is the broader one, so an approved category subsuming a billed service is
the answer you want. `not-subsumed` means the two are in unrelated branches, which is the case
worth surfacing to an operator before submission rather than after rejection.

## Building it so it stays fast

<BuildSequence
  steps={[
    {
      do: "Fetch roots once, at start-up",
      detail:
        "The top layer of a vocabulary changes rarely. Load it with the rest of your terminology sync rather than when a patient is waiting.",
    },
    {
      do: "Cache each branch by node id",
      detail:
        "A user who collapses and reopens Cardiology should not cost you a round-trip. Cache the children keyed on the node, and invalidate the whole cache on a version change rather than per node.",
    },
    {
      do: "Search server-side, do not filter your cache",
      detail:
        "A search box that filters only the branches already fetched will confidently report no results for a code that exists. Send the term to the service.",
    },
    {
      do: "Use concept_class to decide what is selectable",
      detail:
        "Grouping nodes render as headings. Do not infer this from child_count, because a leaf category with nothing under it yet is still not billable.",
    },
    {
      do: "Store the code and the version, never the display",
      detail:
        "A display string you stored is a claim you cannot defend once the wording changes. The code is what was actually chosen, and the label is only one way of showing it.",
    },
  ]}
/>

## What breaks here

<CostTable
  rows={[
    {
      where: "First render of a large vocabulary",
      symptom: "\"The picker takes twelve seconds to open\"",
      cause:
        "Every concept fetched up front. Fetch roots, then children on demand, since the user only ever sees one branch at a time anyway.",
    },
    {
      where: "A list that arrives partway down",
      symptom: "\"We are showing sub-services with no idea what they belong to\"",
      cause:
        "Leaves rendered as though they were roots. Resolve ancestors and group under them.",
    },
    {
      where: "Submission",
      symptom: "\"The code was rejected and it is definitely in the list\"",
      cause:
        "A grouping node was offered as an option. Read concept_class before rendering something selectable.",
    },
    {
      where: "Search",
      symptom: "\"Searching finds nothing, but browsing finds it\"",
      cause:
        "The search box is filtering the local cache rather than calling the service, so it can only find branches already opened.",
    },
    {
      where: "Weeks after a terminology release",
      symptom: "\"Our tree shows a parent that no longer exists\"",
      cause:
        "A client-assembled hierarchy, cached and never invalidated. Read ancestors from the service and key the cache on the version.",
    },
  ]}
/>

**Next:** [Validating a code](/rail/reference/Terminology-Validating-Codes) ·
[Pinning to a release](/rail/reference/Terminology-Pinning-Versions) · or the whole surface at
[the terminology service](/rail/reference/Terminology-Service)
