pub struct OkfVectorStore<S: OkfStore> { /* private fields */ }Expand description
Vectors kept beside the records, in the same store the OKF Markdown uses.
One document per record, holding the text hash and the vector as base64
f16. That encoding is worth explaining, because it is the largest thing
this type adds to a deployment and an earlier revision wrote it the obvious
way — lowercase hex f32 — at three times the size.
| encoding | vector bytes | per record | at 16,000 | × the Markdown |
|---|---|---|---|---|
hex f32 (what this used to write) | 6,144 | 6,161 | 98.6 MB | 4.75× |
base64 f32 | 4,096 | 4,113 | 65.8 MB | 3.17× |
hex f16 | 3,072 | 3,089 | 49.4 MB | 2.38× |
base64 f16 (what ships) | 2,048 | 2,065 | 33.0 MB | 1.59× |
The Markdown column compares against the records these annotate, which
memory_at_scale measures at 20,259 KiB for the same 16,000. Hex f32 made
the vectors nearly five times the corpus they describe; base64 f16 makes
them about one and a half.
§Why f16 is free here, measured rather than assumed
f16 keeps 10 mantissa bits against f32’s 23, so the obvious worry is
that a lossy store quietly degrades retrieval. It does not, and the reason
is that PrecomputedSemanticIndex reads these floats for exactly two
things with very different sensitivities:
- The packed scan is sign bits, and
f16preserves sign. Over 920,832 coordinates of a real corpus, zero flipped — so the shortlist the rerank sees is bit-identical, not merely similar. The one way it could flip is a coordinate belowf16’s smallest subnormal (2⁻²⁴ ≈ 6e-8) underflowing to-0.0, which the packer reads as non-negative; three coordinates underflowed on that corpus and all three were positive. The probe asserts on the count rather than trusting the argument. - The rerank is a dot product, where rounding could reorder candidates. Over 93 questions it did not move the answer’s rank once.
tests/storage_encoding_probe.rs runs both stores through this crate’s own
search_vector and reports identical top-1 (64/93), top-5 (71/93) and MRR
(0.727), and identical fused numbers against BM25 at 2:1 (65/93, 79/93,
0.760). Worst coordinate movement is 1.2e-4. Five questions reorder within
the top five, all among non-answers.
So this is a 3× saving for no measured quality cost — but the measurement is
on one 1,199-record corpus at 768 dimensions, and the property it rests on
is that embedding coordinates sit comfortably inside f16’s normal range.
A model whose vectors are not L2-normalised, or are far wider, deserves a
re-run of that probe before the same conclusion is assumed.
§Reading what an older version wrote
A payload tagged f16b64: is base64 f16; anything else is the legacy
lowercase-hex f32, and is decoded as such. That matters more than a format
flag usually does: without it, upgrading would invalidate every stored
vector and re-embedding a 16,000-record corpus is over an hour of wall
clock. Existing stores therefore keep their old size until each record is
next rewritten, which happens when its text changes.
What all of this buys is the whole reason to pay it: without persistence a restart re-embeds every record at 259 ms each, which is over an hour at this corpus size, on every deploy and every replica.
Implementations§
Trait Implementations§
Source§impl<S: OkfStore> VectorStore for OkfVectorStore<S>
impl<S: OkfStore> VectorStore for OkfVectorStore<S>
Source§fn load<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<(MemoryId, String, Vec<f32>)>, MemoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn load<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<(MemoryId, String, Vec<f32>)>, MemoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
(id, text hash, vector).Source§fn save<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
id: &'life1 MemoryId,
hash: &'life2 str,
vector: &'life3 [f32],
) -> Pin<Box<dyn Future<Output = Result<(), MemoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
fn save<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
id: &'life1 MemoryId,
hash: &'life2 str,
vector: &'life3 [f32],
) -> Pin<Box<dyn Future<Output = Result<(), MemoryError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Auto Trait Implementations§
impl<S> Freeze for OkfVectorStore<S>
impl<S> RefUnwindSafe for OkfVectorStore<S>where
S: RefUnwindSafe,
impl<S> Send for OkfVectorStore<S>
impl<S> Sync for OkfVectorStore<S>
impl<S> Unpin for OkfVectorStore<S>
impl<S> UnwindSafe for OkfVectorStore<S>where
S: RefUnwindSafe,
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].