Skip to main content

lc_embeddings/local/
mod.rs

1// lc-embeddings/src/local/mod.rs
2//! Local embedding implementations
3//!
4//! Contains two implementations:
5//! - `BagOfWordsEmbeddings`: Lightweight word-frequency hash embedding (pure Rust, no external deps), always available
6//! - `LocalEmbeddings`: ONNX Runtime-based neural network embedding (requires `local-embeddings` feature)
7//!
8//! `BagOfWordsEmbeddings` is suitable for offline, privacy, zero-cost coarse-grained retrieval;
9//! `LocalEmbeddings` is suitable for high-quality semantic embedding scenarios (e.g., BGE/E5 models).
10
11use async_trait::async_trait;
12
13#[cfg(feature = "local-embeddings")]
14use std::path::Path;
15
16use crate::{EmbeddingError, Embeddings};
17
18// ---------------------------------------------------------------------------
19// BagOfWordsEmbeddings — word-frequency hash + L2 normalization (always available)
20// ---------------------------------------------------------------------------
21
22/// Lightweight local embedding (word-frequency hash + L2 normalization)
23///
24/// Based on word frequency + hashing, no API calls, suitable for offline, privacy, zero-cost coarse-grained retrieval.
25///
26/// Note: This is a lightweight implementation (bag-of-words hash) with limited semantic quality;
27/// for high-quality neural network embeddings (BGE/E5 via `ort`), enable the `local-embeddings` feature
28/// and use [`LocalEmbeddings`].
29pub struct BagOfWordsEmbeddings {
30    dim: usize,
31}
32
33impl BagOfWordsEmbeddings {
34    /// Create local embedding with specified dimension
35    pub fn new(dim: usize) -> Self {
36        Self { dim: dim.max(1) }
37    }
38
39    /// Default dimension 256
40    pub fn default_dim() -> Self {
41        Self::new(256)
42    }
43
44    /// Tokenize: English by non-alphanumeric split (lowercased), Chinese/non-ASCII by single character
45    fn tokenize(text: &str) -> Vec<String> {
46        let mut tokens = Vec::new();
47        let mut current = String::new();
48        for c in text.chars() {
49            if c.is_alphanumeric() {
50                if c.is_ascii() {
51                    current.push(c.to_ascii_lowercase());
52                } else {
53                    // Non-ASCII (Chinese etc.) single character as token
54                    if !current.is_empty() {
55                        tokens.push(std::mem::take(&mut current));
56                    }
57                    tokens.push(c.to_string());
58                }
59            } else if !current.is_empty() {
60                tokens.push(std::mem::take(&mut current));
61            }
62        }
63        if !current.is_empty() {
64            tokens.push(current);
65        }
66        tokens
67    }
68
69    /// FNV-1a hash
70    fn hash(s: &str) -> u64 {
71        let mut h: u64 = 0xcbf29ce484222325;
72        for b in s.bytes() {
73            h ^= b as u64;
74            h = h.wrapping_mul(0x100000001b3);
75        }
76        h
77    }
78
79    /// Compute embedding vector (word-frequency hash + L2 normalization)
80    fn embed(&self, text: &str) -> Vec<f32> {
81        let mut v = vec![0.0f32; self.dim];
82        for token in Self::tokenize(text) {
83            let idx = (Self::hash(&token) as usize) % self.dim;
84            v[idx] += 1.0;
85        }
86        // L2 normalization
87        let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
88        if norm > 0.0 {
89            for x in &mut v {
90                *x /= norm;
91            }
92        }
93        v
94    }
95}
96
97impl Default for BagOfWordsEmbeddings {
98    fn default() -> Self {
99        Self::default_dim()
100    }
101}
102
103#[async_trait]
104impl Embeddings for BagOfWordsEmbeddings {
105    async fn embed_query(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
106        if text.trim().is_empty() {
107            return Err(EmbeddingError::EmptyInput);
108        }
109        Ok(self.embed(text))
110    }
111
112    fn dimension(&self) -> usize {
113        self.dim
114    }
115
116    fn model_name(&self) -> &str {
117        "local-bow"
118    }
119}
120
121// ---------------------------------------------------------------------------
122// LocalEmbeddings — ONNX Runtime neural network embedding (requires local-embeddings feature)
123// ---------------------------------------------------------------------------
124
125#[cfg(feature = "local-embeddings")]
126mod nn;
127
128// When local-embeddings feature is enabled, re-export LocalEmbeddings and its builder
129#[cfg(feature = "local-embeddings")]
130pub use nn::{LocalEmbeddings, LocalEmbeddingsBuilder};
131
132// ---------------------------------------------------------------------------
133// Backward compatibility: LocalEmbeddings without feature points to BagOfWordsEmbeddings
134// ---------------------------------------------------------------------------
135
136/// Without the `local-embeddings` feature, `LocalEmbeddings` is a type alias for `BagOfWordsEmbeddings`,
137/// maintaining backward compatibility.
138///
139/// With the `local-embeddings` feature enabled, `LocalEmbeddings` becomes the ONNX Runtime-based neural network implementation.
140///
141/// P2-1: 消除静默降级。无 feature 时 `LocalEmbeddings` 静默退化为词袋哈希嵌入,
142/// 用户以为在用语义向量、实际是词频——"好像能用,但不对"。这里加
143/// `#[deprecated]` 让降级在编译期可见:使用者需显式改用 `BagOfWordsEmbeddings`,
144/// 或开启 `local-embeddings` feature 使用真正的 ONNX 神经嵌入。
145#[cfg(not(feature = "local-embeddings"))]
146#[deprecated(
147    note = "LocalEmbeddings without the `local-embeddings` feature degrades to \
148            BagOfWordsEmbeddings (bag-of-words hash), not semantic neural embedding. \
149            Enable the `local-embeddings` feature, or use BagOfWordsEmbeddings explicitly."
150)]
151pub type LocalEmbeddings = BagOfWordsEmbeddings;
152
153// ---------------------------------------------------------------------------
154// Tests
155// ---------------------------------------------------------------------------
156
157#[cfg(test)]
158mod tests;