Skip to content

Commit 68493ab

Browse files
committed
Refactoring of autocomplete
1 parent 42fe72a commit 68493ab

2 files changed

Lines changed: 77 additions & 41 deletions

File tree

src/bioregistry/app/api.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from fastapi.responses import JSONResponse, Response
1414
from pydantic import BaseModel, Field
1515

16-
from .utils import FORMAT_MAP, _autocomplete, _search
16+
from .utils import FORMAT_MAP, AutocompleteResult, PrefixSearchResult, _autocomplete, _search
1717
from ..export.rdf_export import (
1818
collection_to_rdf_str,
1919
metaresource_to_rdf_str,
@@ -721,34 +721,37 @@ def generate_context_json_ld(
721721
)
722722

723723

724-
@api_router.get("/autocomplete", tags=["search"])
724+
@api_router.get("/autocomplete", tags=["search"], response_model_exclude_none=True)
725725
def autocomplete(
726726
manager: DependsManager,
727727
q: Annotated[str, Query(description="A query for the prefix")],
728-
) -> JSONResponse:
728+
) -> AutocompleteResult:
729729
"""Complete a resolution query."""
730-
return JSONResponse(_autocomplete(manager, q))
730+
return _autocomplete(manager, q)
731731

732732

733-
@api_router.get("/search", tags=["search"])
733+
@api_router.get("/search", tags=["search"], response_model_exclude_none=True)
734734
def search(
735735
manager: DependsManager,
736736
q: Annotated[str, Query(description="A query for the prefix")],
737-
) -> JSONResponse:
737+
) -> list[PrefixSearchResult]:
738738
"""Search for a prefix."""
739-
return JSONResponse(_search(manager, q))
739+
return _search(manager, q)
740740

741741

742742
class ReverseURILookupResults(BaseModel):
743+
"""Results from reverse URI lookup."""
744+
743745
query: str
744746
resources: list[Resource]
745747

746748

747-
@api_router.get("/uri-search", tags=["search"])
749+
@api_router.get("/reverse", tags=["search"])
748750
def reverse_uri_lookup(
749751
manager: DependsManager,
750752
uri_prefix: Annotated[str, Query(description="A URI prefix for lookup")],
751753
) -> ReverseURILookupResults:
754+
"""Lookup a URI, inspired by https://prefix.cc/reverse."""
752755
return ReverseURILookupResults(
753756
query=uri_prefix,
754757
resources=[

src/bioregistry/app/utils.py

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
from __future__ import annotations
44

55
import json
6-
from collections.abc import Callable, Mapping, Sequence
6+
from collections import defaultdict
7+
from collections.abc import Callable, Sequence
78
from functools import partial
8-
from typing import Any, cast
9+
from typing import cast
910

11+
import curies
1012
import werkzeug
1113
import yaml
1214
from flask import (
@@ -71,17 +73,48 @@ def _normalize_prefix_or_404(
7173
return norm_prefix
7274

7375

74-
def _search(manager_: Manager, q: str) -> list[tuple[str, str]]:
75-
q_norm = _norm(q)
76-
results = [
77-
(prefix, lookup if _norm(prefix) != lookup else "")
78-
for lookup, prefix in manager_.synonyms.items()
79-
if q_norm in lookup
76+
class PrefixLookup(BaseModel):
77+
"""A prefix lookup."""
78+
79+
lookup: str
80+
position: int
81+
complete: bool
82+
83+
84+
class PrefixSearchResult(BaseModel):
85+
"""Results from searching for a prefix."""
86+
87+
prefix: str
88+
lookups: list[PrefixLookup]
89+
90+
91+
def _search(manager_: Manager, query: str) -> list[PrefixSearchResult]:
92+
query = _norm(query)
93+
prefix_to_lookups = defaultdict(list)
94+
for lookup, prefix in manager_.synonyms.items():
95+
if query in lookup:
96+
start = lookup.find(query)
97+
prefix_to_lookups[prefix].append(
98+
PrefixLookup(lookup=lookup, position=start, complete=query == lookup)
99+
)
100+
return [
101+
PrefixSearchResult(prefix=prefix, lookups=lookups)
102+
for prefix, lookups in prefix_to_lookups.items()
80103
]
81-
return sorted(results)
82104

83105

84-
def _autocomplete(manager_: Manager, q: str, base_url: str | None = None) -> Mapping[str, Any]:
106+
class AutocompleteResult(BaseModel):
107+
"""Results from autocomplete."""
108+
109+
query: str
110+
success: bool
111+
reason: str
112+
results: curies.Reference | list[PrefixSearchResult]
113+
url: str | None = None
114+
pattern: str | None = None
115+
116+
117+
def _autocomplete(manager_: Manager, q: str, base_url: str | None = None) -> AutocompleteResult:
85118
r"""Run the autocomplete algorithm.
86119
87120
:param manager_: A manager
@@ -130,23 +163,23 @@ def _autocomplete(manager_: Manager, q: str, base_url: str | None = None) -> Map
130163
else:
131164
reason = "searched prefix"
132165
url = None
133-
return {
134-
"query": q,
135-
"results": _search(manager_, q),
136-
"success": True,
137-
"reason": reason,
138-
"url": url,
139-
}
166+
return AutocompleteResult(
167+
query=q,
168+
success=True,
169+
reason=reason,
170+
url=url,
171+
results=_search(manager_, q),
172+
)
173+
140174
prefix, identifier = q.split(":", 1)
141175
resource = manager_.get_resource(prefix)
142176
if resource is None:
143-
return {
144-
"query": q,
145-
"prefix": prefix,
146-
"identifier": identifier,
147-
"success": False,
148-
"reason": "bad prefix",
149-
}
177+
return AutocompleteResult(
178+
query=q,
179+
success=False,
180+
reason="bad prefix",
181+
results=curies.Reference(prefix=prefix, identifier=identifier),
182+
)
150183
pattern = manager_.get_pattern(prefix)
151184
if pattern is None:
152185
success = True
@@ -162,15 +195,15 @@ def _autocomplete(manager_: Manager, q: str, base_url: str | None = None) -> Map
162195
success = False
163196
reason = "failed validation"
164197
url = None
165-
return {
166-
"query": q,
167-
"prefix": prefix,
168-
"pattern": pattern,
169-
"identifier": identifier,
170-
"success": success,
171-
"reason": reason,
172-
"url": url,
173-
}
198+
199+
return AutocompleteResult(
200+
query=q,
201+
success=success,
202+
reason=reason,
203+
url=url,
204+
results=curies.Reference(prefix=prefix, identifier=identifier),
205+
pattern=pattern,
206+
)
174207

175208

176209
def serialize(

0 commit comments

Comments
 (0)