Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format

## [Unreleased]

## [1.1.1] - 2026-09-07

### Changed

- Autocomplete now models the API's flat seven-field suggestion entries,
supports `domainIds` and `includeContext`, and maps the deprecated `domains`
option to `domain_ids`.
- Similarity search now exposes the API's complete option set, pagination and
source-concept metadata, score omission, explanations, and degraded/lower-
bound result indicators. Its documented default algorithm is `semantic`.

### Fixed

- `mappings.map()` now types the API's actual per-source result: required
`unmapped_sources` and `summary` fields replace the never-emitted
`failed_mappings` shape. The unsupported `summary` field was removed from
`mappings.get()` results.

## [1.1.0] - 2026-08-11

### Added
Expand Down Expand Up @@ -115,8 +133,8 @@ Standalone: omophubFhirUrl, getApiKey, setApiKey, hasApiKey
```

<!-- Reference-style version links. -->
[Unreleased]: https://github.com/OMOPHub/omophub-node/compare/v1.1.0...HEAD
[Unreleased]: https://github.com/OMOPHub/omophub-node/compare/v1.1.1...HEAD
[1.1.1]: https://github.com/OMOPHub/omophub-node/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/OMOPHub/omophub-node/releases/tag/v1.1.0
[1.0.1]: https://github.com/OMOPHub/omophub-node/releases/tag/v1.0.1
[1.0.0]: https://github.com/OMOPHub/omophub-node/releases/tag/v1.0.0

9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Query millions of standardized medical concepts from TypeScript with full type safety**

Access SNOMED CT, ICD-10, RxNorm, LOINC, and 100+ OHDSI ATHENA vocabularies without downloading, installing, or maintaining local databases.
Access SNOMED CT, ICD-10, RxNorm, LOINC, and 120+ OHDSI ATHENA vocabularies without downloading, installing, or maintaining local databases.

[![npm version](https://img.shields.io/npm/v/@omophub/omophub-node.svg)](https://www.npmjs.com/package/@omophub/omophub-node)
[![Node Version](https://img.shields.io/node/v/@omophub/omophub-node.svg)](https://www.npmjs.com/package/@omophub/omophub-node)
Expand Down Expand Up @@ -64,6 +64,12 @@ const mapping = await client.mappings.map({
targetVocabulary: 'SNOMED',
sourceCodes: [{ vocabulary_id: 'ICD10CM', concept_code: 'E11.9' }],
});
if (mapping.data) {
console.log(mapping.data.summary);
// Every source without a mapping is reported with source_not_found or
// no_mapping_found, rather than being silently dropped.
console.log(mapping.data.unmapped_sources);
}

// Navigate concept hierarchy
const ancestors = await client.hierarchy.ancestors(201826, { maxLevels: 3 });
Expand Down Expand Up @@ -231,6 +237,7 @@ Find concepts similar to a known concept or natural language query:

```ts
// Find concepts similar to a known concept
// `algorithm` defaults to 'semantic'; 'lexical' and 'hybrid' are also available.
const sim = await client.search.similar({ conceptId: 201826, algorithm: 'hybrid' });
for (const r of sim.data?.similar_concepts ?? []) {
console.log(`${r.concept_name} (score: ${r.similarity_score.toFixed(2)})`);
Expand Down
4 changes: 2 additions & 2 deletions e2e/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ describe('e2e: client.search.autocomplete', () => {
expect(Array.isArray(data?.suggestions)).toBe(true);
expect(data?.suggestions.length).toBeGreaterThan(0);
const first = data?.suggestions[0];
expect(typeof first?.suggestion.concept_id).toBe('number');
expect(typeof first?.suggestion.concept_name).toBe('string');
expect(typeof first?.concept_id).toBe('number');
expect(typeof first?.suggestion).toBe('string');
});

runOrSkip('echoes the query field with vocabulary filter applied', async () => {
Expand Down
10 changes: 6 additions & 4 deletions examples/map-between-vocabularies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,12 @@ async function mapConcepts(): Promise<void> {
}

console.log(`Mapped ${data.mappings.length} concepts to ICD-10-CM`);
if (data.summary) {
console.log(
` ${data.summary.mapped_concepts ?? '?'}/${data.summary.total_source_concepts ?? '?'} source concepts mapped`,
);
console.log(
` ${data.summary.mapped_sources}/${data.summary.requested_sources} source concepts mapped`,
);

for (const source of data.unmapped_sources) {
console.log(` Unmapped source ${source.source_concept_id}: ${source.reason}`);
}

for (const m of data.mappings) {
Expand Down
4 changes: 2 additions & 2 deletions examples/search-concepts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,12 @@ async function bulkSemanticSearch(client: OMOPHub): Promise<void> {

async function autocompleteExample(client: OMOPHub): Promise<void> {
console.log('\n=== Autocomplete ===');
// Returns `{ query, suggestions: [{ suggestion: Concept, match_score?, match_type? }] }`.
// Returns `{ query, suggestions: [{ suggestion: string, concept_id, ... }] }`.
const { data, error } = await client.search.autocomplete('hypert', { pageSize: 5 });
if (error) throw new Error(error.message);
console.log(`Suggestions for '${data.query}':`);
for (const s of data.suggestions.slice(0, 5)) {
console.log(` [${s.suggestion.vocabulary_id}] ${s.suggestion.concept_name}`);
console.log(` [${s.vocabulary_id}] ${s.suggestion}`);
}
}

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@omophub/omophub-node",
"version": "1.1.0",
"version": "1.1.1",
"description": "Official OMOPHub Node.js / TypeScript SDK for the OHDSI medical vocabularies API - search, lookup, map, and navigate concepts across SNOMED, ICD10, RxNorm, LOINC, and more.",
"type": "module",
"main": "dist/index.js",
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ export type {
ResponseMeta,
} from './interfaces.js';
export type {
FailedMapping,
GetMappingsOptions,
MapConceptsOptions,
MapConceptsResult,
Expand All @@ -98,6 +97,8 @@ export type {
MappingsListResult,
MappingsSummary,
SourceCodeRef,
UnmappedSource,
UnmappedSourceReason,
} from './mappings/interfaces/index.js';
export type {
GetRelationshipsOptions,
Expand Down Expand Up @@ -135,6 +136,7 @@ export type {
SimilarSearchMetadata,
SimilarSearchOptions,
SimilarSearchResult,
SimilarSourceConcept,
} from './search/interfaces/index.js';
export { __version__ } from './version.js';
export type {
Expand Down
3 changes: 2 additions & 1 deletion src/mappings/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
export type { GetMappingsOptions } from './get-mappings-options.js';
export type { MapConceptsOptions } from './map-concepts-options.js';
export type {
FailedMapping,
MapConceptsResult,
Mapping,
MappingContext,
MappingQuality,
MappingsListResult,
MappingsSummary,
SourceCodeRef,
UnmappedSource,
UnmappedSourceReason,
} from './mapping.js';
42 changes: 21 additions & 21 deletions src/mappings/interfaces/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface MappingContext {
* `targetVocabulary` does NOT add the vocabulary/code fields —
* measured against production 2026-08-12. Resolve a target's
* vocabulary and code with `concepts.get(target_concept_id)`.
* - `mappings.map` (`POST /mappings/map`) additionally returns
* - `mappings.map` (`POST /concepts/map`) additionally returns
* `source_*` / `target_*` `vocabulary_id` and `concept_code`.
*/
export interface Mapping {
Expand All @@ -50,37 +50,37 @@ export interface Mapping {
}

export interface MappingsSummary {
total_source_concepts?: number;
total_mappings?: number;
mapped_concepts?: number;
unmapped_concepts?: number;
requested_sources: number;
mapped_sources: number;
unmapped_sources: number;
total_mappings: number;
}

export interface MappingsListResult {
mappings: Mapping[];
summary?: MappingsSummary;
}

/**
* Failed-mapping entry in `MapConceptsResult.failed_mappings`. Discriminated
* by which input variant the failure traces back to — every failed mapping
* carries either a `source_concept_id` (from the `sourceConcepts` input) or
* a `source_code` (from the `sourceCodes` input), never an empty object.
* `reason` is optional, mirroring `BatchConceptResult.failed_concepts` in
* `concepts/interfaces/`.
*/
export type FailedMapping =
| { source_concept_id: number; source_code?: never; reason?: string }
export type UnmappedSourceReason = 'source_not_found' | 'no_mapping_found';

/** A submitted source that produced no mappings. */
export type UnmappedSource =
| {
source_concept_id: number;
vocabulary_id?: never;
concept_code?: never;
reason: UnmappedSourceReason;
}
| {
source_concept_id?: never;
source_code: { vocabulary_id: string; concept_code: string };
reason?: string;
source_concept_id?: number;
vocabulary_id: string;
concept_code: string;
reason: UnmappedSourceReason;
};

export interface MapConceptsResult {
mappings: Mapping[];
failed_mappings?: FailedMapping[];
summary?: MappingsSummary;
unmapped_sources: UnmappedSource[];
summary: MappingsSummary;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/search/interfaces/autocomplete-options.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export interface AutocompleteOptions {
vocabularyIds?: string[];
domainIds?: string[];
/** @deprecated Use `domainIds`. */
domains?: string[];
/** Maximum number of suggestions. Default 10 at the API; max 100. */
/** Maximum number of suggestions. Default 10 at the API; max 20. */
pageSize?: number;
includeContext?: boolean;
}
1 change: 1 addition & 0 deletions src/search/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export type {
SimilarConcept,
SimilarSearchMetadata,
SimilarSearchResult,
SimilarSourceConcept,
} from './similar-search-result.js';
20 changes: 13 additions & 7 deletions src/search/interfaces/search-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ export interface SearchResult {
search_metadata?: SearchMetadata;
}

/**
* One entry in `AutocompleteResult.suggestions`. The server nests the
* concept under a `suggestion` field and may add scoring fields alongside.
*/
/** One concept-name suggestion returned by `GET /search/suggest`. */
export interface AutocompleteEntry {
suggestion: Concept;
match_score?: number;
match_type?: string;
suggestion: string;
concept_id: Concept['concept_id'];
concept_code: Concept['concept_code'];
vocabulary_id: Concept['vocabulary_id'];
domain_id: Concept['domain_id'];
concept_class_id: Concept['concept_class_id'];
standard_concept: Concept['standard_concept'];
context?: {
vocabulary_id: string;
domain_id: string;
concept_class_id: string;
};
}

/**
Expand Down
28 changes: 26 additions & 2 deletions src/search/interfaces/similar-search-options.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
interface SimilarSearchBase {
/** `'semantic'`, `'lexical'`, or `'hybrid'` (default). */
/** `'semantic'` (the API's default), `'lexical'`, or `'hybrid'`. */
algorithm?: 'semantic' | 'lexical' | 'hybrid';
/** Similarity floor (0–1). Default 0.7 at the API. */
/**
* Similarity floor (0–1). Default 0.7 at the API. `0` is a valid value and
* is honoured — it returns every candidate the retrieval produced.
*/
similarityThreshold?: number;
/**
* Page of the ranked candidate pool, 1-based.
*
* Every algorithm ranks a bounded pool, so reachable depth is bounded too;
* page while `meta.pagination.has_next` is true rather than comparing `page`
* to `total_pages`, and check `search_metadata.totals_are_lower_bound`
* before treating a total as exact.
*/
page?: number;
pageSize?: number;
vocabularyIds?: string[];
domainIds?: string[];
conceptClassIds?: string[];
/** `'N'` selects non-standard concepts, which OMOP stores as a null column. */
standardConcept?: 'S' | 'C' | 'N';
/**
* Include invalid/deprecated concepts. Defaults to `false`, and is supported
* only with `algorithm: 'lexical'` — the embedding index holds valid
* concepts only, so the API answers 400 for the other two rather than
* ignoring the filter.
*/
includeInvalid?: boolean;
/** Include `similarity_score` on each concept. Default true. */
includeScores?: boolean;
/** Include an `explanation` on each concept. Default false. */
includeExplanations?: boolean;
/** Exclude the reference concept from its own results. Default true. */
excludeSelf?: boolean;
}

/**
Expand Down
45 changes: 44 additions & 1 deletion src/search/interfaces/similar-search-result.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { ConceptSummary } from '../../concepts/interfaces/concept.js';

export interface SimilarConcept extends ConceptSummary {
similarity_score: number;
/**
* Absent when the request set `includeScores: false`, which the API honours
* by omitting the key rather than zeroing it.
*/
similarity_score?: number;
domain_id?: string;
concept_class_id?: string;
standard_concept?: 'S' | 'C' | 'N' | null;
Expand All @@ -10,20 +14,59 @@ export interface SimilarConcept extends ConceptSummary {
lexical?: number;
hybrid?: number;
};
/** Present when the request set `includeExplanations: true`. */
explanation?: string;
/**
* @deprecated Duplicate of `explanation`, emitted by the API for one release
* so clients that read the old name keep working. Read `explanation`.
*/
similarity_explanation?: string;
}

export interface SimilarSearchMetadata {
original_query: string;
algorithm_used: 'semantic' | 'lexical' | 'hybrid';
similarity_threshold: number;
/**
* How many concepts cleared `similarity_threshold` inside the bounded
* retrieval pool — not how many were evaluated. The pool holds up to 500 and
* everything below the threshold is discarded before this is counted.
*/
total_candidates: number;
results_returned: number;
processing_time_ms: number;
embedding_latency_ms?: number;
/**
* True when retrieval hit its candidate bound, so `total_candidates` and the
* pagination totals count only what qualified inside the pool that was
* searched, rather than across the whole corpus. Treat them as "at least this
* many".
*/
totals_are_lower_bound?: boolean;
/**
* The algorithm that was asked for, when a fallback served the request
* instead — `hybrid` degrades to `lexical` if the embedding service is
* unavailable. Present only when it differs from `algorithm_used`.
*/
degraded_from?: 'semantic' | 'lexical' | 'hybrid';
/** The concept the search started from, when `conceptId` was supplied. */
source_concept_id?: number;
}

/** The reference concept a similarity search started from. */
export interface SimilarSourceConcept {
concept_id: number;
concept_name: string;
concept_code?: string;
vocabulary_id?: string;
domain_id?: string;
concept_class_id?: string;
standard_concept?: 'S' | 'C' | 'N' | null;
}

export interface SimilarSearchResult {
similar_concepts: SimilarConcept[];
search_metadata: SimilarSearchMetadata;
/** Returned when the search started from a `conceptId`. */
source_concept?: SimilarSourceConcept;
}
Loading
Loading