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
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ for result in client.search.semantic_iter("chronic kidney disease", page_size=50
print(f"{result['concept_id']}: {result['concept_name']}")
```

### Bulk Search

Search for multiple terms in a single API call — much faster than individual requests:

```python
# Bulk lexical search (up to 50 queries)
results = client.search.bulk_basic([
{"search_id": "q1", "query": "diabetes mellitus"},
{"search_id": "q2", "query": "hypertension"},
{"search_id": "q3", "query": "aspirin"},
], defaults={"vocabulary_ids": ["SNOMED"], "page_size": 5})

for item in results["results"]:
print(f"{item['search_id']}: {len(item['results'])} results")

# Bulk semantic search (up to 25 queries)
results = client.search.bulk_semantic([
{"search_id": "s1", "query": "heart failure treatment options"},
{"search_id": "s2", "query": "type 2 diabetes medication"},
], defaults={"threshold": 0.5, "page_size": 10})
```

### Similarity Search

Find concepts similar to a known concept or natural language query:
Expand Down Expand Up @@ -173,7 +195,7 @@ suggestions = client.concepts.suggest("diab", vocabulary_ids=["SNOMED"], page_si
| Resource | Description | Key Methods |
|----------|-------------|-------------|
| `concepts` | Concept lookup and batch operations | `get()`, `get_by_code()`, `batch()`, `suggest()` |
| `search` | Full-text and semantic search | `basic()`, `advanced()`, `semantic()`, `semantic_iter()`, `similar()`, `fuzzy()` |
| `search` | Full-text and semantic search | `basic()`, `advanced()`, `semantic()`, `similar()`, `bulk_basic()`, `bulk_semantic()` |
| `hierarchy` | Navigate concept relationships | `ancestors()`, `descendants()` |
| `mappings` | Cross-vocabulary mappings | `get()`, `map()` |
| `vocabularies` | Vocabulary metadata | `list()`, `get()`, `stats()` |
Expand Down
117 changes: 117 additions & 0 deletions src/omophub/resources/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
from ..types.common import PaginationMeta
from ..types.concept import Concept
from ..types.search import (
BulkSearchDefaults,
BulkSearchInput,
BulkSearchResponse,
BulkSemanticSearchDefaults,
BulkSemanticSearchInput,
BulkSemanticSearchResponse,
SearchResult,
SemanticSearchResult,
SimilarSearchResult,
Expand Down Expand Up @@ -372,6 +378,77 @@ def fetch_page(

yield from paginate_sync(fetch_page, page_size)

def bulk_basic(
self,
searches: list[BulkSearchInput],
*,
defaults: BulkSearchDefaults | None = None,
) -> BulkSearchResponse:
"""Execute multiple lexical searches in a single request.

Sends up to 50 search queries in one API call. Each search can have
its own filters, or you can set shared defaults.

Args:
searches: List of search inputs, each with a unique ``search_id``
and ``query``. Max 50 items.
defaults: Default filters applied to all searches. Individual
search-level values override defaults.

Returns:
Bulk results with per-search status, results, and timing.

Example::

results = client.search.bulk_basic([
{"search_id": "q1", "query": "diabetes"},
{"search_id": "q2", "query": "hypertension"},
], defaults={"vocabulary_ids": ["SNOMED"], "page_size": 5})

for item in results["results"]:
print(item["search_id"], len(item["results"]))
"""
body: dict[str, Any] = {"searches": searches}
if defaults:
body["defaults"] = defaults
return self._request.post("/search/bulk", json_data=body)

def bulk_semantic(
self,
searches: list[BulkSemanticSearchInput],
*,
defaults: BulkSemanticSearchDefaults | None = None,
) -> BulkSemanticSearchResponse:
"""Execute multiple semantic searches in a single request.

Sends up to 25 natural-language queries in one API call using neural
embeddings. Each search can have its own filters and threshold.

Args:
searches: List of search inputs, each with a unique ``search_id``
and ``query`` (1-500 chars). Max 25 items.
defaults: Default filters applied to all searches. Individual
search-level values override defaults.

Returns:
Bulk results with per-search status, similarity scores, and
optional query enhancements.

Example::

results = client.search.bulk_semantic([
{"search_id": "s1", "query": "heart failure treatment"},
{"search_id": "s2", "query": "type 2 diabetes medication"},
], defaults={"threshold": 0.8, "page_size": 10})

for item in results["results"]:
print(item["search_id"], item.get("result_count", 0))
"""
body: dict[str, Any] = {"searches": searches}
if defaults:
body["defaults"] = defaults
return self._request.post("/search/semantic-bulk", json_data=body)

def similar(
self,
*,
Expand Down Expand Up @@ -630,6 +707,46 @@ async def semantic_iter(

page += 1

async def bulk_basic(
self,
searches: list[BulkSearchInput],
*,
defaults: BulkSearchDefaults | None = None,
) -> BulkSearchResponse:
"""Execute multiple lexical searches in a single request.

Args:
searches: List of search inputs (max 50).
defaults: Default filters for all searches.

Returns:
Bulk results with per-search status and results.
"""
body: dict[str, Any] = {"searches": searches}
if defaults:
body["defaults"] = defaults
return await self._request.post("/search/bulk", json_data=body)

async def bulk_semantic(
self,
searches: list[BulkSemanticSearchInput],
*,
defaults: BulkSemanticSearchDefaults | None = None,
) -> BulkSemanticSearchResponse:
"""Execute multiple semantic searches in a single request.

Args:
searches: List of search inputs (max 25).
defaults: Default filters for all searches.

Returns:
Bulk results with per-search status and similarity scores.
"""
body: dict[str, Any] = {"searches": searches}
if defaults:
body["defaults"] = defaults
return await self._request.post("/search/semantic-bulk", json_data=body)

async def similar(
self,
*,
Expand Down
26 changes: 18 additions & 8 deletions src/omophub/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
RelationshipType,
)
from .search import (
BulkSearchDefaults,
BulkSearchInput,
BulkSearchResponse,
BulkSearchResultItem,
BulkSemanticSearchDefaults,
BulkSemanticSearchInput,
BulkSemanticSearchResponse,
BulkSemanticSearchResultItem,
QueryEnhancement,
SearchFacet,
SearchFacets,
SearchMetadata,
Expand All @@ -53,16 +62,20 @@
)

__all__ = [
# Common
"APIResponse",
# Hierarchy
"Ancestor",
"BatchConceptResult",
# Concept
"BulkSearchDefaults",
"BulkSearchInput",
"BulkSearchResponse",
"BulkSearchResultItem",
"BulkSemanticSearchDefaults",
"BulkSemanticSearchInput",
"BulkSemanticSearchResponse",
"BulkSemanticSearchResultItem",
"Concept",
"ConceptSummary",
"Descendant",
# Domain
"Domain",
"DomainCategory",
"DomainStats",
Expand All @@ -71,20 +84,18 @@
"ErrorResponse",
"HierarchyPath",
"HierarchySummary",
# Mapping
"Mapping",
"MappingContext",
"MappingQuality",
"MappingSummary",
"PaginationMeta",
"PaginationParams",
"QueryEnhancement",
"RelatedConcept",
# Relationship
"Relationship",
"RelationshipSummary",
"RelationshipType",
"ResponseMeta",
# Search
"SearchFacet",
"SearchFacets",
"SearchMetadata",
Expand All @@ -96,7 +107,6 @@
"SimilarSearchResult",
"Suggestion",
"Synonym",
# Vocabulary
"Vocabulary",
"VocabularyDomain",
"VocabularyStats",
Expand Down
108 changes: 107 additions & 1 deletion src/omophub/types/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import TYPE_CHECKING, Any, TypedDict

from typing_extensions import NotRequired
from typing_extensions import NotRequired, Required

if TYPE_CHECKING:
from .concept import Concept
Expand Down Expand Up @@ -77,6 +77,112 @@ class SimilarSearchResult(TypedDict):
search_metadata: SimilarSearchMetadata


# ---------------------------------------------------------------------------
# Bulk search types
# ---------------------------------------------------------------------------


class BulkSearchInput(TypedDict, total=False):
"""Input for a single query in a bulk lexical search."""

search_id: Required[str]
query: Required[str]
vocabulary_ids: list[str]
domain_ids: list[str]
concept_class_ids: list[str]
standard_concept: str
include_invalid: bool
page_size: int


class BulkSearchDefaults(TypedDict, total=False):
"""Default filters applied to all searches in a bulk lexical request."""

vocabulary_ids: list[str]
domain_ids: list[str]
concept_class_ids: list[str]
standard_concept: str
include_invalid: bool
page_size: int


class BulkSearchResultItem(TypedDict):
"""Result for a single query in a bulk lexical search."""

search_id: str
query: str
results: list[dict[str, Any]]
status: str # "completed" | "failed"
error: NotRequired[str]
duration: NotRequired[int]


class BulkSearchResponse(TypedDict):
"""Response from bulk lexical search."""

results: list[BulkSearchResultItem]
total_searches: int
completed_searches: int
failed_searches: int


class BulkSemanticSearchInput(TypedDict, total=False):
"""Input for a single query in a bulk semantic search."""

search_id: Required[str]
query: Required[str] # 1-500 characters
page_size: int
threshold: float
vocabulary_ids: list[str]
domain_ids: list[str]
standard_concept: str
concept_class_id: str


class BulkSemanticSearchDefaults(TypedDict, total=False):
"""Default filters applied to all searches in a bulk semantic request."""

page_size: int
threshold: float
vocabulary_ids: list[str]
domain_ids: list[str]
standard_concept: str
concept_class_id: str


class QueryEnhancement(TypedDict, total=False):
"""Query enhancement info from semantic search."""

original_query: str
enhanced_query: str
abbreviations_expanded: list[str]
misspellings_corrected: list[str]


class BulkSemanticSearchResultItem(TypedDict):
"""Result for a single query in a bulk semantic search."""

search_id: str
query: str
results: list[dict[str, Any]]
status: str # "completed" | "failed"
error: NotRequired[str]
similarity_threshold: NotRequired[float]
result_count: NotRequired[int]
duration: NotRequired[int]
query_enhancement: NotRequired[QueryEnhancement]


class BulkSemanticSearchResponse(TypedDict):
"""Response from bulk semantic search."""

results: list[BulkSemanticSearchResultItem]
total_searches: int
completed_count: int
failed_count: int
total_duration: NotRequired[int]


class SearchFacet(TypedDict):
"""Search facet with count."""

Expand Down
Loading
Loading