pub struct PrecomputedSemanticIndex { /* private fields */ }Expand description
A semantic index built ahead of time and searched in process.
Implementations§
Source§impl PrecomputedSemanticIndex
impl PrecomputedSemanticIndex
Sourcepub async fn build(
records: &[CanonicalMemory],
embedder: Arc<dyn Embedder>,
) -> Result<Self, MemoryError>
pub async fn build( records: &[CanonicalMemory], embedder: Arc<dyn Embedder>, ) -> Result<Self, MemoryError>
Embed a corpus and build the index.
Only active records are indexed: a superseded fact should not be retrievable by paraphrase when it is not retrievable by name.
Each record is embedded as embedding_text renders it — the statement
plus its frontmatter as prose — which is the text that measured best by
a wide margin. Passing anything else is the single easiest way to lose
most of what the semantic layer is worth.
Sourcepub fn from_vectors(
vectors: Vec<(MemoryId, String, Vec<f32>)>,
embedder: Arc<dyn Embedder>,
) -> Self
pub fn from_vectors( vectors: Vec<(MemoryId, String, Vec<f32>)>, embedder: Arc<dyn Embedder>, ) -> Self
Build from vectors that were embedded elsewhere.
The path for a caller that already batches its embedding — concurrently,
or in a nightly job — rather than awaiting one record at a time as
build does.
Each entry is (id, the text that was embedded, the vector). The text is
required rather than convenient: the index hashes it so that
SemanticFallback::reconcile can tell an unchanged record from one
whose wording has moved. Without it every reconcile would re-embed the
whole corpus, which is the cost this type exists to avoid.
Sourcepub async fn restore(
store: Arc<dyn VectorStore>,
embedder: Arc<dyn Embedder>,
) -> Result<Self, MemoryError>
pub async fn restore( store: Arc<dyn VectorStore>, embedder: Arc<dyn Embedder>, ) -> Result<Self, MemoryError>
Keep vectors in store, and load whatever it already holds.
This is the constructor a long-lived process wants. Without it every start pays one embedding round trip per record — 259 ms each, so an hour and a quarter at 16,000 records before the first semantic answer, again on every deploy and every replica.
A stored vector is only trusted while the text that produced it is
unchanged; SemanticFallback::reconcile checks the hash and
re-embeds anything that has moved. So a restore is a fast start, never a
stale one.
Sourcepub fn with_store(self, store: Arc<dyn VectorStore>) -> Self
pub fn with_store(self, store: Arc<dyn VectorStore>) -> Self
Attach a store to an index built in memory.
Sourcepub fn without_rerank(self) -> Self
pub fn without_rerank(self) -> Self
Drop the float vectors, keeping only the packed codes.
Trades about one question in 93 for roughly 24× less memory — 2 MB against 49 MB at 16,000 records. Worth it when the index is resident per user and there are many users; not worth it otherwise.
Sourcepub fn bytes_per_record(&self) -> usize
pub fn bytes_per_record(&self) -> usize
Bytes held per record, packed codes plus float vectors if reranking.
Exposed because the memory figure is the reason to quantize at all, and a number a caller can assert on is more useful than a claim in a doc comment.
Trait Implementations§
Source§impl SemanticFallback for PrecomputedSemanticIndex
impl SemanticFallback for PrecomputedSemanticIndex
Source§fn reconcile<'life0, 'life1, 'async_trait>(
&'life0 self,
active: &'life1 [(MemoryId, String)],
revision: u64,
) -> Pin<Box<dyn Future<Output = Result<(), MemoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn reconcile<'life0, 'life1, 'async_trait>(
&'life0 self,
active: &'life1 [(MemoryId, String)],
revision: u64,
) -> Pin<Box<dyn Future<Output = Result<(), MemoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Bring the index in line with the active corpus.
Idempotent by construction: active is the whole desired state, so this
embeds the ids it does not hold, drops the ids no longer present, and
leaves the rest alone. Calling it twice costs one pass over a hash set
the second time.
Only genuinely new records are embedded, which is the difference between a correction costing one 259 ms round trip and costing one per record in the corpus. The lock is not held across any of those awaits — embedding happens first, and the index is only taken for the swap at the end — so a recall running concurrently sees either the old set or the new one and never blocks on the network.
Source§fn search<'life0, 'life1, 'async_trait>(
&'life0 self,
query: &'life1 str,
limit: usize,
) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryId>, MemoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn search<'life0, 'life1, 'async_trait>(
&'life0 self,
query: &'life1 str,
limit: usize,
) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryId>, MemoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Embed the query, then scan.
The embed is the only network call on this path, and on the interactive budget it is almost certainly too slow — 259 ms measured against 10 ms. The retriever bounds it with a timeout and treats a miss as “no semantic opinion”, so an over-budget embedder degrades to lexical results rather than delaying the turn. That is a real degradation, not a free one: with a remote embedder the semantic layer effectively only runs on the speculative path, and only if that budget is raised past the round trip.
Auto Trait Implementations§
impl !Freeze for PrecomputedSemanticIndex
impl !RefUnwindSafe for PrecomputedSemanticIndex
impl Send for PrecomputedSemanticIndex
impl Sync for PrecomputedSemanticIndex
impl Unpin for PrecomputedSemanticIndex
impl !UnwindSafe for PrecomputedSemanticIndex
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<S> FromSample<S> for S
impl<S> FromSample<S> for S
fn from_sample_(s: S) -> S
§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> 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].