Skip to content

Commit ac96629

Browse files
author
alex-omophub
committed
Enhance autocomplete and similarity search functionality
- Updated README.md to clarify default algorithm options for similarity search. - Modified autocomplete response structure in `search.test.ts` and `search-concepts.ts` to reflect changes in suggestion format. - Improved `AutocompleteOptions` interface to include `domainIds` and updated documentation for deprecated fields. - Enhanced `SimilarSearchOptions` and `SimilarConcept` interfaces to provide more detailed response handling and options. - Added tests to ensure correct handling of new options and response structures in autocomplete and similarity search functionalities.
1 parent 1487462 commit ac96629

12 files changed

Lines changed: 223 additions & 20 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ Find concepts similar to a known concept or natural language query:
231231

232232
```ts
233233
// Find concepts similar to a known concept
234+
// `algorithm` defaults to 'semantic'; 'lexical' and 'hybrid' are also available.
234235
const sim = await client.search.similar({ conceptId: 201826, algorithm: 'hybrid' });
235236
for (const r of sim.data?.similar_concepts ?? []) {
236237
console.log(`${r.concept_name} (score: ${r.similarity_score.toFixed(2)})`);

‎e2e/search.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ describe('e2e: client.search.autocomplete', () => {
122122
expect(Array.isArray(data?.suggestions)).toBe(true);
123123
expect(data?.suggestions.length).toBeGreaterThan(0);
124124
const first = data?.suggestions[0];
125-
expect(typeof first?.suggestion.concept_id).toBe('number');
126-
expect(typeof first?.suggestion.concept_name).toBe('string');
125+
expect(typeof first?.concept_id).toBe('number');
126+
expect(typeof first?.suggestion).toBe('string');
127127
});
128128

129129
runOrSkip('echoes the query field with vocabulary filter applied', async () => {

‎examples/__docsnippet_check.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { OMOPHub } from '../src/index.js';
2+
3+
const client = new OMOPHub('oh_test');
4+
5+
export async function fromOldDocs(): Promise<void> {
6+
const { data } = await client.search.autocomplete('diab', { pageSize: 10 });
7+
console.log(data?.query);
8+
9+
for (const entry of data?.suggestions ?? []) {
10+
console.log(entry.suggestion.concept_name);
11+
console.log(entry.match_score);
12+
}
13+
}

‎examples/search-concepts.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ async function bulkSemanticSearch(client: OMOPHub): Promise<void> {
100100

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

‎src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export type {
135135
SimilarSearchMetadata,
136136
SimilarSearchOptions,
137137
SimilarSearchResult,
138+
SimilarSourceConcept,
138139
} from './search/interfaces/index.js';
139140
export { __version__ } from './version.js';
140141
export type {
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
export interface AutocompleteOptions {
22
vocabularyIds?: string[];
3+
domainIds?: string[];
4+
/** @deprecated Use `domainIds`. */
35
domains?: string[];
4-
/** Maximum number of suggestions. Default 10 at the API; max 100. */
6+
/** Maximum number of suggestions. Default 10 at the API; max 20. */
57
pageSize?: number;
8+
includeContext?: boolean;
69
}

‎src/search/interfaces/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ export type {
3232
SimilarConcept,
3333
SimilarSearchMetadata,
3434
SimilarSearchResult,
35+
SimilarSourceConcept,
3536
} from './similar-search-result.js';

‎src/search/interfaces/search-result.ts‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,20 @@ export interface SearchResult {
3434
search_metadata?: SearchMetadata;
3535
}
3636

37-
/**
38-
* One entry in `AutocompleteResult.suggestions`. The server nests the
39-
* concept under a `suggestion` field and may add scoring fields alongside.
40-
*/
37+
/** One concept-name suggestion returned by `GET /search/suggest`. */
4138
export interface AutocompleteEntry {
42-
suggestion: Concept;
43-
match_score?: number;
44-
match_type?: string;
39+
suggestion: string;
40+
concept_id: Concept['concept_id'];
41+
concept_code: Concept['concept_code'];
42+
vocabulary_id: Concept['vocabulary_id'];
43+
domain_id: Concept['domain_id'];
44+
concept_class_id: Concept['concept_class_id'];
45+
standard_concept: Concept['standard_concept'];
46+
context?: {
47+
vocabulary_id: string;
48+
domain_id: string;
49+
concept_class_id: string;
50+
};
4551
}
4652

4753
/**

‎src/search/interfaces/similar-search-options.ts‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
11
interface SimilarSearchBase {
2-
/** `'semantic'`, `'lexical'`, or `'hybrid'` (default). */
2+
/** `'semantic'` (the API's default), `'lexical'`, or `'hybrid'`. */
33
algorithm?: 'semantic' | 'lexical' | 'hybrid';
4-
/** Similarity floor (0–1). Default 0.7 at the API. */
4+
/**
5+
* Similarity floor (0–1). Default 0.7 at the API. `0` is a valid value and
6+
* is honoured — it returns every candidate the retrieval produced.
7+
*/
58
similarityThreshold?: number;
9+
/**
10+
* Page of the ranked candidate pool, 1-based.
11+
*
12+
* Every algorithm ranks a bounded pool, so reachable depth is bounded too;
13+
* page while `meta.pagination.has_next` is true rather than comparing `page`
14+
* to `total_pages`, and check `search_metadata.totals_are_lower_bound`
15+
* before treating a total as exact.
16+
*/
17+
page?: number;
618
pageSize?: number;
719
vocabularyIds?: string[];
820
domainIds?: string[];
21+
conceptClassIds?: string[];
22+
/** `'N'` selects non-standard concepts, which OMOP stores as a null column. */
923
standardConcept?: 'S' | 'C' | 'N';
24+
/**
25+
* Include invalid/deprecated concepts. Defaults to `false`, and is supported
26+
* only with `algorithm: 'lexical'` — the embedding index holds valid
27+
* concepts only, so the API answers 400 for the other two rather than
28+
* ignoring the filter.
29+
*/
1030
includeInvalid?: boolean;
31+
/** Include `similarity_score` on each concept. Default true. */
1132
includeScores?: boolean;
33+
/** Include an `explanation` on each concept. Default false. */
1234
includeExplanations?: boolean;
35+
/** Exclude the reference concept from its own results. Default true. */
36+
excludeSelf?: boolean;
1337
}
1438

1539
/**
Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import type { ConceptSummary } from '../../concepts/interfaces/concept.js';
22

33
export interface SimilarConcept extends ConceptSummary {
4-
similarity_score: number;
4+
/**
5+
* Absent when the request set `includeScores: false`, which the API honours
6+
* by omitting the key rather than zeroing it.
7+
*/
8+
similarity_score?: number;
59
domain_id?: string;
610
concept_class_id?: string;
711
standard_concept?: 'S' | 'C' | 'N' | null;
@@ -10,20 +14,59 @@ export interface SimilarConcept extends ConceptSummary {
1014
lexical?: number;
1115
hybrid?: number;
1216
};
17+
/** Present when the request set `includeExplanations: true`. */
1318
explanation?: string;
19+
/**
20+
* @deprecated Duplicate of `explanation`, emitted by the API for one release
21+
* so clients that read the old name keep working. Read `explanation`.
22+
*/
23+
similarity_explanation?: string;
1424
}
1525

1626
export interface SimilarSearchMetadata {
1727
original_query: string;
1828
algorithm_used: 'semantic' | 'lexical' | 'hybrid';
1929
similarity_threshold: number;
30+
/**
31+
* How many concepts cleared `similarity_threshold` inside the bounded
32+
* retrieval pool — not how many were evaluated. The pool holds up to 500 and
33+
* everything below the threshold is discarded before this is counted.
34+
*/
2035
total_candidates: number;
2136
results_returned: number;
2237
processing_time_ms: number;
2338
embedding_latency_ms?: number;
39+
/**
40+
* True when retrieval hit its candidate bound, so `total_candidates` and the
41+
* pagination totals count only what qualified inside the pool that was
42+
* searched, rather than across the whole corpus. Treat them as "at least this
43+
* many".
44+
*/
45+
totals_are_lower_bound?: boolean;
46+
/**
47+
* The algorithm that was asked for, when a fallback served the request
48+
* instead — `hybrid` degrades to `lexical` if the embedding service is
49+
* unavailable. Present only when it differs from `algorithm_used`.
50+
*/
51+
degraded_from?: 'semantic' | 'lexical' | 'hybrid';
52+
/** The concept the search started from, when `conceptId` was supplied. */
53+
source_concept_id?: number;
54+
}
55+
56+
/** The reference concept a similarity search started from. */
57+
export interface SimilarSourceConcept {
58+
concept_id: number;
59+
concept_name: string;
60+
concept_code?: string;
61+
vocabulary_id?: string;
62+
domain_id?: string;
63+
concept_class_id?: string;
64+
standard_concept?: 'S' | 'C' | 'N' | null;
2465
}
2566

2667
export interface SimilarSearchResult {
2768
similar_concepts: SimilarConcept[];
2869
search_metadata: SimilarSearchMetadata;
70+
/** Returned when the search started from a `conceptId`. */
71+
source_concept?: SimilarSourceConcept;
2972
}

0 commit comments

Comments
 (0)