Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

EntityCountIdentifierExample
Protein~575,503UniProt accessionP38398 (BRCA1)
Trial~589,453NCT idNCT00001496
Disease~139,194Mondo idMONDO_0016419
Gene~566,984gene symbolBRCA1
GO term~28,507GO idGO: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

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:

FieldMeaning
id, typethe identifier and entity type
namehuman-readable name / title
search_textthe text used for semantic matching (good for display/snippets too)
synonymsalternative names / symbols
native_xrefsreferences to the upstream source databases
outbound_source_linksURLs to the authoritative record(s)
attributestype-specific metadata (e.g. protein organism, trial phase)
edgesforward 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 GET a URL.
  • Search — one POST endpoint 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:

PathWhat you get
index.jsonthe root: entity types, counts, navigation
index/list/<type>/root.jsonhow a type's listing is sharded (count + shards)
index/list/<type>/<n>.jsonone shard: up to 1000 {id, name} entries
nodes/<type>/<id>.jsona single entity (the node and its forward edges)

And the cross-reference (backlink) indexes that make the graph two-way:

PathWhat 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 is POST. 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.json to 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.

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 }
  }
}
FieldMeaning
queryVectoryour text embedded to 384 floats (see below). Required.
topKhow many results to return.
indexeswhich sets to search — "proteins", "trials", or both.
filtersoptional 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:

SettingValue
ModelXenova/all-MiniLM-L6-v2
Revision751bff37182d3f1213fa05d7196b954e230abad9
Poolingmean
Normalizetrue
Dimension384

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

FilterExampleMeaning
organism"9606"NCBI taxon id (9606 = human)
has_disease_linktrueonly proteins with a disease edge

trials

FilterExampleMeaning
phase"Phase 3"trial phase
overall_status"Recruiting"recruitment status
last_update_year2024year 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.json to 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.jsonindex/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

SourceProvidesIdentifierHome
UniProt / Swiss-Protreviewed proteinsaccession (P38398)https://www.uniprot.org/
ClinicalTrials.govclinical trialsNCT id (NCT00001496)https://clinicaltrials.gov/
Mondo Disease Ontologydisease normalizationMondo id (MONDO_0016419)https://mondo.monarchinitiative.org/
ClinVargene–disease clinical significancegene symbol + varianthttps://www.ncbi.nlm.nih.gov/clinvar/
Gene Ontology (GO)functional annotationGO 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.