Introduction
lmmol.com is a cross-referenced biomedical knowledge graph assembled from open public data. It brings five kinds of entities into one navigable, machine-readable structure:
| Entity | Count | Identifier | Example |
|---|---|---|---|
| Protein | ~575,503 | UniProt accession | P38398 (BRCA1) |
| Trial | ~589,453 | NCT id | NCT00001496 |
| Disease | ~139,194 | Mondo id | MONDO_0016419 |
| Gene | ~566,984 | gene symbol | BRCA1 |
| GO term | ~28,507 | GO id | GO:0006281 |
These are cross-referenced: a protein links to its diseases, genes, and the literature; a clinical trial links to the conditions it studies; diseases link back to their proteins and trials; genes link to diseases through clinically-significant variants. The result is a graph you can walk from any starting point.
Two ways to find things
lmmol is built around the idea that search and browse are two ends of one spectrum of information-finding — and it serves both, for humans and for LLM agents.
- Search — give a natural-language query, get back the most relevant entities ranked by semantic similarity. Good for "what is relevant to this idea?"
- Browse — navigate a complete, multi-level index to land on exact entries and everything connected to them, with no loss. Good for "give me exactly this record, and all of its cross-references."
Browsing as an alternative to vector RAG
Most retrieval-augmented systems lean entirely on vector search: embed a query, fetch the nearest chunks, and hope the right facts are in there. lmmol's browse index is a plain, deterministic alternative. Every entity is reachable by following ids and paths — no embeddings, no similarity threshold, no truncation, nothing hidden. An LLM agent can walk
index.json → index/list/<type> → an entry → its cross-reference backlinks
to retrieve exactly the records it needs. Search is then one optional entry point into that graph, not the whole story. The rest of this book shows you how to use both.
What this book covers
- Concepts & Terms — the entities, identifiers, and the node/edge model.
- Accessing the Data — base URL, endpoints, and the no-auth browse layer.
- Browsing the Index — the index, listings, entries, and backlinks.
- Searching — the search API and how to embed a query.
- Recipes — copy-paste tasks with
curland Python. - Data Sources & Licensing — where the data comes from and your obligations.
Command-line tooling lives in separate projects; the data needs no special client, so everything here works with any HTTP library in any language.
Scope. This guide is about using lmmol.com's published data. It does not describe how that data is produced.
Concepts & Terms
This chapter defines the vocabulary you need to read the data. If you already know bioinformatics, skim the Data Model section.
Entity types & identifiers
- Protein — a protein from UniProt/Swiss-Prot (the curated, reviewed set).
Identified by a UniProt accession, a short alphanumeric code such as
P38398. lmmol includes proteins from all organisms. - Trial — a study registered on ClinicalTrials.gov. Identified by its NCT id,
e.g.
NCT00001496. - Disease — a disease or condition, normalized to the Mondo disease ontology.
Identified by a Mondo id written with an underscore, e.g.
MONDO_0016419(hereditary breast carcinoma). Normalizing to Mondo is what lets a protein's disease and a trial's condition meet at the same node. - Gene — a gene, identified by its gene symbol, e.g.
BRCA1. - GO term — a Gene Ontology term describing a
molecular function, biological process, or cellular component. Identified by a
GO id, e.g.
GO:0006281(DNA repair).
Other terms you'll meet
- Cross-reference (xref) — a link from one entity to another (protein → disease, trial → condition, gene → disease, …). The backbone of the graph.
- Pathogenic / Likely pathogenic — clinical-significance labels from ClinVar that mark gene–disease associations supported by disease-causing variants. lmmol surfaces these as gene ↔ disease links.
- Accession / NCT / Mondo id / GO id — the stable identifiers above. Stable ids are how you address and link records.
The data model
Everything is JSON. There are two layers:
1. Nodes — the entities themselves
A node lives at nodes/<type>/<id>.json and looks like this (abridged, real shape):
{
"type": "protein",
"id": "P38398",
"name": "Breast cancer type 1 susceptibility protein",
"search_text": "Breast cancer type 1 susceptibility protein E3 ubiquitin-protein ligase ...",
"synonyms": ["BRCA1", "RNF53", "..."],
"native_xrefs": [ { "...": "..." } ],
"outbound_source_links": [ "https://www.uniprot.org/uniprotkb/P38398" ],
"attributes": { "organism": "9606", "...": "..." },
"normalized": true,
"edges": [
{ "relation": "protein_disease",
"source": { "type": "protein", "id": "P38398" },
"target": { "type": "disease", "id": "MONDO_0016419" } }
]
}
Key fields:
| Field | Meaning |
|---|---|
id, type | the identifier and entity type |
name | human-readable name / title |
search_text | the text used for semantic matching (good for display/snippets too) |
synonyms | alternative names / symbols |
native_xrefs | references to the upstream source databases |
outbound_source_links | URLs to the authoritative record(s) |
attributes | type-specific metadata (e.g. protein organism, trial phase) |
edges | forward cross-references — see below |
2. Edges — the cross-references
Each edge is directional and carries a relation:
{ "relation": "protein_disease",
"source": { "type": "protein", "id": "P38398" },
"target": { "type": "disease", "id": "MONDO_0016419" } }
Relations you will see include protein_disease, protein_has_gene,
protein_annotated_with_go, protein_references_paper, trial_disease, and
trial_references_paper. Edges stored on a node are its forward links. To go the
other way (e.g. which proteins and trials point at this disease?), use the
cross-reference indexes described in Browsing the Index — that is
where the graph becomes fully navigable in both directions.
Disease nodes: canonical vs. raw
Most disease nodes are canonical ("normalized": true, a MONDO_* id). A few may
be raw/unmapped terms when no Mondo mapping exists; those are flagged
"normalized": false. Prefer canonical Mondo ids when you link or merge.
Accessing the Data
There are two ways in, and they share one base URL:
- Browse — plain, static JSON over HTTPS. No key, no auth, no rate-limit
handshake. Just
GETa URL. - Search — one
POSTendpoint that takes a query vector and returns ranked hits.
This chapter covers the conventions common to both. The next two chapters go deep on each.
Base URL
https://lmmol.com
Every browse path in this book is relative to that origin. All responses are JSON
(Content-Type: application/json) served over HTTPS through a CDN, so they are cacheable
and fast to fetch in bulk.
The browse layer (no auth)
Everything you can browse is a static file at a predictable path. The four shapes you will use most:
| Path | What you get |
|---|---|
index.json | the root: entity types, counts, navigation |
index/list/<type>/root.json | how a type's listing is sharded (count + shards) |
index/list/<type>/<n>.json | one shard: up to 1000 {id, name} entries |
nodes/<type>/<id>.json | a single entity (the node and its forward edges) |
And the cross-reference (backlink) indexes that make the graph two-way:
| Path | What you get |
|---|---|
index/disease/<MONDO_id>.json | {proteins, trials} pointing at a disease |
index/gene/<symbol>.json | {proteins} for a gene |
index/clinvar/gene/<symbol>.json | {diseases: [{id, name, significance}]} |
index/clinvar/disease/<MONDO_id>.json | {genes: [{symbol, significance}]} |
There is also a machine-oriented guide at llms.txt (see below).
<type> is one of protein, trial, disease, gene, go. Ids go in the path
exactly as written, except Mondo ids use an underscore (MONDO_0016419). URL-encode
ids that contain unusual characters (some gene symbols do).
Start at the root
curl https://lmmol.com/index.json
index.json reports each entity type with its count and where its listing lives.
Treat it as the entry point you crawl from — and the one file to re-read when you want
fresh counts.
The search endpoint
Semantic search is a single HTTP API:
POST https://rtxz9xxyz7.execute-api.us-east-1.amazonaws.com/search
It accepts a JSON body containing a query vector (you embed the text yourself — see Searching) and returns ranked results. It is metered and may be disabled for cost control; browse is always available. Because of that, prefer browse for anything you can address by id or path, and reach for search only when you actually need "what's semantically relevant?"
Conventions
- HTTP verbs — browse is
GET; search isPOST. Nothing else. - Missing records return
404. A successful browse fetch is always valid JSON. - Stable ids — ids are the durable contract. Paths are derived from ids, so once you have an id you can construct every URL for that entity yourself.
- Caching — browse files are static and CDN-cached. Fetch freely; cache locally
when you crawl at scale, and re-fetch
index.jsonto learn when counts have moved.
For LLM agents: llms.txt
curl https://lmmol.com/llms.txt
llms.txt is a short, link-free description of the data and these access patterns,
written for an agent to read once and then navigate on its own. It is deliberately
concise. An agent can read it, then walk index.json → listings → nodes → backlinks
without any further instructions. See Browsing the Index for why this
deterministic walk is a practical alternative to vector RAG.
Browsing the Index
Browsing is how you reach an exact record and everything connected to it, with no loss and no ranking in the way. It is the deterministic half of lmmol — and, for LLM agents, often the more useful half. This chapter walks the index top-to-bottom.
The shape of the index
index.json the root: types, counts, where to go
└─ index/list/<type>/root.json a type's listing: count + shard list
└─ index/list/<type>/<n>.json one shard: up to 1000 {id, name}
└─ nodes/<type>/<id>.json the entity itself (+ forward edges)
└─ index/.../<id>.json backlinks: who points *at* it
Each level is a plain static JSON file. You can crawl the whole thing with nothing but an HTTP client and the rules below.
1. The root
curl https://lmmol.com/index.json
The root lists every entity type, its count, and the navigation into its listing. Use it to discover what exists and how big each set is before you crawl.
2. Listings (paged by shards)
A type can have hundreds of thousands of entries, so listings are sharded into files of up to 1000 entries each.
# How is the protein listing sharded?
curl https://lmmol.com/index/list/protein/root.json
root.json gives you the total count and the list of shards. Then fetch a shard:
# The first shard: up to 1000 {id, name} pairs
curl https://lmmol.com/index/list/protein/0.json
Each shard entry is a compact { "id": "...", "name": "..." }. That is enough to show
a list, pick an id, and drill in — without downloading full nodes. To enumerate an
entire type, iterate shard 0.json, 1.json, … up to the count.
3. Nodes (the entities)
Once you have an id, fetch the node:
curl https://lmmol.com/nodes/protein/P38398.json
A node carries its name, synonyms, attributes, outbound_source_links (URLs to
the authoritative records), and its forward edges — its own cross-references out to
other entities. See Concepts & Terms for the full field list and the
edge shape.
Forward edges answer "what does this entity point to?" For the reverse — "what points at it?" — use the backlink indexes.
4. Backlinks (the graph goes both ways)
Edges stored on a node are one-directional. The cross-reference indexes give you the inbound side, so the graph is fully navigable from any node, in any direction.
Disease → its proteins and trials
curl https://lmmol.com/index/disease/MONDO_0016419.json
# -> { "proteins": [...], "trials": [...] }
This is the join that the Mondo normalization makes possible: a protein's disease and a trial's condition resolve to the same Mondo id, so this one file gives you both sides at once.
Gene → its proteins
curl https://lmmol.com/index/gene/BRCA1.json
# -> { "proteins": [...] }
Clinical associations (ClinVar)
Gene ↔ disease links backed by clinically-significant variants, each carrying a
significance label (e.g. Pathogenic, Likely pathogenic):
# Diseases associated with a gene
curl https://lmmol.com/index/clinvar/gene/BRCA1.json
# -> { "diseases": [ { "id": "MONDO_...", "name": "...", "significance": "Pathogenic" }, ... ] }
# Genes associated with a disease
curl https://lmmol.com/index/clinvar/disease/MONDO_0016419.json
# -> { "genes": [ { "symbol": "BRCA1", "significance": "Pathogenic" }, ... ] }
Browsing as an alternative to vector RAG
Vector RAG embeds a query, fetches the nearest chunks, and hopes the right facts landed in the window. The browse index removes the guesswork: every fact is reachable by following ids and paths — no embeddings, no similarity threshold, no truncation, nothing hidden. For an LLM agent that already knows an entity (a gene symbol, an accession, a Mondo id), the most reliable retrieval is not a similarity search — it is:
nodes/<type>/<id>.json # the record, exactly
index/disease/<id>.json # everything linked to it, exactly
index/clinvar/gene/<symbol>.json # the clinical associations, exactly
No relevance score can lose a record this walk would find. Search (next chapter) is one optional entry point into this graph for when you only have a fuzzy description — not a replacement for it.
A complete walk
A worked example: start from a disease, collect its proteins, and read one.
# 1. The disease's neighborhood
curl https://lmmol.com/index/disease/MONDO_0016419.json
# 2. Pick a protein id from the "proteins" array, then read it
curl https://lmmol.com/nodes/protein/P38398.json
# 3. Follow that protein's gene, then the gene's clinical associations
curl https://lmmol.com/index/clinvar/gene/BRCA1.json
Three deterministic hops, no ranking, no loss. Recipes turns walks like this into reusable scripts.
Searching
Search answers "what is relevant to this idea?" You send a query vector — an embedding of your text — and get back the nearest entities ranked by semantic similarity. The key thing to understand up front: you compute the embedding, the API matches it. That keeps the service simple and lets you embed in the browser, in a script, or wherever you like.
Search is metered and can be turned off for cost control. Browse is always on. If a task can be addressed by id or path, prefer browsing.
The endpoint
POST https://rtxz9xxyz7.execute-api.us-east-1.amazonaws.com/search
Content-Type: application/json
Request
{
"queryVector": [0.012, -0.043, ...], // 384 floats
"topK": 10,
"indexes": ["proteins", "trials"],
"filters": {
"proteins": { "organism": "9606", "has_disease_link": true },
"trials": { "phase": "Phase 3", "overall_status": "Recruiting",
"last_update_year": 2024 }
}
}
| Field | Meaning |
|---|---|
queryVector | your text embedded to 384 floats (see below). Required. |
topK | how many results to return. |
indexes | which sets to search — "proteins", "trials", or both. |
filters | optional per-index constraints (see Filters). |
Response
{
"results": [
{ "id": "P38398", "type": "protein", "distance": 0.21, "metadata": { "...": "..." } }
],
"tookMs": 18
}
Lower distance is better — it is a distance, not a score, so the closest match
sorts first. Each result carries its id and type, so the natural next step is to
fetch nodes/<type>/<id>.json and continue by browsing.
Embedding your query
The vectors are produced by a specific model, and your query must use the same model, same settings or the distances are meaningless:
| Setting | Value |
|---|---|
| Model | Xenova/all-MiniLM-L6-v2 |
| Revision | 751bff37182d3f1213fa05d7196b954e230abad9 |
| Pooling | mean |
| Normalize | true |
| Dimension | 384 |
The site reads these from the data's embedding_config, so they stay in sync with what
the index was built with. If the config ever advertises a new model, follow it.
In the browser (Transformers.js)
import { pipeline } from '@xenova/transformers';
const embed = await pipeline(
'feature-extraction',
'Xenova/all-MiniLM-L6-v2',
{ revision: '751bff37182d3f1213fa05d7196b954e230abad9' }
);
const out = await embed('BRCA1 DNA repair and breast cancer risk',
{ pooling: 'mean', normalize: true });
const queryVector = Array.from(out.data); // 384 floats
In Python (sentence-transformers)
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
query_vector = model.encode(
'BRCA1 DNA repair and breast cancer risk',
normalize_embeddings=True,
).tolist() # 384 floats
all-MiniLM-L6-v2 already mean-pools, and normalize_embeddings=True matches the
normalize: true setting. The end-to-end request appears in Recipes.
Filters
Filters narrow a result set server-side, before ranking, per index:
proteins
| Filter | Example | Meaning |
|---|---|---|
organism | "9606" | NCBI taxon id (9606 = human) |
has_disease_link | true | only proteins with a disease edge |
trials
| Filter | Example | Meaning |
|---|---|---|
phase | "Phase 3" | trial phase |
overall_status | "Recruiting" | recruitment status |
last_update_year | 2024 | year of last update |
Omit filters (or any index within it) to search without constraints.
When to search vs. browse
- Search when you only have a fuzzy description — a concept, a symptom, a mechanism — and need the system to surface candidates.
- Browse when you already hold an id, a symbol, or an accession. It is exact, always available, and never silently drops a record.
A common, robust pattern is search to discover, browse to complete: one search to find entry points, then deterministic browsing to pull every connected fact without loss.
Recipes
Copy-paste tasks that combine the browse and search layers. Each is self-contained;
swap in your own ids and queries. They assume curl, jq, and Python 3 with
sentence-transformers for the search examples.
BASE=https://lmmol.com
SEARCH=https://rtxz9xxyz7.execute-api.us-east-1.amazonaws.com/search
1. Look up one entity
curl -s $BASE/nodes/protein/P38398.json | jq '{id, name, synonyms}'
2. Everything connected to a disease
# Proteins and trials for hereditary breast carcinoma
curl -s $BASE/index/disease/MONDO_0016419.json \
| jq '{proteins: (.proteins|length), trials: (.trials|length)}'
3. Disease → proteins → read each one
BASE=https://lmmol.com
for id in $(curl -s $BASE/index/disease/MONDO_0016419.json | jq -r '.proteins[].id'); do
curl -s $BASE/nodes/protein/$id.json | jq -r '"\(.id)\t\(.name)"'
done
4. Clinical (ClinVar) associations for a gene
curl -s $BASE/index/clinvar/gene/BRCA1.json \
| jq -r '.diseases[] | "\(.significance)\t\(.id)\t\(.name)"'
5. Enumerate an entire type
# Walk every shard of the disease listing, printing id and name
BASE=https://lmmol.com
root=$(curl -s $BASE/index/list/disease/root.json)
shards=$(echo "$root" | jq -r '.shards | length') # or derive from count
for n in $(seq 0 $((shards - 1))); do
curl -s $BASE/index/list/disease/$n.json | jq -r '.[] | "\(.id)\t\(.name)"'
done
Be polite when crawling at scale: the files are CDN-cached, so cache locally and avoid re-fetching what you already have. Re-fetch
index.jsonto learn when counts have changed.
6. Semantic search, end to end (Python)
import requests
from sentence_transformers import SentenceTransformer
SEARCH = "https://rtxz9xxyz7.execute-api.us-east-1.amazonaws.com/search"
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
def search(text, indexes=("proteins", "trials"), top_k=10, filters=None):
qv = model.encode(text, normalize_embeddings=True).tolist()
body = {"queryVector": qv, "topK": top_k, "indexes": list(indexes)}
if filters:
body["filters"] = filters
r = requests.post(SEARCH, json=body, timeout=30)
r.raise_for_status()
return r.json()["results"] # sorted by distance, ascending
for hit in search("BRCA1 DNA repair and breast cancer risk"):
print(f'{hit["distance"]:.3f} {hit["type"]:8} {hit["id"]}')
7. Filtered search: recruiting Phase 3 trials
results = search(
"immunotherapy for advanced melanoma",
indexes=("trials",),
filters={"trials": {"phase": "Phase 3", "overall_status": "Recruiting"}},
)
8. Search to discover, browse to complete
The robust pattern: one search for entry points, then deterministic browsing to pull every connected fact without loss.
import requests
BASE = "https://lmmol.com"
# 1. Discover candidate proteins by concept
hits = search("tumor suppressor involved in homologous recombination",
indexes=("proteins",), top_k=5)
# 2. For each, browse its gene's clinical associations — exact, no ranking
for h in hits:
node = requests.get(f'{BASE}/nodes/protein/{h["id"]}.json').json()
for syn in node.get("synonyms", []):
cv = requests.get(f'{BASE}/index/clinvar/gene/{syn}.json')
if cv.ok:
for d in cv.json().get("diseases", []):
print(node["id"], syn, d["significance"], d["name"])
break
9. For an LLM agent: bootstrap from llms.txt
curl -s https://lmmol.com/llms.txt
Read it once, then navigate index.json → index/list/<type> → nodes/<type>/<id> →
backlink indexes with no further instructions. The walk is deterministic, so the agent
never has to guess whether a relevant record was left out of a similarity window — see
Browsing the Index.
Data Sources & Licensing
lmmol.com does not generate biomedical facts — it reorganizes and cross-references
open public data so it can be searched and browsed as one graph. Every record traces
back to an authoritative upstream source, linked from the node's
outbound_source_links. You are responsible for complying with each upstream
source's license and terms of use for whatever you do with the data.
Sources
| Source | Provides | Identifier | Home |
|---|---|---|---|
| UniProt / Swiss-Prot | reviewed proteins | accession (P38398) | https://www.uniprot.org/ |
| ClinicalTrials.gov | clinical trials | NCT id (NCT00001496) | https://clinicaltrials.gov/ |
| Mondo Disease Ontology | disease normalization | Mondo id (MONDO_0016419) | https://mondo.monarchinitiative.org/ |
| ClinVar | gene–disease clinical significance | gene symbol + variant | https://www.ncbi.nlm.nih.gov/clinvar/ |
| Gene Ontology (GO) | functional annotation | GO id (GO:0006281) | http://geneontology.org/ |
Genes are identified by gene symbol and tied to proteins, diseases, and clinical significance through the sources above.
Licensing of the upstream data
Each source sets its own terms; always check the current terms at the source before redistributing or building a product on the data. As of writing:
- UniProt — distributed under CC BY 4.0; attribute UniProt.
- ClinicalTrials.gov — U.S. National Library of Medicine data; review the terms and conditions.
- Mondo — released under CC BY 4.0 by the Monarch Initiative.
- ClinVar — public-domain U.S. government data; see NCBI's policies.
- Gene Ontology — released under CC BY 4.0.
When in doubt, follow the link in a node's outbound_source_links to the authoritative
record and cite that.
How to cite
Cite the upstream source for any specific fact, and acknowledge lmmol.com for the cross-referenced, navigable structure. lmmol is an organizing layer over public data, not a primary source.
Freshness
Counts and contents change as upstream sources are refreshed. index.json reports the
current per-type counts — fetch it to see what is live now rather than relying on the
example numbers in this book.
Scope reminder
This book documents how to use lmmol.com's published data. How that data is assembled — the ingest, normalization, and build pipeline — is out of scope and lives in a separate, private project.