Establishes the kb-embed trait crate so concrete embedding adapters
(p3-2 fastembed, future ollama-embed/candle) target a stable surface.
Pure re-export of kb_core::{Embedder, EmbeddingInput, EmbeddingKind,
EmbeddingModelId, EmbeddingVersion} plus a feature-gated deterministic
mock for downstream tests.
MockEmbedder (cfg(feature = "mock"), default OFF):
- Per-component hash recipe: blake3(seed_le8 || kind_byte ||
text_len_le8 || text || i_le8). Length-prefixed text avoids the
domain-separation ambiguity where two (text, i) pairs could shift
bytes between text tail and the i field.
- Document = 0u8, Query = 1u8 — same text different kind yields
different vectors (mirrors e5 prefix behaviour).
- Per component: blake3 first 8 bytes → u64 → reinterpret as i64 →
f64/i64::MAX → f32. i64::MIN gives -1.0000000000000002 which f32
rounds to -1.0; range [-1, 1] holds.
- L2 unit-normalised. Norm sums in f64 (avoid catastrophic precision
loss) before f32 cast. Zero-norm guard skips the divide.
- with_seed(...) constructor lets two embedders share identity but
produce different vectors — useful for downstream parametric tests.
Helpers:
- assert_vector_shape(vecs, dims) — len + finite check.
- assert_unit_norm(vecs, tolerance) — caller-supplied tolerance;
5e-4 documented as safe for dims=384 under f32 epsilon × √dims.
Tests:
- cargo test -p kb-embed (no features): 2 reexport/dyn-dispatch tests.
- cargo test -p kb-embed --features mock: 7 tests including 100-case
proptest asserting len == dims, all finite, ‖v‖ ≈ 1.0 within
tolerance, Doc(text) byte-equal Doc(text), Doc(text) ≠ Query(text),
Doc(text1) ≠ Doc(text2).
- All 220 workspace tests pass; clippy clean for both default and
mock-on feature configurations.
Symbol gating: nm on the release rlib confirms zero MockEmbedder
symbols under default features; three trait impl symbols under
--features mock. Spec invariant "release builds MUST NOT include
MockEmbedder" verified at the symbol level.
Allowed deps respected: kb-core, kb-config, serde, thiserror, tracing,
plus anyhow (forced by trait return type) and blake3 (justified by
the determinism contract; already in workspace lockfile via kb-core).
No fastembed/ort/tokenizers anywhere.
Out of scope: real adapter (p3-2), reranker traits (P+).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
62 lines
1.9 KiB
Rust
62 lines
1.9 KiB
Rust
//! Compile-only test: verifies the crate's public surface (trait re-exports
|
|
//! and the `assert_vector_shape` helper) is reachable without the `mock`
|
|
//! feature.
|
|
//!
|
|
//! Runs under both `cargo test -p kb-embed` and
|
|
//! `cargo test -p kb-embed --features mock`.
|
|
|
|
use kb_embed::{
|
|
Embedder, EmbeddingInput, EmbeddingKind, EmbeddingModelId, EmbeddingVersion,
|
|
assert_vector_shape,
|
|
};
|
|
|
|
/// A trivial in-test impl that does NOT rely on the `mock` feature — proves
|
|
/// the trait surface alone is enough to write an `Embedder`.
|
|
struct ZeroEmbedder {
|
|
dims: usize,
|
|
}
|
|
|
|
impl Embedder for ZeroEmbedder {
|
|
fn model_id(&self) -> EmbeddingModelId {
|
|
EmbeddingModelId("zero".into())
|
|
}
|
|
fn model_version(&self) -> EmbeddingVersion {
|
|
EmbeddingVersion("0".into())
|
|
}
|
|
fn dimensions(&self) -> usize {
|
|
self.dims
|
|
}
|
|
fn embed(&self, inputs: &[EmbeddingInput<'_>]) -> anyhow::Result<Vec<Vec<f32>>> {
|
|
Ok(inputs.iter().map(|_| vec![0.0; self.dims]).collect())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reexports_compile_without_mock_feature() {
|
|
let e: Box<dyn Embedder> = Box::new(ZeroEmbedder { dims: 4 });
|
|
let inputs = [
|
|
EmbeddingInput {
|
|
text: "hello",
|
|
kind: EmbeddingKind::Document,
|
|
},
|
|
EmbeddingInput {
|
|
text: "world",
|
|
kind: EmbeddingKind::Query,
|
|
},
|
|
];
|
|
let v = e.embed(&inputs).expect("zero embed");
|
|
assert_eq!(v.len(), 2);
|
|
assert_vector_shape(&v, 4);
|
|
}
|
|
|
|
/// Sanity: when built WITHOUT `--features mock`, the `MockEmbedder` symbol
|
|
/// is absent. We can't usefully test `nm` from inside a unit test, but we
|
|
/// can at least confirm the cfg gate parses both ways. See PR notes for the
|
|
/// CI-side `nm`/`cargo bloat` symbol scan.
|
|
#[cfg(not(feature = "mock"))]
|
|
#[test]
|
|
fn mock_feature_off_compiles() {
|
|
// No-op — the test's existence proves the `not(feature = "mock")` gate
|
|
// compiles and the crate is usable without `MockEmbedder`.
|
|
}
|