RecallService

Struct RecallService 

Source
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 SQLite FTS5
  • 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::Vector falls back to empty results with a warning
  • SearchMode::Hybrid falls 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: u64

Search timeout in milliseconds (RES-M5).

Implementations§

Source§

impl RecallService

Source

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.

Source

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.

Source

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 - SQLite index backend for BM25 text search
  • embedder - Embedder for generating query embeddings
  • vector - Vector backend for similarity search
Source

pub fn with_embedder(self, embedder: Arc<dyn Embedder>) -> Self

Adds an embedder to an existing recall service.

Source

pub fn with_vector(self, vector: Arc<dyn VectorBackend + Send + Sync>) -> Self

Adds a vector backend to an existing recall service.

Source

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.

Source

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.

Source

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.
Source

pub const fn timeout_ms(&self) -> u64

Returns the configured search timeout in milliseconds.

Returns whether vector search is available.

Source

fn effective_filter<'a>( &'a self, filter: &'a SearchFilter, ) -> Cow<'a, SearchFilter>

Source

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 Text and Hybrid modes)
  • The index backend search operation fails
  • The search timeout is exceeded (RES-M5)
Source

fn lazy_tombstone_stale_branches( &self, hits: &mut Vec<SearchHit>, filter: &SearchFilter, )

Processes stale branch memories in search results.

Orchestrates CQS-compliant lazy tombstoning:

  1. Query: Identifies and marks stale branch memories (pure transformation)
  2. Command: Persists tombstone status to index (side effect)
  3. Query: Filters out tombstoned if needed (pure transformation)

This is the entry point that coordinates the separate concerns (ARCH-HIGH-001).

Source

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).

Source

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).

Source

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).

Source

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.

Source

fn load_branch_names() -> Option<HashSet<String>>

Source

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
Source

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

Performs BM25 text search.

Note: Scores are NOT normalized here. Normalization is applied:

  • In hybrid_search after RRF fusion
  • The caller is responsible for normalization in text-only mode

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.

Performs hybrid search with RRF fusion.

Source

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 document d in ranking r (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”
Source

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
Source

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.

Source

pub fn search_authorized( &self, query: &str, mode: SearchMode, filter: &SearchFilter, limit: usize, auth: &AuthContext, ) -> Result<SearchResult>

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 query
  • mode - Search mode (text, vector, or hybrid)
  • filter - Optional filters for namespace, domain, etc.
  • limit - Maximum number of results to return
  • auth - Authorization context with permissions
§Errors

Returns Error::Unauthorized if read permission is not granted. Returns other errors as per search.

Source

pub fn get_by_id_authorized( &self, id: &MemoryId, auth: &AuthContext, ) -> Result<Option<Memory>>

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§

Source§

impl Default for RecallService

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> FutureExt for T

§

fn with_context(self, otel_cx: Context) -> WithContext<Self>

Attaches the provided Context to this type, returning a WithContext wrapper. Read more
§

fn with_current_context(self) -> WithContext<Self>

Attaches the current Context to this type, returning a WithContext wrapper. Read more
§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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

§

fn into_request(self) -> Request<T>

Wrap the input message T in a tonic::Request
§

impl<L> LayerExt<L> for L

§

fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>
where L: Layer<S>,

Applies the layer to a service and wraps it in [Layered].
§

impl<T> Pointable for T

§

const ALIGN: usize

The alignment of pointer.
§

type Init = T

The type for initializers.
§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more