Summary
The Suggestions tab shows, for each source concept, OMOP candidates ranked by a combined score. These suggestions come from two places: precomputed scores — syntactic and semantic — produced by two scripts run in a terminal, then loaded into Linkr; and the proposals of an AI agent, which follows the concept-mapping skill and works live in the project through the MCP server, with no export. Either way, you are the one who creates each mapping.
Why a separate tab
Mapping 1,000 local codes by hand against the 4 million concepts of the OMOP vocabulary is slow. Each algorithm family has its own bias:
- String comparison misses synonyms (“HR” vs “heart rate”).
- Semantic embeddings occasionally confuse close-but-different concepts (“serum creatinine” vs “urine creatinine”).
- An AI agent adds clinical context but is expensive to run over 4 million pairs.
The Suggestions tab combines several methods into a single score and surfaces the per-method details, so you keep the final decision.
Where suggestions come from
Precomputed scores
Jaro-Winkler and BioLORD, for every source concept at once.
Computed outside Linkr by two Python scripts, then loaded into the Suggestions tab. Works in both deployment modes. See Precomputing the scores.
AI agent suggestions
A few justified candidates per concept, batch by batch.
Written live into the project by an agent connected to Linkr’s MCP server. Server mode only. See Mapping with an AI agent.
The two complement each other: the agent reads the precomputed scores when there are any, and uses them to find its candidates faster — especially for drugs.
The Suggestions tab in Linkr
| Score | Methods | Vocab | Concept name | Code | Std | |
|---|---|---|---|---|---|---|
92% | 95%88%91% | LOINC | Creatinine [Mass/volume] in Serum or Plasma | 2160-0 | S | |
81% | 83%79%82% | LOINC | Creatinine [Mass/volume] in Blood | 38483-4 | S | |
76% | 80%71%77% | LOINC | Creatinine [Moles/volume] in Serum or Plasma | 14682-9 | S | |
68% | 72%64% | SNOMED | Serum creatinine measurement | 113075003 | S | |
54% | 58%51% | LOINC | Creatinine renal clearance | 2164-2 | S |
Similarity methods
Four method families can feed the candidate ranking. Each attacks the problem from a different angle, and their failure modes don’t overlap — which is why combining them beats any single family.
Syntactic
Compares the label character strings.
Semantic
Compares meaning via biomedical embeddings.
Statistical
Compares the distributions of numerical values.
AI
An LLM re-reads top candidates with the clinical context.
In Linkr, every method is represented next to the combined score by a coloured dot (colours above) followed by its percentage. On hover, a tooltip names the exact algorithm. This helps understand why a score is high: a candidate scoring 92% from the syntactic method alone is likely a textual false friend, while one scoring 80% from semantic + AI is much more solid.
1. Syntactic — compare character strings
We compare label strings as-is, without any understanding of their meaning. Useful because source and OMOP labels often follow different writing conventions: case, spaces or underscores, accents, abbreviations, word order, punctuation. Three algorithms in the catalogue:
- Jaro-Winkler — letter-level proximity with a bonus for shared prefixes. Matches “hemoglobin_a1c” with “Hemoglobin A1c” despite the case and separator differences.
- Token-Sort — splits labels into words, sorts them alphabetically, then compares. Word-order agnostic (“blood pressure systolic” ≈ “systolic blood pressure”).
- N-gram IDF — splits into character n-grams (subsequences of N letters), weights each n-gram by its rarity in the vocabulary. Usagi’s historical method.
hemoglobin_a1c(source label, no accents, no spaces)Hemoglobin A1cA1c hemoglobinHemoglobinGlucoseJaro-Winkler — letter-level proximity, bonus for shared prefix; Token-Sort lets "A1c hemoglobin" match the source despite word-order differences.
Strengths — catches near-literal matches despite writing variants (case, spaces, abbreviations, word order), very fast (milliseconds per pair).
Limits — does not account for synonyms: “creatinine” and “creat” are close, but “heart rate” and “pulse” score zero.
2. Semantic — compare meaning
Each label is encoded as a numerical vector through a sentence-embedding model, then we measure the angle between vectors via cosine similarity. Two labels with close meanings will produce close vectors, even if the words differ.
From label to embedding vector
BioLORD-2023-M projects each label into a 768-dimensional space. Here are 24 dimensions as a heatmap — each cell is one dimension, colour encodes the value.
Heart ratePulseBlood pressureCosine similarity
cos(θ) = (A · B) / (‖A‖ × ‖B‖)Heart ratevsPulseHeart ratevsBlood pressureThe patterns for "Heart rate" and "Pulse" are nearly identical cell by cell → very close vectors. "Blood pressure" has a different pattern → distant vector. The closer the cosine score gets to 1, the closer the meanings.
Default model: BioLORD-2023-M, trained on biomedical text and multilingual (French, English, Spanish, German…).
Configurable alternatives — any Sentence-Transformers compatible BERT model:
- PubMedBERT, SapBERT, ClinicalBERT — biomedical-specialised but English-only.
- all-MiniLM-L6-v2 — general-purpose, English-only, much faster.
- paraphrase-multilingual-MiniLM-L12-v2, LaBSE — general-purpose, multilingual.
Multilingual coverage
If your source labels are in French (or any non-English language), stick with BioLORD-2023-M or switch to an explicitly multilingual model. PubMedBERT, SapBERT, ClinicalBERT and all-MiniLM-L6-v2 will produce poor-quality embeddings on non-English text.
Strengths — catches synonyms (“heart rate” ≈ “pulse”), rephrasings, terminology variants.
Limits — can conflate two close-but-distinct concepts (“serum creatinine” vs “urine creatinine”). Expensive to pre-compute over the ~4M OMOP concepts (GPU recommended).
3. Statistical — compare value distributions
For numerical variables (Measurement only), we compare the statistical distribution of values between source concept and candidates: min, P25, median, P75, max, mean, standard deviation. Two variables taking values in the same range with the same shape are probably the same measurement.
Concretely, we use a statistical test to quantify the gap between the two distributions:
- Kolmogorov-Smirnov test (KS) — maximum gap between cumulative distribution functions. KS = 0 → identical distributions; KS close to 1 → very different distributions.
- Wasserstein distance — “cost” of transporting the mass of one distribution onto the other. More robust than KS when supports are shifted (unit change).
Source concept: serum creatinine (µmol/L)
12,450 measurements, 1,842 patients
LOINC 2160-0 — Creatinine [Mass/volume] in Serum or Plasmaunit: mg/dLLOINC 14682-9 — Creatinine [Moles/volume] in Serum or Plasmaunit: µmol/LLOINC 2951-2 — Sodium [Moles/volume] in Serum or Plasmaunit: mmol/LKolmogorov-Smirnov test: KS close to 0 = identical distributions; KS > 0.5 = very different distributions. Smoothed curves (kernel density estimate) let you compare shape and position without the distributions hiding each other.
Strengths — works even when the label is ambiguous or missing. Very discriminating: hard to confuse creatinine (60–120 µmol/L) with sodium (135–145 mmol/L).
Limits — only applicable to numerical variables with enough values for the distribution to be representative. Sensitive to data-entry artifacts (outliers, mixed units).
No script for this score yet
Linkr can display and weight statistical scores (statistical/… methods) when a scores file contains them, but the two scripts provided do not compute them yet. In practice, the AI agent is what compares distributions: it reads each source concept’s distribution in its metadata before choosing a target.
4. Agentic AI — let an LLM reason about it
An LLM driven agentically — it decides which tools to call: reading a source concept and its metadata, searching the OMOP vocabulary, browsing a candidate’s hierarchy — examines the best candidates with all the context available: source label, unit, value distribution, wards where the code is used. It proposes one or more mappings carrying a SKOS equivalence (exactMatch, closeMatch, broadMatch, narrowMatch, relatedMatch) and a comment justifying the choice.
Its proposals show up in the tab as one more method, named ai/<model> after the model that produced them, in the Agentic category. The Mapping with an AI agent page explains how to start it.
TA_systolique_invasiveICCA · M0118 · "Invasive systolic arterial pressure" · 24,380 measurements · median 122 mmHg
SNOMED 251071003 — Invasive systolic arterial pressure91%SNOMED 72313002 — Systolic arterial pressure78%LOINC 8480-6 — Systolic blood pressure74%
The label "TA_systolique_invasive" refers to systolic arterial pressure measured via an arterial catheter (arterial line), not by cuff. SNOMED 251071003 matches this definition exactly — a direct mapping. LOINC 8480-6 is a broader concept (systolic, any method): the target is more general than the source, which is written broadMatch in SKOS. The observed distribution (median 122 mmHg, right tail up to ~180) is consistent with an invasive ICU measurement.
SNOMED 251071003 — Invasive systolic arterial pressureConfidence: 97%skos:exactMatch — The target concept describes precisely the same measurement.LOINC 8480-6 — Systolic blood pressureConfidence: 88%skos:broadMatch — The target is more general (any measurement method), so broader than the invasive source.Strengths — the only method that “understands” clinical semantics (qualifiers, measurement context, nuanced equivalences). Can say “this is not exact, the target is more general” via broadMatch, and says in its comment what differs.
Limits — slow and compute-hungry, risk of hallucinations on rare cases, still requires human validation.
Frugality principle — start with the cheapest method
The methods sit at different resource tiers, roughly grouped as:
- Syntactic and statistical — light CPU, local computations on strings or on values already in memory. Order of magnitude: milliseconds per pair.
- Semantic (BERT / embeddings) — a GPU is recommended for inference and for pre-computing the OMOP vectors. Order of magnitude: minutes to hours depending on hardware, then near-free at query time.
- Agentic AI — calls to the model, several tool round-trips per concept. Order of magnitude: seconds to minutes per concept.
The rule: don’t reach for a higher tier when a lower one suffices.
- Run the syntactic scores first — they are almost free, and already resolve a good share of easy cases.
- The semantic method takes over on unresolved concepts (synonyms, rephrasings, languages).
- The agent works on a chosen scope — a category, the most frequent concepts — and starts from candidates already pre-ranked, never blindly from the 4M OMOP concepts.
It’s at once a question of cost (compute, GPU, and the bill for a remote model), environmental impact and speed: running a 1,000-concept project entirely through the agent takes hours, where the precomputed scores handle the same load in minutes and leave the agent the hard residue.
The combined score
The combined score (left column, green bar) is a weighted average of each method’s score. Default weights are:
| Method | Weight | Role |
|---|---|---|
| Syntactic | ×1 | Catches near-literal matches despite writing variants. |
| Statistical | ×1 | Compares value distributions (creatinine vs sodium). |
| Semantic | ×2 | Picks up synonyms and rephrasings. |
| Agentic AI | ×3 | Justified proposals from an AI agent. |
The Manage suggestions button (top left) opens a dialog with sliders to tune each weight in real time — the table re-sorts automatically. It also shows the number of loaded scores, and lets you Import a file or Delete all.
In server mode, the Methods in the file section lists every method present — syntactic/jaro-winkler, semantic/biolord, ai/<model>… — each with a bin: Remove the suggestions of a method deletes all its rows from the scores file, keeping the other methods and the mappings. This is how you remove in one go what an agent left.
Manage suggestions
Tune the weight of each method in the combined score.
Method weights
Methods in the file
ai/mistral-small-3.2semantic/biolordsyntactic/jaro-winkler
similarity-scores.parquetSelection and mapping
SNOMED 251071003 — Invasive systolic arterial pressure | 91% | ||
SNOMED 72313002 — Systolic arterial pressure | 78% | ||
LOINC 8480-6 — Systolic blood pressure | 74% |
Candidates already mapped for this source concept are greyed out and not re-selectable.
- Click a row to select it. The action bar at the top then shows the usual mapping buttons (Exact match + SKOS dropdown + comment button).
- Candidates already mapped for this source concept are greyed out and not re-selectable.
- The ⓘ icon at the row end opens the target OMOP concept’s detail (description, synonyms, hierarchy).
The main alignment button “Exact” is pre-filled with the selected suggestion’s SKOS equivalence — the one the agent proposed, for an ai/… row (exactMatch, closeMatch, broadMatch, narrowMatch, relatedMatch). You can accept it in one click, or open the dropdown to pick a different equivalence if you disagree. Linkr never maps on its own from a suggestion: that click is what creates the mapping.
AI agent suggestions
An AI agent can leave its own suggestions in the tab, each with a SKOS equivalence and a comment justifying the choice. The Mapping with an AI agent page explains how to put it to work, what it writes into the project and how to follow its work.
Precomputing the scores
Syntactic (Jaro-Winkler) and semantic (BioLORD embeddings) scores give every source concept a ranked list of candidates before any agent looks at it. They are computed outside Linkr, in a terminal, with the two Python scripts that ship with the skill, in its scripts/ folder. The resulting file is then loaded into the project; this part also works in an install without a backend.
Requirements
| Resource | Why |
|---|---|
Python 3 + rapidfuzz, pandas, pyarrow, numpy, duckdb | For both scripts. |
sentence-transformers and faiss-cpu | For the semantic scores only. |
OHDSI vocabularies (CONCEPT.parquet or .csv) | Downloaded from ATHENA. |
| Several GB of disk | The OMOP vocabulary embeddings weigh several GB. |
| GPU recommended | Computing the embeddings can take hours on CPU. |
The two scripts
embed_concepts.py— computes BioLORD embeddings of the OMOP concepts and writesconcept_embeddings.parquetnext toCONCEPT.parquet. Run it once per vocabulary release: the file serves every project.compute_scores.py— compares a project’s source concepts with the OMOP concepts and writessimilarity-scores.parquet. Run it once per project. By default it computes Jaro-Winkler and BioLORD; withoutconcept_embeddings.parquet, only the syntactic score.
The scoring script expects a source concepts CSV with three columns: terminology, concept_code and concept_name. The source-concepts.csv from a Linkr ZIP export of the project does the job, with its columns renamed if needed.
Always filter the targets
The semantic step builds an index over the target concepts. Unfiltered, that is about 4 million vectors and up to 25 GB of memory: enough to bring down a laptop. Always pass --only-standard --only-valid and a --domain (or --vocabulary) matching what you are mapping. Drugs — about 2 million RxNorm concepts — are handled on their own, only if you are mapping them.
python scripts/compute_scores.py \
--source source-concepts.csv \
--concept /path/to/vocabulary/CONCEPT.parquet \
--embeddings /path/to/vocabulary/concept_embeddings.parquet \
--only-standard --only-valid --domain Measurement Observation
Run it in your own terminal, where you can watch memory and stop it with Ctrl-C. Each script lists all its options with --help.
Loading the scores into Linkr
In the mapping editor, Suggestions mode: Manage suggestions → Import a file, then pick similarity-scores.parquet.
Loading a file replaces the previous one
The loaded file replaces the project’s scores file, including the suggestions an agent left. To keep them, fetch the current file first: export the project as Linkr ZIP with Similarity scores ticked, then pass the archive’s similarity-scores.parquet to the script with --output. The script adds its rows to the existing file without touching the others.
Output files
The scoring script writes two files:
source_embeddings.parquet— the embedding vectors of the source concepts. Kept so the embeddings can be reused on the next run without re-encoding everything.similarity-scores.parquet— one score per(source, candidate, method)triple. This is the file you load into Linkr, and also where Linkr stores the agents’ suggestions.
Preview of source_embeddings.parquet:
source_embeddings.parquet| source_vocabulary_id | source_concept_code | source_concept_name | embedding (excerpt) | model | created_at |
|---|---|---|---|---|---|
| ICCA | M0118 | TA_systolique_invasive | [ 0.42,-0.18, 0.71, 0.05,-0.33, 0.58, 0.22,-0.41, …] | BioLORD-2023-M | 2026-05-14T09:31:02Z |
| ICCA | M0042 | Frequence_cardiaque | [ 0.39,-0.22, 0.68, 0.08,-0.30, 0.61, 0.19,-0.38, …] | BioLORD-2023-M | 2026-05-14T09:31:02Z |
| ICCA | M0071 | Natremie | [-0.15, 0.44, 0.12, 0.59, 0.31,-0.22,-0.48, 0.27, …] | BioLORD-2023-M | 2026-05-14T09:31:02Z |
The embedding column holds the full vector (768 values); only the first 8 dimensions are shown here for readability.
Preview of similarity-scores.parquet for one source concept (invasive systolic BP), after an agent’s pass:
similarity-scores.parquet| source_vocabulary_id | source_concept_code | concept_id | method | score | equivalence | comment | created_at |
|---|---|---|---|---|---|---|---|
| ICCA | M0118 | 4353843 | syntactic/jaro-winkler | 0.83 | skos:exactMatch | — | 2026-05-14T09:32:11Z |
| ICCA | M0118 | 4353843 | semantic/biolord | 0.91 | skos:exactMatch | — | 2026-05-14T09:34:02Z |
| ICCA | M0118 | 4353843 | ai/mistral-small-3.2 | 0.97 | skos:exactMatch | Concept dedicated to systolic pressure measured by arterial catheter, like the source; the observed median of 122 mmHg is consistent. | 2026-05-14T09:35:48Z |
| ICCA | M0118 | 3004249 | ai/mistral-small-3.2 | 0.88 | skos:broadMatch | The target is more general: it covers any measurement method and does not keep the invasive nature of the source. | 2026-05-14T09:35:48Z |
Two more columns complete the file: concept_set_uid and concept_set_source_repo, filled when a suggestion was taken from a data-dictionary concept set.
Resume on interrupt
The scripts write their output incrementally, so a subsequent run resumes where the previous one stopped:
embed_concepts.pycheckpoints every 50 batches (~25,000 concepts) — you can interrupt without losing work.compute_scores.pycheckpoints every 100 source concepts —(source_vocabulary_id, source_concept_code)pairs already scored are skipped on restart.
This matters for complete vocabularies (4M concepts) that can take hours.
Going further
- Mapping with an AI agent — putting an agent to work, following and reviewing its suggestions.
- Agents — what an agent can do in Linkr, and how you stay in control.
- MCP server — create your API key and connect LibreChat or OpenCode.
- Evaluation — review and validate mappings, including those created by an agent.