diff --git a/.github/workflows/release-nameres-loading.yml b/.github/workflows/release-nameres-loading.yml index cc7e3753..27c62c99 100644 --- a/.github/workflows/release-nameres-loading.yml +++ b/.github/workflows/release-nameres-loading.yml @@ -17,9 +17,6 @@ jobs: steps: - name: Check out the repo uses: actions/checkout@v2 - - name: Get the version - id: get_version - run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 @@ -44,3 +41,16 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: BRANCH_NAME=${{ github.event.release.target_commitish }} + # Without a cache every run rebuilds the whole image: apt upgrade, + # seven apt installs and a ~250MB Solr tarball fetched through an + # Apache mirror, which is why a build takes ~25 minutes. Layers + # before the first change now come from the GitHub Actions cache. + # mode=max keeps intermediate layers too, so a change late in the + # Dockerfile (the COPYs) does not re-download Solr. + # + # The trade is that `apt upgrade` is cached as well, so an image + # built from an unchanged Dockerfile stops picking up new Debian + # packages. That is the right way round here: this is short-lived + # build tooling, and any Dockerfile edit invalidates the cache. + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 9902ed0b..546e5653 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -22,8 +22,19 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Run SOLR - run: docker run --name name_lookup -d -p 8983:8983 -t solr -DzkRun + - name: Run Solr (standalone) + run: docker run --name name_lookup -d -p 8983:8983 solr:9.10 + + - name: Create the name_lookup core from the checked-in configset + run: | + for _ in $(seq 60); do + curl -sf 'http://localhost:8983/solr/admin/cores?action=STATUS' > /dev/null && break + sleep 2 + done + curl -sf 'http://localhost:8983/solr/admin/cores?action=STATUS' > /dev/null \ + || { echo "Solr did not come up:"; docker logs name_lookup; exit 1; } + docker cp data-loading/configsets/name_lookup name_lookup:/tmp/name_lookup + docker exec name_lookup solr create -c name_lookup -d /tmp/name_lookup - name: Install dependencies run: | @@ -38,3 +49,62 @@ jobs: - name: Run the tests run: | python -m pytest tests/ + + # The loader's guard is what stands between us and a silently half-loaded + # 130 GB index, so check that a bad file actually stops it. LOAD_APPEND=1 + # because the core is loaded by now, and without it the loader would refuse + # for that reason instead -- the test would pass without testing anything. + - name: The loader must abort on a file that does not load + run: | + echo 'this is not json' > /tmp/malformed.json + if LOAD_APPEND=1 ./data-loading/setup-and-load-solr.sh /tmp/malformed.json; then + echo "Expected the loader to fail on a malformed file, but it succeeded." + exit 1 + fi + echo "Loader aborted as expected." + + # The other half of that guard: loading is additive and assigns fresh UUIDs, so + # a re-run over the same files doubles the index and the document-count check + # cannot see it (the delta is still exactly right). Refusing a non-empty core is + # the only thing that catches it. + - name: The loader must refuse a core that already has documents + run: | + if ./data-loading/setup-and-load-solr.sh tests/data/test-synonyms.json; then + echo "Expected the loader to refuse a non-empty core, but it loaded again." + exit 1 + fi + echo "Loader refused the non-empty core as expected." + + # The backup is the actual product of data-loading/, so check that a core can be + # tarred up, dropped into a fresh Solr, and served without any restore-time setup. + # + # The tar flags are the ones the Makefile uses, and the restore deliberately does + # NOT chown afterwards: the loading image runs as uid 1000 and the serving image + # as 8983, so if the tarball did not stamp the ownership the restored core would + # be unwritable and Solr would fail to take its write lock. chown-ing here would + # hide exactly the bug this is meant to catch. The chown to 1000 below stands in + # for the loading image, so the source ownership is wrong before we start. + - name: Verify the backup roundtrip (tar the core, restore it into a fresh Solr) + run: | + docker exec -u root name_lookup chown -R 1000:1000 /var/solr/data/name_lookup + docker exec -u root name_lookup tar -C /var/solr/data \ + --numeric-owner --owner=8983 --group=8983 \ + --exclude='name_lookup/data/tlog' \ + -cf - name_lookup > snapshot.backup.tar + docker run --name name_lookup_restored -d -p 8984:8983 solr:9.10 + for _ in $(seq 60); do + curl -sf 'http://localhost:8984/solr/admin/cores?action=STATUS' > /dev/null && break + sleep 2 + done + docker cp snapshot.backup.tar name_lookup_restored:/tmp/snapshot.backup.tar + docker exec -u root name_lookup_restored tar -C /var/solr/data --numeric-owner -xf /tmp/snapshot.backup.tar + docker restart name_lookup_restored + for _ in $(seq 60); do + curl -sf 'http://localhost:8984/solr/name_lookup/admin/ping' > /dev/null && break + sleep 2 + done + count=$(curl -sf 'http://localhost:8984/solr/name_lookup/query?q=*:*&rows=0' \ + | grep -oE '"numFound":[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+') + expected=$(grep -c '[^[:space:]]' tests/data/test-synonyms.json) + echo "Restored core reports ${count:-0} documents; expected ${expected}." + [ "${count:-0}" = "${expected}" ] diff --git a/CLAUDE.md b/CLAUDE.md index 3cce0fb5..960b4201 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,12 @@ NameRes (Name Resolver) is a biomedical entity name resolution service that maps ### Running Tests ```bash -# Load test data into Solr first (requires Solr running) +# Start standalone Solr and create the name_lookup core from the checked-in configset +docker run --name name_lookup -d -p 8983:8983 solr:9.10 +docker cp data-loading/configsets/name_lookup name_lookup:/tmp/name_lookup +docker exec name_lookup solr create -c name_lookup -d /tmp/name_lookup + +# Load test data into the core (parallel load, with a document-count guard) ./data-loading/setup-and-load-solr.sh tests/data/test-synonyms.json # Run all tests @@ -54,38 +59,56 @@ pip install -r requirements.txt 4. Results are scored, normalized, and returned as JSON ### Key Files -- `api/server.py` - Core FastAPI application (~717 lines): all endpoints, Pydantic models, Solr query construction, environment config +- `api/server.py` - Core FastAPI application: all endpoints, Pydantic models, Solr query construction, environment config - `api/apidocs.py` - Custom OpenAPI schema construction -- `api/resources/.openapi.yml` - OpenAPI 3.0.2 spec with service metadata +- `api/resources/openapi.yml` - OpenAPI 3.0.2 spec with service metadata - `main.py` / `main.sh` - WSGI/ASGI entry points (port 2433) - `tests/test_service.py` - Integration tests using FastAPI `TestClient` - `tests/data/test-synonyms.json` - Test dataset for Solr ### Environment Variables -- `SOLR_HOST` / `SOLR_PORT` - Solr connection (default: `localhost:8983`) +See `documentation/Deployment.md` for the full list. The main ones: +- `SOLR_HOST` / `SOLR_PORT` / `SOLR_CORE` - Solr connection (default: `localhost:8983`, core `name_lookup`) - `LOGLEVEL` - Logging level -- `SERVER_ROOT` - API root path prefix +- `SERVER_NAME` / `SERVER_ROOT` - Infores ID and API root path prefix - `MATURITY_VALUE` / `LOCATION_VALUE` - TRAPI metadata fields +- `BABEL_VERSION` / `BABEL_VERSION_URL` / `BIOLINK_MODEL_TAG` - reported by `/status`; describe the + data the index was built from +- `OTEL_ENABLED` / `JAEGER_*` - OpenTelemetry ### API Endpoints - `GET/POST /lookup` - Primary name-to-CURIE lookup with scoring - `POST /bulk-lookup` - Batch queries via `NameResQuery` model -- `GET /reverse-lookup` - CURIE-to-names lookup -- `POST /synonyms` - Get synonyms for a list of CURIEs -- `POST /lookup-curies` - Filter existing CURIEs with type subsetting -- `GET /status` - Health check with Solr document counts +- `GET/POST /synonyms` - Get synonyms for a list of preferred CURIEs +- `GET/POST /reverse_lookup` - Deprecated alias for `/synonyms` +- `GET /status` - Health check with Solr document counts, plus the Babel and Biolink versions ### Data Model Solr documents contain: `curie`, `preferred_name`, `names` (synonym list), and biolink type information. Lookup results are `LookupResult` objects with scoring fields. Results are conflated using GeneProtein and DrugChemical conflation rules. ### Infrastructure - **Stateless API container** - Python 3.11.5/FastAPI -- **Persistent Solr 9.1** - Data in volume-mounted `./data/solr` +- **Persistent Solr 9.10 (standalone)** - Data in volume-mounted `./data/solr` - **Data loading** - Separate pipeline in `data-loading/` (Makefile-driven, also has Kubernetes configs) - **CI/CD** - GitHub Actions: runs tests on push, publishes Docker image to GitHub Packages on release ## Documentation - `documentation/API.md` - Endpoint reference +- `documentation/Babel.md` - Where the data comes from, and the Babel behaviour visible through this + API. This is the only file that should link to a *specific file* inside the Babel repository; keep + new cross-repo links here so a reorganization there is a one-file fix. - `documentation/Deployment.md` - Docker/Kubernetes deployment guide - `documentation/Scoring.md` - Scoring algorithm details -- `documentation/NameResolution.ipynb` - Interactive usage examples \ No newline at end of file +- `documentation/NameResolution.ipynb` - Interactive usage examples +- `documentation/TranslatorGuide.md` - Translator-specific usage guidance + +### Linking to GitHub + +NameResolution, Babel and NodeNormalization all use `main` as their default branch. Babel's was +renamed from `master` fairly recently and NameResolution has no `master` branch at all, so +`/blob/master/` URLs resolve only through GitHub's post-rename redirect — they look fine until +that redirect goes away. Always write `/blob/main/`. + +Do not check this against a local clone's `origin/HEAD`: that ref is cached at clone time and does +not follow a remote rename, so it will still say `master` long after the rename. Use +`gh repo view / --json defaultBranchRef`. \ No newline at end of file diff --git a/README.md b/README.md index 79c60fe0..2eaa89a9 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,30 @@ Name Resolver (Name Lookup or NameRes) takes lexical strings and attempts to map along with many other options. Given a preferred CURIE, the known synonyms of that CURIE can also be retrieved. Multiple results may be returned representing possible conceptual matches, but all of the identifiers have been correctly normalized using the [Node Normalization](https://github.com/NCATSTranslator/NodeNormalization) service. -Note that the results returned by this service have been conflated using both GeneProtein and DrugChemical conflation; you can read more about this at the [Conflation documentation](https://github.com/NCATSTranslator/Babel/blob/master/docs/Conflation.md). +The concepts NameRes searches are built by the [Babel](https://github.com/NCATSTranslator/Babel) +pipeline. Note that the results returned by this service have been conflated using both GeneProtein +and DrugChemical conflation; you can read more about this, and about the other Babel behaviour +visible through this API, at [Where NameRes data comes from](documentation/Babel.md). -* See this [Jupyter Notebook](documentation/NameResolution.ipynb) for examples of use. -* See the [API documentation](documentation/API.md) for information about the NameRes API. -* See [Scoring](documentation/Scoring.md) for information about the scoring algorithm used by NameRes. -* See [Deployment](documentation/Deployment.md) for instructions on deploying NameRes. +## Getting started + +The best place to start is the Jupyter Notebook, which walks through the most common use cases with live examples: + +* [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/NCATSTranslator/NameResolution/blob/main/documentation/NameResolution.ipynb) [Jupyter Notebook](documentation/NameResolution.ipynb) — interactive examples covering lookup, filtering, autocomplete, bulk lookup, and synonyms + +## Documentation + +* [Translator Guide](documentation/TranslatorGuide.md) — what to do when results are unexpected, when to use `/synonyms` vs. NodeNorm, and performance tips +* [API documentation](documentation/API.md) — full reference for all NameRes endpoints +* [Where NameRes data comes from](documentation/Babel.md) — how Babel builds the concepts NameRes searches, and the gotchas that follow from that +* [Scoring](documentation/Scoring.md) — how NameRes scores and ranks results +* [Deployment](documentation/Deployment.md) — instructions for deploying NameRes + +## Reporting a problem + +If a *concept* is wrong -- identifiers merged that shouldn't be, a wrong Biolink type or preferred +label, missing synonyms -- report it to [Babel](https://github.com/NCATSTranslator/Babel/issues), +which builds the concepts. If the *service* is wrong -- errors, badly ranked results, a parameter +not behaving as documented -- report it +[here](https://github.com/NCATSTranslator/NameResolution/issues). If you're not sure, file it in +Babel. See [Where NameRes data comes from](documentation/Babel.md#reporting-a-problem) for details. diff --git a/api/apidocs.py b/api/apidocs.py index 9434b6b3..61d9c1c0 100644 --- a/api/apidocs.py +++ b/api/apidocs.py @@ -35,8 +35,10 @@ def construct_open_api_schema(app) -> Dict[str, str]: with open(Path(__file__).parent / 'resources' / 'openapi.yml', 'r') as apd_file: api_docs = load(apd_file, Loader=SafeLoader) + # Already built (FastAPI caches it on the app), so hand back the cached schema. + # This is a dict, not a callable -- calling it raises TypeError. if app.openapi_schema: - return app.openapi_schema() + return app.openapi_schema open_api_schema = get_openapi( title=api_docs['info']['title'], diff --git a/api/resources/openapi.yml b/api/resources/openapi.yml index 07313170..3a2e0d2a 100644 --- a/api/resources/openapi.yml +++ b/api/resources/openapi.yml @@ -1,7 +1,7 @@ openapi: 3.0.2 info: title: Name Resolver - version: 1.6.2 + version: 1.7.0 email: bizon@renci.org name: Chris Bizon x-id: https://github.com/cbizon @@ -13,8 +13,11 @@ info: have been correctly normalized using the Node Normalization service.

You can read more about this API on the NameResolution GitHub repository.

- Note that the returned by this service have been conflated using both GeneProtein and DrugChemical conflation; - you can read more about this at the Conflation documentation.' +

Note that the results returned by this service have been conflated using both GeneProtein and DrugChemical + conflation; the active conflations for any deployment can be discovered via the /status endpoint. + You can read more about this, and about the other Babel + behaviour visible through this API, at + Where NameRes data comes from.

' license: name: MIT url: https://opensource.org/licenses/MIT diff --git a/api/server.py b/api/server.py index 2a2277b6..4cf31c95 100755 --- a/api/server.py +++ b/api/server.py @@ -22,6 +22,9 @@ SOLR_HOST = os.getenv("SOLR_HOST", "localhost") SOLR_PORT = os.getenv("SOLR_PORT", "8983") +# The Solr core to query. In standalone mode this is the core name; the cloud-mode +# backups we used to ship called it name_lookup_shard1_replica_n1 instead (see status()). +SOLR_CORE = os.getenv("SOLR_CORE", "name_lookup") app = FastAPI(**get_app_info()) logger = logging.getLogger(__name__) @@ -48,7 +51,7 @@ async def docs_redirect(): @app.get("/status", summary="Get status and counts for this NameRes instance.", description="

This endpoint will return status information and a list of counts from the underlying Solr database instance for this NameRes instance.

" - "

You can find out more about this endpoint in the API documentation.

" + "

You can find out more about this endpoint in the API documentation.

