pub struct RecallService {
index: Option<SqliteBackend>,
embedder: Option<Arc<dyn Embedder>>,
vector: Option<Arc<dyn VectorBackend + Send + Sync>>,
graph: Option<Arc<dyn GraphBackend>>,
scope_filter: Option<SearchFilter>,
timeout_ms: u64,
}Expand description
Service for searching and retrieving memories.
Supports three search modes:
- Text: BM25 full-text search via
SQLiteFTS5 - Vector: Semantic similarity search via embedding + vector backend
- Hybrid: Combines both using Reciprocal Rank Fusion (RRF)
§Graceful Degradation
If embedder or vector backend is unavailable:
SearchMode::Vectorfalls back to empty results with a warningSearchMode::Hybridfalls back to text-only search- No errors are raised; partial results are returned
§Timeout Enforcement (RES-M5)
Search operations respect a configurable timeout (default 5 seconds). If the deadline is exceeded, the search returns partial results or an error.
Fields§
§index: Option<SqliteBackend>SQLite index backend for BM25 text search.
embedder: Option<Arc<dyn Embedder>>Embedder for generating query embeddings (optional).
vector: Option<Arc<dyn VectorBackend + Send + Sync>>Vector backend for similarity search (optional).
graph: Option<Arc<dyn GraphBackend>>Graph backend for entity-based filtering (optional).
scope_filter: Option<SearchFilter>Scope filter applied to every search (e.g., project facets).
timeout_ms: u64Search timeout in milliseconds (RES-M5).
Implementations§
Source§impl RecallService
impl RecallService
Sourcepub const fn new() -> Self
pub const fn new() -> Self
Creates a new recall service without any backends.
This is primarily for testing. Use with_index or
with_backends for production use.
Sourcepub const fn with_index(index: SqliteBackend) -> Self
pub const fn with_index(index: SqliteBackend) -> Self
Creates a recall service with an index backend (text search only).
Vector search will be disabled; hybrid search falls back to text-only.
Sourcepub fn with_backends(
index: SqliteBackend,
embedder: Arc<dyn Embedder>,
vector: Arc<dyn VectorBackend + Send + Sync>,
) -> Self
pub fn with_backends( index: SqliteBackend, embedder: Arc<dyn Embedder>, vector: Arc<dyn VectorBackend + Send + Sync>, ) -> Self
Creates a recall service with full hybrid search support.
§Arguments
index-SQLiteindex backend for BM25 text searchembedder- Embedder for generating query embeddingsvector- Vector backend for similarity search
Sourcepub fn with_embedder(self, embedder: Arc<dyn Embedder>) -> Self
pub fn with_embedder(self, embedder: Arc<dyn Embedder>) -> Self
Adds an embedder to an existing recall service.
Sourcepub fn with_vector(self, vector: Arc<dyn VectorBackend + Send + Sync>) -> Self
pub fn with_vector(self, vector: Arc<dyn VectorBackend + Send + Sync>) -> Self
Adds a vector backend to an existing recall service.
Sourcepub fn with_graph(self, graph: Arc<dyn GraphBackend>) -> Self
pub fn with_graph(self, graph: Arc<dyn GraphBackend>) -> Self
Adds a graph backend for entity-based filtering.
When a graph backend is configured and SearchFilter::entity_names is non-empty,
search results are filtered to only include memories that mention the specified entities.
Sourcepub fn with_scope_filter(self, filter: SearchFilter) -> Self
pub fn with_scope_filter(self, filter: SearchFilter) -> Self
Sets a scope filter that is applied to every search.
This is used to enforce project-scoped searches using project facets while still using a user-level index.
Sourcepub const fn with_timeout_ms(self, timeout_ms: u64) -> Self
pub const fn with_timeout_ms(self, timeout_ms: u64) -> Self
Sets the search timeout in milliseconds (RES-M5).
Default: 5000ms (5 seconds).
§Arguments
timeout_ms- Timeout in milliseconds. Use 0 for no timeout.
Sourcepub const fn timeout_ms(&self) -> u64
pub const fn timeout_ms(&self) -> u64
Returns the configured search timeout in milliseconds.
Sourcepub fn has_vector_search(&self) -> bool
pub fn has_vector_search(&self) -> bool
Returns whether vector search is available.
fn effective_filter<'a>( &'a self, filter: &'a SearchFilter, ) -> Cow<'a, SearchFilter>
Sourcepub fn search(
&self,
query: &str,
mode: SearchMode,
filter: &SearchFilter,
limit: usize,
) -> Result<SearchResult>
pub fn search( &self, query: &str, mode: SearchMode, filter: &SearchFilter, limit: usize, ) -> Result<SearchResult>
Searches for memories matching a query.
§Errors
Returns Error::InvalidInput if:
- The query is empty or contains only whitespace
Returns Error::OperationFailed if:
- No index backend is configured (for
TextandHybridmodes) - The index backend search operation fails
- The search timeout is exceeded (RES-M5)
Sourcefn lazy_tombstone_stale_branches(
&self,
hits: &mut Vec<SearchHit>,
filter: &SearchFilter,
)
fn lazy_tombstone_stale_branches( &self, hits: &mut Vec<SearchHit>, filter: &SearchFilter, )
Processes stale branch memories in search results.
Orchestrates CQS-compliant lazy tombstoning:
- Query: Identifies and marks stale branch memories (pure transformation)
- Command: Persists tombstone status to index (side effect)
- Query: Filters out tombstoned if needed (pure transformation)
This is the entry point that coordinates the separate concerns (ARCH-HIGH-001).
Sourcefn mark_stale_branch_hits(
hits: &mut [SearchHit],
project_id: &str,
) -> Vec<usize>
fn mark_stale_branch_hits( hits: &mut [SearchHit], project_id: &str, ) -> Vec<usize>
Marks stale branch memories as tombstoned in search results.
This is a pure transformation that modifies hits in place. Returns the indices of hits that were tombstoned for later persistence.
CQS: Query - transforms data, no side effects (ARCH-HIGH-001).
Sourcefn persist_tombstones_to_index(&self, hits: &[SearchHit], indices: &[usize])
fn persist_tombstones_to_index(&self, hits: &[SearchHit], indices: &[usize])
Persists tombstone status to the index for the given hit indices.
CQS: Command - performs side effects, no return value (ARCH-HIGH-001).
Sourcefn apply_entity_filter(
&self,
hits: &mut Vec<SearchHit>,
entity_names: &[String],
)
fn apply_entity_filter( &self, hits: &mut Vec<SearchHit>, entity_names: &[String], )
Filters search results to only include memories that mention specified entities.
Uses the graph backend to look up entity mentions. If no graph backend is configured, this is a no-op (graceful degradation).
Sourcefn collect_mentions_for_entity_name(
&self,
graph: &dyn GraphBackend,
entity_name: &str,
) -> Vec<MemoryId>
fn collect_mentions_for_entity_name( &self, graph: &dyn GraphBackend, entity_name: &str, ) -> Vec<MemoryId>
Collects memory IDs that mention entities matching the given name.
fn load_branch_names() -> Option<HashSet<String>>
Sourcepub fn list_all(
&self,
filter: &SearchFilter,
limit: usize,
) -> Result<SearchResult>
pub fn list_all( &self, filter: &SearchFilter, limit: usize, ) -> Result<SearchResult>
Lists all memories, optionally filtered by namespace.
Unlike search, this doesn’t require a query and returns all matching memories.
Returns minimal metadata (id, namespace) without content - details via drill-down.
§Errors
Returns Error::OperationFailed if:
- No index backend is configured
- The index backend list operation fails
- Batch memory retrieval fails
Sourcepub fn list_all_with_content(
&self,
filter: &SearchFilter,
limit: usize,
) -> Result<SearchResult>
pub fn list_all_with_content( &self, filter: &SearchFilter, limit: usize, ) -> Result<SearchResult>
Lists all memories with full content.
Unlike list_all, this variant preserves memory content
for use cases requiring topic extraction or full memory analysis (e.g., statistics).
§Errors
Returns Error::OperationFailed if:
- No index backend is configured
- The index backend list operation fails
- Batch memory retrieval fails
Sourcefn text_search(
&self,
query: &str,
filter: &SearchFilter,
limit: usize,
) -> Result<Vec<SearchHit>>
fn text_search( &self, query: &str, filter: &SearchFilter, limit: usize, ) -> Result<Vec<SearchHit>>
Performs BM25 text search.
Note: Scores are NOT normalized here. Normalization is applied:
- In
hybrid_searchafter RRF fusion - The caller is responsible for normalization in text-only mode
Sourcefn vector_search(
&self,
query: &str,
filter: &SearchFilter,
limit: usize,
) -> Result<Vec<SearchHit>>
fn vector_search( &self, query: &str, filter: &SearchFilter, limit: usize, ) -> Result<Vec<SearchHit>>
Performs vector similarity search.
§Graceful Degradation
Returns empty results (not an error) if:
- Embedder is not configured
- Vector backend is not configured
- Embedding generation fails
- Vector search fails
This allows hybrid search to fall back to text-only.
Sourcefn hybrid_search(
&self,
query: &str,
filter: &SearchFilter,
limit: usize,
) -> Result<Vec<SearchHit>>
fn hybrid_search( &self, query: &str, filter: &SearchFilter, limit: usize, ) -> Result<Vec<SearchHit>>
Performs hybrid search with RRF fusion.
Sourcefn rrf_fusion(
&self,
text_results: &[SearchHit],
vector_results: &[SearchHit],
limit: usize,
) -> Vec<SearchHit>
fn rrf_fusion( &self, text_results: &[SearchHit], vector_results: &[SearchHit], limit: usize, ) -> Vec<SearchHit>
Applies Reciprocal Rank Fusion (RRF) to combine search results.
§Algorithm
RRF is a rank aggregation technique that combines ranked lists from multiple
retrieval systems. For each document d appearing in ranking r:
RRF_score(d) = Σ 1 / (k + rank_r(d))Where:
k= 60 (standard constant, prevents division by zero and dampens high ranks)rank_r(d)= position of documentdin rankingr(1-indexed)
§Why RRF?
- Score normalization: Raw scores from different retrievers (BM25 vs cosine) are not comparable. RRF uses ranks, which are always comparable.
- Robust fusion: Documents ranked highly in multiple systems get boosted.
- Simple and effective: No hyperparameter tuning needed (k=60 works well).
§Example
BM25 results: [doc_A@1, doc_B@2, doc_C@3]
Vector results: [doc_B@1, doc_C@2, doc_D@3]
RRF scores:
- doc_A: 1/(60+1) = 0.0164 (only in BM25)
- doc_B: 1/(60+2) + 1/(60+1) = 0.0161 + 0.0164 = 0.0325 (in both!)
- doc_C: 1/(60+3) + 1/(60+2) = 0.0159 + 0.0161 = 0.0320 (in both)
- doc_D: 1/(60+3) = 0.0159 (only in vector)
Final ranking: [doc_B, doc_C, doc_A, doc_D]§References
- Cormack, G. V., Clarke, C. L., & Buettcher, S. (2009). “Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods”
Sourcepub fn get_by_id(&self, id: &MemoryId) -> Result<Option<Memory>>
pub fn get_by_id(&self, id: &MemoryId) -> Result<Option<Memory>>
Retrieves a memory by ID with full content.
Use this for targeted fetch when full content is needed.
§Errors
Returns Error::OperationFailed if:
- No index backend is configured
- The index backend get operation fails
Sourcepub const fn recent(
&self,
_limit: usize,
_filter: &SearchFilter,
) -> Result<Vec<Memory>>
pub const fn recent( &self, _limit: usize, _filter: &SearchFilter, ) -> Result<Vec<Memory>>
Retrieves recent memories.
§Errors
Returns Error::OperationFailed if:
- No persistence backend is configured
- The persistence backend retrieval fails
§Note
Currently returns empty results as persistence backend integration is pending.
Searches for memories with authorization check (CRIT-006).
This method requires super::auth::Permission::Read to be present in the auth context.
Use this for MCP/HTTP endpoints where authorization is required.
§Arguments
query- The search querymode- Search mode (text, vector, or hybrid)filter- Optional filters for namespace, domain, etc.limit- Maximum number of results to returnauth- Authorization context with permissions
§Errors
Returns Error::Unauthorized if read permission is not granted.
Returns other errors as per search.
Retrieves a memory by ID with authorization check (CRIT-006).
This method requires super::auth::Permission::Read to be present in the auth context.
§Errors
Returns Error::Unauthorized if read permission is not granted.
Trait Implementations§
Auto Trait Implementations§
impl !Freeze for RecallService
impl !RefUnwindSafe for RecallService
impl Send for RecallService
impl Sync for RecallService
impl Unpin for RecallService
impl !UnwindSafe for RecallService
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
§impl<T> FutureExt for T
impl<T> FutureExt for T
§fn with_context(self, otel_cx: Context) -> WithContext<Self>
fn with_context(self, otel_cx: Context) -> WithContext<Self>
§fn with_current_context(self) -> WithContext<Self>
fn with_current_context(self) -> WithContext<Self>
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request§impl<L> LayerExt<L> for L
impl<L> LayerExt<L> for L
§fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>where
L: Layer<S>,
fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>where
L: Layer<S>,
Layered].