|
| 1 | +--- |
| 2 | +name: nodenorm |
| 3 | +description: Normalize biomedical identifiers (CURIEs) using the Translator NodeNorm API — find the preferred identifier for a concept, list its equivalent identifiers in other databases, and get its Biolink type. Use when working with CURIEs such as MESH:D014867, NCBIGene:1756, CHEBI:15377, UniProtKB:P11532 or MONDO:0005148, to connect an identifier to another data source, to normalize a whole column of identifiers, or to check whether two identifiers refer to the same concept. |
| 4 | +--- |
| 5 | + |
| 6 | +# Normalizing biomedical identifiers with NodeNorm |
| 7 | + |
| 8 | +NodeNorm answers the question **"what is this identifier?"** Given a CURIE, it returns the preferred |
| 9 | +identifier for that concept, every equivalent identifier it knows about, and the concept's Biolink |
| 10 | +types. |
| 11 | + |
| 12 | +- **Base URL:** `https://nodenormalization-sri.renci.org/` |
| 13 | +- **Machine-readable spec:** `GET /openapi.json` — the authority on parameter names and defaults. |
| 14 | + Prefer it over `/docs`, which is a JavaScript-rendered Swagger UI and near-useless to read. |
| 15 | +- The one endpoint that matters is `/get_normalized_nodes`. Everything else is supporting. |
| 16 | + |
| 17 | +## Is this the right service? |
| 18 | + |
| 19 | +| You have | Use | |
| 20 | +|---|---| |
| 21 | +| An identifier (`CHEBI:15377`, `NCBIGene:1756`) | **NodeNorm** — this skill | |
| 22 | +| A name or string (`"aspirin"`, `"type 2 diabetes"`) | **NameRes** (`https://name-resolution-sri.renci.org/`), then normalize the CURIE it returns | |
| 23 | + |
| 24 | +If you have free text, resolve it to a CURIE with NameRes first. NodeNorm will not look up names. |
| 25 | + |
| 26 | +## One identifier |
| 27 | + |
| 28 | +``` |
| 29 | +GET https://nodenormalization-sri.renci.org/get_normalized_nodes?curie=MESH:D014867 |
| 30 | +``` |
| 31 | + |
| 32 | +Repeat `curie=` for a handful of identifiers. Response: |
| 33 | + |
| 34 | +```json |
| 35 | +{ |
| 36 | + "MESH:D014867": { |
| 37 | + "id": { "identifier": "CHEBI:15377", "label": "Water" }, |
| 38 | + "equivalent_identifiers": [ |
| 39 | + { "identifier": "CHEBI:15377" }, |
| 40 | + { "identifier": "PUBCHEM.COMPOUND:962" }, |
| 41 | + { "identifier": "UNII:059QF0KO0R" } |
| 42 | + ], |
| 43 | + "type": ["biolink:SmallMolecule", "biolink:MolecularEntity", "biolink:ChemicalEntity"], |
| 44 | + "information_content": 47.5 |
| 45 | + } |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +### Reading the response |
| 50 | + |
| 51 | +- **`id.identifier`** — the preferred identifier. Two CURIEs that normalize to the same |
| 52 | + `id.identifier` refer to the same concept. This is what you store, join on, and send downstream. |
| 53 | +- **`id.label`** — the preferred name. **Not necessarily the label of `id.identifier`**; for |
| 54 | + chemicals especially, the name may come from a different member of the clique. |
| 55 | +- **`equivalent_identifiers`** — every identifier for this concept, in the Biolink Model's preferred |
| 56 | + prefix order. **This is the field that answers "connect this to another database."** Note that |
| 57 | + **`label` is often absent** on individual entries, so read it with `.get("label")` rather than |
| 58 | + `["label"]`. |
| 59 | +- **`type`** — Biolink classes, most specific first. |
| 60 | +- **`information_content`** — 0.0 (broad concept) to 100.0 (very specific). Absent when unknown. |
| 61 | + |
| 62 | +Optional flags: `description=true` adds descriptions (many identifiers have none), |
| 63 | +`individual_types=true` adds a `type` to each equivalent identifier, `include_taxa=true` (the |
| 64 | +default) adds a `taxa` array of NCBITaxon CURIEs both at the top level and on the individual |
| 65 | +entries that have one. |
| 66 | + |
| 67 | +Use GET for one or two identifiers you are inspecting by hand. For anything programmatic use POST — |
| 68 | +a long `curie=` list will eventually exceed the server's URL length limit, and POST has no such |
| 69 | +ceiling. |
| 70 | + |
| 71 | +## Many identifiers |
| 72 | + |
| 73 | +Use POST. It is fast — 1000 identifiers in roughly 2 seconds — so **never loop over GET requests**. |
| 74 | + |
| 75 | +``` |
| 76 | +POST https://nodenormalization-sri.renci.org/get_normalized_nodes |
| 77 | +Content-Type: application/json |
| 78 | +
|
| 79 | +{ |
| 80 | + "curies": ["MESH:D014867", "NCBIGene:1756", "RUBBISH:1234"], |
| 81 | + "conflate": true, |
| 82 | + "drug_chemical_conflate": false |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +The response is keyed by input CURIE, with the same value shape as the GET method. |
| 87 | + |
| 88 | +- **Deduplicate before sending.** The response is a dictionary, so duplicates buy you nothing. |
| 89 | +- **Chunk at around 1000 per request.** Larger batches work but tie up a shared public service. |
| 90 | +- **An identifier that cannot be normalized comes back as `null`** — the key is present with a null |
| 91 | + value, it is *not* omitted. Check for null values, not for missing keys. |
| 92 | +- **Response keys echo your input exactly**, including any surrounding whitespace. Look results up |
| 93 | + by the exact string you sent, not a cleaned-up version. |
| 94 | + |
| 95 | +## Conflation — decide this deliberately |
| 96 | + |
| 97 | +Conflation merges concepts that are not strictly identical but are often treated as one. It changes |
| 98 | +both the preferred identifier and the size of `equivalent_identifiers`, sometimes dramatically. |
| 99 | + |
| 100 | +| Flag | Merges | Preferred identifier becomes | |
| 101 | +|---|---|---| |
| 102 | +| `conflate` | A gene with the protein it encodes | the **gene** | |
| 103 | +| `drug_chemical_conflate` | A drug with its active ingredient | the **active ingredient** | |
| 104 | + |
| 105 | +Real numbers from the live service: |
| 106 | + |
| 107 | +- `NCBIGene:1756` — 5 equivalent identifiers unconflated, **22** with `conflate=true` |
| 108 | + (5 `biolink:Gene` + 17 `biolink:Protein`). |
| 109 | +- `MESH:D014867` (water) — 30 equivalent identifiers unconflated, **206** with |
| 110 | + `drug_chemical_conflate=true`. |
| 111 | + |
| 112 | +### Always set both flags explicitly |
| 113 | + |
| 114 | +**The defaults differ between GET and POST.** The same query returns different results: |
| 115 | + |
| 116 | +``` |
| 117 | +GET /get_normalized_nodes?curie=MESH:D014867 → 206 equivalent identifiers |
| 118 | +POST /get_normalized_nodes {"curies":["MESH:D014867"]} → 30 equivalent identifiers |
| 119 | +``` |
| 120 | + |
| 121 | +GET defaults both flags to true; the POST body defaults `drug_chemical_conflate` to false. This is a |
| 122 | +known bug ([NodeNormalization#398](https://github.com/NCATSTranslator/NodeNormalization/issues/398)). |
| 123 | +Until it is fixed, **set `conflate` and `drug_chemical_conflate` explicitly on every request** and |
| 124 | +you will not be caught by it. |
| 125 | + |
| 126 | +### Which setting do you want? |
| 127 | + |
| 128 | +**Conflation changes which question you are asking, not just how many rows come back.** |
| 129 | +`conflate=false` asks *"what is this protein?"*; `conflate=true` asks *"what gene is this protein a |
| 130 | +product of?"* Both are valid; picking the wrong one gives you a confident, error-free, useless |
| 131 | +answer. |
| 132 | + |
| 133 | +- **Matching an existing knowledge graph** (Translator, ROBOKOP) — use the conflation that graph was |
| 134 | + built with. If you do not know, try both and see which one's identifiers appear in the target. |
| 135 | +- **Crossing between a gene and a protein** — you need `conflate=true`. A UniProtKB accession |
| 136 | + unconflated has no gene identifiers in its clique at all, so a gene lookup will silently come back |
| 137 | + empty rather than failing. |
| 138 | +- **Asking about one specific molecule or one specific protein as itself** — turn conflation off, so |
| 139 | + you get that concept rather than a merged one. |
| 140 | +- **Linking loosely across sources** — turn it on for the widest set of equivalents. |
| 141 | + |
| 142 | +Conflated results are a **single flat list** — all the members of the first clique, then the second, |
| 143 | +with no marker between them. Use `individual_types=true` to tell gene from protein. |
| 144 | + |
| 145 | +## Recipes |
| 146 | + |
| 147 | +**Connect an identifier to another database.** Normalize it with `individual_types=true`, then look |
| 148 | +in `equivalent_identifiers` for the prefix you want — **and filter on `type`, never on prefix |
| 149 | +alone.** |
| 150 | + |
| 151 | +``` |
| 152 | +GET /get_normalized_nodes?curie=UniProtKB:P11532&conflate=true&drug_chemical_conflate=false&individual_types=true |
| 153 | +→ id.identifier = NCBIGene:1756 ("DMD"), plus HGNC:2928, OMIM:300377, and three ENSEMBL entries: |
| 154 | + ENSEMBL:ENSG00000198947 biolink:Gene ← the Ensembl *gene* |
| 155 | + ENSEMBL:ENSP00000288447 biolink:Protein |
| 156 | + ENSEMBL:ENSP00000288447.4 biolink:Protein |
| 157 | +``` |
| 158 | + |
| 159 | +A conflated clique routinely contains **several identifiers sharing one prefix**, because it holds |
| 160 | +both halves of a gene/protein or drug/chemical pair — and they may be versioned duplicates too. |
| 161 | +Taking the first `ENSEMBL:` you find gets the right answer here only by luck of ordering. Decide |
| 162 | +which one you want by its `type`. |
| 163 | + |
| 164 | +**Normalize a column in a file.** Read the column, deduplicate, POST in chunks of 1000, build a |
| 165 | +`{input → id.identifier}` map, and report the nulls separately — they are usually retired |
| 166 | +identifiers, typos, or prefixes NodeNorm does not cover. |
| 167 | + |
| 168 | +**Are these two identifiers the same concept?** Normalize both with identical conflation settings |
| 169 | +and compare `id.identifier`. This is the reliable way; prefer it. See `/get_setid` below if you want |
| 170 | +a single comparable hash for a whole set. |
| 171 | + |
| 172 | +**What kind of thing is this?** Read `type[0]` — the most specific Biolink class. |
| 173 | + |
| 174 | +## `/get_setid` — a stable hash for a set of identifiers |
| 175 | + |
| 176 | +Normalizes a set of CURIEs, deduplicates, sorts, and hashes the result. Two sets containing the same |
| 177 | +concepts produce the same hash, whatever order or spelling of identifiers you started from. Useful |
| 178 | +for comparing or caching sets of concepts without storing the members. |
| 179 | + |
| 180 | +``` |
| 181 | +GET /get_setid?curie=MESH:D014867&curie=NCBIGene:1756&conflation=GeneProtein&conflation=DrugChemical |
| 182 | +``` |
| 183 | + |
| 184 | +**This endpoint does not take the same conflation parameters as `/get_normalized_nodes`, and it will |
| 185 | +not tell you when you get them wrong.** |
| 186 | + |
| 187 | +- The parameter is **`conflation`** (singular), repeated once per conflation, with the values |
| 188 | + `GeneProtein` and `DrugChemical` — not the `conflate` / `drug_chemical_conflate` booleans used |
| 189 | + everywhere else. |
| 190 | +- **It defaults to no conflation at all**, the opposite of GET `/get_normalized_nodes`. |
| 191 | +- **Wrong parameter names are silently ignored.** `conflations=GeneProtein` (plural) and |
| 192 | + `conflate=true` both return HTTP 200 with an unconflated hash and no warning: |
| 193 | + |
| 194 | + ``` |
| 195 | + /get_setid?curie=UniProtKB:P11532 → uuid:e9a5e65f… (UniProtKB:P11532) |
| 196 | + /get_setid?curie=UniProtKB:P11532&conflation=GeneProtein → uuid:e020b733… (NCBIGene:1756) |
| 197 | + /get_setid?curie=UniProtKB:P11532&conflations=GeneProtein → uuid:e9a5e65f… silently unconflated |
| 198 | + ``` |
| 199 | + |
| 200 | + Always check the `conflations` field echoed back in the response — if it is `[]` when you asked for |
| 201 | + a conflation, your parameter name was wrong. |
| 202 | + |
| 203 | +One request is **one set**, not a batch. To hash several sets in one call, POST a list of |
| 204 | +`{"curies": [...], "conflations": [...]}` objects — note that the POST body field *is* `conflations` |
| 205 | +(plural), differing from the GET query parameter. |
| 206 | + |
| 207 | +The response also carries `normalized_curies` (what the hash was built from — check this if a result |
| 208 | +surprises you), `normalized_string`, and `error`. |
| 209 | + |
| 210 | +## Gotchas |
| 211 | + |
| 212 | +- **Lookups are case-insensitive and whitespace-trimmed.** `" mesh:d014867 "` resolves fine. But |
| 213 | + the response key is still the string you sent. |
| 214 | +- **A missing identifier may be missing by design.** Babel keeps only identifiers whose prefix is |
| 215 | + valid for the concept's Biolink type and drops the rest, so a CURIE you expected can be |
| 216 | + legitimately absent from a clique. |
| 217 | +- **The data is a fixed snapshot**, not a live database. `GET /status` reports `babel_version` (a |
| 218 | + build name like `2025sep1`). Record it if you need reproducible results — a later build may |
| 219 | + normalize the same identifier differently. |
| 220 | +- **Information content from builds before `2025sep1` is unreliable** (it was compared as a string, |
| 221 | + so some values are wrongly 100). Check `/status` first. |
| 222 | +- **This is a shared public service.** Batch with POST; do not hammer it with parallel requests. |
| 223 | + |
| 224 | +## Other endpoints |
| 225 | + |
| 226 | +| Endpoint | Purpose | |
| 227 | +|---|---| |
| 228 | +| `GET /status` | Which Babel build and Biolink Model version this instance serves | |
| 229 | +| `GET /get_allowed_conflations` | The conflation names this instance supports | |
| 230 | +| `GET/POST /get_setid` | A stable hash for a set of CURIEs — see above, its parameters differ | |
| 231 | +| `GET /get_semantic_types` | Every Biolink type present in this instance | |
| 232 | +| `GET /get_curie_prefixes` | CURIE prefix counts per Biolink type (approximate) | |
| 233 | +| `POST /query`, `/asyncquery` | Normalize a whole TRAPI message — **deprecated**, do not use | |
| 234 | + |
| 235 | +## Why cliques look the way they do |
| 236 | + |
| 237 | +The identifiers, preferred names, types and information content all come from |
| 238 | +[Babel](https://github.com/NCATSTranslator/Babel), which is what decides that two identifiers are |
| 239 | +equivalent. If a clique looks wrong — two concepts merged, or one concept split in two — that is a |
| 240 | +Babel issue, not a NodeNorm one. See |
| 241 | +[Where NodeNorm's data comes from](https://github.com/NCATSTranslator/NodeNormalization/blob/master/documentation/Babel.md). |
0 commit comments