Source code for corerec.retrieval.base

"""
Base classes for retrieval stage.

Retrievers are responsible for quickly narrowing down millions of items
to hundreds of candidates. Speed matters more than precision here.
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union
import numpy as np


[docs] @dataclass class Candidate: """A single candidate item from retrieval.""" #: unique identifier for the item item_id: Any #: retrieval score (higher = more relevant) score: float #: which retriever produced this candidate source: str = "unknown" #: optional extra info (embeddings, features, etc) metadata: Dict[str, Any] = field(default_factory=dict) def __lt__(self, other: "Candidate") -> bool: # for sorting - higher score = better return self.score < other.score def __repr__(self) -> str: return f"Candidate(id={self.item_id}, score={self.score:.4f}, src={self.source})"
[docs] @dataclass class RetrievalResult: """Result from a retrieval operation.""" #: list of retrieved candidates candidates: List[Candidate] #: identifier for the query (user_id, session_id, etc) query_id: Any = None #: name of the retriever that produced this retriever_name: str = "unknown" #: how long retrieval took in milliseconds timing_ms: float = 0.0 def __len__(self) -> int: return len(self.candidates) def __iter__(self): return iter(self.candidates)
[docs] def top_k(self, k: int) -> List[Candidate]: """Return top k candidates by score.""" return sorted(self.candidates, reverse=True)[:k]
[docs] def item_ids(self) -> List[Any]: """Extract just the item IDs.""" return [c.item_id for c in self.candidates]
[docs] def scores(self) -> np.ndarray: """Extract scores as numpy array.""" return np.array([c.score for c in self.candidates])
[docs] class BaseRetriever(ABC): """ Abstract base class for all retrievers. A retriever takes a query (user, context, or text) and returns a set of candidate items. Subclasses implement different retrieval strategies: collaborative filtering, semantic search, popularity-based, etc. Design goals: - Fast: O(log n) or O(1) lookup, not O(n) - Recall-focused: better to include irrelevant than miss relevant - Composable: multiple retrievers can be ensembled """ def __init__(self, name: Optional[str] = None): self.name = name or self.__class__.__name__ self._is_fitted = False @property def is_fitted(self) -> bool: return self._is_fitted
[docs] @abstractmethod def fit(self, **kwargs) -> "BaseRetriever": """ Fit the retriever on training data. What "fit" means depends on the retriever type: - CollaborativeRetriever: build item-item similarity matrix - SemanticRetriever: encode items into embeddings - PopularityRetriever: count item frequencies Returns self for method chaining. """ pass
[docs] @abstractmethod def retrieve( self, query: Any, top_k: int = 100, **kwargs ) -> RetrievalResult: """ Retrieve candidate items for a query. Args: query: the query to retrieve for. Can be: - user_id for collaborative retrieval - text string for semantic retrieval - embedding vector for ANN search top_k: number of candidates to return **kwargs: retriever-specific parameters Returns: RetrievalResult containing ranked candidates """ pass
[docs] def retrieve_batch( self, queries: List[Any], top_k: int = 100, **kwargs ) -> List[RetrievalResult]: """ Retrieve for multiple queries. Default impl just loops. Subclasses can override for batched efficiency. """ return [self.retrieve(q, top_k, **kwargs) for q in queries]
def _check_fitted(self) -> None: """Raise if not fitted.""" if not self._is_fitted: raise RuntimeError( f"{self.name} must be fitted before retrieval. Call fit() first." ) def __repr__(self) -> str: status = "fitted" if self._is_fitted else "not fitted" return f"{self.__class__.__name__}(name={self.name}, {status})"