API reference¶
Generated from the apogee-ai-search source with mkdocstrings. Every symbol below is exported from apogee_ai_search, so it is part of the supported public surface.
Application · DTOs¶
BenchDTO
dataclass
¶
IndexDTO
dataclass
¶
QueryDTO
dataclass
¶
Application · Use cases¶
BenchSearchUseCase
¶
Synthetic corpus + queries: measure indexing + search throughput.
execute
async
¶
Source code in apogee_ai_search/application/use_cases/bench_search_use_case.py
Python
async def execute(self, documents: int, queries: int) -> dict[str, float]:
if documents <= 0 or queries <= 0:
raise ValueError("documents and queries must be positive")
rng = random.Random(42)
index = InMemoryInvertedIndex()
docs = []
for i in range(documents):
topic = _TOPICS[i % len(_TOPICS)]
docs.append(SearchDocument(
id=f"d-{i:05d}",
text=f"Documento {i}: {topic}. " + ("blah " * 30),
))
idx_start = time.perf_counter()
await index.add(docs)
idx_elapsed = (time.perf_counter() - idx_start) * 1000.0
searcher = BM25Searcher(index)
hit_count = 0
q_start = time.perf_counter()
for q in range(queries):
topic = _TOPICS[q % len(_TOPICS)]
words = topic.split()
rng.shuffle(words)
result = await searcher.search(SearchQuery(
text=" ".join(words[: max(2, len(words) // 2)]),
k=5,
))
hit_count += len(result.hits)
q_elapsed = (time.perf_counter() - q_start) * 1000.0
return {
"documents": float(documents),
"queries": float(queries),
"index_ms": idx_elapsed,
"queries_ms": q_elapsed,
"queries_per_second": (queries / q_elapsed * 1000.0) if q_elapsed > 0 else 0.0,
"avg_hits": hit_count / queries,
}
BulkIndexUseCase
¶
Indexes documents in batches.
Source code in apogee_ai_search/application/use_cases/bulk_index_use_case.py
execute
async
¶
Python
execute(documents: Iterable[SearchDocument]) -> int
Source code in apogee_ai_search/application/use_cases/bulk_index_use_case.py
IndexDocumentsUseCase
¶
Source code in apogee_ai_search/application/use_cases/index_documents_use_case.py
execute
async
¶
Python
execute(documents: Iterable[SearchDocument]) -> int
SearchUseCase
¶
Source code in apogee_ai_search/application/use_cases/search_use_case.py
execute
async
¶
Python
execute(query: SearchQuery) -> SearchResult
Domain¶
SearchDocument
dataclass
¶
SearchHit
dataclass
¶
Python
SearchHit(document: SearchDocument, score: float, metadata: dict[str, str] = dict())
metadata
class-attribute
instance-attribute
¶
SearchQuery
dataclass
¶
SearchResult
dataclass
¶
Python
SearchResult(query: str, hits: tuple[SearchHit, ...] = (), searcher: str = 'bm25', latency_ms: float = 0.0, metadata: dict[str, str] = dict())
metadata
class-attribute
instance-attribute
¶
Domain · Enums¶
IndexKind
¶
Bases: str, Enum
SearcherKind
¶
Domain · Exceptions¶
IndexNotReadyError
¶
Bases: SearchError
SearchError
¶
Bases: Exception
Base for apogee-ai-search errors.
Domain · Protocols (ports)¶
IIndex
¶
Bases: Protocol
add
async
¶
Python
add(documents: Iterable[SearchDocument]) -> None
remove
async
¶
clear
async
¶
count
async
¶
ISearcher
¶
Bases: Protocol
search
async
¶
Python
search(query: SearchQuery) -> SearchResult
Infrastructure¶
BM25Searcher
¶
Python
BM25Searcher(index: InMemoryInvertedIndex)
Source code in apogee_ai_search/infrastructure/searchers/bm25_searcher.py
search
async
¶
Python
search(query: SearchQuery) -> SearchResult
Source code in apogee_ai_search/infrastructure/searchers/bm25_searcher.py
Python
async def search(self, query: SearchQuery) -> SearchResult:
start = time.perf_counter()
q_tokens = tokenize(query.text)
scored: list[SearchHit] = []
for doc in self._index.documents():
if query.filters and not all(
doc.metadata.get(k) == v for k, v in query.filters.items()
):
continue
score = self._index.score_bm25(q_tokens, doc.id)
if score > 0:
scored.append(SearchHit(document=doc, score=score))
scored.sort(key=lambda h: h.score, reverse=True)
elapsed = (time.perf_counter() - start) * 1000.0
return SearchResult(
query=query.text,
hits=tuple(scored[: query.k]),
searcher=self.name,
latency_ms=elapsed,
)
HybridSearcher
¶
Reciprocal Rank Fusion across multiple searchers.
Source code in apogee_ai_search/infrastructure/searchers/hybrid_searcher.py
search
async
¶
Python
search(query: SearchQuery) -> SearchResult
Source code in apogee_ai_search/infrastructure/searchers/hybrid_searcher.py
Python
async def search(self, query: SearchQuery) -> SearchResult:
start = time.perf_counter()
scores: dict[str, float] = {}
docs: dict[str, object] = {}
contributions: dict[str, int] = {}
for searcher in self._searchers:
try:
result = await searcher.search(query)
except Exception: # noqa: BLE001
continue
for rank, hit in enumerate(result.hits, 1):
doc_id = hit.document.id
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (self._k + rank)
docs.setdefault(doc_id, hit.document)
contributions[doc_id] = contributions.get(doc_id, 0) + 1
merged = sorted(
(
SearchHit(
document=docs[doc_id],
score=score,
metadata={"contributors": str(contributions[doc_id])},
)
for doc_id, score in scores.items()
),
key=lambda h: h.score,
reverse=True,
)
elapsed = (time.perf_counter() - start) * 1000.0
return SearchResult(
query=query.text,
hits=tuple(merged[: query.k]),
searcher=self.name,
latency_ms=elapsed,
metadata={"searchers": str(len(self._searchers))},
)
InMemoryInvertedIndex
¶
In-memory inverted index supporting BM25 retrieval over text field.
Source code in apogee_ai_search/infrastructure/indexes/inverted_index.py
add
async
¶
Python
add(documents: Iterable[SearchDocument]) -> None
remove
async
¶
clear
async
¶
count
async
¶
score_bm25
¶
Source code in apogee_ai_search/infrastructure/indexes/inverted_index.py
Python
def score_bm25(self, query_tokens: list[str], doc_id: str) -> float:
tokens = self._tokens.get(doc_id, [])
if not tokens:
return 0.0
tf = Counter(tokens)
dl = len(tokens) or 1
score = 0.0
for term in query_tokens:
if term not in tf:
continue
f = tf[term]
idf = self._idf(term)
num = f * (self.k1 + 1)
den = f + self.k1 * (1 - self.b + self.b * dl / (self._avg_dl or 1))
score += idf * num / den
return score
documents
¶
Python
documents() -> list[SearchDocument]
get
¶
Python
get(doc_id: str) -> SearchDocument | None
InMemoryVectorIndex
¶
Cosine search over an in-memory dict of documents with vector.
Source code in apogee_ai_search/infrastructure/indexes/vector_index.py
add
async
¶
Python
add(documents: Iterable[SearchDocument]) -> None
remove
async
¶
clear
async
¶
count
async
¶
score
¶
documents
¶
Python
documents() -> list[SearchDocument]
get
¶
Python
get(doc_id: str) -> SearchDocument | None
VectorSearcher
¶
Python
VectorSearcher(index: InMemoryVectorIndex)
Source code in apogee_ai_search/infrastructure/searchers/vector_searcher.py
search
async
¶
Python
search(query: SearchQuery) -> SearchResult
Source code in apogee_ai_search/infrastructure/searchers/vector_searcher.py
Python
async def search(self, query: SearchQuery) -> SearchResult:
if not query.vector:
raise SearchError("vector searcher needs query.vector")
start = time.perf_counter()
scored: list[SearchHit] = []
for doc in self._index.documents():
if query.filters and not all(
doc.metadata.get(k) == v for k, v in query.filters.items()
):
continue
score = self._index.score(query.vector, doc.id)
scored.append(SearchHit(document=doc, score=score))
scored.sort(key=lambda h: h.score, reverse=True)
elapsed = (time.perf_counter() - start) * 1000.0
return SearchResult(
query=query.text,
hits=tuple(scored[: query.k]),
searcher=self.name,
latency_ms=elapsed,
)