" ) async def status_get() -> Dict: """ Return status and count information from the underyling Solr instance. """ @@ -71,6 +74,11 @@ async def status() -> Dict: babel_version = os.environ.get("BABEL_VERSION", "unknown") babel_version_url = os.environ.get("BABEL_VERSION_URL", "") + # Which conflations are active in this deployment? Baked in at data-loading time. + conflations_raw = os.environ.get("CONFLATIONS", "GeneProtein,DrugChemical") + conflations = [c.strip() for c in conflations_raw.split(",") if c.strip()] + conflation_url = "https://github.com/NCATSTranslator/Babel/blob/main/docs/Conflation.md" + # Look up the BIOLINK_MODEL_TAG. # Note: this should be a tag from the Biolink Model repo, e.g. "master" or "v4.3.6". biolink_model_tag = os.environ.get("BIOLINK_MODEL_TAG", "master") @@ -83,10 +91,16 @@ async def status() -> Dict: if 'version' in app_info and app_info['version']: nameres_version = 'v' + app_info['version'] - # We should have a status for name_lookup_shard1_replica_n1. - if 'status' in result and 'name_lookup_shard1_replica_n1' in result['status']: - core = result['status']['name_lookup_shard1_replica_n1'] + # We should have a status for our core. Standalone Solr calls it ${SOLR_CORE} + # (name_lookup); the older cloud-mode backups called it + # name_lookup_shard1_replica_n1. A NameRes Solr only ever has one core, so if the + # expected name isn't there but there is exactly one core, report on that one. + cores = result.get('status', {}) + core = cores.get(SOLR_CORE) + if core is None and len(cores) == 1: + core = next(iter(cores.values())) + if core is not None: index = {} if 'index' in core: index = core['index'] @@ -101,8 +115,14 @@ async def status() -> Dict: 'url': biolink_model_url, 'download_url': biolink_model_download_url, }, + 'conflations': conflations, + 'conflation_url': conflation_url, 'nameres_version': nameres_version, - 'startTime': core['startTime'], + # .get() rather than [], like every field below it: Solr's core STATUS + # returns a sparse entry for a core that is still initializing, and + # /status is what the Kubernetes probes call. A KeyError here would turn + # a startup window into a 500 instead of a report with blank counts. + 'startTime': core.get('startTime', ''), 'numDocs': index.get('numDocs', ''), 'maxDoc': index.get('maxDoc', ''), 'deletedDocs': index.get('deletedDocs', ''), @@ -122,6 +142,8 @@ async def status() -> Dict: 'url': biolink_model_url, 'download_url': biolink_model_download_url, }, + 'conflations': conflations, + 'conflation_url': conflation_url, 'nameres_version': nameres_version, } @@ -166,8 +188,8 @@ async def reverse_lookup_get( "/synonyms", summary="Look up synonyms for a CURIE.", description="

Returns a list of synonyms for a particular preferred CURIE. You can normalize a CURIE to a preferred CURIE using NodeNorm.

" - "

You can find out more about this endpoint in the API documentation.

" - "

Note that CURIEs are conflated with both GeneProtein and DrugChemical conflation, so that e.g. when searching for a protein, the identifier of the gene that encodes the protein will be returned itself. See Conflation documentation for more information.

", + "

You can find out more about this endpoint in the API documentation.

" + "

Note that CURIEs are conflated with both GeneProtein and DrugChemical conflation, so that e.g. when searching for a protein, the identifier of the gene that encodes the protein will be returned itself. See Conflation documentation for more information.

", response_model=Dict[str, Dict], tags=["lookup"], ) @@ -202,8 +224,8 @@ async def lookup_names_post( "/synonyms", summary="Look up synonyms for a CURIE.", description="

Returns a list of synonyms for a particular preferred CURIE. You can normalize a CURIE to a preferred CURIE using NodeNorm.

" - "

You can find out more about this endpoint in the API documentation.

" - "

Note that CURIEs are conflated with both GeneProtein and DrugChemical conflation, so that e.g. a protein that encodes a gene can be looked up with the gene's CURIE, not the protein's CURIE. See Conflation documentation for more information.

", + "

You can find out more about this endpoint in the API documentation.

" + "

Note that CURIEs are conflated with both GeneProtein and DrugChemical conflation, so that e.g. a protein that encodes a gene can be looked up with the gene's CURIE, not the protein's CURIE. See Conflation documentation for more information.

", response_model=Dict[str, Dict], tags=["lookup"], ) @@ -219,7 +241,7 @@ async def synonyms_post( async def name_lookup(curies) -> Dict[str, Dict]: """Returns a list of synonyms for a particular CURIE.""" time_start = time.time_ns() - query = f"http://{SOLR_HOST}:{SOLR_PORT}/solr/name_lookup/select" + query = f"http://{SOLR_HOST}:{SOLR_PORT}/solr/{SOLR_CORE}/select" curie_filter = " OR ".join( f"curie:\"{curie}\"" for curie in curies @@ -260,8 +282,8 @@ class LookupResult(BaseModel): @app.get("/lookup", summary="Look up cliques for a fragment of a name or synonym.", description="

Returns cliques with a name or synonym that contains a specified string.

" - "

You can find out more about this endpoint in the API documentation.

" - "

Note that CURIEs are conflated with both GeneProtein and DrugChemical conflation, so that e.g. when searching for a protein, the identifier of the gene that encodes the protein will be returned itself. See Conflation documentation for more information.

", + "

You can find out more about this endpoint in the API documentation.

" + "

Note that CURIEs are conflated with both GeneProtein and DrugChemical conflation, so that e.g. when searching for a protein, the identifier of the gene that encodes the protein will be returned itself. See Conflation documentation for more information.

", response_model=List[LookupResult], tags=["lookup"] ) @@ -281,7 +303,7 @@ async def lookup_curies_get( ge=0 )] = 0, limit: Annotated[int, Query( - description="The number of results to skip. Can be used to page through the results of a query.", + description="The number of results to return. Can be used to page through the results of a query.", # Limit should be greater than or equal to zero and less than or equal to 1000. ge=0, le=1000 @@ -324,8 +346,8 @@ async def lookup_curies_get( @app.post("/lookup", summary="Look up cliques for a fragment of a name or synonym.", description="

Returns cliques with a name or synonym that contains a specified string.

" - "

You can find out more about this endpoint in the API documentation.

" - "

Note that CURIEs are conflated with both GeneProtein and DrugChemical conflation, so that e.g. when searching for a protein, the identifier of the gene that encodes the protein will be returned itself. See Conflation documentation for more information.

", + "

You can find out more about this endpoint in the API documentation.

" + "

Note that CURIEs are conflated with both GeneProtein and DrugChemical conflation, so that e.g. when searching for a protein, the identifier of the gene that encodes the protein will be returned itself. See Conflation documentation for more information.

", response_model=List[LookupResult], tags=["lookup"] ) @@ -345,7 +367,7 @@ async def lookup_curies_post( ge=0 )] = 0, limit: Annotated[int, Query( - description="The number of results to skip. Can be used to page through the results of a query.", + description="The number of results to return. Can be used to page through the results of a query.", # Limit should be greater than or equal to zero and less than or equal to 1000. ge=0, le=1000 @@ -534,7 +556,7 @@ async def lookup(string: str, logger.debug(f"Query: {json.dumps(params, indent=2)}") time_solr_start = time.time_ns() - query_url = f"http://{SOLR_HOST}:{SOLR_PORT}/solr/name_lookup/select" + query_url = f"http://{SOLR_HOST}:{SOLR_PORT}/solr/{SOLR_CORE}/select" async with httpx.AsyncClient(timeout=None) as client: response = await client.post(query_url, json=params) if response.status_code >= 300: @@ -638,7 +660,7 @@ class NameResQuery(BaseModel): ) limit: Optional[int] = Field( 10, - description="The number of results to skip. Can be used to page through the results of a query.", + description="The number of results to return. Can be used to page through the results of a query.", # Limit should be greater than or equal to zero and less than or equal to 1000. ge=0, le=1000 @@ -678,8 +700,8 @@ class NameResQuery(BaseModel): @app.post("/bulk-lookup", summary="Look up cliques for a fragment of multiple names or synonyms.", description="

Returns cliques for each query.

" - "

You can find out more about this endpoint in the API documentation.

" - "

Note that CURIEs are conflated with both GeneProtein and DrugChemical conflation, so that e.g. a protein that encodes a gene can be looked up with the gene's CURIE, not the protein's CURIE. See Conflation documentation for more information.

", + "

You can find out more about this endpoint in the API documentation.

" + "

Note that CURIEs are conflated with both GeneProtein and DrugChemical conflation, so that e.g. a protein that encodes a gene can be looked up with the gene's CURIE, not the protein's CURIE. See Conflation documentation for more information.

", response_model=Dict[str, List[LookupResult]], tags=["lookup"] ) diff --git a/data-loading/Dockerfile b/data-loading/Dockerfile index 331b9fcc..208a09f0 100644 --- a/data-loading/Dockerfile +++ b/data-loading/Dockerfile @@ -7,6 +7,29 @@ # # [1] https://github.com/helxplatform/translator-devops/blob/affcf34cf103230d25bdb859098d2a5ac81a49fb/helm/name-lookup/templates/scripts-config-map.yaml#L8-L105 +# Solr is lifted out of the official Solr image rather than downloaded. +# +# It used to be fetched with `ADD https://www.apache.org/dyn/closer.lua/...`, which +# redirects to a randomly chosen Apache mirror. Mirrors vary from fast to barely +# moving, and ADD-from-a-URL has no timeout, no retry and no resume, so a bad draw +# hung the build until the job timed out. Nothing verified the download either. +# +# Pulling the official image instead removes all of that: it comes over a CDN, the +# daemon verifies it by digest, and it is cached like any other layer. It also means +# the Solr that builds a backup is bit-for-bit the Solr that serves it. +# +# Pinned exactly, on purpose: this version is the floor for every Solr that serves the +# resulting backup, while the servers track the 9.10 line and float up. See +# "Restoring the backup" in README.md. Latest at https://solr.apache.org/downloads.html +# --platform is pinned because the RENCI base image below is published for amd64 only, +# so that is the only architecture this image can be. Without it, a build on an arm64 +# machine resolves this stage to arm64 (solr is multi-arch) while the final stage falls +# back to amd64, and the copied JDK is silently the wrong architecture: every java call +# then fails with "No such file or directory", which is the dynamic loader's unhelpful +# way of saying the ELF header is foreign. +ARG SOLR_VERSION=9.10.1 +FROM --platform=linux/amd64 solr:${SOLR_VERSION} AS solr + # Use the RENCI Python image to make it easier to work with other # RENCI Docker packages and to make sure we have an up to date image. # (https://github.com/TranslatorSRI/RENCI-Python-image) @@ -15,9 +38,6 @@ FROM ghcr.io/translatorsri/renci-python-image:3.11.5 # Configuration options: # - ${ROOT} is where the source code will be copied. ARG ROOT=/code/nameres-data-loading -# - ${SOLR_VERSION} is the SOLR version to install. -# You can find the latest version at https://solr.apache.org/downloads.html -ARG SOLR_VERSION=9.10.0 # - ${SOLR_DIR} is the SOLR directory to use. ARG SOLR_DIR=/var/solr @@ -29,12 +49,13 @@ RUN apt -y upgrade RUN apt install -y python3-venv RUN pip3 install --upgrade pip -# We need Java 11 to run SOLR. -RUN apt install -y openjdk-11-jre - -# SOLR uses lsof to check on its status. +# SOLR uses lsof to check on its status. (It also needs ps, which the base image has.) RUN apt install -y lsof +# pigz (parallel gzip) is used by the Makefile to compress the backup; gzip alone +# takes hours on a 130 GB index. +RUN apt install -y pigz + # The following packages are useful in debugging, but can be # removed once this container is working properly. RUN apt install -y htop @@ -53,10 +74,13 @@ RUN chown nru:nru ${ROOT} WORKDIR ${ROOT} USER nru -# Download Solr into the Solr directory. -RUN mkdir -p "${ROOT}/solr" -ADD --chown=nru:nru https://www.apache.org/dyn/closer.lua/solr/solr/${SOLR_VERSION}/solr-${SOLR_VERSION}.tgz?action=download "${ROOT}/solr" -RUN tar zxvf "${ROOT}/solr/solr-${SOLR_VERSION}.tgz" --directory "${ROOT}/solr" +# Solr itself, and the JDK it ships with, taken straight from the official image. +# Owned by nru so `bin/solr` can write its own logs when SOLR_LOGS_DIR is not set +# (the Makefile does set it; the ENTRYPOINT below does not). +COPY --from=solr --chown=nru:nru /opt/solr /opt/solr +COPY --from=solr /opt/java /opt/java +ENV JAVA_HOME=/opt/java/openjdk +ENV PATH="${JAVA_HOME}/bin:${PATH}" # Set up VENV. ENV VIRTUAL_ENV=${ROOT}/venv @@ -67,12 +91,22 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH" COPY --chown=nru requirements.txt ${ROOT} RUN pip3 install -r requirements.txt -# Copy necessary files. +# Copy necessary files. The configset carries the Solr schema/config; the core is +# created from it with `solr create -c name_lookup -d configsets/name_lookup`. COPY --chown=nru setup-and-load-solr.sh ${ROOT} +COPY --chown=nru available-cpus.sh ${ROOT} COPY --chown=nru README.md ${ROOT} COPY --chown=nru Makefile ${ROOT} +COPY --chown=nru configsets ${ROOT}/configsets -# On entry, start the Solr instance. -ENV SOLR_EXEC="${ROOT}/solr/solr-${SOLR_VERSION}/bin/solr" +# On entry, start the Solr instance (standalone -- no ZooKeeper for a one-node load). +# +# SOLR_MEM matches the Makefile's default, and for the same reason: `solr -m` sets +# both -Xms and -Xmx, and indexing wants the memory as page cache rather than heap +# (see the SOLR_MEM comment in the Makefile). Note that the Kubernetes pod replaces +# this command entirely and lets the Makefile start Solr, so this entrypoint only +# matters when the image is run directly. +ENV SOLR_EXEC="/opt/solr/bin/solr" ENV SOLR_DIR="$SOLR_DIR" -ENTRYPOINT ${SOLR_EXEC} -cloud -f -p 8983 -m 64G -s ${SOLR_DIR} +ENV SOLR_MEM="31G" +ENTRYPOINT ${SOLR_EXEC} start -f --user-managed -p 8983 -m ${SOLR_MEM} -s ${SOLR_DIR} diff --git a/data-loading/Makefile b/data-loading/Makefile index e0028bfd..173a3ebe 100644 --- a/data-loading/Makefile +++ b/data-loading/Makefile @@ -1,83 +1,186 @@ # This Makefile contains all the instructions necessary to -# download Babel synonym files from a web location, create -# and load them into a Solr dataset and generate a Solr backup -# that can be used to start a NameRes instance. +# download Babel synonym files from a web location, load them into a +# standalone Solr core (using the checked-in configset under +# configsets/name_lookup) and generate a self-contained Solr backup that can be +# used to start a NameRes instance. # +# The backup is the whole Solr core -- config, schema AND index -- so restoring +# it is just "untar into the Solr home and start Solr" (see ../data-loading/README.md). -# Configuration -SYNONYMS_URL=https://stars.renci.org/var/babel_outputs/2025sep1/synonyms/ +# Recipes below use pipefail, which /bin/sh (dash on Debian) does not support. +SHELL := /bin/bash -# How much memory should Solr use. -SOLR_MEM=220G +# Configuration. These use ?= so that they can be overridden from the environment +# (e.g. from a Kubernetes pod spec) without editing this file. +SYNONYMS_URL ?= https://stars.renci.org/var/babel_outputs/2026jul22/synonyms/ -# SOLR_DIR should be set up to point to the Solr data directory (usually /var/solr) -# and SOLR_EXEC should be set up to point to the Solr executable. -# These will both be set up by the Dockerfile. +# Solr's heap during the load. `solr -m` sets BOTH -Xms and -Xmx, so this is +# committed, not just a ceiling. +# +# Indexing does not want a big heap: Lucene buffers documents in ramBufferSizeMB +# (2 GB, set in the configset) and streams merges through the OS page cache, so +# heap beyond a few GB is memory taken away from that cache. Staying under 32G also +# keeps compressed object pointers, which a larger heap silently gives up. +# See data-loading/kubernetes/README.md for how to check this against Grafana. +SOLR_MEM ?= 31G + +# How many CPUs we may actually use -- the cgroup limit in a container, not the +# node's core count. Used for the parallel compress below; setup-and-load-solr.sh +# works this out for itself. +NCPU := $(shell bash ./available-cpus.sh 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4) + +# A 130 GB Lucene index is already largely compressed, and gzip is single-threaded, +# so compressing the backup with gzip takes hours for a few percent. Use pigz +# (parallel gzip, same format) when it is available. -p keeps pigz inside our CPU +# quota; left alone it starts one thread per *node* core and spends the difference +# being throttled. +GZIP_CMD := $(shell command -v pigz > /dev/null 2>&1 && echo "pigz -p $(NCPU)" || echo gzip) + +# The checked-in configset used to create the core (contains conf/). +# Absolute, and it has to be: `solr create -d` resolves a *relative* path against +# Solr's own server/solr/configsets directory, not the working directory, so a +# relative path here fails with "Can't find resource 'solrconfig.xml'". +CONFIGSET ?= $(CURDIR)/configsets/name_lookup + +# The Solr core name. Do not change without also changing NameRes and the configset. +CORE=name_lookup + +# The uid/gid the backup's files are owned by. This is `solr` in the official Solr +# image, which is what serves the backup everywhere: docker-compose, the Helm chart +# and CI. The loading image runs as `nru` (1000) instead, so without this the +# restored core is owned by a user that does not exist on the serving side and Solr +# cannot take its write lock -- a core that fails to load with a permissions error at +# the end of a multi-hour restore. Stamping the ownership at tar time fixes it for +# every restore path at once, rather than asking each one to remember a chown. +BACKUP_UID ?= 8983 +BACKUP_GID ?= 8983 + +# Split any synonym file larger than SPLIT_SIZE into chunks of SPLIT_LINES lines. +# Splitting only helps parallelism, so we only bother with the big files; JSON is +# one document per line, so we must split on line boundaries, never on bytes. +SPLIT_SIZE ?= 2G +SPLIT_LINES ?= 10000000 + +# SOLR_DIR should be set up to point to the Solr data directory (usually /var/solr, +# which is also the Solr home) and SOLR_EXEC should be set up to point to the Solr +# executable. These will both be set up by the Dockerfile. +SOLR_DIR ?= /var/solr + +# Stamp files live on the volume whose state they describe. These three describe +# the Solr volume -- a core exists, it has been loaded, a server is running -- and +# on Kubernetes that volume may be ephemeral NVMe (see kubernetes/README.md), so +# they have to disappear along with it. Keeping them in data/, which is a separate +# and persistent volume, would mean a recreated pod found an empty index and a set +# of stamps swearing it had been loaded. +SOLR_STARTED = ${SOLR_DIR}/solr.started +CORE_DONE = ${SOLR_DIR}/core.done +SETUP_DONE = ${SOLR_DIR}/setup.done + +# Logs go to data/, which outlives the pod: they are most useful precisely when the +# run has died and the Solr volume has gone with it. +LOG_DIR = ${CURDIR}/data/logs # All and clean targets. .PHONY: all clean -all: data/setup.done +all: ${SETUP_DONE} echo Solr has now been set up and loaded with the synonym data. - echo Run 'make start-solr-backup' to start a backup. Run 'make check-solr-backup' to check - echo if the backup has completed. Once that has completed, run 'make data/backup.done' to + echo Run 'make data/backup.done' to optimize the index, shut down Solr and echo generate a snapshot.backup.tar.gz file that can be used in NameRes. +# Leaves the index itself alone -- to start over from an empty index, delete +# ${SOLR_DIR}/${CORE} (or, on Kubernetes, just recreate the pod). clean: rm -rf data/* - mkdir data + mkdir -p data + rm -f ${SOLR_STARTED} ${CORE_DONE} ${SETUP_DONE} -# This is a three step process. -# -# Step 1. Download an uncompress synonym files. +# Step 1. Download and uncompress synonym files, then split the large ones so the +# parallel loader has more units of work. data/synonyms/done: mkdir -p data/synonyms - wget -c -r -l1 -nd -P data/synonyms ${SYNONYMS_URL} - gunzip data/synonyms/*.txt.gz + # -A restricts what is saved to the synonym files themselves. Everything in + # data/synonyms is loaded into Solr (the glob in step 4 is *.txt*), so anything + # else wget leaves behind -- a robots.txt, most obviously -- becomes a file the + # loader tries to index. Solr answers 200 and indexes nothing for it, so the + # document-count guard catches it, but only at the end of a multi-hour load. + wget -c -r -l1 -nd -A '*.txt,*.txt.gz' -P data/synonyms ${SYNONYMS_URL} + # -exec (rather than a glob) so that a rerun where everything is already + # uncompressed is a no-op instead of a "no such file" failure. + find data/synonyms -maxdepth 1 -type f -name '*.txt.gz' -exec gunzip {} + echo Downloaded synonyms from ${SYNONYMS_URL} - # split -d -l 10000000 data/synonyms/SmallMolecule.txt data/synonyms/SmallMolecule.txt. && rm data/synonyms/SmallMolecule.txt - split -d -l 10000000 data/synonyms/DrugChemicalConflated.txt data/synonyms/DrugChemicalConflated.txt. && rm data/synonyms/DrugChemicalConflated.txt - split -d -l 10000000 data/synonyms/GeneProteinConflated.txt data/synonyms/GeneProteinConflated.txt. && rm data/synonyms/GeneProteinConflated.txt - echo Split DrugChemicalConflated.txt and GeneProteinConflated.txt, and deleted the original files. + # pipefail plus `|| exit 1` so that a failed split fails the target. Without both, + # the loop's status is whichever iteration ran last and a file that failed to + # split halfway through the set is silently loaded whole (or not at all). + set -o pipefail; find data/synonyms -maxdepth 1 -type f -name '*.txt' -size +${SPLIT_SIZE} -print | while read -r f; do \ + echo "Splitting large file $$f (larger than ${SPLIT_SIZE})..."; \ + split -d -l ${SPLIT_LINES} "$$f" "$$f." && rm "$$f" || exit 1; \ + done touch $@ -# Step 3. Start Solr server. -data/solr.pid: - mkdir -p ${SOLR_DIR}/logs - ${SOLR_EXEC} -cloud -p 8983 -v -m ${SOLR_MEM} -s ${SOLR_DIR} >> ${SOLR_DIR}/logs/solr.txt 2>> ${SOLR_DIR}/logs/solr.err.txt - while [ ! -s $@ ]; do \ - ${SOLR_EXEC} status | grep -Po 'Solr process \K([0-9]+)' > $@; \ +# Step 2. Start the Solr server (standalone -- no ZooKeeper needed for one node). +# --user-managed keeps standalone mode explicit (Solr 10 will default to cloud). +${SOLR_STARTED}: + mkdir -p ${LOG_DIR} + SOLR_LOGS_DIR=${LOG_DIR} ${SOLR_EXEC} start -p 8983 --user-managed -v -m ${SOLR_MEM} -s ${SOLR_DIR} >> ${LOG_DIR}/solr.txt 2>> ${LOG_DIR}/solr.err.txt + # Wait for Solr to actually answer, then stamp. This used to parse `solr status` + # for a PID, which reports "No Solr nodes are running" against a perfectly healthy + # Solr in a container -- the stock solr image does it too -- so the loop never + # terminated. Nothing ever used the PID (stop-solr stops Solr by port), so asking + # the admin endpoint whether Solr is up is both simpler and the actual question. + # Bounded, so a Solr that fails to start stops the build instead of hanging it. + @tries=0; until curl -sf 'http://localhost:8983/solr/admin/cores?action=STATUS' > /dev/null 2>&1; do \ + tries=$$((tries + 1)); \ + if [ $$tries -gt 60 ]; then \ + echo "Solr did not start after two minutes; see ${LOG_DIR}/solr.err.txt" >&2; \ + exit 1; \ + fi; \ + sleep 2; \ done - $(info Solr started with PID file at $@) - cat $@ - -# Step 4. Load JSON files into Solr server. -data/setup.done: data/synonyms/done data/solr.pid - mkdir -p data/logs - bash setup-and-load-solr.sh "data/synonyms/*.txt*" >> data/logs/setup-and-load-solr.sh.log 2>> data/logs/setup-and-load-solr.sh.err.log && touch $@ - -# Step 5. Start a Solr backup. -.PHONY: start-solr-backup -start-solr-backup: data/setup.done - curl 'http://localhost:8983/solr/name_lookup/replication?command=backup&name=backup' - -# Step 6. Wait for the backup to complete. -.PHONY: check-solr-backup -check-solr-backup: - curl 'http://localhost:8983/solr/name_lookup/replication?command=details' - -# Step 6. Shutdown the Solr instance. -### data/stop-solr: -### docker exec name_lookup solr stop -p 8983 -verbose - -# Step 7. Generate the backup tarball. -data/backup.done: - mkdir -p data/var/solr/data - mv /var/solr/name_lookup_shard1_replica_n1/data/snapshot.backup data/var/solr/data - cd data && tar zcvf snapshot.backup.tar.gz var && touch backup.done + @date -u '+Solr started at %Y-%m-%dT%H:%M:%SZ on port 8983' > $@ + @cat $@ + +# Step 3. Create the core from the checked-in configset (schema lives there). +# solr.pid is order-only (after the |): the core needs a running Solr, but restarting +# Solr must not make the core look stale -- re-running `solr create` on a core that +# already exists is an error. +${CORE_DONE}: | ${SOLR_STARTED} + ${SOLR_EXEC} create -c ${CORE} -d ${CONFIGSET} + touch $@ + +# Step 4. Load the synonym files into the core (parallel; with a doc-count guard). +${SETUP_DONE}: data/synonyms/done ${CORE_DONE} + mkdir -p ${LOG_DIR} + bash setup-and-load-solr.sh "data/synonyms/*.txt*" >> ${LOG_DIR}/setup-and-load-solr.sh.log 2>> ${LOG_DIR}/setup-and-load-solr.sh.err.log && touch $@ + +# Step 5. Optimize the index, shut down Solr, and tar the whole core into a +# self-contained backup. Extracting snapshot.backup.tar.gz into the Solr home +# yields a ready-to-serve name_lookup/ core -- no restore-time schema setup. +# The PID file is an order-only prerequisite because optimizing needs a *running* +# Solr: the stamps survive a container restart, but the server does not, so a +# restarted container should start Solr again rather than fail here. +data/backup.done: ${SETUP_DONE} | ${SOLR_STARTED} + @echo "Optimizing the index before export (this can take a while on a large index)..." + curl -sf --show-error 'http://localhost:8983/solr/${CORE}/update?optimize=true' + # The optimize above commits, so the on-disk index is consistent. `solr stop` + # can exit non-zero under Docker even on a clean stop (SOLR-16463), so ignore it. + ${SOLR_EXEC} stop -p 8983 || true + rm -f ${SOLR_STARTED} + # --owner/--group stamp the serving Solr's uid onto every member (see BACKUP_UID + # above); --numeric-owner makes the restoring tar use those numbers rather than + # looking up whatever "solr" means on the restoring host. + # + # tlog is left out: the optimize above committed, so there is nothing uncommitted + # to replay, and Solr recreates the directory on startup. It is dead weight in a + # tarball this size. + set -o pipefail; tar -C ${SOLR_DIR} \ + --numeric-owner --owner=${BACKUP_UID} --group=${BACKUP_GID} \ + --exclude='${CORE}/data/tlog' \ + -cf - ${CORE} | ${GZIP_CMD} > data/snapshot.backup.tar.gz + touch $@ .PHONY: stop-solr stop-solr: - rm data/solr.pid - ${SOLR_EXEC} stop + rm -f ${SOLR_STARTED} + ${SOLR_EXEC} stop -p 8983 || true $(info Solr stopped.) diff --git a/data-loading/README.md b/data-loading/README.md index 376c9e9f..abe58589 100644 --- a/data-loading/README.md +++ b/data-loading/README.md @@ -1,105 +1,162 @@ # Loading NameResolution data -NameResolution data needs to be loaded as a compressed [Apache Solr](https://solr.apache.org/) database. -To create this dataset is a three-step process: synonym data generated by Babel must first be loaded into -Solr, a Solr backup created, and then compressed with a particular directory structure to be used in NameRes. +NameResolution answers queries out of an [Apache Solr](https://solr.apache.org/) index. +This directory builds that index from the synonym files produced by +[Babel](https://github.com/NCATSTranslator/Babel) and packages it as a +**self-contained Solr backup** that a NameRes instance can download and serve directly. + +"Self-contained" is the important part: the backup is a whole Solr *core* -- +its configuration, its schema **and** its built index -- tarred up together. +Restoring it is just "untar into the Solr home and start Solr". There is no +ZooKeeper, no collection to create, and no schema to re-apply at restore time. +The schema lives in exactly one place, the checked-in configset at +[`configsets/name_lookup/conf`](configsets/name_lookup/conf). + +## How it works + +Solr runs in **standalone** mode (a single node, one core -- ZooKeeper adds +nothing for a one-shard job). The core is created from the checked-in configset +with `solr create -c name_lookup -d configsets/name_lookup`. Data is loaded by +streaming each Babel synonym file to Solr **in parallel**, with a single commit +at the end; there is no per-file commit and no sleeping. Finally the index is +optimized and the core directory is tarred into `snapshot.backup.tar.gz`. + +Because a parallel bulk load of a ~130 GB dataset would be painful to debug if +it silently dropped documents, `setup-and-load-solr.sh` **counts the input +documents before loading and compares against Solr's document count afterward**, +and every upload uses `curl --fail`. A failed upload or a count mismatch aborts +with a non-zero exit code. + +## The configset + +Two files, ~220 lines: [`solrconfig.xml`](configsets/name_lookup/conf/solrconfig.xml) +and [`managed-schema.xml`](configsets/name_lookup/conf/managed-schema.xml). They are +written for NameRes rather than copied from Solr's `_default` configset, and that is +the point: **everything in them is a decision we made**, so there is nothing to +reconcile against upstream when Solr is upgraded, and nothing pinned that we never +chose. Solr supplies the defaults for everything we leave out, and the handlers we +do not declare (`/update`, `/update/json/docs`, `/admin/ping`, `/admin/*`) are +registered implicitly. + +This replaced a wholesale copy of `_default`: 44 files, 70 field types and 69 dynamic +fields, of which NameRes used 6 field types and 13 fields. 42 of those files were +language-specific stopword and contraction lists, which could never have done +anything here -- Babel synonyms carry no language, and neither of our two field types +stems, removes stopwords or applies synonyms. + +Two consequences worth knowing: + +- **The schema is immutable.** `schemaFactory` declares `mutable=false`, so the Schema + API refuses to write and this file cannot drift from a live core. (Left undeclared, + Solr defaults to a *mutable* managed schema -- the same trap as the + `configoverlay.json` that used to live here.) The only thing that modifies a live + index is the Helm blocklist step, which deletes documents by query and needs no + schema change. +- **An unexpected Babel key is now an error.** With no dynamic fields and no + schemaless mode, a synonym file containing a key we did not declare fails the load + with `unknown field` instead of having a field invented for it. If Babel's output + gains a field, add it to the schema deliberately. + +### Upgrading Solr + +Bump the image tag and let CI do the work: it creates a core from this configset, +loads it, queries it and round-trips a backup, so anything removed or renamed upstream +turns CI red at core creation. If you want to see what changed in Solr's own defaults, +diff against the `_default` configset inside the image: + +```shell +$ docker run --rm --entrypoint cat solr:9.10 \ + /opt/solr/server/solr/configsets/_default/conf/solrconfig.xml > /tmp/upstream.xml +$ diff /tmp/upstream.xml configsets/name_lookup/conf/solrconfig.xml +``` + +Expect that diff to be enormous and mostly uninteresting -- it is the 900 lines of +upstream commentary and examples we are deliberately not carrying. It is useful for +spotting a renamed element, not as a routine check. + +`luceneMatchVersion` controls analyzer back-compatibility, not which Solr this runs +on. It does not need to track the release, and changing it can change how text is +analysed, so it should only move together with a reindex. ## Using the Makefile -This directory includes a Makefile that can be used to run most of these steps -automatically. This is a seven-step process: - -1. Edit the Makefile to choose the directory containing Babel synonym files. Note that - all files in that directory will be used, and any files named `.txt.gz` will uncompressed. -2. Run `make all` to download the synonym data, uncompress Gzipped files, split the larger files - and delete the split files to avoid duplicate loading. `make all` will also start the Solr server -- - you can check this by looking for a PID file Solr in `data/solr.pid`. -3. (Optional) Access the Solr server and confirm that all the data has been loaded. -4. Run `make start-solr-backup` to start the Solr backup. -5. Run `make check-solr-backup` to check on the Solr backup. Look for `"status":"success"` to confirm that the - backup has completed. -6. Run `make data/backup.done` to move the backup into the `data/` directory, place it in the correct directory - structure for NameRes, and create a `snapshot.backup.tar.gz` file. -7. Copy the `snapshot.backup.tar.gz` file to a web server so that it can be loaded from NameRes. - -## Step-by-step instructions - -1. Set up a Solr server locally. The easiest way to do this is via Docker: - - ```shell - $ docker run -v "$PWD/data/solrdata:/var/solr" --name name_lookup -p 8983:8983 -t solr -cloud -p 8983 -m 12G - ``` - - You can adjust the `12G` to increase the amount of memory available to Solr. You can also add `-d` to the - Docker arguments if you would like to run this node in the background. - -2. Copy the synonym files into the `data/synonyms` directory. Synonym files that are too large will - need to split it into smaller files. (`gsplit` is the GNU version of `split`, which includes support - for adding an additional suffix to files) - - ```shell - $ gsplit -l 5000000 -d --additional-suffix .txt SmallMolecule.txt SmallMolecule - $ gsplit -l 5000000 -d --additional-suffix .txt MolecularMixture.txt MolecularMixture - ``` - -3. Download all the synonym text files into the `data/json` folder. You can download this by running `make`. - - ```shell - $ pip install -r requirements.txt - $ make - ``` - -4. Load the JSON files into the Solr database by running: - - ```shell - $ ./setup-and-load-solr.sh "data/json/*.json" - ``` - - Note the double-quotes: setup-and-load-solr.sh requires a glob pattern as its first argument, not a list of files to process! - -5. Generate a backup of the Solr instance. The first command will create a directory at - `solrdata/data/name_lookup_shard1_repical_n1/data/snapshot.backup` -- you can track its progress by comparing the - number of files in that directory to the number of files in `../data/index` (as I write this, it has 513 files). - - ```shell - $ curl 'http://localhost:8983/solr/name_lookup/replication?command=backup&name=backup' - $ curl 'http://localhost:8983/solr/name_lookup/replication?command=details' - ``` - - Once the backup is complete, you'll see a part of the `details` response that looks like this: - - ```json - "backup":{ - "startTime":"2022-09-13T18:42:43.678219123Z", - "fileCount":512, - "indexFileCount":512, - "status":"success", - "snapshotCompletedAt":"2022-09-13T19:36:00.599797304Z", - "endTime":"2022-09-13T19:36:00.599797304Z", - "snapshotName":"backup", - "directoryName":"snapshot.backup" - } - ``` - -6. Shutdown the Solr instance. - - ```shell - $ docker exec name_lookup solr stop -p 8983 -verbose - ``` - -7. Generate the backup tarball. At the moment, this is expected to be in the format - `var/solr/data/snapshot.backup/[index files]`. The easiest way to generate this tarball correctly is to run: - - ```shell - $ mkdir -p data/var/solr/data - $ mv /var/solr/name_lookup_shard1_replica_n1/data/snapshot.backup data/var/solr/data - $ cd data - $ tar zcvf snapshot.backup.tar.gz var - ``` - -8. Publish `snapshot.backup.tar.gz` to a publicly-accessible URL. - -9. Use the instructions at https://github.com/helxplatform/translator-devops/tree/develop/helm/name-lookup to set up an - instance of NameRes that downloads snapshot.backup.tar.gz from this publicly-accessible URL. - -The Makefile included in this directory contains targets for more of these steps. +The Makefile runs the whole pipeline. It expects `SOLR_DIR` (the Solr home, +usually `/var/solr`) and `SOLR_EXEC` (the `bin/solr` executable) to be set -- the +[`Dockerfile`](Dockerfile) sets both. + +1. Edit `SYNONYMS_URL` in the Makefile to point at the Babel synonyms directory + you want to load. +2. Run `make all`. This downloads and uncompresses the synonym files, splits any + file larger than `SPLIT_SIZE` (so the parallel loader has more units of work), + starts Solr, creates the `name_lookup` core from the configset, and loads the + data with the document-count guard. +3. (Optional) Query the Solr server at to confirm the + data looks right. +4. Run `make data/backup.done`. This optimizes the index, shuts down Solr and + writes `data/snapshot.backup.tar.gz`. +5. Publish `snapshot.backup.tar.gz` to a publicly-accessible URL and point a + NameRes instance at it (see below). + +Tuning knobs: `SOLR_MEM` (Solr heap for the load), `LOAD_PARALLELISM` +(concurrent uploads; defaults to the cgroup CPU limit, which inside a container is +the pod's limit rather than the node's core count — see +[`available-cpus.sh`](available-cpus.sh)), `SPLIT_SIZE` / `SPLIT_LINES`. + +Loading is **additive**, not idempotent: every document is given a fresh UUID, so +loading the same files twice puts every document in twice. The loader therefore +refuses to load into a core that already has documents. If a load dies partway +through, do not just re-run it — delete the core and start it again: + +```shell +$ make stop-solr && rm -rf "${SOLR_DIR:-/var/solr}/name_lookup" "${SOLR_DIR:-/var/solr}"/*.done +$ make all +``` + +(`LOAD_APPEND=1` overrides the refusal, for the genuine case of loading a second, +distinct set of files into an existing core.) + +## Restoring the backup + +The backup extracts to a ready-to-serve `name_lookup/` core, so restoring it is +trivial in every environment: + +- **Locally (docker-compose):** extract the tarball into the Solr data directory + (`./data/solr` by default) so you get `./data/solr/name_lookup/`, then + `docker-compose up`. Solr auto-discovers the core on startup. +- **In Kubernetes (Helm):** the [`name-lookup` chart](../data/name-lookup) does + this automatically -- an init container downloads and extracts the backup into + the Solr volume, and the Solr StatefulSet auto-discovers the core. The restore + Job no longer creates a collection or re-applies the schema; its only remaining + job is deleting blocklisted CURIEs. + +Solr needs to *write* to the core it serves — it takes a write lock, and refuses to +load the core if it cannot. The loading image runs as uid 1000 and the official Solr +image runs as uid 8983, so the Makefile stamps 8983 onto every file as it builds the +tarball (`BACKUP_UID` / `BACKUP_GID`) and the restore needs no `chown`. Extract as +root, or as a user that can set ownership; extracting as an unprivileged user gives +the files to whoever extracted them, and Solr will not be able to write. + +A backup built before this (anything whose files come out owned by uid 1000) needs +`chown -R 8983:8983` on the extracted core. + +The Solr version used to serve the backup must be **>=** the version that built +it (an older Solr cannot read a newer Lucene index). The builder is pinned exactly +(`SOLR_VERSION` in the [`Dockerfile`](Dockerfile), 9.10.1 today) because it names a +release tarball; the servers (`docker-compose.yml`, CI, the Helm chart) track the +`9.10` line, so they float forward onto patch releases and stay at or above the +builder without anyone having to remember to bump them. + +## Why it is built this way + +Three decisions here are easy to undo by accident, so they are worth stating: + +- **Standalone, not cloud mode.** Solr used to run with `-DzkRun` everywhere. + ZooKeeper buys nothing for a single node, and cloud mode is what forced the + restore to recreate the collection and re-declare the schema over the Schema + API -- which is how the schema came to be defined in two places and drift. +- **`tar` the stopped core, not the replication backup API.** Tarring the core + directory after a clean commit is simpler, and the result is self-contained: + config, schema and index in one artifact, so restore is "untar and start". +- **Optimize before export.** `optimize=true` before tarring merges the index to + one segment, which makes it smaller and faster to serve (fewer files to hold + in the OS page cache), at the cost of a one-time forced merge during the build. diff --git a/data-loading/available-cpus.sh b/data-loading/available-cpus.sh new file mode 100755 index 00000000..2298320e --- /dev/null +++ b/data-loading/available-cpus.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Print the number of CPUs this process is actually allowed to use. +# +# getconf and nproc report the *node's* CPU count even inside a container with a +# CPU limit: in a pod limited to 8 CPUs on a 64-core node, both say 64. Sizing a +# thread pool from that number means 64 workers fighting over 8 CPUs' worth of +# quota, which is slower than 8 workers, not faster. So read the cgroup quota and +# only fall back to the node count when there isn't one. + +set -uo pipefail + +fallback=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4) + +if [ -r /sys/fs/cgroup/cpu.max ]; then + # cgroup v2: " ", where quota is the string "max" if unlimited. + read -r quota period < /sys/fs/cgroup/cpu.max +elif [ -r /sys/fs/cgroup/cpu/cpu.cfs_quota_us ]; then + # cgroup v1: separate files, quota is -1 if unlimited. + quota=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us) + period=$(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us) +fi + +# Left to right, and [ stops at the first true test: "max" is never compared +# numerically, which would be an error. +if [ "${quota:-max}" = "max" ] || [ "${quota:-0}" -le 0 ] || [ "${period:-0}" -le 0 ]; then + echo "$fallback" + exit 0 +fi + +# Round down (a 1.5-CPU quota gets one worker), but never below one. +cpus=$((quota / period)) +[ "$cpus" -lt 1 ] && cpus=1 +echo "$cpus" diff --git a/data-loading/configsets/name_lookup/conf/managed-schema.xml b/data-loading/configsets/name_lookup/conf/managed-schema.xml new file mode 100644 index 00000000..2f470674 --- /dev/null +++ b/data-loading/configsets/name_lookup/conf/managed-schema.xml @@ -0,0 +1,88 @@ + + + + + id + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data-loading/configsets/name_lookup/conf/solrconfig.xml b/data-loading/configsets/name_lookup/conf/solrconfig.xml new file mode 100644 index 00000000..a40ab99a --- /dev/null +++ b/data-loading/configsets/name_lookup/conf/solrconfig.xml @@ -0,0 +1,139 @@ + + + + + + 9.12 + + + + false + managed-schema.xml + + + + + + + + + + 2048 + + + + + + ${solr.ulog.dir:} + + + + + ${solr.autoCommit.maxTime:15000} + false + + + + + ${solr.autoSoftCommit.maxTime:-1} + + + + + + + + + + + + + false + + + + + + + + + + explicit + 10 + + + + + + + explicit + json + true + + + + + + + diff --git a/data-loading/kubernetes/README.md b/data-loading/kubernetes/README.md new file mode 100644 index 00000000..f11405a6 --- /dev/null +++ b/data-loading/kubernetes/README.md @@ -0,0 +1,201 @@ +# Loading NameRes data on Kubernetes + +These three files create a pod on a Kubernetes cluster big enough to load a whole +Babel release into Solr and produce the `snapshot.backup.tar.gz` that NameRes +instances restore. The pipeline itself is described in +[`../README.md`](../README.md); this file is about the resources it needs and how to +tell, from Grafana, whether it has the right ones. + +| File | What it is | +| --- | --- | +| `nameres-loading.k8s.yaml` | The pod. A workspace you exec into, not a job. | +| `nameres-loading-solr.k8s.yaml` | PVC mounted at `/var/solr` -- the Solr home, i.e. the index. | +| `nameres-loading-data.k8s.yaml` | PVC mounted at `data/` -- synonym files in, tarball out. | + +## Two volumes + +- **`/var/solr`, the index.** Takes the entire write load of the indexing run and then + the read-and-write of the optimize, so its throughput is what decides how long a + load takes. Pure intermediate state: once the tarball exists the index is worthless. +- **`data/`, the working directory.** Holds the ~130G download and, at the end, the + backup tarball -- the only thing the whole exercise actually produces. + +Both are persistent PVCs today, so **both outlive the pod and both need deleting by +hand** once the tarball is somewhere safe. + +The index would be much better off on node-local NVMe: it is the volume that most +wants speed, and it is the one we could afford to lose, since it can be rebuilt from +the synonym files next door. That change is written and waiting in a separate PR, +blocked on [issue #280](https://github.com/NCATSTranslator/NameResolution/issues/280), +which asks for the namespace to support `nvme-ephemeral` volumes. + +Being persistent does buy one thing in the meantime. The Makefile's stamp files -- +"the core exists", "the data is loaded" -- live on the Solr volume rather than in +`data/`, so a replacement pod skips the steps that already finished instead of +starting from nothing. Logs go the other way, to `data/logs/`, because logs matter +most when a run has died. + +That resumption is per *step*, though, and the load is one step. A pod that dies +mid-load leaves a partly-filled core and no stamp, and the load cannot be continued: +it is additive and assigns fresh UUIDs, so re-running it would index the already- +loaded files a second time. The loader refuses a non-empty core for that reason. The +recovery is to throw the index away and load again -- the download, which is the part +worth keeping, is on the other volume and survives: + +```shell +$ make stop-solr && rm -rf /var/solr/name_lookup /var/solr/*.done +$ make all +``` + +## Running a load + +```shell +$ kubectl apply -f nameres-loading-solr.k8s.yaml +$ kubectl apply -f nameres-loading-data.k8s.yaml +$ kubectl apply -f nameres-loading.k8s.yaml +$ kubectl exec -it nameres-loading -- /bin/bash + +# Inside the pod: +$ cd /code/nameres-data-loading +$ make all SYNONYMS_URL=https://stars.renci.org/var/babel_outputs//synonyms/ +$ make data/backup.done +``` + +### Which pipeline runs: the image tag decides + +The Makefile, loader script and configset are all **baked into the image** -- editing +them in a checkout does nothing, and the `image:` tag in `nameres-loading.k8s.yaml` is +what decides which version of the pipeline runs. So run a load from a released version +tag, not `latest`: the publish workflow only tags `latest` on a published release, so +between merging a change and cutting a release, `latest` is still the *previous* +pipeline. To resolve any tag to an exact commit, the image carries its git SHA in the +`org.opencontainers.image.revision` label (stamped by `docker/metadata-action` at +publish), which `docker inspect` or the ghcr UI will show. + +If you do need to run a not-yet-released change, you can edit these files inside the +pod before `make`. The configset in particular is only read when the core is created, +so changing `solrconfig.xml` before `make all` takes full effect. + +`make all` downloads and splits the synonym files, starts Solr, creates the +`name_lookup` core from the checked-in configset and loads it. `make +data/backup.done` optimizes the index, stops Solr and writes +`data/snapshot.backup.tar.gz`. Copy that out (`kubectl cp`, or push it straight to +wherever `dataUrl` will point), then delete the pod and **both** PVCs -- they are +persistent, and between them they hold around a terabyte. + +Run it under `screen` or `tmux`: a full load is hours long, and `kubectl exec` does +not survive a dropped connection. + +If the pod dies partway through, recreate it and run the same commands: both volumes +survive, so make picks up from the last completed step rather than starting over. The +exception is dying *during* the load, which cannot be resumed -- see "Two volumes" +above for how to clear the core and restart it. + +## What actually costs time + +In order, the load is: + +1. **Download** (`wget`), network-bound. One connection, ~130G. +2. **Index**, CPU-bound. Solr analyses every synonym as it arrives -- tokenizing and + lowercasing each name for `names` and again for the `names_exactish` copy field. + This is where the parallel uploads earn their keep. +3. **Merge**, I/O-bound, overlapping the above. +4. **Optimize**, I/O-bound. One forced merge of the entire index into a single + segment: read ~130G, write ~130G. +5. **Compress**, CPU-bound, parallelised with `pigz`. + +So the two levers are **CPU** (steps 2 and 5) and **disk throughput** (steps 3 and +4). Memory beyond Solr's heap only helps as page cache, and disk *space* buys +nothing but the absence of failure. + +## Sizing + +**CPU -- the main lever, and the one to try first.** The loader runs one upload per +CPU and `pigz` gets the same number, so raising `cpu` in the pod spec is picked up +automatically; there is no second setting to keep in sync. The pod asks for 32, +which is deliberately short of what the namespace has spare: the real limit is that +the request has to fit on a **single node**. +Namespace quota is the easy test to pass and the wrong one to plan against -- check +`kubectl describe node` before raising this. Note that both numbers +come from the cgroup *limit* via [`../available-cpus.sh`](../available-cpus.sh), not +from `nproc`: inside a container `nproc` reports the node's cores, so on a 64-core +node a pod limited to 8 CPUs would otherwise start 64 uploads and spend the +difference being throttled. If you need to override it, set `LOAD_PARALLELISM`. + +**Memory -- less than you would think.** `SOLR_MEM` (default `31G`) becomes +`solr -m`, which sets *both* `-Xms` and `-Xmx`, so it is committed rather than a +ceiling. Indexing does not want a large heap: Lucene buffers documents in +`ramBufferSizeMB` (2 GB, in the configset) and streams merges through the OS page +cache, so heap past a few GB is memory taken away from that cache. Staying under 32G +also keeps compressed object pointers, which a bigger heap silently gives up. The +pod's remaining memory is not wasted -- the kernel uses it to cache index files, and +page cache is charged to the container's limit, so the limit is what decides how much +of the index can be cached during merges. + +This is the same lesson as the serving side, where measurements showed 11-13Gi of +Solr RSS against 111Gi of page cache (see `solr.resources` in the chart's +`values.yaml`). + +**Disk space** is a floor, not a lever: + +- `/var/solr` needs **2-3x the finished index**, because `optimize=true` writes the + new single segment before deleting the old ones. It is 600Gi, sized well above the + finished index (~127Gi for Babel 2025nov4, ~111Gi for 2026jul22 -- release size + moves both ways) to leave room for a larger future one, because running out of room + happens during the optimize -- the last step of a multi-hour load. +- `data/` needs the uncompressed synonyms plus the tarball. It no longer needs room + for an uncompressed copy of the backup -- that staging step is gone. + +**Disk speed** is a lever, and probably the biggest one left. Both PVCs use +`storageClassName: basic`, so the index sits on network storage and the merge and +optimize phases are bounded by it -- no amount of Makefile tuning substitutes for the +volume being fast. + +Moving the index onto a node-local NVMe ephemeral volume is written and ready in a +separate PR, blocked on +[issue #280](https://github.com/NCATSTranslator/NameResolution/issues/280): the +namespace cannot currently create `nvme-ephemeral` volumes. When that lands, the swap +is one block in `nameres-loading.k8s.yaml` and nothing else changes -- the stamp files +follow `SOLR_DIR` either way. + +## Watching a load in Grafana + +Take a look at these while a load is running; each one tells you what to change next +time. + +| What to look at | What it means | What to do | +| --- | --- | --- | +| **CPU throttling** (`container_cpu_cfs_throttled_seconds_total`, or the "throttling" panel) | Sustained throttling means the pod wants more CPU than its limit. | Raise `cpu`. The loader picks it up on its own. | +| **CPU usage vs. limit** | Flat at the limit during the load = CPU-bound, and more will help. Well below it = something else is the constraint, probably disk. | If it is below the limit, look at disk before adding CPU. | +| **Memory usage (RSS/WSS)** | This is roughly the JVM. It should sit near `SOLR_MEM` and be stable. | If it is far below `SOLR_MEM`, lower `SOLR_MEM` and give the memory back as cache. | +| **Memory usage (cache)** | Page cache: the index data the kernel is holding. Rising to fill the headroom is healthy. | If it is pinned at (limit - heap) for the whole run, more memory may speed up merges. | +| **Disk read/write throughput on the Solr volume** | Flat-topped during merges and the optimize = saturated volume. Expected on `basic` storage, and the main argument for issue #280. | Nothing to tune here; it is the storage class. | +| **Wall-clock time of each `make` step** | The logs in `data/logs/` are timestamped per step. | Tells you which of the five phases above to attack at all. | + +Two things worth checking specifically, since they are new: + +- **Does parallelism help linearly?** Note the load's wall-clock time and the CPU + usage at the current setting. If CPU sits at the limit and throttling is high, the + next load with more CPU should be faster in proportion. If it does not improve, + the bottleneck has moved to disk. +- **Is 31G of heap right?** Watch RSS during the load. If it plateaus well below 31G + (likely), drop `SOLR_MEM` further -- 16G is probably enough -- and let the page + cache have the difference. If Solr spends its time in GC or dies with an + OutOfMemoryError, raise it, but suspect something else first: a bulk load into a + single core should not need tens of gigabytes of live heap. + +## Knobs + +All of these can be set on the `make` command line, or as environment variables in +the pod spec (the Makefile uses `?=`). + +| Setting | Default | What it does | +| --- | --- | --- | +| `SYNONYMS_URL` | Babel 2026jul22 | Which Babel release to load. | +| `SOLR_MEM` | `31G` | Solr's heap during the load (`-Xms` and `-Xmx`). | +| `LOAD_PARALLELISM` | CPU limit | Concurrent uploads. Only set this to override the cgroup-derived default. | +| `SPLIT_SIZE` / `SPLIT_LINES` | `2G` / 10M | How the big synonym files are split. More, smaller chunks give the parallel loader a shorter tail at the end of the run; fewer, larger ones mean less `split` time up front. | +| `ramBufferSizeMB` | 2048 (in the configset) | Not an environment variable -- it lives in `solrconfig.xml`. It is a budget shared across indexing threads, so the segment size at flush is roughly this divided by the number of concurrent uploads: ~64 MB at 32 uploads. Raise it further if the Grafana disk panels show the run still merging long after the uploads have finished; there is heap to spare. | +| `SOLR_STARTUP_TRIES` | 60 | How long the loader waits for Solr (3s each) before giving up. | +| `LOAD_APPEND` | unset | Set to `1` to load into a core that already has documents. The loader refuses by default, because loading is additive and a re-run would duplicate every document. Only correct when you are deliberately adding a second, distinct set of files. | +| `BACKUP_UID` / `BACKUP_GID` | `8983` | The uid/gid stamped onto the backup's files, so the serving Solr can write to the core it restores without a `chown`. | diff --git a/data-loading/kubernetes/nameres-loading-data.k8s.yaml b/data-loading/kubernetes/nameres-loading-data.k8s.yaml index 9929c66c..17c0a405 100644 --- a/data-loading/kubernetes/nameres-loading-data.k8s.yaml +++ b/data-loading/kubernetes/nameres-loading-data.k8s.yaml @@ -1,13 +1,18 @@ -# Kubernetes file for setting up a PVC to use for nameres-Loading. -# nameres-loading-data is a directory for storing synonym files, -# the generated Solr back and its compressed version. +# Kubernetes file for setting up a PVC to use for nameres-loading. +# nameres-loading-data is the working directory (data/): the downloaded synonym +# files and the finished backup tarball. # -# As of 2023jun1, this directory needs to contain: -# - 129G of synonym files (in JSON) -# - 119G of snapshot.backup files moved here from Solr -# - 85G of snapshot.backup.tar.gz after compressing -# Therefore it needs to be a minimum of 350G. I'm going to set a -# size of 400G in case we need some extra space. +# It needs to hold, at once: +# - the uncompressed synonym files (129G as of Babel 2023jun1; check the file sizes +# in the SYNONYMS_URL directory listing for the release you are actually loading) +# - snapshot.backup.tar.gz (85G as of 2023jun1) +# +# It no longer needs room for an uncompressed copy of the backup. The old pipeline +# moved a ~119G snapshot.backup directory here before compressing it; the backup is +# now tarred straight out of the Solr volume into the tarball, which also means the +# two volumes are read and written in parallel rather than one after the other. +# +# 500Gi therefore leaves comfortable headroom for a couple of Babel releases' growth. apiVersion: v1 kind: PersistentVolumeClaim diff --git a/data-loading/kubernetes/nameres-loading-solr.k8s.yaml b/data-loading/kubernetes/nameres-loading-solr.k8s.yaml index 72ddc10a..2ff30c7b 100644 --- a/data-loading/kubernetes/nameres-loading-solr.k8s.yaml +++ b/data-loading/kubernetes/nameres-loading-solr.k8s.yaml @@ -1,7 +1,37 @@ # Kubernetes file for setting up a PVC to use for nameres-loading. -# -# As of 2022dec11, this seems to come to 37G for files + 30G for snapshot.backup. -# I'm going to set the size to 150Gi so we have a bit of spare space if needed. +# +# This is the Solr home (/var/solr): it holds the name_lookup core, which is the +# index itself and, at the end, the thing that gets tarred into the backup. +# +# ## Why 600Gi for a ~110-130Gi index +# +# Not padding. `optimize=true` merges everything into one segment by writing the new +# segment before dropping the old ones, so peak usage during that step is roughly +# double the finished index, and Lucene's guidance is to budget 2-3x. +# +# Release size moves both ways: the finished index was ~127Gi for Babel 2025nov4 but +# ~111Gi for 2026jul22 (fewer proteins). The sizing has to hold for whichever release +# is actually being loaded (SYNONYMS_URL in the Makefile) and leave room for a larger +# one, not track the last one measured: at a 180Gi index, 3x is 540Gi and 400Gi would +# not be enough. The failure mode is the reason for the headroom -- running out of +# space happens during the optimize, which is the *last* step of a multi-hour load, +# and it takes the whole load with it. +# +# If storage quota is tight this can come down, but not below roughly 2x the finished +# index, and check the index size from the previous load before trimming it. +# +# ## Speed +# +# This volume takes the whole write load of the indexing run plus the read-and-write +# of the optimize, so its throughput is what decides how long a load takes. On +# `basic` (network storage) those two phases dominate. Moving the index to a +# node-local NVMe ephemeral volume is ready in a separate PR, blocked on issue #280, +# which asks for the namespace to support that storage class. +# +# ## Cleanup +# +# This is a PVC, so it outlives the pod: delete it by hand once the backup tarball +# has been copied somewhere safe, or it keeps several hundred GB reserved. apiVersion: v1 kind: PersistentVolumeClaim @@ -14,5 +44,5 @@ spec: - ReadWriteOnce resources: requests: - storage: 400Gi + storage: 600Gi storageClassName: basic diff --git a/data-loading/kubernetes/nameres-loading.k8s.yaml b/data-loading/kubernetes/nameres-loading.k8s.yaml index 9460c284..bb582c8a 100644 --- a/data-loading/kubernetes/nameres-loading.k8s.yaml +++ b/data-loading/kubernetes/nameres-loading.k8s.yaml @@ -1,4 +1,15 @@ -# Kubernetes file for Nameres-loading +# Kubernetes file for Nameres-loading. +# +# This pod is a workspace, not a job: it starts, does nothing, and waits for you to +# exec in and drive the load by hand (see README.md in this directory). The command +# below deliberately replaces the image's entrypoint, which would otherwise start a +# Solr of its own -- the Makefile starts Solr itself, using SOLR_MEM as its heap. +# +# Both volumes are persistent PVCs, so both outlive the pod and both have to be +# deleted by hand when the load is finished. Putting the index on a node-local NVMe +# ephemeral volume instead would make the merge and optimize phases substantially +# faster; that is ready to go in a separate PR and is blocked on issue #280, which +# asks for the namespace to support it. See README.md. apiVersion: v1 kind: Pod @@ -12,9 +23,8 @@ spec: - name: nameres-loading image: ghcr.io/ncatstranslator/nameresolution-data-loading:latest imagePullPolicy: Always - # I just need something to run while I figure out how to make this work - command: [ "/bin/bash", "-c", "--" ] - args: [ "while true; echo Running; do sleep 30; done;" ] + # Idle until someone execs in. `make all` is run by hand, not by the pod. + command: ["sleep", "infinity"] ports: - containerPort: 8983 volumeMounts: @@ -22,19 +32,59 @@ spec: name: nameres-loading-solr - mountPath: "/code/nameres-data-loading/data" name: nameres-loading-data + # Sized for the parallel loader. The load is CPU-bound (Solr analyses every + # synonym on the way in), then I/O-bound (merges and the final optimize), so CPU + # and disk speed are the levers; memory beyond the heap only helps as page cache. + # requests == limits keeps this Guaranteed: a load runs for hours and should not + # be evicted halfway through. + # + # The loader reads the CPU *limit* out of the cgroup and runs that many parallel + # uploads, and pigz is given the same number, so raising cpu here is picked up + # automatically -- there is no separate parallelism setting to remember. + # + # Memory: SOLR_MEM (31G by default) is the heap; the rest is JVM overhead and + # page cache for merges. This used to be 220G/256G with a 220G heap, which spent + # almost all of it in the one place a large heap does not help. See README.md + # before changing either number. + # + # These have to fit on ONE node, which is a stricter test than the namespace + # quota. Check node capacity before raising them, not just what the namespace has + # left. resources: requests: ephemeral-storage: "1G" - memory: "220G" - cpu: "6" + memory: "128Gi" + cpu: "32" limits: ephemeral-storage: "1G" - memory: "256G" - cpu: "8" + memory: "128Gi" + cpu: "32" + # The image runs as nru, uid/gid 1000. fsGroup hands the volumes to that group; + # without it a freshly provisioned volume is root-owned and Solr cannot write a + # thing. runAsUser only restates what the image already does. + # + # OnRootMismatch matters here because these are persistent volumes holding a + # ~130G index: the default (Always) makes the kubelet chown every file on every + # mount, which on a volume this size is minutes of pod startup for nothing. With + # OnRootMismatch it checks the top directory and skips the walk once it is right. + securityContext: + runAsUser: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch volumes: + # The Solr home: the index, and at the end the thing that gets tarred into the + # backup. This takes the entire write load of the indexing run and then the + # read-and-write of the optimize, so it is the volume whose speed decides how long + # a load takes -- see issue #280 for moving it onto node-local NVMe. + # + # Being persistent has one consolation prize: the Makefile's stamp files live on + # this volume, so if the pod dies partway through, a replacement picks up exactly + # where it left off instead of reindexing from scratch. - name: nameres-loading-solr persistentVolumeClaim: claimName: nameres-loading-solr + # This one stays persistent: it holds the ~130G download and, at the end, the + # backup tarball, which is the only thing the whole exercise produces. - name: nameres-loading-data persistentVolumeClaim: claimName: nameres-loading-data diff --git a/data-loading/setup-and-load-solr.sh b/data-loading/setup-and-load-solr.sh index 2b0a9bec..d8340a22 100755 --- a/data-loading/setup-and-load-solr.sh +++ b/data-loading/setup-and-load-solr.sh @@ -1,35 +1,154 @@ #!/usr/bin/env bash +# +# Load Babel synonym files (JSON, one document per line) into a Solr core that +# already exists. The core is created from the checked-in configset with +# `solr create -c name_lookup -d configsets/name_lookup` (see the Makefile and +# the CI workflow) -- this script does NOT create it, so the schema lives in +# exactly one place: data-loading/configsets/name_lookup/conf. +# +# Speed: files are streamed to Solr in parallel with commit=false, and a single +# commit is issued at the end. There is no per-file commit and no sleeping. +# +# Safety: because the load is parallel and the data is huge, we count the input +# documents (lines) before loading and compare against the *increase* in Solr's +# document count afterwards. Any failed upload (curl --fail) or a count mismatch +# aborts with a non-zero exit code, so a partial/dropped load cannot pass silently. +# +# The count is the important half of that: Solr rejects some bad input outright +# (an unknown field is a 400, which --fail catches), but it answers 200 and +# indexes nothing for other junk -- a file of plain text, for instance. Only the +# count notices that. -# We don't use set -e because the loop test relies on failures being ignored. set -uo pipefail -# Configuration options -SOLR_SERVER="http://localhost:8983" +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +# Configuration (overridable from the environment). +SOLR_SERVER="${SOLR_SERVER:-http://localhost:8983}" +CORE="${SOLR_CORE:-name_lookup}" +# available-cpus.sh reads the cgroup quota, so in a container this is the pod's CPU +# limit rather than the node's core count. See data-loading/kubernetes/README.md. +PARALLELISM="${LOAD_PARALLELISM:-$(bash "${SCRIPT_DIR}/available-cpus.sh" 2>/dev/null \ + || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)}" +# How long to wait for Solr to come up, in 3-second increments. +STARTUP_TRIES="${SOLR_STARTUP_TRIES:-60}" + +GLOB="${1:?usage: setup-and-load-solr.sh \"data/synonyms/*.txt*\" (quote the glob!)}" -# Step 1. Make sure the Solr service is up and running. -HEALTH_ENDPOINT="${SOLR_SERVER}/solr/admin/cores?action=STATUS" -response=$(wget --spider --server-response ${HEALTH_ENDPOINT} 2>&1 | grep "HTTP/" | awk '{ print $2 }') >&2 -until [ "$response" = "200" ]; do - response=$(wget --spider --server-response ${HEALTH_ENDPOINT} 2>&1 | grep "HTTP/" | awk '{ print $2 }') >&2 - echo " -- SOLR is unavailable - sleeping" +# Number of documents currently in the core. The regex tolerates optional whitespace +# after the colon: /query runs with indent=true, so Solr's JSON writer may format this +# as `"numFound": 89`. head -1 guards against sibling keys like numFoundExact. +solr_count() { + curl -sf "${SOLR_SERVER}/solr/${CORE}/query?q=*:*&rows=0" \ + | grep -oE '"numFound":[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+' +} + +# Step 1. Wait for the core to be available (bounded: a Solr that never comes up +# should fail the build, not hang it). +ready="" +for _ in $(seq "${STARTUP_TRIES}"); do + if curl -sf "${SOLR_SERVER}/solr/${CORE}/admin/ping" >/dev/null 2>&1; then + ready="yes" + break + fi + echo " -- Solr core '${CORE}' is unavailable - sleeping" sleep 3 done -echo "SOLR is up and running at ${SOLR_SERVER}." +if [ -z "$ready" ]; then + echo "Solr core '${CORE}' at ${SOLR_SERVER} did not come up after $((STARTUP_TRIES * 3))s. Aborting." >&2 + exit 1 +fi +echo "Solr core '${CORE}' is up at ${SOLR_SERVER}; loading with ${PARALLELISM} parallel uploads." -# Step 2. Create fields for search. -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/setup_solr.sh" -echo Solr database has been set up. - -# Step 3. Load specified files. -for f in $1; do - echo "Loading $f..." - # curl -d @$f needs to load the entire file into memory before uploading it, whereas - # curl -X POST -T $f will stream it. See https://github.com/TranslatorSRI/NameResolution/issues/194 - curl -H 'Content-Type: application/json' -X POST -T $f \ - "$SOLR_SERVER/solr/name_lookup/update/json/docs?processor=uuid&uuid.fieldName=id&commit=true" - sleep 60 +# Step 2. Expand the glob and count the input documents (one JSON doc per line). +# Emptying IFS stops the shell from splitting the glob on spaces, so filenames +# containing spaces survive; pathname expansion still yields one word per file. +saved_ifs="$IFS" +IFS= +shopt -s nullglob +files=( $GLOB ) +shopt -u nullglob +IFS="$saved_ifs" +if [ ${#files[@]} -eq 0 ]; then + echo "No files matched '${GLOB}'." >&2 + exit 1 +fi +# `grep -c '[^[:space:]]'` counts every non-blank line, including a final one with +# no trailing newline. Blank lines are skipped because Solr ignores them, and we +# count per file so a missing newline never merges two files' records. This assumes +# one document per line -- pretty-printed JSON would be counted wrong. +echo "Counting documents in ${#files[@]} file(s)..." +expected=0 +for f in "${files[@]}"; do + n=$(grep -c '[^[:space:]]' "$f") + echo " ${n} docs in ${f}" + expected=$((expected + n)) done -echo "Check solr" -curl -s --negotiate -u: "$SOLR_SERVER/solr/name_lookup/query?q=*:*&rows=0" +echo "Expecting to add ${expected} documents in total." + +# The core may already contain documents (every load assigns fresh UUIDs, so +# loading is additive, never idempotent). Compare the delta, not the total. +before=$(solr_count) +if [ -z "${before}" ]; then + echo "Could not read the current document count from Solr. Aborting." >&2 + exit 1 +fi + +# Refuse to load into a non-empty core by default. Loading is additive and every +# document gets a fresh UUID, so a second load over the same files silently doubles +# the index -- and the count guard below cannot see it, because the *delta* is still +# exactly the number of input documents. That is the one way this script can hand +# back a corrupt index while reporting success, so it is a hard error rather than a +# warning. It fires in the two cases that actually happen: a load re-run after the +# previous one died partway through, and make re-running the load because a stamp +# file went missing (see the stamp-file comment in the Makefile). +if [ "${before}" -ne 0 ]; then + if [ "${LOAD_APPEND:-0}" = "1" ]; then + echo "NOTE: core '${CORE}' already contains ${before} documents; LOAD_APPEND=1, so this load adds to them." + else + echo "Core '${CORE}' already contains ${before} documents, and loading is additive:" >&2 + echo "re-running the load would duplicate every document rather than resume it." >&2 + echo >&2 + echo "To start over, delete the core's index and let make recreate it:" >&2 + echo " make stop-solr && rm -rf \"\${SOLR_DIR:-/var/solr}/${CORE}\" \"\${SOLR_DIR:-/var/solr}\"/*.done" >&2 + echo "To append anyway (you are loading a second, distinct set of files), set LOAD_APPEND=1." >&2 + exit 1 + fi +fi + +# Step 3. Load the files in parallel, streaming each one, without committing. +# curl -T streams the file rather than buffering it in memory (issue #194), and +# --fail turns any HTTP >=400 (e.g. an unknown field) into a non-zero exit. Input +# that Solr accepts but does not index is caught by the count check in step 5. +load_one() { + local f="$1" + echo "Loading ${f}..." + curl -sf --show-error -H 'Content-Type: application/json' -X POST -T "$f" \ + "${SOLR_SERVER}/solr/${CORE}/update/json/docs?processor=uuid&uuid.fieldName=id&commit=false" \ + || { echo "FAILED to load ${f}" >&2; return 1; } +} +export -f load_one +export SOLR_SERVER CORE + +printf '%s\0' "${files[@]}" \ + | xargs -0 -P "$PARALLELISM" -I{} bash -c 'load_one "$@"' _ {} +load_rc=$? +if [ "$load_rc" -ne 0 ]; then + echo "One or more files failed to load (xargs exit ${load_rc}). Aborting." >&2 + exit 1 +fi + +# Step 4. Commit once. +echo "Committing..." +curl -sf --show-error "${SOLR_SERVER}/solr/${CORE}/update?commit=true" >/dev/null \ + || { echo "Commit failed." >&2; exit 1; } +# Step 5. Verify that the number of documents added matches the input. +after=$(solr_count) +added=$(( ${after:-0} - before )) +echo "Solr reports ${after:-0} documents (${added} added); expected to add ${expected}." +if [ "${added}" != "${expected}" ]; then + echo "DOCUMENT COUNT MISMATCH: added ${added}, expected ${expected}. Aborting." >&2 + exit 1 +fi +echo "Load complete: ${after} documents in '${CORE}'." diff --git a/data-loading/setup_solr.sh b/data-loading/setup_solr.sh deleted file mode 100644 index 0ea2842f..00000000 --- a/data-loading/setup_solr.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env bash -# -# Set up the fields and types needed by NameRes. -# -# This file should be sourced, not called directly. - -# require sourcing -[[ "${BASH_SOURCE[0]}" != "$0" ]] || { - echo "Must be sourced: source $0" >&2 - exit 1 -} - -# require SOLR_SERVER -: "${SOLR_SERVER:?SOLR_SERVER must be set}" - -echo "Setting up Solr database with SOLR_SERVER='$SOLR_SERVER'" - -# add collection -curl -X POST "$SOLR_SERVER/solr/admin/collections?action=CREATE&name=name_lookup&numShards=1&replicationFactor=1" - -# do not autocreate fields -curl "$SOLR_SERVER/solr/name_lookup/config" -d '{"set-user-property": {"update.autoCreateFields": "false"}}' - -# add lowercase text type -curl -X POST -H 'Content-type:application/json' --data-binary '{ - "add-field-type" : { - "name": "LowerTextField", - "class": "solr.TextField", - "positionIncrementGap": "100", - "analyzer": { - "tokenizer": { - "class": "solr.StandardTokenizerFactory" - }, - "filters": [{ - "class": "solr.LowerCaseFilterFactory" - }] - } - } -}' "$SOLR_SERVER/solr/name_lookup/schema" - -# add exactish text type (as described at https://stackoverflow.com/a/29105025/27310) -curl -X POST -H 'Content-type:application/json' --data-binary '{ - "add-field-type" : { - "name": "exactish", - "class": "solr.TextField", - "positionIncrementGap": "100", - "analyzer": { - "tokenizer": { - "class": "solr.KeywordTokenizerFactory" - }, - "filters": [{ - "class": "solr.LowerCaseFilterFactory" - }] - } - } -}' "$SOLR_SERVER/solr/name_lookup/schema" - - - -# add fields -curl -X POST -H 'Content-type:application/json' --data-binary '{ - "add-field": [ - { - "name":"names", - "type":"LowerTextField", - "indexed":true, - "stored":true, - "multiValued":true - }, - { - "name":"names_exactish", - "type":"exactish", - "indexed":true, - "stored":false, - "multiValued":true - }, - { - "name":"curie", - "type":"string", - "stored":true - }, - { - "name":"preferred_name", - "type":"LowerTextField", - "stored":true - }, - { - "name":"preferred_name_exactish", - "type":"exactish", - "indexed":true, - "stored":false, - "multiValued":false - }, - { - "name":"types", - "type":"string", - "stored":true - "multiValued":true - }, - { - "name":"shortest_name_length", - "type":"pint", - "stored":true - }, - { - "name":"curie_suffix", - "type":"plong", - "docValues":true, - "stored":true, - "required":false, - "sortMissingLast":true - }, - { - "name":"taxa", - "type":"string", - "stored":true, - "multiValued":true - }, - { - "name":"taxon_specific", - "type":"boolean", - "stored":true, - "multiValued":false, - "sortMissingLast":true - }, - { - "name":"clique_identifier_count", - "type":"pint", - "stored":true - } - ] }' "$SOLR_SERVER/solr/name_lookup/schema" - -# Add a copy field to copy names into names_exactish. -curl -X POST -H 'Content-type:application/json' --data-binary '{ - "add-copy-field": { - "source": "names", - "dest": "names_exactish" - } -}' "$SOLR_SERVER/solr/name_lookup/schema" - -# Add a copy field to copy preferred_name into preferred_name_exactish. -curl -X POST -H 'Content-type:application/json' --data-binary '{ - "add-copy-field": { - "source": "preferred_name", - "dest": "preferred_name_exactish" - } -}' "$SOLR_SERVER/solr/name_lookup/schema" diff --git a/docker-compose.yml b/docker-compose.yml index ba710749..7c1c426c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,10 @@ services: nameres_solr: container_name: nameres_solr - image: solr:9.1 + # Pinned to the 9.10 line rather than an exact patch: a serving Solr must be >= the + # Solr that built the backup (data-loading pins that exactly), and any 9.10.x can + # read a 9.10.x index, so floating within the line only picks up fixes. + image: solr:9.10 mem_limit: 18G environment: # Change this setting to control how much memory you would like your Solr setup to have. @@ -9,7 +12,8 @@ services: SOLR_JAVA_MEM: '-Xmx16G' ports: - '8983:8983' - command: ['-DzkRun'] + # Standalone mode (no ZooKeeper). To load data, extract a NameRes backup into + # ./data/solr so the name_lookup/ core is auto-discovered on startup. # Solr needs to store its data somewhere. It defaults to `./data`, but you can reconfigure this to any # directory you want. diff --git a/documentation/API.md b/documentation/API.md index 57bcbdea..2179aa98 100644 --- a/documentation/API.md +++ b/documentation/API.md @@ -3,8 +3,9 @@ The Name Resolver API is intended to provide an [Apache Solr](https://solr.apache.org/)-based interface to the [Babel](https://github.com/NCATSTranslator/Babel) cliques of equivalent identifiers. Apache Solr is a document-based search engine: the documents in this case are descriptions of cliques as generated by the -[Babel](https://github.com/NCATSTranslator/Babel) pipeline in its -[Synonyms format](https://github.com/NCATSTranslator/Babel/blob/master/docs/DataFormats.md#synonym-files), including lists of all known synonyms. +[Babel](https://github.com/NCATSTranslator/Babel) pipeline in its Synonyms format, including lists of all known synonyms. +See [Where NameRes data comes from](./Babel.md) for what Babel puts in these documents, which Babel files a NameRes +instance is built from, and the behaviour that follows from both. Here is an example document for [NCBIGene:1756](https://name-resolution-sri.renci.org/synonyms?preferred_curies=NCBIGene%3A1756) (compared with the same CURIE [on NodeNorm](https://nodenormalization-sri.renci.org/get_normalized_nodes?curie=NCBIGene:1756)): @@ -91,18 +92,27 @@ The Name Resolver largely consists of two [search endpoints](#search-endpoints): ## Conflation Unlike the Node Normalizer, the Name Resolution Service does not currently support on-the-fly conflation. Instead, -all the [Babel conflations](https://github.com/NCATSTranslator/Babel/blob/master/docs/Conflation.md) are turned on when Solr database is built. At the moment, this includes: -* GeneProtein conflation: protein-encoding genes are conflated with the protein(s) they encode, and the gene identifier - is used to identify this concept. Therefore, if you search for "" -* DrugChemical conflation: drugs are conflated with their active ingredient, and the identifier for the active ingredient - is used to identify this concept. -This means that -- for example -- protein-encoding genes will include the synonyms found -for the protein they encode, and that no separate entry will be available for those proteins. +all the Babel conflations are baked in when the Solr database is built. At the moment, this includes: + +* **GeneProtein conflation:** protein-encoding genes are conflated with the protein(s) they encode, and the gene + identifier is used to identify this concept. The `NCBIGene:1756` document shown above is an example: it is a gene, but + its `names` include protein synonyms such as `dystrophin` and `DMD_HUMAN Dystrophin (sprot)`, and there is no separate + document for the protein itself. +* **DrugChemical conflation:** drugs are conflated with their active ingredient, and the identifier for the active + ingredient is used to identify this concept. + +This means that protein-encoding genes include the synonyms found for the protein they encode, and no separate +entry is available for those proteins in NameRes. + +The active conflations for any NameRes deployment can be queried programmatically via the [`/status` endpoint](#status). Once you have an identifier from Name Resolver, you can use the [Node Normalizer](https://nodenormalization-sri.renci.org/) to look up the equivalent identifiers for that CURIE with and without conflation. Please use the Node Normalizer corresponding to the instance of Name Resolver you are using. +See [Where NameRes data comes from](./Babel.md#geneproteinconflated-and-drugchemicalconflated-replace-the-per-type-files) +for why conflation is baked into the index rather than applied per query, and what each conflation merges. + We are currently working on supporting [on-the-fly conflation](https://github.com/NCATSTranslator/NameResolution/issues/224) in Name Resolver. @@ -122,7 +132,7 @@ Search for cliques by a fragment of a name or synonym. - `autocomplete` (optional, boolean, default: false): If `true`, treats the input string as incomplete and looks for terms that start with the final word. If `false`, treats the entire phrase as complete (entity linker mode). - `highlighting` (optional, boolean, default: false): If `true`, returns information on which labels and synonyms matched the search query. - `offset` (optional, integer, default: 0, minimum: 0): The number of results to skip. Used for pagination. -- `limit` (optional, integer, default: 10, minimum: 0, maximum: 1000): The maximum number of results to return. Used for pagination. +- `limit` (optional, integer, default: 10, minimum: 0, maximum: 1000): The number of results to return. Used for pagination. - `biolink_type` (optional, list of strings): Filter results to specific Biolink types. Types can be specified with or without the `biolink:` prefix (e.g., `biolink:Disease` or `Disease`). Multiple types are combined with OR logic, so filtering for `PhenotypicFeature` and `Disease` will return concepts that are either phenotypic features OR diseases. - `only_prefixes` (optional, string): Pipe-separated, case-sensitive list of CURIE prefixes to include (e.g., `MONDO|EFO`). Only results with matching prefixes will be returned. - `exclude_prefixes` (optional, string): Pipe-separated, case-sensitive list of CURIE prefixes to exclude (e.g., `UMLS|EFO`). Results with matching prefixes will be filtered out. @@ -250,8 +260,11 @@ Look up all synonyms and information for a CURIE. - `preferred_name`: The preferred name for this concept. - `names`: A list of all known synonyms for this concept. - `types`: A list of Biolink types for this concept. -- `taxa`: A list of taxa associated with this concept. +- `taxa`: A list of taxa associated with this concept. Unlike `/lookup`, this key is omitted + entirely rather than returned as an empty list when the concept has no taxa. +- `taxon_specific`: Whether this concept is associated with one or more specific taxa. - `clique_identifier_count`: The number of identifiers in this clique. +- `shortest_name_length`: The length of the shortest synonym in `names`. - Additional metadata fields such as `curie_suffix`, `id`, and `_version_`. **Example requests:** @@ -302,6 +315,12 @@ POST /synonyms with body: - This endpoint provides a complete view of all synonyms and properties for a given concept. - CURIEs are conflated with both GeneProtein and DrugChemical conflation. For example, when looking up a protein CURIE, the synonyms for the gene that encodes the protein will also be included. See the [Conflation section](#conflation) for more information. - If a CURIE is not found in the database, it will still appear in the response dictionary with an empty object as its value. +- Concept descriptions are not included: Babel collects them, but they are not carried into the synonym files NameRes + indexes. Use [NodeNorm](https://nodenormalization-sri.renci.org/)'s `description` flag if you need them. +- This endpoint returns the underlying Solr document, so it describes a concept differently from `/lookup`: it uses + `preferred_name`/`names` rather than `label`/`synonyms`, returns `types` without the `biolink:` prefix, and includes + index metadata. We intend to + [harmonize the two response shapes](https://github.com/NCATSTranslator/NameResolution/issues/291). - For large batches of CURIEs, consider using this endpoint instead of making individual requests, as it is optimized for batch lookups. ## Health endpoints @@ -319,13 +338,15 @@ Solr database. "status": "ok", "message": "Reporting results from primary core.", "babel_version": "2025sep1", - "babel_version_url": "https://github.com/ncatstranslator/Babel/blob/master/releases/2025sep1.md", + "babel_version_url": "https://github.com/ncatstranslator/Babel/blob/main/releases/2025sep1.md", "biolink_model": { "tag": "v4.2.6-rc5", "url": "https://github.com/biolink/biolink-model/tree/v4.2.6-rc5", "download_url": "https://raw.githubusercontent.com/biolink/biolink-model/v4.2.6-rc5/biolink-model.yaml" }, - "nameres_version": "v1.5.1", + "conflations": ["GeneProtein", "DrugChemical"], + "conflation_url": "https://github.com/NCATSTranslator/Babel/blob/main/docs/Conflation.md", + "nameres_version": "v1.7.0", "startTime": "2025-12-19T11:53:09.638Z", "numDocs": 425583391, "maxDoc": 425586610, diff --git a/documentation/Babel.md b/documentation/Babel.md new file mode 100644 index 00000000..cb4e3816 --- /dev/null +++ b/documentation/Babel.md @@ -0,0 +1,152 @@ +# Where NameRes data comes from + +NameRes does not decide what a concept is. It is a search interface over concepts built by +[Babel](https://github.com/NCATSTranslator/Babel), the pipeline that merges identifiers from +dozens of biomedical vocabularies into "cliques" of equivalent identifiers and collects every +synonym for each one. + +The division of labour: + +- **Babel decides** which identifiers belong together, which one leads the clique, what the + preferred name is, which Biolink type it gets, and which synonyms it has. +- **NameRes decides** how those concepts are indexed, matched and ranked (see + [Scoring](./Scoring.md)), and what the API looks like (see [API](./API.md)). + +This document covers the Babel behaviour that is *visible through the NameRes API* — the things +that will otherwise look like NameRes bugs. It is also the only place in this repository that +links into the Babel repository, so that a reorganization there is a one-file fix here. + +## What is actually loaded + +NameRes is built from Babel's published `synonyms/` directory for one release +(`SYNONYMS_URL` in [`data-loading/Makefile`](../data-loading/Makefile)); the loader takes every +`*.txt`/`*.txt.gz` file in it. As of the `2025sep1` and `2026jul22` releases that is: + +```text +AnatomicalEntity BiologicalProcess Cell CellLine CellularComponent Disease +DrugChemicalConflated GeneFamily GeneProteinConflated GrossAnatomicalStructure +MacromolecularComplex MolecularActivity OrganismTaxon Pathway PhenotypicFeature +Publication umls +``` + +The set of searchable Biolink types is therefore Babel's decision, not NameRes's: if a Babel +release adds a synonym file, the next NameRes load picks it up with no change to this repository. + +Two entries in that list are worth reading twice. + +### `GeneProteinConflated` and `DrugChemicalConflated` replace the per-type files + +There is no `Gene.txt`, `Protein.txt` or `SmallMolecule.txt` in the published directory — the +conflated files stand in for them. This is why NameRes results are conflated and why, unlike +NodeNorm, NameRes cannot turn conflation off on a per-query basis: it is baked into the index at +load time. + +The practical consequence is that **there is no separate document to find for the conflated-away +member.** A protein is searchable, but the result you get back is identified by the gene that +encodes it and carries the union of both sets of synonyms; likewise a drug is identified by its +active ingredient. What each conflation merges, and in what order, is described in +[Babel's Conflation documentation](https://github.com/NCATSTranslator/Babel/blob/main/docs/Conflation.md). + +Once you have an identifier from NameRes you can use +[NodeNorm](https://nodenormalization-sri.renci.org/) to see the clique with and without each +conflation. Use the NodeNorm instance that corresponds to the NameRes instance you are querying. + +### `umls` is Babel's leftover-UMLS compendium + +Babel's last pipeline step sweeps up every valid UMLS concept that no other compendium claimed and +writes each one out as a **single-identifier clique**. These are in your search results. They are +usually the reason for a result that has a `UMLS:` CURIE, a `clique_identifier_count` of 1, and a +broader Biolink type than you expected — Babel derives the type from the concept's UMLS semantic +type, falling back to `biolink:NamedThing` where UMLS asserts none. + +This is deliberate coverage, not a defect: it means a UMLS concept still resolves to *something* +even when Babel could not merge it into a richer clique. See +[Babel's leftover-UMLS documentation](https://github.com/NCATSTranslator/Babel/blob/main/docs/sources/UMLS/Leftover.md). + +## Gotchas in the returned fields + +Every field NameRes returns from `/lookup` and `/synonyms` comes from Babel's +[synonym file format](https://github.com/NCATSTranslator/Babel/blob/main/docs/DataFormats.md). +Four of them behave in ways that surprise people. + +### `label` is not necessarily the label of the CURIE you looked up + +`label` is Babel's `preferred_name` for the whole clique, and Babel may deliberately choose a name +that is not the label of the clique leader — to disambiguate the concept, or to give the +Translator UI something better to display. Looking up a CURIE and getting back a different-looking +name is expected. How the preferred name is chosen (including the chemical-specific boost prefixes +and the length demotion) is in +[Understanding Babel outputs](https://github.com/NCATSTranslator/Babel/blob/main/docs/Understanding.md). + +### `synonyms` is not ordered by quality + +Babel writes `names` shortest-first — *except* for conflated cliques, where it writes all the +synonyms of the first clique, then all the synonyms of the second, and so on. Since almost +everything NameRes loads is conflated, `synonyms[0]` is not reliably the best or shortest name. +Use `label` when you want one name for a concept. + +### `clique_identifier_count` measures coverage, not importance + +NameRes multiplies each search score by the logarithm of this count (see +[Scoring](./Scoring.md)), on the theory that a widely cross-referenced concept is the one a user +more likely meant. What it literally counts is how many identifiers Babel merged into the clique, +which tracks how many source vocabularies happen to cover the concept. It is a good proxy in +practice, but it is why a leftover-UMLS singleton ranks below a heavily cross-referenced chemical +even when the two match the query equally well. + +### `taxa` is a union, and an empty list means "nobody said" + +Babel collects taxa per identifier and the clique's `taxa` is the union of those. An empty `taxa` +means no source asserted a taxon for any member, not that the concept is taxon-agnostic. This is +why `only_taxa` filtering keeps results that have *no* taxon alongside results with a matching +one — excluding them would drop concepts that are simply unannotated. + +Note that "no taxon" looks different between the two endpoints: `/lookup` always returns a `taxa` +key and gives it an empty list, while `/synonyms` returns the raw Solr document, which simply omits +the field. Babel's own `taxon_specific` flag is carried through to `/synonyms` if you want the +distinction as a boolean. That inconsistency is ours, not Babel's, and we intend to +[remove it](https://github.com/NCATSTranslator/NameResolution/issues/291). + +### There are no descriptions + +Babel collects descriptions (from [UberGraph](https://github.com/INCATools/ubergraph/)) and +NodeNorm can return them, but they are not carried into the synonym files and so are not in the +NameRes index. Use NodeNorm's `description` flag if you need them. + +## The index is a snapshot of one Babel release + +A NameRes instance serves a fixed build, not live Babel output. `/status` reports which one: + +```json +{ + "babel_version": "2025sep1", + "babel_version_url": "https://github.com/ncatstranslator/Babel/blob/main/releases/2025sep1.md", + "biolink_model": { "tag": "v4.2.6-rc5" } +} +``` + +A concept that Babel has since fixed will still be wrong here until the instance is rebuilt and +redeployed. Check `/status` before reporting a stale clique, and see +[Babel's releases](https://github.com/NCATSTranslator/Babel/blob/main/releases/README.md) for +what changed between builds. + +## Reporting a problem + +Babel and NameRes have separate issue trackers, and the split is by *what is wrong*: + +- **The concept itself is wrong** — wrong identifiers merged or not merged, wrong Biolink type, + wrong preferred label, missing or bogus synonyms → the + [Babel issue tracker](https://github.com/NCATSTranslator/Babel/issues). +- **The service is wrong** — errors, results ranked badly for a query, a parameter not behaving as + documented → the + [NameRes issue tracker](https://github.com/NCATSTranslator/NameResolution/issues). + +If you are not sure, file it in Babel and it will be sorted out. +[Babel's guide to filing an issue](https://github.com/NCATSTranslator/Babel/blob/main/docs/NewIssue.md) +describes what to include and how the priority/impact/size fields are used. + +## Going deeper + +Everything above is what a NameRes user needs. If you want to know how the cliques are actually +built — the per-source ingestion, the concord files, the union-find merge, running the pipeline +yourself — start at [Babel's documentation index](https://github.com/NCATSTranslator/Babel/blob/main/docs/). diff --git a/documentation/Deployment.md b/documentation/Deployment.md index d4ba344e..f36a0423 100644 --- a/documentation/Deployment.md +++ b/documentation/Deployment.md @@ -17,16 +17,30 @@ instance or from Translator. with [Docker Compose](https://docs.docker.com/compose/install/). 2. Create the local directory where your Solr data will be stored -- by default, this is `./data/solr` in this directory, but you can change this in - [docker-compose.yml](./docker-compose.yml). This directory will need to have a maximum + [docker-compose.yml](../docker-compose.yml). This directory will need to have a maximum storage of approx 400G: 104G of the downloaded file (which can be deleted once decompressed), 147G of uncompressed backup (both of which can be deleted once restored) and 147G of Apache Solr databases. 3. Download the Solr backup URL you want to use and save it in `./data/solr`. It should be approximately 104G in size. -4. Uncompress the Solr backup file. It should produce a `var/solr/data/snapshot.backup` directory - in the Solr data (by default, `./data/solr/var/solr/data/snapshot.backup`). You can delete - the downloaded file (`snapshot.backup.tar.gz`) once it has been decompressed. -5. Check the [docker-compose.yml](./docker-compose.yml) file to ensure that it is +4. Uncompress the Solr backup file into the Solr data directory. Backups produced by + [data-loading](../data-loading/README.md) are whole Solr cores -- configuration, schema + and index -- so this produces a ready-to-serve `name_lookup/` directory (by default, + `./data/solr/name_lookup`). You can delete the downloaded file + (`snapshot.backup.tar.gz`) once it has been decompressed. + + ```shell + $ sudo tar -C ./data/solr --numeric-owner -xzvf snapshot.backup.tar.gz + ``` + + Solr writes to the core it serves (it takes a write lock, and will not load the core + if it cannot), and it runs as uid 8983 inside the container. The tarball records that + ownership, so extract it as root — `--numeric-owner` then applies the recorded uid + rather than looking up what `solr` happens to mean on your machine. Extracting as an + ordinary user gives the files to you instead, and Solr will fail to load the core. + For a backup built before this was the case, or if you see permission errors in the + Solr log, fix it with `sudo chown -R 8983:8983 ./data/solr/name_lookup`. +5. Check the [docker-compose.yml](../docker-compose.yml) file to ensure that it is as you expect. * The Docker Compose file will use the latest released version of NameRes as the frontend. To use the source code in this repository, you will need to change @@ -35,40 +49,37 @@ instance or from Translator. If you want to run many Solr queries, you might want to increase this. To do this, you will need to change BOTH the `mem_limit` setting in the `nameres_solr` service in `docker-compose.yml` and the `SOLR_JAVA_MEM` setting. - * The `docker-compose.yml` file also mounts the local `data/` directory into the Solr - container as `/var/solr`. This will allow you to start a new NameRes from the same + * The `docker-compose.yml` file also mounts the local `data/solr` directory into the Solr + container as `/var/solr/data` (the Solr home, which is where Solr looks for cores). + This will allow you to start a new NameRes from the same directory in the future. If you want to use a different directory, please change the `volumes` setting in the `nameres_solr` service in `docker-compose.yml`. Removing the binding will cause the Solr data to be stored in the Docker instance, and the data will be lost when the container is stopped. 6. Start the Solr and NameRes pods by running `docker compose up`. By default, Docker Compose will download and start the relevant pods and show you logs from both sources. You may - press `Ctrl+C` to stop the pods. -7. Trigger the Solr restore by running the restore script using `bash`, i.e. - `bash solr-restore/restore.sh`. This script assumes that the Solr pod is available on `localhost:8983` - and contains a `var/solr/data/snapshot.backup` directory with the data to restore. It will set up - some data types needed by NameRes and then triggering a restore of a backup. It will then go into a - sleep loop until the restore is complete, which should take 15-20 minutes. -8. Check that the script ended properly (`Solr restore complete!`). Look up http://localhost:2433/status - to ensure that the database has been loaded as expected. You can now delete the uncompressed database - backup in `$SOLR_DATA/var` to save disk space. -9. With the default settings, NameRes should be running on localhost on port 2433 (i.e. http://localhost:2433/). + press `Ctrl+C` to stop the pods. Solr discovers the `name_lookup` core on startup, so + there is no restore step: no collection to create and no schema to apply. +7. Look up http://localhost:2433/status to ensure that the database has been loaded as + expected -- `numDocs` should match the number of documents in the backup. +8. With the default settings, NameRes should be running on localhost on port 2433 (i.e. http://localhost:2433/). You should see a message in the NameRes pod log saying something like `Uvicorn running on http://0.0.0.0:2433 (Press CTRL+C to quit)` to confirm this. * By default, the web frontend (http://0.0.0.0:2433/docs) defaults to using the [NameRes RENCI Dev](https://name-resolution-sri.renci.org/docs) — you will need to change the "Servers" setting to use your local NameRes instance. - * If you try this before the restore has finished, looking up http://0.0.0.0:2433/status will give you an error - (`Expected core not found.`). This is because the Solr database and indexes have not yet been loaded. - Once this is finished, the NameRes instance should be ready to use. + * If http://0.0.0.0:2433/status reports an error (`Expected core not found.`), Solr has not + found the `name_lookup` core: check that the backup was extracted into the Solr data + directory (you should have a `name_lookup/core.properties` file in there) and that Solr + can read it. #### Loading from synonyms files -The best way to do this is by using the [data-loading Docker image](./data-loading/README.md). +The best way to do this is by using the [data-loading Docker image](../data-loading/README.md). ### Python packaging -Currently, NameRes is only packaged as a Docker image (see [Dockerfile](./Dockerfile)), but you can +Currently, NameRes is only packaged as a Docker image (see [Dockerfile](../Dockerfile)), but you can also run it directly via Uvicorn. ```bash @@ -93,10 +104,24 @@ curl -X POST "http://localhost:2433/lookup?string=oxycod&offset=0&limit=10" -H " NameRes can be configured by setting environmental variables: * `SOLR_HOST` and `SOLR_PORT`: Hostname and port for the Solr database containing NameRes information. +* `SOLR_CORE`: The Solr core to query (defaults to `name_lookup`). * `SERVER_NAME`: The name of this server (defaults to `infores:sri-name-resolver`) * `SERVER_ROOT`: The server root (defaults to `/`) * `MATURITY_VALUE`: How mature is this NameRes (defaults to `maturity`, e.g. `development`) * `LOCATION_VALUE`: Where is this NameRes setup (defaults to `location`, e.g. `RENCI`) +* `LOGLEVEL`: The Python logging level (defaults to `INFO`). +* `BABEL_VERSION`: The [Babel](https://github.com/NCATSTranslator/Babel) release this Solr index was + built from, reported by `/status` (defaults to `unknown`, e.g. `2025sep1`). Set this when you load + an index -- it is the only record of which data an instance is serving, and `/status` is where + users look for it. See [Where NameRes data comes from](./Babel.md). +* `BABEL_VERSION_URL`: URL of the changelog for that Babel release, also reported by `/status` + (defaults to empty). +* `BIOLINK_MODEL_TAG`: The [Biolink Model](https://github.com/biolink/biolink-model) tag the Solr + index was built against (defaults to `master`, e.g. `v4.2.6-rc5`). `/status` derives the model URL + and download URL from it. +* `CONFLATIONS`: Comma-separated list of the Babel conflations baked into this index, reported by + `/status` (defaults to `GeneProtein,DrugChemical`). Set this if you load an index built with a + different set -- it is not detected from the data. * `OTEL_ENABLED`: Turn on Open TELemetry (default: `'false'`) -- only `'true'` will turn this on. * `JAEGER_HOST` and `JAEGER_PORT`: Hostname and port for the Jaegar instance to provide telemetry to. * `JAEGER_SERVICE_NAME`: The name of this service (defaults to the value of `SERVER_NAME`) diff --git a/documentation/NameResolution.ipynb b/documentation/NameResolution.ipynb index 1fd3b1ab..d60bba96 100644 --- a/documentation/NameResolution.ipynb +++ b/documentation/NameResolution.ipynb @@ -6,7 +6,7 @@ "source": [ "# Name Resolver (NameRes)\n", "\n", - "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/NCATSTranslator/NameResolution/blob/master/documentation/NameResolution.ipynb)\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/NCATSTranslator/NameResolution/blob/main/documentation/NameResolution.ipynb)\n", "\n", "The [Babel pipelines](https://github.com/NCATSTranslator/Babel) generates sets of equivalent identifiers to be used by the [Node Normalizer](https://github.com/NCATSTranslator/NodeNormalization) to harmonize identifiers from different sources, but it also collects all known synonyms for these identifiers. The Name Resolver can be used to search through those synonyms." ] diff --git a/documentation/Scoring.md b/documentation/Scoring.md index 3da11b7b..e1a04dbe 100644 --- a/documentation/Scoring.md +++ b/documentation/Scoring.md @@ -14,9 +14,9 @@ If multiple terms are matched in the same document, the sum of the score for eac The TF*IDF score will be multiplied by [several boosts](https://github.com/NCATSTranslator/NameResolution/blob/56e2151bb9e6fd120644cebdf4ff45b3bc47da05/api/server.py#L436-L461) that depend on four factors: -* We index two fields: the "preferred name" of every clique and the "synonyms" of every clique. The [preferred name - is chosen by Babel](https://github.com/NCATSTranslator/Babel?tab=readme-ov-file#how-does-babel-choose-a-preferred-label-for-a-clique), - while the synonyms are collected from all the different Babel sources. +* We index two fields: the "preferred name" of every clique and the "synonyms" of every clique. The preferred name + is chosen by Babel, while the synonyms are collected from all the different Babel sources; see + [Where NameRes data comes from](./Babel.md) for what that means for the values you get back. * We set up two indexes: a [StandardTokenizer](https://solr.apache.org/guide/solr/latest/indexing-guide/tokenizers.html#standard-tokenizer) that splits the field into tokens at whitespace and punctuation characters, and a [KeywordTokenizer](https://solr.apache.org/guide/solr/latest/indexing-guide/tokenizers.html#keyword-tokenizer) that @@ -27,6 +27,8 @@ that depend on four factors: (NOTE: this might be removed soon.) * We use the number of identifiers in the clique as a measure of how widely used a clique is. Since some cliques share the same preferred name or label, we can use this to promote the clique most likely to be useful. + (What this count literally measures, and where the proxy breaks down, is described in + [Where NameRes data comes from](./Babel.md#clique_identifier_count-measures-coverage-not-importance).) We combine these factors in this way in a standard query matches: diff --git a/documentation/TranslatorGuide.md b/documentation/TranslatorGuide.md new file mode 100644 index 00000000..13a4eacc --- /dev/null +++ b/documentation/TranslatorGuide.md @@ -0,0 +1,190 @@ +# NameRes Translator Guide + +This guide is aimed at Translator developers and users who are integrating NameRes into their workflows. +It covers what to do when results are unexpected, how `/synonyms` (reverse-lookup) relates to NodeNorm, +and tips for improving performance. + +## What to do when a name lookup returns unexpected results + +NameRes ranks results by a [Solr TF*IDF score](./Scoring.md) — the top result is the best *textual* match, +not necessarily the biologically intended concept. If the results don't look right, try these steps. + +### 1. Use `highlighting` to understand what matched + +Set `highlighting=true` on a `/lookup` call to see which label or synonym drove the match: + +``` +GET /lookup?string=cold&highlighting=true&limit=5 +``` + +This tells you which synonym triggered the match, which helps diagnose why an unexpected concept ranked high. + +### 2. Filter by Biolink type + +Use `biolink_type` to restrict results to the category you expect. Multiple types are combined with OR logic: + +``` +GET /lookup?string=cold&biolink_type=Disease&biolink_type=PhenotypicFeature +``` + +Common types: `Disease`, `Gene`, `ChemicalEntity`, `PhenotypicFeature`, `BiologicalProcess`, `AnatomicalEntity`. +Types can be specified with or without the `biolink:` prefix. + +### 3. Restrict to trusted prefixes + +Use `only_prefixes` to limit results to a specific ontology, or `exclude_prefixes` to drop a noisy one. +Prefixes are pipe-separated and case-sensitive: + +``` +# Only MONDO disease identifiers +GET /lookup?string=diabetes&biolink_type=Disease&only_prefixes=MONDO + +# Exclude UMLS (often produces many ambiguous matches) +GET /lookup?string=NIH&exclude_prefixes=UMLS +``` + +Common trusted prefixes by category: + +| Category | Recommended prefixes | +|---|---| +| Disease | `MONDO`, `OMIM`, `ORPHANET` | +| Gene | `NCBIGene`, `HGNC` | +| Chemical/Drug | `CHEBI`, `DRUGBANK` | +| Phenotype | `HP`, `MP` | +| Anatomy | `UBERON`, `CL` | + +### 4. Filter by taxon for gene/protein queries + +When searching for a gene or protein, results may include entries from multiple species. Use `only_taxa` +to restrict to a specific organism. The value is a pipe-separated list of NCBI Taxon CURIEs: + +``` +# Human genes only +GET /lookup?string=APOE&biolink_type=Gene&only_taxa=NCBITaxon:9606 + +# Human and mouse +GET /lookup?string=APOE&only_taxa=NCBITaxon:9606|NCBITaxon:10090 +``` + +Common taxa: human `NCBITaxon:9606`, mouse `NCBITaxon:10090`, rat `NCBITaxon:10116`, zebrafish `NCBITaxon:7955`. + +### 5. Try autocomplete mode for partial strings + +If your search string is a fragment of a name (e.g., typed by a user mid-word), set `autocomplete=true`. +This expands the final word with a wildcard so that `"diab"` matches `"diabetes"`, `"diabetic"`, etc.: + +``` +GET /lookup?string=diab&autocomplete=true&limit=5 +``` + +Without `autocomplete`, `"diab"` will only match documents that literally contain the token `"diab"`. + +### 6. If the correct concept is consistently missing + +If your filtering is correct but the expected result never appears, the concept may be missing from the +Babel data that NameRes is built from — or it may be there under a conflated identifier you weren't +expecting. [Where NameRes data comes from](./Babel.md) covers which Babel files an instance is built +from and why, for example, there is no separate document for a protein. If the concept really is +absent, file an issue on: +- [NameRes GitHub](https://github.com/NCATSTranslator/NameResolution/issues) — for search/ranking problems +- [Babel GitHub](https://github.com/NCATSTranslator/Babel/issues) — for missing synonyms or identifiers + +--- + +## Using `/synonyms` (reverse-lookup) vs. NodeNorm + +These two services answer different questions. + +### Use `/synonyms` when you want to inspect synonyms for a known CURIE + +The `/synonyms` endpoint returns all names and synonyms that NameRes knows for a given concept, along with +its Biolink types, taxa, and clique identifier count. This is useful for verifying synonym coverage or +debugging why a particular name did or did not match. + +``` +GET /synonyms?preferred_curies=NCBIGene:1756 +``` + +**Important:** `/synonyms` requires the *preferred* (normalized) CURIE. If you pass a non-preferred +identifier (e.g. a UniProtKB accession for a gene), you will get an empty result. Before calling +`/synonyms`, normalize your CURIE with NodeNorm (see below). + +You can look up multiple CURIEs in one request: + +``` +GET /synonyms?preferred_curies=MONDO:0005148&preferred_curies=NCBIGene:1756 +``` + +### Use NodeNorm when you need identifier normalization or equivalent identifiers + +The [Node Normalization service](https://nodenormalization-sri.renci.org/) is the right tool when you need to: + +- Convert a non-preferred identifier to its preferred CURIE +- Find all equivalent identifiers for a concept across ontologies +- Check which Biolink types a CURIE maps to +- Determine whether two CURIEs refer to the same concept + +To normalize a CURIE before passing it to `/synonyms`, call NodeNorm with GeneProtein and DrugChemical +conflation enabled (to match the conflation used by NameRes): + +``` +GET https://nodenormalization-sri.renci.org/get_normalized_nodes?curie=UniProtKB:A0A0S2Z3B5&conflate=true&drug_chemical=true +``` + +The `id.identifier` field in the response is the preferred CURIE you can then pass to `/synonyms`. + +### Quick decision guide + +| Question | Tool | +|---|---| +| What synonyms does NameRes know for this CURIE? | `/synonyms` | +| What is the preferred identifier for this concept? | NodeNorm | +| Are these two CURIEs equivalent? | NodeNorm | +| What Biolink types does this CURIE have? | NodeNorm | +| Why didn't a particular name match in `/lookup`? | `/synonyms` + `highlighting` | +| Which conflations are active in this NameRes deployment? | `/status` (`conflations` field) | + +--- + +## Performance tips + +### Batch multiple queries with `/bulk-lookup` + +Instead of making N separate `/lookup` calls, send them all in one POST request to `/bulk-lookup`. +It returns a dictionary keyed by input string: + +```json +POST /bulk-lookup +{ + "strings": ["diabetes", "hypertension", "asthma"], + "limit": 5, + "biolink_types": ["Disease"] +} +``` + +This is significantly more efficient than sequential individual requests. + +### Add filters before processing results + +Apply `biolink_type`, `only_prefixes`, and `only_taxa` at query time rather than filtering the response +yourself. Server-side filtering reduces the result set before it is serialized and transmitted. + +### Set `limit` to what you actually need + +The default `limit` is 10 and the maximum is 1000. If you only need the top result, set `limit=1`. +If you need to page through a large result set, use `offset` for server-side pagination rather than +requesting a large `limit` and slicing client-side. + +### Cache results between Babel data releases + +NameRes results are stable between Babel data releases (which happen a few times per year). If your +application calls NameRes repeatedly for the same input strings, cache the results locally. Check the +`/status` endpoint to detect when the Babel version changes and invalidate your cache accordingly: + +``` +GET /status +``` + +The `babel_version` field in the response changes with each data release. An instance serves one +fixed Babel build rather than live output — see +[Where NameRes data comes from](./Babel.md#the-index-is-a-snapshot-of-one-babel-release). diff --git a/releases/TranslatorDecember2023.md b/releases/TranslatorDecember2023.md index 60812ed6..84b8824f 100644 --- a/releases/TranslatorDecember2023.md +++ b/releases/TranslatorDecember2023.md @@ -1,7 +1,7 @@ # NameRes Translator December 2023 Release - Babel: [2023nov5](https://stars.renci.org/var/babel_outputs/2023nov5/) -- NameRes: [v1.3.11](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.3.11) +- NameRes: [v1.3.11](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.3.11) Next release: [Translator May 2024](TranslatorMay2024.md) @@ -24,9 +24,9 @@ Next release: [Translator May 2024](TranslatorMay2024.md) ## Releases since [Translator October 2023 release](TranslatorOctober2023.md) -* https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.3.7 +* https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.3.7 * fixes reverse lookup get endpoint. by @YaphetKG in #102 -* https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.3.8 +* https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.3.8 * Fix Terms of Service and service description by @gaurav in #114 * Return entire document in reverse lookups by @gaurav in #115 * Report scores for reverse lookup by @gaurav in #116 @@ -34,9 +34,9 @@ Next release: [Translator May 2024](TranslatorMay2024.md) * Add an autocomplete flag by @gaurav in #118 * Added a /status endpoint that tells us about this NameRes instance by @gaurav in #119 * Added instruction to make data/synonyms directory in target data/synonyms/done by @gaurav in #123 -* https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.3.9 +* https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.3.9 * Otel instrumentation by @YaphetKG in #121 -* https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.3.10 +* https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.3.10 * Update server.py by @YaphetKG in #130 -* https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.3.11 +* https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.3.11 * Incremented version that I forgot to do in Name Resolver v1.3.10. diff --git a/releases/TranslatorFuguJuly2024.md b/releases/TranslatorFuguJuly2024.md index aaef527b..cb2c0540 100644 --- a/releases/TranslatorFuguJuly2024.md +++ b/releases/TranslatorFuguJuly2024.md @@ -1,7 +1,7 @@ # NameRes Translator "Fugu" July 2024 Release - Babel: [2024jul13](https://stars.renci.org/var/babel_outputs/2024jul13/) - ([Babel Translator July 2024 Release](https://github.com/TranslatorSRI/Babel/blob/master/releases/TranslatorFuguJuly2024.md)) -- NameRes: [v1.3.14](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.3.14) + ([Babel Translator July 2024 Release](https://github.com/NCATSTranslator/Babel/blob/main/releases/TranslatorFuguJuly2024.md)) +- NameRes: [v1.3.14](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.3.14) Next release: [Translator "Guppy" August 2024](./TranslatorGuppyAugust2024.md) @@ -22,6 +22,6 @@ Next release: [Translator "Guppy" August 2024](./TranslatorGuppyAugust2024.md) * [Update] Updated PANTHER pathways from SequenceAssociationPathway3.6.7.txt to SequenceAssociationPathway3.6.8.txt. ## Releases since [Translator May 2024](./TranslatorMay2024.md) -* [1.3.14](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.3.14): Forgot to increment version number in +* [1.3.14](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.3.14): Forgot to increment version number in previous release. -* [1.3.13](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.3.13): Added release notes for Translator May 2024. \ No newline at end of file +* [1.3.13](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.3.13): Added release notes for Translator May 2024. \ No newline at end of file diff --git a/releases/TranslatorGuppyAugust2024.md b/releases/TranslatorGuppyAugust2024.md index eee66d41..0eb1f2f7 100644 --- a/releases/TranslatorGuppyAugust2024.md +++ b/releases/TranslatorGuppyAugust2024.md @@ -1,28 +1,28 @@ # NameRes Translator "Guppy" August 2024 Release - Babel: [2024aug18](https://stars.renci.org/var/babel_outputs/2024aug18/) - ([Babel Translator "Guppy" August 2024 Release](https://github.com/TranslatorSRI/Babel/blob/master/releases/TranslatorGuppyAugust2024.md)) -- NameRes: [v1.4.3](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.3) + ([Babel Translator "Guppy" August 2024 Release](https://github.com/NCATSTranslator/Babel/blob/main/releases/TranslatorGuppyAugust2024.md)) +- NameRes: [v1.4.3](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.3) Next release: [Translator "Hammerhead" November 2024](./TranslatorHammerheadNovember2024.md) Previous release: [Translator "Fugu" July 2024](./TranslatorFuguJuly2024.md) ## New features -* Added exactish index for synonyms ([#150](https://github.com/TranslatorSRI/NameResolution/pull/150)) -* Added a Solr highlighter to report on matching labels and synonyms ([#156](https://github.com/TranslatorSRI/NameResolution/pull/156)) -* Added support for multitype filtering ([#158](https://github.com/TranslatorSRI/NameResolution/pull/158)) +* Added exactish index for synonyms ([#150](https://github.com/NCATSTranslator/NameResolution/pull/150)) +* Added a Solr highlighter to report on matching labels and synonyms ([#156](https://github.com/NCATSTranslator/NameResolution/pull/156)) +* Added support for multitype filtering ([#158](https://github.com/NCATSTranslator/NameResolution/pull/158)) -## Babel updates (from [Babel Translator "Guppy" August 2024 Release](https://github.com/TranslatorSRI/Babel/blob/master/releases/TranslatorGuppyAugust2024.md)) +## Babel updates (from [Babel Translator "Guppy" August 2024 Release](https://github.com/NCATSTranslator/Babel/blob/main/releases/TranslatorGuppyAugust2024.md)) * [Feature] Added support for generating DuckDB and Parquet files from the compendium and synonym files, allowing us to run queries such as looking for all the identically labeled cliques across all the compendia. Increased Babel Outputs file size to support DuckDB. -* [Feature] Added labels from DrugBank (https://github.com/TranslatorSRI/Babel/pull/335). -* [Feature] Improved cell anatomy concords using Wikidata (https://github.com/TranslatorSRI/Babel/pull/329). -* [Feature] Added manual concords for the DrugChemical conflation (https://github.com/TranslatorSRI/Babel/pull/337). -* [Feature] Wrote a script for comparing between two summary files (https://github.com/TranslatorSRI/Babel/pull/320). +* [Feature] Added labels from DrugBank (https://github.com/NCATSTranslator/Babel/pull/335). +* [Feature] Improved cell anatomy concords using Wikidata (https://github.com/NCATSTranslator/Babel/pull/329). +* [Feature] Added manual concords for the DrugChemical conflation (https://github.com/NCATSTranslator/Babel/pull/337). +* [Feature] Wrote a script for comparing between two summary files (https://github.com/NCATSTranslator/Babel/pull/320). * [Feature] Added timestamping as an option to Wget. * [Feature] Reorganized primary label determination so that we can include it in compendia files as well. * This isn't currently used by the loader, but might be in the future. For now, this is only useful in helping track what labels are being chosen as the preferred label. -* [Bugfixes] Added additional ENSEMBL datasets to skip (https://github.com/TranslatorSRI/Babel/pull/297). +* [Bugfixes] Added additional ENSEMBL datasets to skip (https://github.com/NCATSTranslator/Babel/pull/297). * [Bugfixes] Fixed a bug in recognizing the end of file when reading the PubChem ID and SMILES files. * [Bugfixes] Fixed the lack of `clique_identifier_count` in leftover UMLS output. * [Bugfixes] Fixed unraised exception in Ensembl BioMart download. @@ -32,12 +32,12 @@ Previous release: [Translator "Fugu" July 2024](./TranslatorFuguJuly2024.md) * [Updates] Changed DrugBank ID types from 'ChemicalEntity' to 'Drug'. ## Releases since [Translator "Fugu" July 2024](./TranslatorFuguJuly2024.md) -* [1.4.3](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.3) - * Fix issue with empty Biolink type by @gaurav in [#159](https://github.com/TranslatorSRI/NameResolution/pull/159) -* [1.4.2](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.2) - * Added support for multitype filtering by @gaurav in [#158](https://github.com/TranslatorSRI/NameResolution/pull/158) -* [1.4.1](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.1) - * Added Translator Fugu release notes by @gaurav in [#155](https://github.com/TranslatorSRI/NameResolution/pull/155) - * Use a Solr highlighter to identify matching terms by @gaurav in [#156](https://github.com/TranslatorSRI/NameResolution/pull/156) -* [1.4.0](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.0) - * Add exactish index for synonyms by @gaurav in [#150](https://github.com/TranslatorSRI/NameResolution/pull/150) +* [1.4.3](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.3) + * Fix issue with empty Biolink type by @gaurav in [#159](https://github.com/NCATSTranslator/NameResolution/pull/159) +* [1.4.2](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.2) + * Added support for multitype filtering by @gaurav in [#158](https://github.com/NCATSTranslator/NameResolution/pull/158) +* [1.4.1](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.1) + * Added Translator Fugu release notes by @gaurav in [#155](https://github.com/NCATSTranslator/NameResolution/pull/155) + * Use a Solr highlighter to identify matching terms by @gaurav in [#156](https://github.com/NCATSTranslator/NameResolution/pull/156) +* [1.4.0](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.0) + * Add exactish index for synonyms by @gaurav in [#150](https://github.com/NCATSTranslator/NameResolution/pull/150) diff --git a/releases/TranslatorHammerheadNovember2024.md b/releases/TranslatorHammerheadNovember2024.md index 0ab82b7b..2e2995af 100644 --- a/releases/TranslatorHammerheadNovember2024.md +++ b/releases/TranslatorHammerheadNovember2024.md @@ -1,27 +1,27 @@ # NameRes Translator "Hammerhead" November 2024 Release - Babel: [2024oct24](https://stars.renci.org/var/babel_outputs/2024oct24/) - ([Babel Translator November 2024 Release](https://github.com/TranslatorSRI/Babel/blob/master/releases/TranslatorHammerheadNovember2024.md)) -- NameRes: [v1.4.5](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.5) + ([Babel Translator November 2024 Release](https://github.com/NCATSTranslator/Babel/blob/main/releases/TranslatorHammerheadNovember2024.md)) +- NameRes: [v1.4.5](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.5) Next release: [NameRes v1.4.7](./v1.4.7.md) Previous release: [Translator "Guppy" August 2024](./TranslatorGuppyAugust2024.md) ## New features -* Searching for an empty string now returns an empty list in [#167](https://github.com/TranslatorSRI/NameResolution/pull/167). +* Searching for an empty string now returns an empty list in [#167](https://github.com/NCATSTranslator/NameResolution/pull/167). ## Cleanup and improvements -* Add an MIT license for NameRes in [#169](https://github.com/TranslatorSRI/NameResolution/pull/169). -* Delete tests/data/test-synonyms.jsonl, which does not appear to be used in [#170](https://github.com/TranslatorSRI/NameResolution/pull/170). +* Add an MIT license for NameRes in [#169](https://github.com/NCATSTranslator/NameResolution/pull/169). +* Delete tests/data/test-synonyms.jsonl, which does not appear to be used in [#170](https://github.com/NCATSTranslator/NameResolution/pull/170). -## Babel updates (from [Babel Translator "Hammerhead" November 2024 Release](https://github.com/TranslatorSRI/Babel/blob/master/releases/TranslatorHammerheadNovember2024.md)) -- [New features] Added taxon information to proteins ([#349](https://github.com/TranslatorSRI/Babel/pull/349)) +## Babel updates (from [Babel Translator "Hammerhead" November 2024 Release](https://github.com/NCATSTranslator/Babel/blob/main/releases/TranslatorHammerheadNovember2024.md)) +- [New features] Added taxon information to proteins ([#349](https://github.com/NCATSTranslator/Babel/pull/349)) - [Updates] Upgraded RxNorm to 09032024. - [Updates] Changed NCBIGene download from FTP to HTTP. -- [Updates] Increased DRUG_CHEMICAL_SMALLER_MAX_LABEL_LENGTH (introduced in [#330](https://github.com/TranslatorSRI/Babel/pull/330)) from 30 to 40. +- [Updates] Increased DRUG_CHEMICAL_SMALLER_MAX_LABEL_LENGTH (introduced in [#330](https://github.com/NCATSTranslator/Babel/pull/330)) from 30 to 40. ## Releases since [Translator "Guppy" August 2024](./TranslatorGuppyAugust2024.md) -* [1.4.5](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.5) - * Searching for an empty string now returns an empty list in [#167](https://github.com/TranslatorSRI/NameResolution/pull/167). - * Add an MIT license for NameRes in [#169](https://github.com/TranslatorSRI/NameResolution/pull/169). - * Delete tests/data/test-synonyms.jsonl, which does not appear to be used in [#170](https://github.com/TranslatorSRI/NameResolution/pull/170). -* [1.4.4](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.4) - * Added release notes for Translator Guppy in [#160](https://github.com/TranslatorSRI/NameResolution/pull/160). +* [1.4.5](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.5) + * Searching for an empty string now returns an empty list in [#167](https://github.com/NCATSTranslator/NameResolution/pull/167). + * Add an MIT license for NameRes in [#169](https://github.com/NCATSTranslator/NameResolution/pull/169). + * Delete tests/data/test-synonyms.jsonl, which does not appear to be used in [#170](https://github.com/NCATSTranslator/NameResolution/pull/170). +* [1.4.4](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.4) + * Added release notes for Translator Guppy in [#160](https://github.com/NCATSTranslator/NameResolution/pull/160). diff --git a/releases/TranslatorMay2024.md b/releases/TranslatorMay2024.md index e187e55b..14760818 100644 --- a/releases/TranslatorMay2024.md +++ b/releases/TranslatorMay2024.md @@ -1,23 +1,23 @@ # NameRes Translator May 2024 Release - Babel: [2024mar24](https://stars.renci.org/var/babel_outputs/2024mar24/) (Babel Translator May 2024 Release) -- NameRes: [v1.3.12](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.3.12) +- NameRes: [v1.3.12](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.3.12) Next release: [Translator "Fugu" July 2024](TranslatorFuguJuly2024.md) ## New features * Solr database now has two additional fields for each clique: * `taxa`: list of taxa that a clique is found in; in Babel 2024mar24, only NCBIGenes have taxa (as generated by - [TranslatorSRI/Babel#211](https://github.com/TranslatorSRI/Babel/pull/211)). + [NCATSTranslator/Babel#211](https://github.com/NCATSTranslator/Babel/pull/211)). * `clique_identifier_count`: the number of identifiers in this clique (as generated by - [TranslatorSRI/Babel#228](https://github.com/TranslatorSRI/Babel/pull/228)). + [NCATSTranslator/Babel#228](https://github.com/NCATSTranslator/Babel/pull/228)). * The `/lookup` endpoints now return `taxa` and `clique_identifier_count`. They also have an `only_taxa` filter that can be used to provide a pipe-delimited list of NCBITaxon entries to filter results with. ## Babel updates (from Babel Translator May 2024 Release) * [New identifiers] 36.9 million PubMed IDs (e.g. `PMID:25061375`) have been added as `biolink:JournalArticle`, as well as the mappings to PMC (e.g. `PMC:PMC4109484`) and DOIs (e.g. `doi:10.3897/zookeys.420.7089`) that are included in PubMed. - Details in [TranslatorSRI/Babel#227](https://github.com/TranslatorSRI/Babel/pull/227). + Details in [NCATSTranslator/Babel#227](https://github.com/NCATSTranslator/Babel/pull/227). * Fixed type determination for DrugChemical conflation. Details in - [TranslatorSRI/Babel#266](https://github.com/TranslatorSRI/Babel/pull/266). + [NCATSTranslator/Babel#266](https://github.com/NCATSTranslator/Babel/pull/266). * Synonym files now include the clique identifier count (the number of identifiers in each clique) in synonyms file. * Minor fixes. diff --git a/releases/TranslatorOctober2023.md b/releases/TranslatorOctober2023.md index d6140cd9..745bebe9 100644 --- a/releases/TranslatorOctober2023.md +++ b/releases/TranslatorOctober2023.md @@ -1,6 +1,6 @@ # NameRes Translator October 2023 Release -- Babel: [2023aug](https://stars.renci.org/var/babel_outputs/2023aug3-with-drugs/) (roughly Babel [v1.1.0](https://github.com/TranslatorSRI/Babel/releases/tag/v1.1.0)) -- NameRes: [v1.3.6](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.3.6) +- Babel: [2023aug](https://stars.renci.org/var/babel_outputs/2023aug3-with-drugs/) (roughly Babel [v1.1.0](https://github.com/NCATSTranslator/Babel/releases/tag/v1.1.0)) +- NameRes: [v1.3.6](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.3.6) Next release: [December 2023](TranslatorDecember2023.md) \ No newline at end of file diff --git a/releases/v1.4.7.md b/releases/v1.4.7.md index fb93f257..e6218fee 100644 --- a/releases/v1.4.7.md +++ b/releases/v1.4.7.md @@ -1,28 +1,28 @@ # NameRes v1.4.7 Release - Babel: [2025jan23](https://stars.renci.org/var/babel_outputs/2025jan23/) - ([Babel 2025jan23](https://github.com/TranslatorSRI/Babel/blob/master/releases/2025jan23.md)) -- NameRes: [v1.4.7](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.7) + ([Babel 2025jan23](https://github.com/NCATSTranslator/Babel/blob/main/releases/2025jan23.md)) +- NameRes: [v1.4.7](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.7) Next release: [v1.5.1](./v1.5.1.md) Previous release: [Translator "Hammerhead" November 2024](./TranslatorHammerheadNovember2024.md) ## New Features -* Add a bulk endpoint for NameRes in [#165](https://github.com/TranslatorSRI/NameResolution/pull/165). +* Add a bulk endpoint for NameRes in [#165](https://github.com/NCATSTranslator/NameResolution/pull/165). ## Improvements -* Replaces /reverse_lookup with a more sensible name in [#168](https://github.com/TranslatorSRI/NameResolution/pull/168). -* Update OTEL to gRPC in [#171](https://github.com/TranslatorSRI/NameResolution/pull/171). +* Replaces /reverse_lookup with a more sensible name in [#168](https://github.com/NCATSTranslator/NameResolution/pull/168). +* Update OTEL to gRPC in [#171](https://github.com/NCATSTranslator/NameResolution/pull/171). ## Bugfixes -* Fix reverse CURIE endpoint in [#172](https://github.com/TranslatorSRI/NameResolution/pull/172). +* Fix reverse CURIE endpoint in [#172](https://github.com/NCATSTranslator/NameResolution/pull/172). * Queries ending with whitespace with autocomplete=true searched for everything by @gaurav in #173 -## Babel updates (from [Babel 2025jan23](https://github.com/TranslatorSRI/Babel/blob/master/releases/2025jan23.md)) -- [New feature] Added a check for duplicate CURIEs [#342](https://github.com/TranslatorSRI/Babel/pull/342). +## Babel updates (from [Babel 2025jan23](https://github.com/NCATSTranslator/Babel/blob/main/releases/2025jan23.md)) +- [New feature] Added a check for duplicate CURIEs [#342](https://github.com/NCATSTranslator/Babel/pull/342). - [New feature] Added some additional manual concords for Disease/Phenotype cliques and DrugChemical - conflation [#360](https://github.com/TranslatorSRI/Babel/pull/360). -- [New feature] Replace use of `has_tradename` with `tradename_of` in RxNorm ([#377](https://github.com/TranslatorSRI/Babel/pull/377)). -- [New feature] Added processes from UMLS ([#395](https://github.com/TranslatorSRI/Babel/pull/395)). -- [New feature] Improved EFO relationships ([#396](https://github.com/TranslatorSRI/Babel/pull/396)). + conflation [#360](https://github.com/NCATSTranslator/Babel/pull/360). +- [New feature] Replace use of `has_tradename` with `tradename_of` in RxNorm ([#377](https://github.com/NCATSTranslator/Babel/pull/377)). +- [New feature] Added processes from UMLS ([#395](https://github.com/NCATSTranslator/Babel/pull/395)). +- [New feature] Improved EFO relationships ([#396](https://github.com/NCATSTranslator/Babel/pull/396)). - [Updates] Various updates - [Bugfix] Fixed a bug in choosing the best label shorter than a particular size in src/babel_utils.py:write_compendium() - [Bugfix] Cleaned up src/createcompendia/chemicals.py:parse_smifile() so that includes the ChEMBL ID and calculates the column index by name, with ValueErrors thrown if the column name is missing. @@ -30,10 +30,10 @@ Previous release: [Translator "Hammerhead" November 2024](./TranslatorHammerhead - [Bugfix] Other minor fixes. ## Releases since [Translator "Hammerhead" November 2024](./TranslatorHammerheadNovember2024.md) -* [1.4.7](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.7) +* [1.4.7](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.7) * Replaces /reverse_lookup with a more sensible name by @gaurav in #168 * Update OTEL to gRPC by @gaurav in #171 * Fix reverse CURIE endpoint by @gaurav in #172 * Add a bulk endpoint for NameRes by @gaurav in #165 -* [1.4.6](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.6) - * Bugfix: queries ending with whitespace with autocomplete=true searched for everything in [#173](https://github.com/TranslatorSRI/NameResolution/pull/173) +* [1.4.6](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.6) + * Bugfix: queries ending with whitespace with autocomplete=true searched for everything in [#173](https://github.com/NCATSTranslator/NameResolution/pull/173) diff --git a/releases/v1.5.1.md b/releases/v1.5.1.md index f9924fb6..a193f959 100644 --- a/releases/v1.5.1.md +++ b/releases/v1.5.1.md @@ -1,7 +1,7 @@ # NameRes v1.5.1 Release - Babel: [2025sep1](https://stars.renci.org/var/babel_outputs/2025sep1/) - ([Babel 2025sep1](https://github.com/TranslatorSRI/Babel/blob/master/releases/2025sep1.md)) -- NameRes: [v1.5.1](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.5.1) + ([Babel 2025sep1](https://github.com/NCATSTranslator/Babel/blob/main/releases/2025sep1.md)) +- NameRes: [v1.5.1](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.5.1) Next release: _None as yet_ Previous release: [NameRes v1.4.7](./v1.4.7.md) @@ -9,44 +9,44 @@ Previous release: [NameRes v1.4.7](./v1.4.7.md) ## Bugfixes - [MAJOR] Entries with clique_identifier_count=1 were being filtered out of search results because we were boosting with the log of clique_identifier_count, and log(1) = 0. This has now been fixed by using the log of - (clique_identifier_count + 1). ([PR #179](https://github.com/TranslatorSRI/NameResolution/pull/179)) + (clique_identifier_count + 1). ([PR #179](https://github.com/NCATSTranslator/NameResolution/pull/179)) - Fixed bug in NameRes Loading Docker image generation (d3f607af95e3fb254298d7f0a637a63ccb63fd6b). ## New Features - [MAJOR] This NameRes has GeneProtein conflation turned on. If you need to know the protein and gene identifiers for a particular NameRes result, you can normalize the identifier with the corresponding NodeNorm instance with `individual_types` set to true ([example](https://nodenormalization-sri.renci.org/1.5/get_normalized_nodes?curie=NCBIGene%3A1756&conflate=true&drug_chemical_conflate=false&description=false&individual_types=true)). -- Added Babel version and version URL to the `/status` endpoint ([PR #180](https://github.com/TranslatorSRI/NameResolution/pull/180)). -- Added instructions and script for running NameRes locally ([PR #186](https://github.com/TranslatorSRI/NameResolution/pull/186)). +- Added Babel version and version URL to the `/status` endpoint ([PR #180](https://github.com/NCATSTranslator/NameResolution/pull/180)). +- Added instructions and script for running NameRes locally ([PR #186](https://github.com/NCATSTranslator/NameResolution/pull/186)). ## Improvements -- [MAJOR] Update exact boosts ([PR #183](https://github.com/TranslatorSRI/NameResolution/pull/183)). +- [MAJOR] Update exact boosts ([PR #183](https://github.com/NCATSTranslator/NameResolution/pull/183)). - Exactish (case-insensitive exact) matches are now boosted by ~100x non-exactish matches. - Increased the basic boost by ~10x so that they override low clique_identifier_count matches. -- Updated curl to use -T instead of -d to upload file in data-loading ([PR #183](https://github.com/TranslatorSRI/NameResolution/pull/197)). -- Fix start-end quotes bug (i.e. "smart" quotes) ([PR #179](https://github.com/TranslatorSRI/NameResolution/pull/179)). +- Updated curl to use -T instead of -d to upload file in data-loading ([PR #183](https://github.com/NCATSTranslator/NameResolution/pull/197)). +- Fix start-end quotes bug (i.e. "smart" quotes) ([PR #179](https://github.com/NCATSTranslator/NameResolution/pull/179)). -## Babel updates (from [Babel 2025sep1](https://github.com/TranslatorSRI/Babel/blob/master/releases/2025sep1.md)) +## Babel updates (from [Babel 2025sep1](https://github.com/NCATSTranslator/Babel/blob/main/releases/2025sep1.md)) - [MAJOR] Information content values were incorrectly calculated as `100` for some cliques when they were in fact lower. - [MAJOR] UMLS proteins have now been removed from chemicals so that they only appear in proteins. - [MAJOR] Improved DrugChemical conflation order. - [MAJOR] Added secondary identifiers for ChEBI. ## Releases since [NameRes v1.4.7](./v1.4.7.md) -- [NameRes v1.4.8](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.8) +- [NameRes v1.4.8](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.8) - Added NameRes release information for November 2024 and January 2025 by @gaurav in #177 -- [NameRes v1.4.9](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.9) +- [NameRes v1.4.9](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.9) - Incremented version number (forgot to do that in NameRes v1.4.8). -- [NameRes v1.4.10](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.10) +- [NameRes v1.4.10](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.10) - Added Babel version and Babel version URL by @gaurav in #180 -- [NameRes v1.4.11](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.11) +- [NameRes v1.4.11](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.11) - Add instructions and script for running NameRes locally by @gaurav in #186 -- [NameRes v1.4.12](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.4.12) +- [NameRes v1.4.12](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.4.12) - Replace renci-python-image with GitHub Packages (d3f607a). -- [NameRes v1.5.0](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.5.0) +- [NameRes v1.5.0](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.5.0) - [MAJOR] Entries with clique_identifier_count=1 were being filtered out of search results because we were boosting with the log of clique_identifier_count, and log(1) = 0. This has now been fixed by using the log of (clique_identifier_count + 1) in #179. - [MAJOR] Tweaked exact boosts by @gaurav in #183 - Updated curl to use -T instead of -d to upload file in data-loading by @gaurav in #197 - Fix start-end quotes bug and stopped filtering out clique_identifier_count=1 records by @gaurav in #179 -- [NameRes v1.5.1](https://github.com/TranslatorSRI/NameResolution/releases/tag/v1.5.1) +- [NameRes v1.5.1](https://github.com/NCATSTranslator/NameResolution/releases/tag/v1.5.1) - Split GeneProteinConflated.txt instead of Gene.txt and Protein.txt by @gaurav in #203 diff --git a/solr-restore/README.md b/solr-restore/README.md deleted file mode 100644 index 9da72dfd..00000000 --- a/solr-restore/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# solr-restore - -This directory contains a script that can be used to restore a local Apache Solr backup to a Solr database -in Docker along with the indexes needed to query them from NameRes. It assumes that the backup is present -on the Solr server in the Solr data directory (by default `./data/solr`) and is -named `snapshot.backup.tar.gz`. If you follow the instructions in [the main README file](../README.md), -this script will be used automatically. - -It is essentially the same script as is included in -[the name-lookup Helm chart](https://github.com/helxplatform/translator-devops/tree/develop/helm/name-lookup) -of the `translator-devops` repository, but with some modifications allowing the script to be used -locally. diff --git a/solr-restore/restore.sh b/solr-restore/restore.sh deleted file mode 100644 index 4bc6133c..00000000 --- a/solr-restore/restore.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# -# restore.sh -# -# Restores a Solr backup located in the Solr data directory (`$SOLR_DATA/var/solr/data/snapshot.backup`). -# -# To do this, it must: -# - Initiate the restore. -# - Wait until the restore has completed. -# - Create the necessary fields (hopefully we can make this unnecessary, see https://github.com/TranslatorSRI/NameResolution/issues/185) -# -# This script should only require the `wget` program. -# -# TODO: This script does not currently implement any Blocklists. - -# We don't use set -e because the loop test relies on failures being ignored. -set -uo pipefail - -# Configuration options -SOLR_SERVER="http://localhost:8983" -SLEEP_INTERVAL=60 - -# Please don't change these values unless you change NameRes appropriately! -COLLECTION_NAME="name_lookup" -BACKUP_NAME="backup" - -# Step 0. Make sure the Solr data directory looks like it contains the uncompressed backup. -if [ ! -d "./data/solr/var" ]; then - echo 'WARNING: No ./data/solr/var directory found; are you sure you uncompressed the NameRes backup into the Solr data directory?' >&2 -fi - -# Step 1. Make sure the Solr service is up and running. -HEALTH_ENDPOINT="${SOLR_SERVER}/solr/admin/cores?action=STATUS" -response=$(wget --spider --server-response ${HEALTH_ENDPOINT} 2>&1 | grep "HTTP/" | awk '{ print $2 }') >&2 -until [ "$response" = "200" ]; do - response=$(wget --spider --server-response ${HEALTH_ENDPOINT} 2>&1 | grep "HTTP/" | awk '{ print $2 }') >&2 - echo " -- SOLR is unavailable - sleeping" - sleep 3 -done -echo "SOLR is up and running at ${SOLR_SERVER}." - -# Step 2. Create fields for search. -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/../data-loading/setup_solr.sh" -echo Solr database has been set up. - -# Step 3. Restore the data -CORE_NAME="${COLLECTION_NAME}_shard1_replica_n1" -echo "Starting Solr restore on core ${CORE_NAME}, with status at ${SOLR_SERVER}/solr/${CORE_NAME}/replication?command=restorestatus" -RESTORE_URL="${SOLR_SERVER}/solr/${CORE_NAME}/replication?command=restore&location=/var/solr/data/var/solr/data/&name=${BACKUP_NAME}" -wget -O - "$RESTORE_URL" -sleep "$SLEEP_INTERVAL" -RESTORE_STATUS_URL="${SOLR_SERVER}/solr/${CORE_NAME}/replication?command=restorestatus" -RESTORE_STATUS=$(wget -q -O - "$RESTORE_STATUS_URL" 2>&1 | grep "success") -RESTORE_STATUS="" -until [ -n "$RESTORE_STATUS" ] ; do - echo "Solr restore in progress. If this takes longer than 30 minutes, please visit ${SOLR_SERVER} with your browser to check Solr." - RESTORE_STATUS=$(wget -q -O - "$RESTORE_STATUS_URL" 2>&1 | grep "success") - sleep "$SLEEP_INTERVAL" -done -echo "Solr restore complete!" - -echo "Solr contents:" -curl -s --negotiate -u: "$SOLR_SERVER/solr/name_lookup/query?q=*:*&rows=0" diff --git a/tests/create_test_synonyms.py b/tests/create_test_synonyms.py index d8a6c3f2..8783117b 100644 --- a/tests/create_test_synonyms.py +++ b/tests/create_test_synonyms.py @@ -13,7 +13,12 @@ smallsyns = list(filter( lambda x: x['curie'] in allcuries , syns )) +# Write as JSON-lines (one JSON document per line), matching the format of the +# Babel synonym files that are loaded in production. The loader counts lines to +# verify every document made it into Solr, so the format must match. with open('tests/data/test-synonyms.json','w') as outf: - json.dump(smallsyns,outf,indent=1) + for syn in smallsyns: + outf.write(json.dumps(syn)) + outf.write('\n') diff --git a/tests/data/test-synonyms.json b/tests/data/test-synonyms.json index bc7046a2..3566fe9b 100644 --- a/tests/data/test-synonyms.json +++ b/tests/data/test-synonyms.json @@ -1,2083 +1,89 @@ -[ - { - "curie": "CHEBI:48407", - "names": [ - "antiparkinson agent" - ], - "preferred_name": "antiparkinson agent", - "types": [ - "NamedThing" - ], - "shortest_name_length": 19, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "CHEBI:74925", - "names": [ - "BACE1 inhibitor", - "BACE1 inhibitors", - "memapsin 2 inhibitor", - "memapsin 2 inhibitors", - "EC 3.4.23.46 inhibitor", - "EC 3.4.23.46 inhibitors", - "beta-secretase inhibitor", - "beta-secretase inhibitors", - "gamma-secretase inhibitor", - "Gamma-secretase inhibitor", - "gamma-secretase inhibitors", - "Gamma-secretase inhibitors", - "memapsin 2 (EC 3.4.23.46) inhibitor", - "EC 3.4.23.46 (memapsin 2) inhibitors", - "memapsin 2 (EC 3.4.23.46) inhibitors", - "beta-site APP-cleaving enzyme 1 inhibitor", - "beta-site APP-cleaving enzyme 1 inhibitors", - "membrane-associated aspartic protease 2 inhibitor", - "membrane-associated aspartic protease 2 inhibitors", - "beta-site Alzheimer's amyloid precursor protein cleaving enzyme 1 inhibitor", - "beta-site Alzheimer's amyloid precursor protein cleaving enzyme 1 inhibitors", - "beta-site Alzheimer's amyloid precursor protein cleaving enzyme 1 (BACE1) inhibitor", - "beta-site Alzheimer's amyloid precursor protein cleaving enzyme 1 (BACE1) inhibitors" - ], - "preferred_name": "BACE1 inhibitor", - "types": [ - "NamedThing" - ], - "shortest_name_length": 15, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "HP:0001300", - "names": [ - "Parkinsonian disease" - ], - "preferred_name": "parkinsonian disorder", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 20, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "HP:0002322", - "names": [ - "Rest tremor", - "Resting tremor", - "Tremor at rest", - "Parkinsonian tremor" - ], - "preferred_name": "Resting tremor", - "types": [ - "PhenotypicFeature", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 11, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "HP:0002511", - "names": [ - "Alzheimer disease", - "Late-onset form of familial Alzheimer disease" - ], - "preferred_name": "Alzheimer disease", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 17, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "HP:0002548", - "names": [ - "Favorable response to levodopa", - "Parkinsonism with favourable response to dopaminergic medication" - ], - "preferred_name": "Parkinsonism with favorable response to dopaminergic medication", - "types": [ - "PhenotypicFeature", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 30, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0000828", - "names": [ - "juvenile-onset Parkinson's disease" - ], - "preferred_name": "juvenile-onset Parkinson disease", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 34, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0001945", - "names": [ - "postencephalitic parkinsonism", - "postencephalitic Parkinsonism" - ], - "preferred_name": "postencephalitic Parkinson disease", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 29, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0004975", - "names": [ - "AD", - "Alzheimer disease", - "Alzheimer dementia", - "Alzheimers disease", - "Alzheimers dementia", - "Alzheimer's disease", - "Alzheimer's dementia", - "Alzheimer disease, familial" - ], - "preferred_name": "Alzheimer disease", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 2, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0005180", - "names": [ - "paralysis agitans", - "Parkinson's disease" - ], - "preferred_name": "Parkinson disease", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 17, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0006966", - "names": [ - "secondary Parkinsonism", - "symptomatic parkinsonism", - "secondary parkinsonism, unspecified", - "secondary parkinsonism (disorder) [ambiguous]", - "disorder presenting primarily with parkinsonism" - ], - "preferred_name": "secondary Parkinson disease", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 22, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0007088", - "names": [ - "AD1", - "Alzheimer disease 1", - "Alzheimer disease; AD", - "presenile and senile dementia", - "Alzheimer disease, familial, 1", - "Alzheimer disease, protection against", - "early-onset familial form of Alzheimer disease", - "Alzheimer disease, early-onset, with cerebral amyloid angiopathy" - ], - "preferred_name": "Alzheimer disease type 1", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0007089", - "names": [ - "AD2", - "LOFAD", - "Alzheimer disease 2", - "Alzheimer disease-2", - "Alzheimer's disease 2", - "Alzheimer disease type 2", - "Alzheimer disease 2; AD2", - "Alzheimer's disease type 2", - "late onset Alzheimer disease", - "Alzheimer disease 2, late onset", - "Alzheimer disease 2, late-onset", - "late onset familial Alzheimer disease", - "Alzheimer disease associated with APOE4", - "Alzheimer disease associated with Apoe4", - "Alzheimer disease associated with APOE E4" - ], - "preferred_name": "Alzheimer disease 2", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0007104", - "names": [ - "PDALS", - "ALS-pDC", - "Guam disease", - "Lytico-Bodig disease", - "Lytigo-Bodig disease", - "Parkinsonism-dementia-ALS complex", - "amyotrophic lateral sclerosis-PARKINSONISM/dementia complex 1", - "amyotrophic lateral sclerosis-Parkinsonism/dementia Complex type 1", - "amyotrophic lateral sclerosis-Parkinsonism/dementia Complex of Guam", - "amyotrophic lateral sclerosis-parkinsonism-dementia of Guam syndrome", - "amyotrophic lateral sclerosis, Parkinsonism/dementia complex of Guam" - ], - "preferred_name": "amyotrophic lateral sclerosis-parkinsonism-dementia complex", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0007312", - "names": [ - "Charcot-Marie-Tooth disease with ptosis and parkinsonism" - ], - "preferred_name": "Charcot-Marie-Tooth disease with ptosis and parkinsonism", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 56, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0007433", - "names": [ - "dementia/parkinsonism with non-Alzheimer amyloid plaques" - ], - "preferred_name": "dementia/parkinsonism with non-Alzheimer amyloid plaques", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 56, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0007488", - "names": [ - "DLB", - "Lewy body disease", - "Lewy body dementia", - "dementia, Lewy body", - "dementia, Lewy body; DLB", - "dementia with Lewy bodies", - "diffuse Lewy body disease", - "Senile dementia of the Lewy body type", - "Lewy body variant of Alzheimer disease", - "diffuse Lewy body disease with gaze palsy" - ], - "preferred_name": "Lewy body dementia", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0007495", - "names": [ - "DRD", - "DYT5a", - "DYT-GCH1", - "dystonia 5", - "dystonia type 5", - "GTPCH1-deficient DRD", - "dystonia, DOPA-responsive", - "dystonia, DOPA-responsive; DRD", - "autosomal dominant Segawa syndrome", - "HPD with marked diurnal fluctuation", - "Segawa syndrome, autosomal dominant", - "GTPCH1-deficient dopa-responsive dystonia", - "autosomal dominant dopa-responsive dystonia", - "Dopa-responsive dystonia, autosomal dominant", - "Dopa-responsive dystonia; Segawa syndrome AD", - "dystonia, Dopa-responsive, autosomal dominant", - "dystonia, progressive, with diurnal variation", - "dystonia-Parkinsonism with diurnal fluctuation", - "GTP cyclohydrolase 1-deficient dopa-responsive dystonia", - "hereditary progressive dystonia with marked diurnal fluctuation" - ], - "preferred_name": "dystonia 5", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0007496", - "names": [ - "RDP", - "DYT12", - "DYT-ATP1A3", - "dystonia 12", - "dystonia type 12", - "dystonia 12; DYT12", - "ATP1A3 dystonic disorder", - "rapid-onset dystonia-parkinsonism", - "dystonia-Parkinsonism, rapid-onset", - "dystonic disorder caused by mutation in ATP1A3" - ], - "preferred_name": "dystonia 12", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0008193", - "names": [ - "paralysis agitans, juvenile, of Hunt", - "Parkinson disease, juvenile, of Hunt" - ], - "preferred_name": "paralysis agitans, juvenile, of Hunt", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 36, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0008199", - "names": [ - "PD", - "park", - "Parkinson disease, late-onset", - "Parkinson disease, late-onset; PD" - ], - "preferred_name": "late-onset Parkinson disease", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 2, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0008200", - "names": [ - "PARK1", - "atypical Parkinson disease", - "Parkinson disease 1, autosomal dominant", - "autosomal dominant Parkinson's disease 1", - "autosomal dominant Parkinson disease type 1", - "Parkinson disease 1, autosomal dominant; PARK1", - "Parkinson disease 1, autosomal dominant Lewy body" - ], - "preferred_name": "autosomal dominant Parkinson disease 1", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0008201", - "names": [ - "Perry syndrome", - "parkinsonism with alveolar hypoventilation and mental depression", - "Parkinsonism with alveolar hypoventilation and mental depression" - ], - "preferred_name": "Perry syndrome", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 14, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0008685", - "names": [ - "WPW", - "Wpw syndrome", - "preexcitation syndrome", - "anomalous A-V excitation", - "Wolff-Parkinson-White pattern", - "Wolff-Parkinson-White syndrome", - "WOLFF-Parkinson-WHITE syndrome", - "accessory atrioventricular pathways", - "anomalous atrioventricular excitation", - "Wolff-Parkinson-White pattern (finding)" - ], - "preferred_name": "Wolff-Parkinson-white syndrome", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0009497", - "names": [ - "Vitsala", - "Kifafa seizure disorder", - "Complex familial seizure disorder", - "parkinsonian features and neurologic abnormalities, mental retardation and transient psychotic episodes" - ], - "preferred_name": "Kifafa seizure disorder", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 7, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0009830", - "names": [ - "PARK15", - "pallidopyramidal syndrome", - "Pallidopyramidal syndrome", - "pallido-pyramidal disease", - "pallido-pyramidal syndrome", - "parkinsonian-pyramidal syndrome", - "autosomal recessive early-onset Parkinson disease 15", - "Parkinson disease 15, autosomal recessive early-onset", - "autosomal recessive early-onset Parkinson's disease 15", - "autosomal recessive early-onset Parkinson disease type 15", - "Parkinson disease 15, autosomal recessive early-onset; PARK15" - ], - "preferred_name": "parkinsonian-pyramidal syndrome", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0009839", - "names": [ - "PSP-p", - "atypical PSP", - "PSP-parkinsonism", - "Parkinson-dementia syndrome", - "progressive supranuclear palsy atypical", - "supranuclear palsy, progressive, 1, atypical", - "Steele-Richardson-Olszewski syndrome, atypical" - ], - "preferred_name": "progressive supranuclear palsy-parkinsonism syndrome", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0010360", - "names": [ - "PARK12", - "Parkinson disease 12", - "Parkinson disease, X-linked", - "Parkinson disease 12; PARK12" - ], - "preferred_name": "parkinson disease 12", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0010422", - "names": [ - "AD16", - "Alzheimer disease 16", - "Alzheimer's disease 16", - "Alzheimer disease 16; AD16", - "Alzheimer's disease type 16" - ], - "preferred_name": "Alzheimer disease 16", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0010482", - "names": [ - "XPDS", - "PARKINSONISM with spasticity, X-linked", - "PARKINSONISM with spasticity, X-linked; XPDS" - ], - "preferred_name": "X-linked parkinsonism-spasticity syndrome", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0010709", - "names": [ - "Wsn", - "BGMR", - "WSMN", - "Waisman syndrome", - "WAISMAN syndrome", - "Laxova-Opitz syndrome", - "WAISMAN syndrome; WSMN", - "Laxova Brown hogan syndrome", - "basal ganglia disorder with mental retardation", - "basal ganglion disorder with mental retardation", - "Parkinsonism, early onset with mental retardation", - "Parkinsonism, early-onset, with mental retardation", - "X-linked recessive basal ganglia disorder with mental retardation" - ], - "preferred_name": "early-onset parkinsonism-intellectual disability syndrome", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0010747", - "names": [ - "XDP", - "DYT3", - "Lubag", - "DYT-TAF1", - "Lubag syndrome", - "dystonia 3, torsion, X-linked", - "X-linked dystonia Parkinsonism", - "dystonia-Parkinsonism, X-linked", - "dystonia 3, torsion, X-linked; DYT3", - "X-linked dystonia-parkinsonism/Lubag", - "X-linked dystonia-Parkinsonism syndrome", - "torsion dystonia-Parkinsonism, Filipino type", - "X-linked torsion dystonia-Parkinsonism syndrome" - ], - "preferred_name": "X-linked dystonia-parkinsonism", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0010783", - "names": [ - "Alzheimer disease, susceptibility to, mitochondrial" - ], - "preferred_name": "Alzheimer disease, susceptibility to, mitochondrial", - "types": [ - "NamedThing" - ], - "shortest_name_length": 51, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0010796", - "names": [ - "Parkinson disease, mitochondrial" - ], - "preferred_name": "Parkinson disease, mitochondrial", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 32, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0010820", - "names": [ - "JP", - "PDJ", - "PARK2", - "Parkinson disease 2", - "juvenile parkinsonism", - "PRKN young-onset Parkinson disease", - "autosomal recessive juvenile Parkinson disease", - "Parkinson disease, juvenile, autosomal recessive", - "Parkinson disease 2, autosomal recessive juvenile", - "autosomal recessive juvenile Parkinson's disease 2", - "Parkinson disease autosomal recessive, early onset", - "Parkinsonism, early onset, with diurnal fluctuation", - "Parkinsonism, early-onset, with diurnal fluctuation", - "autosomal recessive juvenile Parkinson disease type 2", - "young-onset Parkinson disease caused by mutation in PRKN", - "Parkinson disease 2, autosomal recessive juvenile; PARK2" - ], - "preferred_name": "autosomal recessive juvenile Parkinson disease 2", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 2, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0010857", - "names": [ - "FTD", - "Ftdp17", - "Pick Complex", - "semantic variant PPA", - "frontotemporal dementia", - "Ftld with Tau inclusions", - "Wilhelmsen-Lynch disease", - "frontotemporal dementia; FTD", - "frontotemporal lobe dementia", - "Pallidopontonigral Degeneration", - "semantic primary progressive aphasia", - "frontotemporal dementia with Parkinsonism", - "dementia, frontotemporal, with Parkinsonism", - "multiple system tauopathy with presenile dementia", - "frontotemporal lobar Degeneration with Tau inclusions", - "disinhibition-dementia-Parkinsonism-amyotrophy Complex" - ], - "preferred_name": "semantic dementia", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "NCBIGene:378899", - "preferred_name": "FTL", - "names": [ - "FTL", - "FTD", - "ferritoid", - "ferritin light chain", - "ferritin, light polypeptide", - "Q8AYG9_CHICK Ferritin (trembl)" - ], - "taxa": [ - "NCBITaxon:9031" - ], - "taxon_specific": true, - "types": [ - "Gene", - "GeneOrGeneProduct", - "GenomicEntity", - "ChemicalEntityOrGeneOrGeneProduct", - "PhysicalEssence", - "OntologyClass", - "BiologicalEntity", - "ThingWithTaxon", - "NamedThing", - "Entity", - "PhysicalEssenceOrOccurrent", - "MacromolecularMachineMixin", - "Protein", - "GeneProductMixin", - "Polypeptide", - "ChemicalEntityOrProteinOrPolypeptide" - ], - "shortest_name_length": 3, - "clique_identifier_count": 3 - }, - { - "curie": "MONDO:0011194", - "names": [ - "AD5", - "Ad5", - "Alzheimer disease 5", - "Alzheimer's disease 5", - "Alzheimer disease type 5", - "Alzheimer's disease type 5", - "Alzheimer disease, familial 5", - "Alzheimer disease, familial, 5" - ], - "preferred_name": "Alzheimer disease 5", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011220", - "names": [ - "PARK3", - "Parkinson disease type 3", - "autosomal dominant Parkinson disease", - "Parkinson disease 3, autosomal dominant", - "Parkinson disease 3, autosomal dominant; PARK3", - "Parkinson disease 3, autosomal dominant Lewy body" - ], - "preferred_name": "parkinson disease 3, autosomal dominant", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011401", - "names": [ - "Alzheimer disease without neurofibrillary tangles", - "Alzheimer's disease without neurofibrillary tangles" - ], - "preferred_name": "Alzheimer disease without neurofibrillary tangles", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 49, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011513", - "names": [ - "Alzheimer disease, familial early-onset, with Coexisting amyloid and prion pathology" - ], - "preferred_name": "Alzheimer disease, familial early-onset, with coexisting amyloid and prion pathology", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 84, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011551", - "names": [ - "DYT5b", - "autosomal recessive Segawa syndrome", - "Segawa syndrome, autosomal recessive", - "autosomal recessive dopa-responsive dystonia", - "Parkinsonism, infantile, autosomal recessive", - "dopa-responsive dystonia, autosomal recessive", - "Dopa-responsive dystonia, autosomal recessive", - "Dopa-responsive dystonia, autosomal recessive", - "DOPA responsive dystonia, autosomal recessive", - "dystonia, Dopa-responsive, autosomal recessive", - "dystonia, DOPA responsive, autosomal recessive", - "tyrosine hydroxylase-deficient dopa-responsive dystonia" - ], - "preferred_name": "TH-deficient dopa-responsive dystonia", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011561", - "names": [ - "AD6", - "AD6", - "Alzheimer disease 6", - "Alzheimer's disease 6", - "Alzheimer disease type 6", - "Alzheimer's disease type 6", - "Alzheimer disease 6, late onset", - "Alzheimer disease 6, late-onset", - "plasma Beta-amyloid-42 level quantitative trait locus" - ], - "preferred_name": "Alzheimer disease 6", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011562", - "names": [ - "PARK4", - "Parkinson disease 4, autosomal dominant", - "autosomal dominant Parkinson's disease 4", - "autosomal dominant Parkinson disease type 4", - "Parkinson disease 4, autosomal dominant; PARK4", - "autosomal dominant Lewy body Parkinson disease 4", - "Parkinson disease 4, autosomal dominant Lewy body" - ], - "preferred_name": "autosomal dominant Parkinson disease 4", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011613", - "names": [ - "PARK6", - "PINK1 Parkinson disease", - "early-onset Parkinson disease 6", - "Parkinson disease 6, early-onset", - "Parkinson disease caused by mutation in PINK1", - "Parkinson disease 6, late-onset, susceptibility to", - "Parkinson disease 6, autosomal recessive early-onset", - "autosomal recessive early-onset Parkinson's disease 6", - "autosomal recessive early-onset Parkinson disease type 6", - "Parkinson disease 6, autosomal recessive early-onset; PARK6", - "Parkinson disease, autosomal recessive early-onset, digenic, Pink1/Dj1" - ], - "preferred_name": "autosomal recessive early-onset Parkinson disease 6", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011647", - "names": [ - "AD7", - "Ad7", - "Alzheimer disease 7", - "Alzheimer's disease 7", - "Alzheimer disease type 7", - "Alzheimer's disease type 7", - "Alzheimer disease, familial 7", - "Alzheimer disease, familial, 7" - ], - "preferred_name": "Alzheimer disease 7", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011658", - "names": [ - "PARK7", - "PARK7 Parkinson disease", - "Parkinson disease caused by mutation in PARK7", - "Parkinson disease 7, autosomal recessive early-onset", - "autosomal recessive early-onset Parkinson's disease 7", - "autosomal recessive early-onset Parkinson disease type 7", - "Parkinson disease 7, autosomal recessive early-onset; PARK7" - ], - "preferred_name": "autosomal recessive early-onset Parkinson disease 7", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011706", - "names": [ - "KRS", - "PARK9", - "KRPPD", - "park 9", - "Kufor-Rakeb syndrome", - "Parkinson disease type 9", - "Kufor-Rakeb syndrome; KRS", - "ceroid lipofuscinosis, neuronal, 12", - "autosomal recessive Parkinson disease 9", - "Parkinson disease 9, autosomal recessive", - "autosomal recessive juvenile onset Parkinson disease 9", - "Parkinson disease 9, autosomal recessive, juvenile-onset", - "Pallidopyramidal Degeneration with supranuclear upgaze paresis and dementia", - "Pallidopyramidal degeneration with supranuclear upgaze paresis, and dementia" - ], - "preferred_name": "Kufor-Rakeb syndrome", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011737", - "names": [ - "PARK10", - "Parkinson disease 10", - "Parkinson disease 10; PARK10", - "Parkinson disease, Age at onset of" - ], - "preferred_name": "parkinson disease 10", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011743", - "names": [ - "AD4", - "Ad4", - "Alzheimer disease 4", - "Alzheimer's disease 4", - "Alzheimer disease type 4", - "Alzheimer's disease type 4", - "Alzheimer disease, familial4", - "Alzheimer disease, familial, 4", - "Alzheimer disease familial type 4", - "familial Alzheimer disease, type 4", - "familial Alzheimer's disease, type 4" - ], - "preferred_name": "Alzheimer disease 4", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011764", - "names": [ - "PARK8", - "LRRK2 Parkinson disease", - "Parkinson disease 8, autosomal dominant", - "autosomal dominant Parkinson's disease 8", - "autosomal dominant Parkinson disease type 8", - "Parkinson disease caused by mutation in LRRK2", - "Parkinson disease 8, autosomal dominant; PARK8" - ], - "preferred_name": "autosomal dominant Parkinson disease 8", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011777", - "names": [ - "AD8", - "Ad8", - "Alzheimer disease 8", - "Alzheimer's disease 8", - "Alzheimer disease type 8", - "Alzheimer's disease type 8", - "Alzheimer disease, familial 8", - "Alzheimer disease, familial, 8" - ], - "preferred_name": "Alzheimer disease 8", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011896", - "names": [ - "PARK11", - "GIGYF2 hereditary late onset Parkinson disease", - "susceptibility to autosomal dominant Parkinson disease 11", - "Parkinson disease 11, autosomal dominant, susceptibility to", - "Parkinson disease 11, autosomal dominant, susceptibility to; PARK11", - "hereditary late onset Parkinson disease caused by mutation in GIGYF2" - ], - "preferred_name": "Parkinson disease 11, autosomal dominant, susceptibility to", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0011913", - "names": [ - "AD3", - "Alzheimer disease 3", - "Alzheimer's disease 3", - "Alzheimer disease 3; AD", - "Alzheimer disease type 3", - "Alzheimer's disease type 3", - "Alzheimer disease familial 3", - "Alzheimer disease, familial, 3", - "Alzheimer disease 3, early onset", - "Alzheimer disease 3, early-onset", - "familial Alzheimer disease, type 3", - "familial Alzheimer's disease, type 3", - "Alzheimer disease early onset type 3", - "PSEN1 early-onset autosomal dominant Alzheimer disease", - "Alzheimer disease, familial, 3, with spastic paraparesis and apraxia", - "early-onset autosomal dominant Alzheimer disease caused by mutation in PSEN1", - "Alzheimer disease, familial, 3, with spastic paraparesis and unusual plaques" - ], - "preferred_name": "Alzheimer disease 3", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0012153", - "names": [ - "AD9", - "Alzheimer disease 9", - "Alzheimer disease 9; AD9", - "Alzheimer disease 9, late-onset", - "Alzheimer disease 9, susceptibility to", - "Alzheimer disease 9, susceptibility to; AD9" - ], - "preferred_name": "Alzheimer disease 9", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0012321", - "names": [ - "AD10", - "Ad10", - "Alzheimer disease 10", - "Alzheimer's disease 10", - "Alzheimer disease type 10", - "Alzheimer's disease type 10", - "Alzheimer disease familial 10", - "Alzheimer disease, familial, 10" - ], - "preferred_name": "Alzheimer disease 10", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0012344", - "names": [ - "AD11", - "Ad11", - "Alzheimer disease 11", - "Alzheimer's disease 11", - "Alzheimer disease type 11", - "Alzheimer's disease type 11", - "Alzheimer disease, familial, 11" - ], - "preferred_name": "Alzheimer disease 11", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0012466", - "names": [ - "PARK13", - "HTRA2 young-onset Parkinson disease", - "young-onset Parkinson disease caused by mutation in HTRA2", - "susceptibility to autosomal dominant Parkinson disease 13", - "Parkinson disease 13, autosomal dominant, susceptibility to", - "Parkinson disease 13, autosomal dominant, susceptibility to; PARK13" - ], - "preferred_name": "Parkinson disease 13, autosomal dominant, susceptibility to", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0012609", - "names": [ - "AD12", - "Ad12", - "Alzheimer disease 12", - "Alzheimer's disease 12", - "Alzheimer disease type 12", - "Alzheimer's disease type 12", - "Alzheimer disease familial 12", - "Alzheimer disease, familial, 12" - ], - "preferred_name": "Alzheimer disease 12", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0012630", - "names": [ - "AD13", - "Alzheimer disease 13", - "Alzheimer's disease 13", - "Alzheimer disease 13; AD13", - "Alzheimer's disease type 13" - ], - "preferred_name": "Alzheimer disease 13", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0012631", - "names": [ - "AD14", - "Alzheimer disease 14", - "Alzheimer's disease 14", - "Alzheimer disease 14; AD14", - "Alzheimer's disease type 14" - ], - "preferred_name": "Alzheimer disease 14", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0012632", - "names": [ - "AD15", - "Alzheimer disease 15", - "Alzheimer's disease 15", - "Alzheimer disease 15; AD15", - "Alzheimer's disease type 15" - ], - "preferred_name": "AD15", - "types": [ - "NamedThing" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0012789", - "names": [ - "DYT16", - "DYT-PRKRA", - "dystonia 16", - "dystonia type 16", - "dystonia 16; DYT16", - "PRKRA dystonic disorder", - "early-onset dystonia parkinsonism", - "Young-onset dystonia-(parkinsonism)", - "dystonic disorder caused by mutation in PRKRA" - ], - "preferred_name": "dystonia 16", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0013060", - "names": [ - "PARK14", - "dystonia-Parkinsonism Adult-onset", - "dystonia-Parkinsonism, adult-onset", - "adult-onset dystonia - parkinsonism", - "PLA2G6-related dystonia-parkinsonism", - "dystonia-parkinsonism, Paisan-Ruiz type", - "Parkinson disease 14, autosomal recessive", - "autosomal recessive Parkinson's disease 14", - "autosomal recessive Parkinson disease type 14", - "PLA2G6 hereditary late onset Parkinson disease", - "Parkinson disease 14, autosomal recessive; PARK14", - "hereditary late onset Parkinson disease caused by mutation in PLA2G6" - ], - "preferred_name": "autosomal recessive Parkinson disease 14", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0013150", - "names": [ - "IPD", - "PKDYS", - "infantile Parkinsonism-dystonia", - "Parkinsonism-dystonia infantile", - "PARKINSONISM-dystonia, infantile", - "PARKINSONISM-dystonia, infantile; PKDYS", - "dopamine transporter deficiency syndrome" - ], - "preferred_name": "parkinsonism-dystonia, infantile", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0013167", - "names": [ - "PARK16", - "Parkinson disease 16", - "Parkinson disease 16; PARK16" - ], - "preferred_name": "parkinson disease 16", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0013340", - "names": [ - "PARK5", - "UCHL1 young-onset Parkinson disease", - "susceptibility to autosomal dominant Parkinson disease 5", - "young-onset Parkinson disease caused by mutation in UCHL1", - "Parkinson disease 5, autosomal dominant, susceptibility to", - "Parkinson disease 5, autosomal dominant, susceptibility to; PARK5" - ], - "preferred_name": "Parkinson disease 5, autosomal dominant, susceptibility to", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0013625", - "names": [ - "PARK17", - "Parkinson disease 17", - "Parkinson's disease 17", - "VPS35 Parkinson disease", - "Parkinson disease type 17", - "Parkinson disease 17; PARK17", - "autosomal dominant Parkinson disease 17", - "Parkinson disease caused by mutation in VPS35" - ], - "preferred_name": "Parkinson disease 17", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0013653", - "names": [ - "PARK18", - "EIF4G1 hereditary late onset Parkinson disease", - "susceptibility to autosomal dominant Parkinson disease 18", - "Parkinson disease 18, autosomal dominant, susceptibility to", - "Parkinson disease 18, autosomal dominant, susceptibility to; PARK18", - "hereditary late onset Parkinson disease caused by mutation in EIF4G1" - ], - "preferred_name": "Parkinson disease 18, autosomal dominant, susceptibility to", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0014036", - "names": [ - "AD17", - "Alzheimer disease 17", - "Alzheimer's disease 17", - "Alzheimer disease 17; AD17", - "Alzheimer's disease type 17", - "Alzheimer disease 17, late onset", - "Alzheimer disease 17, late-onset" - ], - "preferred_name": "Alzheimer disease 17", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0014231", - "names": [ - "PARK19", - "PARK19A", - "Park19, formerly", - "DNAJC6 Parkinson disease", - "Parkinson disease 19B, early-onset", - "Parkinson disease 19, juvenile-onset", - "Parkinson disease 19A, juvenile-onset", - "juvenile onset Parkinson's disease 19A", - "juvenile onset Parkinson disease type 19A", - "Parkinson disease 19, juvenile-onset; PARK19", - "Parkinson disease caused by mutation in DNAJC6", - "Parkinson disease 19A, juvenile-onset; PARK19A" - ], - "preferred_name": "juvenile onset Parkinson disease 19A", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0014233", - "names": [ - "PARK20", - "SYNJ1 Parkinson disease", - "Parkinson disease 20, early-onset", - "early-onset Parkinson's disease 20", - "early-onset Parkinson disease type 20", - "Parkinson disease 20, early-onset; PARK20", - "Parkinson disease caused by mutation in SYNJ1" - ], - "preferred_name": "early-onset Parkinson disease 20", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0014265", - "names": [ - "AD18", - "Alzheimer disease 18", - "Alzheimer's disease 18", - "ADAM10 Alzheimer disease", - "Alzheimer disease type 18", - "Alzheimer disease 18; AD18", - "Alzheimer's disease type 18", - "Alzheimer disease 18, late-onset", - "Alzheimer disease caused by mutation in ADAM10" - ], - "preferred_name": "Alzheimer disease 18", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0014316", - "names": [ - "AD19", - "Alzheimer disease 19", - "Alzheimer's disease 19", - "PLD3 Alzheimer disease", - "Alzheimer disease type 19", - "Alzheimer disease 19; AD19", - "Alzheimer's disease type 19", - "Alzheimer disease 19 late onset", - "Alzheimer disease 19, late-onset", - "Alzheimer disease caused by mutation in PLD3" - ], - "preferred_name": "Alzheimer disease 19", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0014604", - "names": [ - "PARK21", - "Parkinson disease 21", - "Parkinson disease type 21", - "Parkinson disease 21; PARK21", - "DNAJC13 hereditary late onset Parkinson disease", - "hereditary late onset Parkinson disease caused by mutation in DNAJC13" - ], - "preferred_name": "Parkinson disease 21", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0014742", - "names": [ - "PARK22", - "CHCHD2 Parkinson disease", - "Parkinson disease 22, autosomal dominant", - "Parkinson disease caused by mutation in CHCHD2", - "Parkinson disease 22, autosomal dominant; PARK22" - ], - "preferred_name": "Parkinson disease 22, autosomal dominant", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0014796", - "names": [ - "PARK23", - "VPS13C young-onset Parkinson disease", - "autosomal recessive early-onset Parkinson disease 23", - "Parkinson disease 23, autosomal recessive early-onset", - "autosomal recessive early-onset Parkinson's disease 23", - "autosomal recessive early-onset Parksinson disease type 23", - "young-onset Parkinson disease caused by mutation in VPS13C", - "Parkinson disease 23, autosomal recessive early-onset; PARK23" - ], - "preferred_name": "autosomal recessive early-onset Parkinson disease 23", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0015140", - "names": [ - "EOFAD", - "early-onset, autosomal dominant Alzheimer disease", - "early-onset familial autosomal dominant Alzheimer disease" - ], - "preferred_name": "early-onset autosomal dominant Alzheimer disease", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0017279", - "names": [ - "YOPD", - "early-onset Parkinson disease", - "early-onset Parkinson's disease" - ], - "preferred_name": "young-onset Parkinson disease", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0017639", - "names": [ - "CO-induced parkinsonism" - ], - "preferred_name": "carbon monoxide-induced parkinsonism", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 23, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0018466", - "names": [ - "LOPD", - "late-onset Parkinson disease", - "late onset Parkinson disease", - "late onset Parkinson's disease", - "hereditary late-onset Parkinson disease", - "autosomal dominant late-onset Parkinson disease" - ], - "preferred_name": "late-onset Parkinson disease", - "types": [ - "NamedThing" - ], - "shortest_name_length": 4, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0018899", - "names": [ - "PCA", - "Benson syndrome", - "biparietal Alzheimer disease" - ], - "preferred_name": "posterior cortical atrophy", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0020352", - "names": [ - "MSA-p", - "MSA, parkinsonian type" - ], - "preferred_name": "multiple system atrophy, parkinsonian type", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0054835", - "names": [ - "PKDYS", - "PKDYS1", - "dopamine transporter deficiency syndrome", - "Parkinsonism-dystonia, infantile, 1; PKDYS1" - ], - "preferred_name": "parkinsonism-dystonia, infantile, 1", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 5, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0054836", - "names": [ - "PKDYS2", - "Parkinsonism-dystonia, infantile, 2; PKDYS2" - ], - "preferred_name": "parkinsonism-dystonia, infantile, 2", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 6, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0100065", - "names": [ - "tyrosine hydroxylase infantile parkinsonism and motor delay" - ], - "preferred_name": "TH-deficient infantile parkinsonism and motor delay", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 59, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0100087", - "names": [ - "FAD", - "GARD:0000632", - "Alzheimer disease, familial" - ], - "preferred_name": "familial Alzheimer disease", - "types": [ - "Disease", - "DiseaseOrPhenotypicFeature", - "ThingWithTaxon", - "BiologicalEntity", - "NamedThing", - "Entity" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "MONDO:0100088", - "names": [ - "Alzheimer disease type 2" - ], - "preferred_name": "Alzheimer disease type 2", - "types": [ - "NamedThing" - ], - "shortest_name_length": 24, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - }, - { - "curie": "www:bget?hsa:11315", - "names": [ - "DJ1", - "DJ-1", - "GATD2", - "HEL-S-67p; Parkinsonism associated deglycase", - "K05687 protein DJ-1 [EC:3.5.1.124] | (RefSeq) PARK7" - ], - "preferred_name": "DJ-1", - "types": [ - "NamedThing" - ], - "shortest_name_length": 3, - "clique_identifier_count": 1, - "taxa": [], "taxon_specific": false - } -] +{"curie": "CHEBI:48407", "names": ["antiparkinson agent"], "preferred_name": "antiparkinson agent", "types": ["NamedThing"], "shortest_name_length": 19, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "CHEBI:74925", "names": ["BACE1 inhibitor", "BACE1 inhibitors", "memapsin 2 inhibitor", "memapsin 2 inhibitors", "EC 3.4.23.46 inhibitor", "EC 3.4.23.46 inhibitors", "beta-secretase inhibitor", "beta-secretase inhibitors", "gamma-secretase inhibitor", "Gamma-secretase inhibitor", "gamma-secretase inhibitors", "Gamma-secretase inhibitors", "memapsin 2 (EC 3.4.23.46) inhibitor", "EC 3.4.23.46 (memapsin 2) inhibitors", "memapsin 2 (EC 3.4.23.46) inhibitors", "beta-site APP-cleaving enzyme 1 inhibitor", "beta-site APP-cleaving enzyme 1 inhibitors", "membrane-associated aspartic protease 2 inhibitor", "membrane-associated aspartic protease 2 inhibitors", "beta-site Alzheimer's amyloid precursor protein cleaving enzyme 1 inhibitor", "beta-site Alzheimer's amyloid precursor protein cleaving enzyme 1 inhibitors", "beta-site Alzheimer's amyloid precursor protein cleaving enzyme 1 (BACE1) inhibitor", "beta-site Alzheimer's amyloid precursor protein cleaving enzyme 1 (BACE1) inhibitors"], "preferred_name": "BACE1 inhibitor", "types": ["NamedThing"], "shortest_name_length": 15, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "HP:0001300", "names": ["Parkinsonian disease"], "preferred_name": "parkinsonian disorder", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 20, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "HP:0002322", "names": ["Rest tremor", "Resting tremor", "Tremor at rest", "Parkinsonian tremor"], "preferred_name": "Resting tremor", "types": ["PhenotypicFeature", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 11, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "HP:0002511", "names": ["Alzheimer disease", "Late-onset form of familial Alzheimer disease"], "preferred_name": "Alzheimer disease", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 17, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "HP:0002548", "names": ["Favorable response to levodopa", "Parkinsonism with favourable response to dopaminergic medication"], "preferred_name": "Parkinsonism with favorable response to dopaminergic medication", "types": ["PhenotypicFeature", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 30, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0000828", "names": ["juvenile-onset Parkinson's disease"], "preferred_name": "juvenile-onset Parkinson disease", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 34, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0001945", "names": ["postencephalitic parkinsonism", "postencephalitic Parkinsonism"], "preferred_name": "postencephalitic Parkinson disease", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 29, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0004975", "names": ["AD", "Alzheimer disease", "Alzheimer dementia", "Alzheimers disease", "Alzheimers dementia", "Alzheimer's disease", "Alzheimer's dementia", "Alzheimer disease, familial"], "preferred_name": "Alzheimer disease", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 2, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0005180", "names": ["paralysis agitans", "Parkinson's disease"], "preferred_name": "Parkinson disease", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 17, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0006966", "names": ["secondary Parkinsonism", "symptomatic parkinsonism", "secondary parkinsonism, unspecified", "secondary parkinsonism (disorder) [ambiguous]", "disorder presenting primarily with parkinsonism"], "preferred_name": "secondary Parkinson disease", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 22, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0007088", "names": ["AD1", "Alzheimer disease 1", "Alzheimer disease; AD", "presenile and senile dementia", "Alzheimer disease, familial, 1", "Alzheimer disease, protection against", "early-onset familial form of Alzheimer disease", "Alzheimer disease, early-onset, with cerebral amyloid angiopathy"], "preferred_name": "Alzheimer disease type 1", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0007089", "names": ["AD2", "LOFAD", "Alzheimer disease 2", "Alzheimer disease-2", "Alzheimer's disease 2", "Alzheimer disease type 2", "Alzheimer disease 2; AD2", "Alzheimer's disease type 2", "late onset Alzheimer disease", "Alzheimer disease 2, late onset", "Alzheimer disease 2, late-onset", "late onset familial Alzheimer disease", "Alzheimer disease associated with APOE4", "Alzheimer disease associated with Apoe4", "Alzheimer disease associated with APOE E4"], "preferred_name": "Alzheimer disease 2", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0007104", "names": ["PDALS", "ALS-pDC", "Guam disease", "Lytico-Bodig disease", "Lytigo-Bodig disease", "Parkinsonism-dementia-ALS complex", "amyotrophic lateral sclerosis-PARKINSONISM/dementia complex 1", "amyotrophic lateral sclerosis-Parkinsonism/dementia Complex type 1", "amyotrophic lateral sclerosis-Parkinsonism/dementia Complex of Guam", "amyotrophic lateral sclerosis-parkinsonism-dementia of Guam syndrome", "amyotrophic lateral sclerosis, Parkinsonism/dementia complex of Guam"], "preferred_name": "amyotrophic lateral sclerosis-parkinsonism-dementia complex", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0007312", "names": ["Charcot-Marie-Tooth disease with ptosis and parkinsonism"], "preferred_name": "Charcot-Marie-Tooth disease with ptosis and parkinsonism", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 56, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0007433", "names": ["dementia/parkinsonism with non-Alzheimer amyloid plaques"], "preferred_name": "dementia/parkinsonism with non-Alzheimer amyloid plaques", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 56, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0007488", "names": ["DLB", "Lewy body disease", "Lewy body dementia", "dementia, Lewy body", "dementia, Lewy body; DLB", "dementia with Lewy bodies", "diffuse Lewy body disease", "Senile dementia of the Lewy body type", "Lewy body variant of Alzheimer disease", "diffuse Lewy body disease with gaze palsy"], "preferred_name": "Lewy body dementia", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0007495", "names": ["DRD", "DYT5a", "DYT-GCH1", "dystonia 5", "dystonia type 5", "GTPCH1-deficient DRD", "dystonia, DOPA-responsive", "dystonia, DOPA-responsive; DRD", "autosomal dominant Segawa syndrome", "HPD with marked diurnal fluctuation", "Segawa syndrome, autosomal dominant", "GTPCH1-deficient dopa-responsive dystonia", "autosomal dominant dopa-responsive dystonia", "Dopa-responsive dystonia, autosomal dominant", "Dopa-responsive dystonia; Segawa syndrome AD", "dystonia, Dopa-responsive, autosomal dominant", "dystonia, progressive, with diurnal variation", "dystonia-Parkinsonism with diurnal fluctuation", "GTP cyclohydrolase 1-deficient dopa-responsive dystonia", "hereditary progressive dystonia with marked diurnal fluctuation"], "preferred_name": "dystonia 5", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0007496", "names": ["RDP", "DYT12", "DYT-ATP1A3", "dystonia 12", "dystonia type 12", "dystonia 12; DYT12", "ATP1A3 dystonic disorder", "rapid-onset dystonia-parkinsonism", "dystonia-Parkinsonism, rapid-onset", "dystonic disorder caused by mutation in ATP1A3"], "preferred_name": "dystonia 12", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0008193", "names": ["paralysis agitans, juvenile, of Hunt", "Parkinson disease, juvenile, of Hunt"], "preferred_name": "paralysis agitans, juvenile, of Hunt", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 36, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0008199", "names": ["PD", "park", "Parkinson disease, late-onset", "Parkinson disease, late-onset; PD"], "preferred_name": "late-onset Parkinson disease", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 2, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0008200", "names": ["PARK1", "atypical Parkinson disease", "Parkinson disease 1, autosomal dominant", "autosomal dominant Parkinson's disease 1", "autosomal dominant Parkinson disease type 1", "Parkinson disease 1, autosomal dominant; PARK1", "Parkinson disease 1, autosomal dominant Lewy body"], "preferred_name": "autosomal dominant Parkinson disease 1", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0008201", "names": ["Perry syndrome", "parkinsonism with alveolar hypoventilation and mental depression", "Parkinsonism with alveolar hypoventilation and mental depression"], "preferred_name": "Perry syndrome", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 14, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0008685", "names": ["WPW", "Wpw syndrome", "preexcitation syndrome", "anomalous A-V excitation", "Wolff-Parkinson-White pattern", "Wolff-Parkinson-White syndrome", "WOLFF-Parkinson-WHITE syndrome", "accessory atrioventricular pathways", "anomalous atrioventricular excitation", "Wolff-Parkinson-White pattern (finding)"], "preferred_name": "Wolff-Parkinson-white syndrome", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0009497", "names": ["Vitsala", "Kifafa seizure disorder", "Complex familial seizure disorder", "parkinsonian features and neurologic abnormalities, mental retardation and transient psychotic episodes"], "preferred_name": "Kifafa seizure disorder", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 7, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0009830", "names": ["PARK15", "pallidopyramidal syndrome", "Pallidopyramidal syndrome", "pallido-pyramidal disease", "pallido-pyramidal syndrome", "parkinsonian-pyramidal syndrome", "autosomal recessive early-onset Parkinson disease 15", "Parkinson disease 15, autosomal recessive early-onset", "autosomal recessive early-onset Parkinson's disease 15", "autosomal recessive early-onset Parkinson disease type 15", "Parkinson disease 15, autosomal recessive early-onset; PARK15"], "preferred_name": "parkinsonian-pyramidal syndrome", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0009839", "names": ["PSP-p", "atypical PSP", "PSP-parkinsonism", "Parkinson-dementia syndrome", "progressive supranuclear palsy atypical", "supranuclear palsy, progressive, 1, atypical", "Steele-Richardson-Olszewski syndrome, atypical"], "preferred_name": "progressive supranuclear palsy-parkinsonism syndrome", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0010360", "names": ["PARK12", "Parkinson disease 12", "Parkinson disease, X-linked", "Parkinson disease 12; PARK12"], "preferred_name": "parkinson disease 12", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0010422", "names": ["AD16", "Alzheimer disease 16", "Alzheimer's disease 16", "Alzheimer disease 16; AD16", "Alzheimer's disease type 16"], "preferred_name": "Alzheimer disease 16", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0010482", "names": ["XPDS", "PARKINSONISM with spasticity, X-linked", "PARKINSONISM with spasticity, X-linked; XPDS"], "preferred_name": "X-linked parkinsonism-spasticity syndrome", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0010709", "names": ["Wsn", "BGMR", "WSMN", "Waisman syndrome", "WAISMAN syndrome", "Laxova-Opitz syndrome", "WAISMAN syndrome; WSMN", "Laxova Brown hogan syndrome", "basal ganglia disorder with mental retardation", "basal ganglion disorder with mental retardation", "Parkinsonism, early onset with mental retardation", "Parkinsonism, early-onset, with mental retardation", "X-linked recessive basal ganglia disorder with mental retardation"], "preferred_name": "early-onset parkinsonism-intellectual disability syndrome", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0010747", "names": ["XDP", "DYT3", "Lubag", "DYT-TAF1", "Lubag syndrome", "dystonia 3, torsion, X-linked", "X-linked dystonia Parkinsonism", "dystonia-Parkinsonism, X-linked", "dystonia 3, torsion, X-linked; DYT3", "X-linked dystonia-parkinsonism/Lubag", "X-linked dystonia-Parkinsonism syndrome", "torsion dystonia-Parkinsonism, Filipino type", "X-linked torsion dystonia-Parkinsonism syndrome"], "preferred_name": "X-linked dystonia-parkinsonism", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0010783", "names": ["Alzheimer disease, susceptibility to, mitochondrial"], "preferred_name": "Alzheimer disease, susceptibility to, mitochondrial", "types": ["NamedThing"], "shortest_name_length": 51, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0010796", "names": ["Parkinson disease, mitochondrial"], "preferred_name": "Parkinson disease, mitochondrial", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 32, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0010820", "names": ["JP", "PDJ", "PARK2", "Parkinson disease 2", "juvenile parkinsonism", "PRKN young-onset Parkinson disease", "autosomal recessive juvenile Parkinson disease", "Parkinson disease, juvenile, autosomal recessive", "Parkinson disease 2, autosomal recessive juvenile", "autosomal recessive juvenile Parkinson's disease 2", "Parkinson disease autosomal recessive, early onset", "Parkinsonism, early onset, with diurnal fluctuation", "Parkinsonism, early-onset, with diurnal fluctuation", "autosomal recessive juvenile Parkinson disease type 2", "young-onset Parkinson disease caused by mutation in PRKN", "Parkinson disease 2, autosomal recessive juvenile; PARK2"], "preferred_name": "autosomal recessive juvenile Parkinson disease 2", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 2, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0010857", "names": ["FTD", "Ftdp17", "Pick Complex", "semantic variant PPA", "frontotemporal dementia", "Ftld with Tau inclusions", "Wilhelmsen-Lynch disease", "frontotemporal dementia; FTD", "frontotemporal lobe dementia", "Pallidopontonigral Degeneration", "semantic primary progressive aphasia", "frontotemporal dementia with Parkinsonism", "dementia, frontotemporal, with Parkinsonism", "multiple system tauopathy with presenile dementia", "frontotemporal lobar Degeneration with Tau inclusions", "disinhibition-dementia-Parkinsonism-amyotrophy Complex"], "preferred_name": "semantic dementia", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "NCBIGene:378899", "preferred_name": "FTL", "names": ["FTL", "FTD", "ferritoid", "ferritin light chain", "ferritin, light polypeptide", "Q8AYG9_CHICK Ferritin (trembl)"], "taxa": ["NCBITaxon:9031"], "taxon_specific": true, "types": ["Gene", "GeneOrGeneProduct", "GenomicEntity", "ChemicalEntityOrGeneOrGeneProduct", "PhysicalEssence", "OntologyClass", "BiologicalEntity", "ThingWithTaxon", "NamedThing", "Entity", "PhysicalEssenceOrOccurrent", "MacromolecularMachineMixin", "Protein", "GeneProductMixin", "Polypeptide", "ChemicalEntityOrProteinOrPolypeptide"], "shortest_name_length": 3, "clique_identifier_count": 3} +{"curie": "MONDO:0011194", "names": ["AD5", "Ad5", "Alzheimer disease 5", "Alzheimer's disease 5", "Alzheimer disease type 5", "Alzheimer's disease type 5", "Alzheimer disease, familial 5", "Alzheimer disease, familial, 5"], "preferred_name": "Alzheimer disease 5", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011220", "names": ["PARK3", "Parkinson disease type 3", "autosomal dominant Parkinson disease", "Parkinson disease 3, autosomal dominant", "Parkinson disease 3, autosomal dominant; PARK3", "Parkinson disease 3, autosomal dominant Lewy body"], "preferred_name": "parkinson disease 3, autosomal dominant", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011401", "names": ["Alzheimer disease without neurofibrillary tangles", "Alzheimer's disease without neurofibrillary tangles"], "preferred_name": "Alzheimer disease without neurofibrillary tangles", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 49, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011513", "names": ["Alzheimer disease, familial early-onset, with Coexisting amyloid and prion pathology"], "preferred_name": "Alzheimer disease, familial early-onset, with coexisting amyloid and prion pathology", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 84, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011551", "names": ["DYT5b", "autosomal recessive Segawa syndrome", "Segawa syndrome, autosomal recessive", "autosomal recessive dopa-responsive dystonia", "Parkinsonism, infantile, autosomal recessive", "dopa-responsive dystonia, autosomal recessive", "Dopa-responsive dystonia, autosomal recessive", "Dopa-responsive dystonia, autosomal recessive", "DOPA responsive dystonia, autosomal recessive", "dystonia, Dopa-responsive, autosomal recessive", "dystonia, DOPA responsive, autosomal recessive", "tyrosine hydroxylase-deficient dopa-responsive dystonia"], "preferred_name": "TH-deficient dopa-responsive dystonia", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011561", "names": ["AD6", "AD6", "Alzheimer disease 6", "Alzheimer's disease 6", "Alzheimer disease type 6", "Alzheimer's disease type 6", "Alzheimer disease 6, late onset", "Alzheimer disease 6, late-onset", "plasma Beta-amyloid-42 level quantitative trait locus"], "preferred_name": "Alzheimer disease 6", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011562", "names": ["PARK4", "Parkinson disease 4, autosomal dominant", "autosomal dominant Parkinson's disease 4", "autosomal dominant Parkinson disease type 4", "Parkinson disease 4, autosomal dominant; PARK4", "autosomal dominant Lewy body Parkinson disease 4", "Parkinson disease 4, autosomal dominant Lewy body"], "preferred_name": "autosomal dominant Parkinson disease 4", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011613", "names": ["PARK6", "PINK1 Parkinson disease", "early-onset Parkinson disease 6", "Parkinson disease 6, early-onset", "Parkinson disease caused by mutation in PINK1", "Parkinson disease 6, late-onset, susceptibility to", "Parkinson disease 6, autosomal recessive early-onset", "autosomal recessive early-onset Parkinson's disease 6", "autosomal recessive early-onset Parkinson disease type 6", "Parkinson disease 6, autosomal recessive early-onset; PARK6", "Parkinson disease, autosomal recessive early-onset, digenic, Pink1/Dj1"], "preferred_name": "autosomal recessive early-onset Parkinson disease 6", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011647", "names": ["AD7", "Ad7", "Alzheimer disease 7", "Alzheimer's disease 7", "Alzheimer disease type 7", "Alzheimer's disease type 7", "Alzheimer disease, familial 7", "Alzheimer disease, familial, 7"], "preferred_name": "Alzheimer disease 7", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011658", "names": ["PARK7", "PARK7 Parkinson disease", "Parkinson disease caused by mutation in PARK7", "Parkinson disease 7, autosomal recessive early-onset", "autosomal recessive early-onset Parkinson's disease 7", "autosomal recessive early-onset Parkinson disease type 7", "Parkinson disease 7, autosomal recessive early-onset; PARK7"], "preferred_name": "autosomal recessive early-onset Parkinson disease 7", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011706", "names": ["KRS", "PARK9", "KRPPD", "park 9", "Kufor-Rakeb syndrome", "Parkinson disease type 9", "Kufor-Rakeb syndrome; KRS", "ceroid lipofuscinosis, neuronal, 12", "autosomal recessive Parkinson disease 9", "Parkinson disease 9, autosomal recessive", "autosomal recessive juvenile onset Parkinson disease 9", "Parkinson disease 9, autosomal recessive, juvenile-onset", "Pallidopyramidal Degeneration with supranuclear upgaze paresis and dementia", "Pallidopyramidal degeneration with supranuclear upgaze paresis, and dementia"], "preferred_name": "Kufor-Rakeb syndrome", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011737", "names": ["PARK10", "Parkinson disease 10", "Parkinson disease 10; PARK10", "Parkinson disease, Age at onset of"], "preferred_name": "parkinson disease 10", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011743", "names": ["AD4", "Ad4", "Alzheimer disease 4", "Alzheimer's disease 4", "Alzheimer disease type 4", "Alzheimer's disease type 4", "Alzheimer disease, familial4", "Alzheimer disease, familial, 4", "Alzheimer disease familial type 4", "familial Alzheimer disease, type 4", "familial Alzheimer's disease, type 4"], "preferred_name": "Alzheimer disease 4", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011764", "names": ["PARK8", "LRRK2 Parkinson disease", "Parkinson disease 8, autosomal dominant", "autosomal dominant Parkinson's disease 8", "autosomal dominant Parkinson disease type 8", "Parkinson disease caused by mutation in LRRK2", "Parkinson disease 8, autosomal dominant; PARK8"], "preferred_name": "autosomal dominant Parkinson disease 8", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011777", "names": ["AD8", "Ad8", "Alzheimer disease 8", "Alzheimer's disease 8", "Alzheimer disease type 8", "Alzheimer's disease type 8", "Alzheimer disease, familial 8", "Alzheimer disease, familial, 8"], "preferred_name": "Alzheimer disease 8", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011896", "names": ["PARK11", "GIGYF2 hereditary late onset Parkinson disease", "susceptibility to autosomal dominant Parkinson disease 11", "Parkinson disease 11, autosomal dominant, susceptibility to", "Parkinson disease 11, autosomal dominant, susceptibility to; PARK11", "hereditary late onset Parkinson disease caused by mutation in GIGYF2"], "preferred_name": "Parkinson disease 11, autosomal dominant, susceptibility to", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0011913", "names": ["AD3", "Alzheimer disease 3", "Alzheimer's disease 3", "Alzheimer disease 3; AD", "Alzheimer disease type 3", "Alzheimer's disease type 3", "Alzheimer disease familial 3", "Alzheimer disease, familial, 3", "Alzheimer disease 3, early onset", "Alzheimer disease 3, early-onset", "familial Alzheimer disease, type 3", "familial Alzheimer's disease, type 3", "Alzheimer disease early onset type 3", "PSEN1 early-onset autosomal dominant Alzheimer disease", "Alzheimer disease, familial, 3, with spastic paraparesis and apraxia", "early-onset autosomal dominant Alzheimer disease caused by mutation in PSEN1", "Alzheimer disease, familial, 3, with spastic paraparesis and unusual plaques"], "preferred_name": "Alzheimer disease 3", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0012153", "names": ["AD9", "Alzheimer disease 9", "Alzheimer disease 9; AD9", "Alzheimer disease 9, late-onset", "Alzheimer disease 9, susceptibility to", "Alzheimer disease 9, susceptibility to; AD9"], "preferred_name": "Alzheimer disease 9", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0012321", "names": ["AD10", "Ad10", "Alzheimer disease 10", "Alzheimer's disease 10", "Alzheimer disease type 10", "Alzheimer's disease type 10", "Alzheimer disease familial 10", "Alzheimer disease, familial, 10"], "preferred_name": "Alzheimer disease 10", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0012344", "names": ["AD11", "Ad11", "Alzheimer disease 11", "Alzheimer's disease 11", "Alzheimer disease type 11", "Alzheimer's disease type 11", "Alzheimer disease, familial, 11"], "preferred_name": "Alzheimer disease 11", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0012466", "names": ["PARK13", "HTRA2 young-onset Parkinson disease", "young-onset Parkinson disease caused by mutation in HTRA2", "susceptibility to autosomal dominant Parkinson disease 13", "Parkinson disease 13, autosomal dominant, susceptibility to", "Parkinson disease 13, autosomal dominant, susceptibility to; PARK13"], "preferred_name": "Parkinson disease 13, autosomal dominant, susceptibility to", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0012609", "names": ["AD12", "Ad12", "Alzheimer disease 12", "Alzheimer's disease 12", "Alzheimer disease type 12", "Alzheimer's disease type 12", "Alzheimer disease familial 12", "Alzheimer disease, familial, 12"], "preferred_name": "Alzheimer disease 12", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0012630", "names": ["AD13", "Alzheimer disease 13", "Alzheimer's disease 13", "Alzheimer disease 13; AD13", "Alzheimer's disease type 13"], "preferred_name": "Alzheimer disease 13", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0012631", "names": ["AD14", "Alzheimer disease 14", "Alzheimer's disease 14", "Alzheimer disease 14; AD14", "Alzheimer's disease type 14"], "preferred_name": "Alzheimer disease 14", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0012632", "names": ["AD15", "Alzheimer disease 15", "Alzheimer's disease 15", "Alzheimer disease 15; AD15", "Alzheimer's disease type 15"], "preferred_name": "AD15", "types": ["NamedThing"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0012789", "names": ["DYT16", "DYT-PRKRA", "dystonia 16", "dystonia type 16", "dystonia 16; DYT16", "PRKRA dystonic disorder", "early-onset dystonia parkinsonism", "Young-onset dystonia-(parkinsonism)", "dystonic disorder caused by mutation in PRKRA"], "preferred_name": "dystonia 16", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0013060", "names": ["PARK14", "dystonia-Parkinsonism Adult-onset", "dystonia-Parkinsonism, adult-onset", "adult-onset dystonia - parkinsonism", "PLA2G6-related dystonia-parkinsonism", "dystonia-parkinsonism, Paisan-Ruiz type", "Parkinson disease 14, autosomal recessive", "autosomal recessive Parkinson's disease 14", "autosomal recessive Parkinson disease type 14", "PLA2G6 hereditary late onset Parkinson disease", "Parkinson disease 14, autosomal recessive; PARK14", "hereditary late onset Parkinson disease caused by mutation in PLA2G6"], "preferred_name": "autosomal recessive Parkinson disease 14", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0013150", "names": ["IPD", "PKDYS", "infantile Parkinsonism-dystonia", "Parkinsonism-dystonia infantile", "PARKINSONISM-dystonia, infantile", "PARKINSONISM-dystonia, infantile; PKDYS", "dopamine transporter deficiency syndrome"], "preferred_name": "parkinsonism-dystonia, infantile", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0013167", "names": ["PARK16", "Parkinson disease 16", "Parkinson disease 16; PARK16"], "preferred_name": "parkinson disease 16", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0013340", "names": ["PARK5", "UCHL1 young-onset Parkinson disease", "susceptibility to autosomal dominant Parkinson disease 5", "young-onset Parkinson disease caused by mutation in UCHL1", "Parkinson disease 5, autosomal dominant, susceptibility to", "Parkinson disease 5, autosomal dominant, susceptibility to; PARK5"], "preferred_name": "Parkinson disease 5, autosomal dominant, susceptibility to", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0013625", "names": ["PARK17", "Parkinson disease 17", "Parkinson's disease 17", "VPS35 Parkinson disease", "Parkinson disease type 17", "Parkinson disease 17; PARK17", "autosomal dominant Parkinson disease 17", "Parkinson disease caused by mutation in VPS35"], "preferred_name": "Parkinson disease 17", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0013653", "names": ["PARK18", "EIF4G1 hereditary late onset Parkinson disease", "susceptibility to autosomal dominant Parkinson disease 18", "Parkinson disease 18, autosomal dominant, susceptibility to", "Parkinson disease 18, autosomal dominant, susceptibility to; PARK18", "hereditary late onset Parkinson disease caused by mutation in EIF4G1"], "preferred_name": "Parkinson disease 18, autosomal dominant, susceptibility to", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0014036", "names": ["AD17", "Alzheimer disease 17", "Alzheimer's disease 17", "Alzheimer disease 17; AD17", "Alzheimer's disease type 17", "Alzheimer disease 17, late onset", "Alzheimer disease 17, late-onset"], "preferred_name": "Alzheimer disease 17", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0014231", "names": ["PARK19", "PARK19A", "Park19, formerly", "DNAJC6 Parkinson disease", "Parkinson disease 19B, early-onset", "Parkinson disease 19, juvenile-onset", "Parkinson disease 19A, juvenile-onset", "juvenile onset Parkinson's disease 19A", "juvenile onset Parkinson disease type 19A", "Parkinson disease 19, juvenile-onset; PARK19", "Parkinson disease caused by mutation in DNAJC6", "Parkinson disease 19A, juvenile-onset; PARK19A"], "preferred_name": "juvenile onset Parkinson disease 19A", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0014233", "names": ["PARK20", "SYNJ1 Parkinson disease", "Parkinson disease 20, early-onset", "early-onset Parkinson's disease 20", "early-onset Parkinson disease type 20", "Parkinson disease 20, early-onset; PARK20", "Parkinson disease caused by mutation in SYNJ1"], "preferred_name": "early-onset Parkinson disease 20", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0014265", "names": ["AD18", "Alzheimer disease 18", "Alzheimer's disease 18", "ADAM10 Alzheimer disease", "Alzheimer disease type 18", "Alzheimer disease 18; AD18", "Alzheimer's disease type 18", "Alzheimer disease 18, late-onset", "Alzheimer disease caused by mutation in ADAM10"], "preferred_name": "Alzheimer disease 18", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0014316", "names": ["AD19", "Alzheimer disease 19", "Alzheimer's disease 19", "PLD3 Alzheimer disease", "Alzheimer disease type 19", "Alzheimer disease 19; AD19", "Alzheimer's disease type 19", "Alzheimer disease 19 late onset", "Alzheimer disease 19, late-onset", "Alzheimer disease caused by mutation in PLD3"], "preferred_name": "Alzheimer disease 19", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0014604", "names": ["PARK21", "Parkinson disease 21", "Parkinson disease type 21", "Parkinson disease 21; PARK21", "DNAJC13 hereditary late onset Parkinson disease", "hereditary late onset Parkinson disease caused by mutation in DNAJC13"], "preferred_name": "Parkinson disease 21", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0014742", "names": ["PARK22", "CHCHD2 Parkinson disease", "Parkinson disease 22, autosomal dominant", "Parkinson disease caused by mutation in CHCHD2", "Parkinson disease 22, autosomal dominant; PARK22"], "preferred_name": "Parkinson disease 22, autosomal dominant", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0014796", "names": ["PARK23", "VPS13C young-onset Parkinson disease", "autosomal recessive early-onset Parkinson disease 23", "Parkinson disease 23, autosomal recessive early-onset", "autosomal recessive early-onset Parkinson's disease 23", "autosomal recessive early-onset Parksinson disease type 23", "young-onset Parkinson disease caused by mutation in VPS13C", "Parkinson disease 23, autosomal recessive early-onset; PARK23"], "preferred_name": "autosomal recessive early-onset Parkinson disease 23", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0015140", "names": ["EOFAD", "early-onset, autosomal dominant Alzheimer disease", "early-onset familial autosomal dominant Alzheimer disease"], "preferred_name": "early-onset autosomal dominant Alzheimer disease", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0017279", "names": ["YOPD", "early-onset Parkinson disease", "early-onset Parkinson's disease"], "preferred_name": "young-onset Parkinson disease", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0017639", "names": ["CO-induced parkinsonism"], "preferred_name": "carbon monoxide-induced parkinsonism", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 23, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0018466", "names": ["LOPD", "late-onset Parkinson disease", "late onset Parkinson disease", "late onset Parkinson's disease", "hereditary late-onset Parkinson disease", "autosomal dominant late-onset Parkinson disease"], "preferred_name": "late-onset Parkinson disease", "types": ["NamedThing"], "shortest_name_length": 4, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0018899", "names": ["PCA", "Benson syndrome", "biparietal Alzheimer disease"], "preferred_name": "posterior cortical atrophy", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0020352", "names": ["MSA-p", "MSA, parkinsonian type"], "preferred_name": "multiple system atrophy, parkinsonian type", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0054835", "names": ["PKDYS", "PKDYS1", "dopamine transporter deficiency syndrome", "Parkinsonism-dystonia, infantile, 1; PKDYS1"], "preferred_name": "parkinsonism-dystonia, infantile, 1", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 5, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0054836", "names": ["PKDYS2", "Parkinsonism-dystonia, infantile, 2; PKDYS2"], "preferred_name": "parkinsonism-dystonia, infantile, 2", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 6, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0100065", "names": ["tyrosine hydroxylase infantile parkinsonism and motor delay"], "preferred_name": "TH-deficient infantile parkinsonism and motor delay", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 59, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0100087", "names": ["FAD", "GARD:0000632", "Alzheimer disease, familial"], "preferred_name": "familial Alzheimer disease", "types": ["Disease", "DiseaseOrPhenotypicFeature", "ThingWithTaxon", "BiologicalEntity", "NamedThing", "Entity"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "MONDO:0100088", "names": ["Alzheimer disease type 2"], "preferred_name": "Alzheimer disease type 2", "types": ["NamedThing"], "shortest_name_length": 24, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} +{"curie": "www:bget?hsa:11315", "names": ["DJ1", "DJ-1", "GATD2", "HEL-S-67p; Parkinsonism associated deglycase", "K05687 protein DJ-1 [EC:3.5.1.124] | (RefSeq) PARK7"], "preferred_name": "DJ-1", "types": ["NamedThing"], "shortest_name_length": 3, "clique_identifier_count": 1, "taxa": [], "taxon_specific": false} diff --git a/tests/test_apidocs.py b/tests/test_apidocs.py new file mode 100644 index 00000000..5af9bcd6 --- /dev/null +++ b/tests/test_apidocs.py @@ -0,0 +1,17 @@ +from api.apidocs import construct_open_api_schema +from api.server import app + + +def test_construct_open_api_schema_returns_cached_schema(): + """FastAPI caches the schema on the app, and construct_open_api_schema() hands the + cached copy back on later calls. It used to call it -- `return app.openapi_schema()` + -- which raises TypeError on a dict.""" + first = construct_open_api_schema(app) + assert isinstance(first, dict) + + # Force the cached branch, whether or not importing the app already populated it. + app.openapi_schema = first + second = construct_open_api_schema(app) + + assert second is first + assert sorted(second["paths"]) == ["/bulk-lookup", "/lookup", "/reverse_lookup", "/status", "/synonyms"] diff --git a/tests/test_docs_links.py b/tests/test_docs_links.py new file mode 100644 index 00000000..b8a950bb --- /dev/null +++ b/tests/test_docs_links.py @@ -0,0 +1,112 @@ +"""Checks on the links in our documentation and in the endpoint descriptions. + +Everything here is offline: it resolves relative paths and heading anchors on disk and +greps for a banned URL form. Nothing fetches a URL, because a test that fails when +GitHub is slow is a test people learn to ignore. +""" + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).parent.parent + +# Markdown we own. The notebook and anything vendored is deliberately excluded. +MARKDOWN_FILES = sorted( + p for p in REPO_ROOT.rglob("*.md") + if not any(part in {"venv", ".venv", "node_modules", ".git", ".pytest_cache"} for part in p.parts) +) + +# Files that carry links out to GitHub in code rather than in prose. +SOURCE_WITH_LINKS = [REPO_ROOT / "api" / "server.py", REPO_ROOT / "api" / "resources" / "openapi.yml"] + +INLINE_LINK = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)") +HEADING = re.compile(r"^#+\s+(.*)$", re.MULTILINE) + + +def anchors_for(markdown_text): + """The anchor slugs GitHub generates for a document's headings.""" + slugs = set() + for heading in HEADING.findall(markdown_text): + slug = re.sub(r"[`*]", "", heading.strip().lower()) + slug = re.sub(r"[^a-z0-9 _-]", "", slug) + slugs.add(slug.replace(" ", "-")) + return slugs + + +def test_relative_links_resolve(): + """A relative link is written relative to the file it sits in, which is easy to get + wrong when a document moves into a subdirectory.""" + broken = [] + for path in MARKDOWN_FILES: + for target in INLINE_LINK.findall(path.read_text()): + if target.startswith(("http://", "https://", "#", "mailto:")): + continue + resolved = (path.parent / target.split("#")[0]).resolve() + if not resolved.exists(): + broken.append(f"{path.relative_to(REPO_ROOT)} -> {target}") + assert not broken, "Relative links that do not resolve:\n " + "\n ".join(broken) + + +def test_anchors_resolve(): + """Anchors are case-sensitive and GitHub lower-cases them, so `#Conflation` silently + lands at the top of the page rather than at the heading.""" + broken = [] + for path in MARKDOWN_FILES: + for target in INLINE_LINK.findall(path.read_text()): + if "#" not in target or target.startswith(("http://", "https://", "mailto:")): + continue + rel, _, anchor = target.partition("#") + if not anchor: + continue + target_path = path if rel == "" else (path.parent / rel) + if target_path.suffix != ".md" or not target_path.exists(): + continue + if anchor not in anchors_for(target_path.read_text()): + broken.append(f"{path.relative_to(REPO_ROOT)} -> {target}") + assert not broken, "Anchors with no matching heading:\n " + "\n ".join(broken) + + +def test_in_repo_anchors_in_source_resolve(): + """The endpoint descriptions link into documentation/API.md by absolute URL, so the + check above cannot see them.""" + api_md = REPO_ROOT / "documentation" / "API.md" + valid = anchors_for(api_md.read_text()) + broken = [] + for path in SOURCE_WITH_LINKS: + for anchor in re.findall(r"documentation/API\.md#([A-Za-z0-9_-]+)", path.read_text()): + if anchor not in valid: + broken.append(f"{path.relative_to(REPO_ROOT)} -> API.md#{anchor}") + assert not broken, "Anchors with no matching heading in API.md:\n " + "\n ".join(broken) + + +def test_no_master_branch_links(): + """NameResolution, Babel and NodeNormalization all default to `main`. A /blob/master/ + URL resolves only through GitHub's post-rename redirect, so it looks fine right up + until that redirect goes away. See CLAUDE.md.""" + offenders = [] + for path in MARKDOWN_FILES + SOURCE_WITH_LINKS: + for line_no, line in enumerate(path.read_text().splitlines(), start=1): + if re.search(r"github\.com/[^/\s]+/[^/\s]+/blob/master/", line, re.IGNORECASE): + offenders.append(f"{path.relative_to(REPO_ROOT)}:{line_no}") + assert not offenders, "Links using /blob/master/ instead of /blob/main/:\n " + "\n ".join(offenders) + + +#: The three repositories that moved from the TranslatorSRI org to NCATSTranslator. Scoped to +#: these by name on purpose -- TranslatorSRI/RENCI-Python-image, TranslatorSRI/babel-validation +#: and TranslatorSRI/r3 really do still live under that org, so a blanket ban on the string +#: would push someone to "fix" three correct links into 404s. +MOVED_TO_NCATSTRANSLATOR = re.compile(r"TranslatorSRI/(Babel|NameResolution|NodeNormalization)\b") + + +def test_no_stale_org_links(): + """These three repositories are under NCATSTranslator now. The old URLs resolve through + GitHub's org-rename redirect, which is more durable than the branch-rename one, but they + still name an org that no longer owns the code.""" + offenders = [] + for path in MARKDOWN_FILES + SOURCE_WITH_LINKS: + for line_no, line in enumerate(path.read_text().splitlines(), start=1): + if MOVED_TO_NCATSTRANSLATOR.search(line): + offenders.append(f"{path.relative_to(REPO_ROOT)}:{line_no}") + assert not offenders, ( + "Links naming the pre-rename TranslatorSRI org:\n " + "\n ".join(offenders) + ) diff --git a/tests/test_status.py b/tests/test_status.py index b48ddc07..e172a532 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -1,5 +1,10 @@ +import json import logging +import re +import warnings +from pathlib import Path +from api.apidocs import get_app_info from api.server import app from fastapi.testclient import TestClient @@ -22,8 +27,74 @@ def test_status(): assert status['size'] != '' assert status['startTime'] + # The conflations baked into this index (documentation/Babel.md explains why they + # are baked in rather than applied per query). + assert status['conflations'] == ['GeneProtein', 'DrugChemical'] + assert status['conflation_url'].startswith('https://github.com/NCATSTranslator/Babel/') + # Count the specific number of test documents we load. assert status['numDocs'] == 89 assert status['maxDoc'] == 89 assert status['deletedDocs'] == 0 + +def test_status_reports_environment(monkeypatch): + """The environment variables documented in documentation/Deployment.md are the only + record of which data an instance is serving, so check they reach /status.""" + monkeypatch.setenv('BABEL_VERSION', '2026jul22') + monkeypatch.setenv('BABEL_VERSION_URL', 'https://example.org/releases/2026jul22.md') + monkeypatch.setenv('BIOLINK_MODEL_TAG', 'v4.2.6-rc5') + + status = TestClient(app).get("/status").json() + + assert status['babel_version'] == '2026jul22' + assert status['babel_version_url'] == 'https://example.org/releases/2026jul22.md' + assert status['biolink_model']['tag'] == 'v4.2.6-rc5' + # The other two Biolink URLs are derived from the tag rather than set separately. + assert status['biolink_model']['url'].endswith('/v4.2.6-rc5') + assert 'v4.2.6-rc5' in status['biolink_model']['download_url'] + + +def test_conflations_can_be_overridden(monkeypatch): + """CONFLATIONS is not detected from the data, so a differently-built index needs it + set. Whitespace and empty entries are tolerated so a trailing comma isn't a bug.""" + monkeypatch.setenv('CONFLATIONS', ' GeneProtein , ,DrugChemical,') + + status = TestClient(app).get("/status").json() + + assert status['conflations'] == ['GeneProtein', 'DrugChemical'] + + +def test_documented_status_example_matches_the_real_response(): + """documentation/API.md shows an example /status payload, which had drifted to a + nameres_version three releases old. + + A stale patch or minor version in the example is cosmetic, so that only warns. A + stale *major* version fails: a major release is exactly when someone should re-run + this example and confirm the endpoint still behaves the way the docs describe. + + The field names are checked strictly either way -- that is what catches a field + added to /status and never documented, which is how `conflations` was missed.""" + api_md = (Path(__file__).parent.parent / "documentation" / "API.md").read_text() + # The first JSON block after the "### `/status`" heading is the example payload. + status_section = api_md.split("### `/status`", 1)[1] + example = json.loads(re.search(r"```json\n(.*?)```", status_section, re.DOTALL).group(1)) + + documented = example["nameres_version"].lstrip("v") + current = get_app_info()["version"] + if documented != current: + message = ( + f"documentation/API.md's /status example shows nameres_version " + f"{documented}, but this build is {current}." + ) + assert documented.split(".")[0] == current.split(".")[0], ( + message + " Refresh the example against a real instance for the new major version." + ) + warnings.warn(message) + + actual = TestClient(app).get("/status").json() + assert set(example) == set(actual), ( + "The documented /status example and the real response have different fields: " + f"only in docs={sorted(set(example) - set(actual))}, " + f"only in response={sorted(set(actual) - set(example))}" + )