Skip to main content

reflex_rs/
encoder.rs

1//! Zero-dependency 384-dimensional dense semantic subword vector encoder.
2//! 100% mathematical parity with Python reflex.embeddings and JavaScript @reflex-ai/sdk.
3
4pub const VECTOR_DIM: usize = 384;
5
6/// Pure Rust RFC 1321 MD5 message-digest implementation with zero external dependencies.
7#[derive(Clone, Debug)]
8pub struct Md5 {
9    state: [u32; 4],
10    count: [u32; 2],
11    buffer: [u8; 64],
12}
13
14impl Default for Md5 {
15    fn default() -> Self {
16        Self::new()
17    }
18}
19
20impl Md5 {
21    pub fn new() -> Self {
22        Self {
23            state: [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476],
24            count: [0, 0],
25            buffer: [0u8; 64],
26        }
27    }
28
29    pub fn update(&mut self, input: &[u8]) {
30        let mut index = ((self.count[0] >> 3) & 0x3f) as usize;
31        let input_len = input.len();
32
33        let shift = (input_len as u32) << 3;
34        self.count[0] = self.count[0].wrapping_add(shift);
35        if self.count[0] < shift {
36            self.count[1] = self.count[1].wrapping_add(1);
37        }
38        self.count[1] = self.count[1].wrapping_add((input_len as u32) >> 29);
39
40        let part_len = 64 - index;
41        let mut i = 0;
42
43        if input_len >= part_len {
44            self.buffer[index..index + part_len].copy_from_slice(&input[0..part_len]);
45            let buf = self.buffer;
46            self.transform(&buf);
47
48            i = part_len;
49            while i + 63 < input_len {
50                let mut chunk = [0u8; 64];
51                chunk.copy_from_slice(&input[i..i + 64]);
52                self.transform(&chunk);
53                i += 64;
54            }
55            index = 0;
56        }
57
58        if i < input_len {
59            self.buffer[index..index + (input_len - i)].copy_from_slice(&input[i..input_len]);
60        }
61    }
62
63    pub fn finalize(mut self) -> [u8; 16] {
64        let mut bits = [0u8; 8];
65        for i in 0..4 {
66            bits[i] = ((self.count[0] >> (i * 8)) & 0xff) as u8;
67            bits[i + 4] = ((self.count[1] >> (i * 8)) & 0xff) as u8;
68        }
69
70        let index = ((self.count[0] >> 3) & 0x3f) as usize;
71        let pad_len = if index < 56 { 56 - index } else { 120 - index };
72        let mut padding = [0u8; 64];
73        padding[0] = 0x80;
74
75        self.update(&padding[..pad_len]);
76        self.update(&bits);
77
78        let mut digest = [0u8; 16];
79        for i in 0..4 {
80            digest[i * 4] = (self.state[i] & 0xff) as u8;
81            digest[i * 4 + 1] = ((self.state[i] >> 8) & 0xff) as u8;
82            digest[i * 4 + 2] = ((self.state[i] >> 16) & 0xff) as u8;
83            digest[i * 4 + 3] = ((self.state[i] >> 24) & 0xff) as u8;
84        }
85        digest
86    }
87
88    pub fn digest(data: &[u8]) -> [u8; 16] {
89        let mut md5 = Self::new();
90        md5.update(data);
91        md5.finalize()
92    }
93
94    fn transform(&mut self, block: &[u8; 64]) {
95        let mut a = self.state[0];
96        let mut b = self.state[1];
97        let mut c = self.state[2];
98        let mut d = self.state[3];
99        let mut x = [0u32; 16];
100
101        for i in 0..16 {
102            let j = i * 4;
103            x[i] = (block[j] as u32)
104                | ((block[j + 1] as u32) << 8)
105                | ((block[j + 2] as u32) << 16)
106                | ((block[j + 3] as u32) << 24);
107        }
108
109        #[inline(always)]
110        fn f(x: u32, y: u32, z: u32) -> u32 {
111            (x & y) | (!x & z)
112        }
113        #[inline(always)]
114        fn g(x: u32, y: u32, z: u32) -> u32 {
115            (x & z) | (y & !z)
116        }
117        #[inline(always)]
118        fn h(x: u32, y: u32, z: u32) -> u32 {
119            x ^ y ^ z
120        }
121        #[inline(always)]
122        fn i_func(x: u32, y: u32, z: u32) -> u32 {
123            y ^ (x | !z)
124        }
125        #[inline(always)]
126        fn rotl(x: u32, n: u32) -> u32 {
127            (x << n) | (x >> (32 - n))
128        }
129
130        macro_rules! step {
131            ($func:ident, $a:expr, $b:expr, $c:expr, $d:expr, $k:expr, $s:expr, $t:expr) => {
132                $a = $a.wrapping_add($func($b, $c, $d).wrapping_add(x[$k]).wrapping_add($t));
133                $a = rotl($a, $s).wrapping_add($b);
134            };
135        }
136
137        // Round 1
138        step!(f, a, b, c, d, 0, 7, 0xd76aa478);
139        step!(f, d, a, b, c, 1, 12, 0xe8c7b756);
140        step!(f, c, d, a, b, 2, 17, 0x242070db);
141        step!(f, b, c, d, a, 3, 22, 0xc1bdceee);
142        step!(f, a, b, c, d, 4, 7, 0xf57c0faf);
143        step!(f, d, a, b, c, 5, 12, 0x4787c62a);
144        step!(f, c, d, a, b, 6, 17, 0xa8304613);
145        step!(f, b, c, d, a, 7, 22, 0xfd469501);
146        step!(f, a, b, c, d, 8, 7, 0x698098d8);
147        step!(f, d, a, b, c, 9, 12, 0x8b44f7af);
148        step!(f, c, d, a, b, 10, 17, 0xffff5bb1);
149        step!(f, b, c, d, a, 11, 22, 0x895cd7be);
150        step!(f, a, b, c, d, 12, 7, 0x6b901122);
151        step!(f, d, a, b, c, 13, 12, 0xfd987193);
152        step!(f, c, d, a, b, 14, 17, 0xa679438e);
153        step!(f, b, c, d, a, 15, 22, 0x49b40821);
154
155        // Round 2
156        step!(g, a, b, c, d, 1, 5, 0xf61e2562);
157        step!(g, d, a, b, c, 6, 9, 0xc040b340);
158        step!(g, c, d, a, b, 11, 14, 0x265e5a51);
159        step!(g, b, c, d, a, 0, 20, 0xe9b6c7aa);
160        step!(g, a, b, c, d, 5, 5, 0xd62f105d);
161        step!(g, d, a, b, c, 10, 9, 0x02441453);
162        step!(g, c, d, a, b, 15, 14, 0xd8a1e681);
163        step!(g, b, c, d, a, 4, 20, 0xe7d3fbc8);
164        step!(g, a, b, c, d, 9, 5, 0x21e1cde6);
165        step!(g, d, a, b, c, 14, 9, 0xc33707d6);
166        step!(g, c, d, a, b, 3, 14, 0xf4d50d87);
167        step!(g, b, c, d, a, 8, 20, 0x455a14ed);
168        step!(g, a, b, c, d, 13, 5, 0xa9e3e905);
169        step!(g, d, a, b, c, 2, 9, 0xfcefa3f8);
170        step!(g, c, d, a, b, 7, 14, 0x676f02d9);
171        step!(g, b, c, d, a, 12, 20, 0x8d2a4c8a);
172
173        // Round 3
174        step!(h, a, b, c, d, 5, 4, 0xfffa3942);
175        step!(h, d, a, b, c, 8, 11, 0x8771f681);
176        step!(h, c, d, a, b, 11, 16, 0x6d9d6122);
177        step!(h, b, c, d, a, 14, 23, 0xfde5380c);
178        step!(h, a, b, c, d, 1, 4, 0xa4beea44);
179        step!(h, d, a, b, c, 4, 11, 0x4bdecfa9);
180        step!(h, c, d, a, b, 7, 16, 0xf6bb4b60);
181        step!(h, b, c, d, a, 10, 23, 0xbebfbc70);
182        step!(h, a, b, c, d, 13, 4, 0x289b7ec6);
183        step!(h, d, a, b, c, 0, 11, 0xeaa127fa);
184        step!(h, c, d, a, b, 3, 16, 0xd4ef3085);
185        step!(h, b, c, d, a, 6, 23, 0x04881d05);
186        step!(h, a, b, c, d, 9, 4, 0xd9d4d039);
187        step!(h, d, a, b, c, 12, 11, 0xe6db99e5);
188        step!(h, c, d, a, b, 15, 16, 0x1fa27cf8);
189        step!(h, b, c, d, a, 2, 23, 0xc4ac5665);
190
191        // Round 4
192        step!(i_func, a, b, c, d, 0, 6, 0xf4292244);
193        step!(i_func, d, a, b, c, 7, 10, 0x432aff97);
194        step!(i_func, c, d, a, b, 14, 15, 0xab9423a7);
195        step!(i_func, b, c, d, a, 5, 21, 0xfc93a039);
196        step!(i_func, a, b, c, d, 12, 6, 0x655b59c3);
197        step!(i_func, d, a, b, c, 3, 10, 0x8f0ccc92);
198        step!(i_func, c, d, a, b, 10, 15, 0xffeff47d);
199        step!(i_func, b, c, d, a, 1, 21, 0x85845dd1);
200        step!(i_func, a, b, c, d, 8, 6, 0x6fa87e4f);
201        step!(i_func, d, a, b, c, 15, 10, 0xfe2ce6e0);
202        step!(i_func, c, d, a, b, 6, 15, 0xa3014314);
203        step!(i_func, b, c, d, a, 13, 21, 0x4e0811a1);
204        step!(i_func, a, b, c, d, 4, 6, 0xf7537e82);
205        step!(i_func, d, a, b, c, 11, 10, 0xbd3af235);
206        step!(i_func, c, d, a, b, 2, 15, 0x2ad7d2bb);
207        step!(i_func, b, c, d, a, 9, 21, 0xeb86d391);
208
209        self.state[0] = self.state[0].wrapping_add(a);
210        self.state[1] = self.state[1].wrapping_add(b);
211        self.state[2] = self.state[2].wrapping_add(c);
212        self.state[3] = self.state[3].wrapping_add(d);
213    }
214}
215
216/// Computes the dot product (cosine similarity) between two unit-normalized vectors.
217pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
218    assert_eq!(a.len(), b.len(), "Vector lengths must match");
219    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
220}
221
222/// Zero-dependency 384-dimensional dense semantic subword vector encoder.
223#[derive(Clone, Debug, Default)]
224pub struct SemanticVectorEncoder;
225
226impl SemanticVectorEncoder {
227    pub fn new() -> Self {
228        Self
229    }
230
231    /// Encodes arbitrary text into an L2-normalized 384-dimensional vector.
232    pub fn encode(&self, text: &str) -> [f32; VECTOR_DIM] {
233        let mut vec = [0.0f32; VECTOR_DIM];
234        let lower = text.to_lowercase();
235        
236        // Extract words matching \w+
237        let tokens: Vec<&str> = lower
238            .split(|c: char| !(c.is_alphanumeric() || c == '_'))
239            .filter(|s| !s.is_empty())
240            .collect();
241
242        if tokens.is_empty() {
243            return vec;
244        }
245
246        // 1. Unigram & Bigram hashing
247        for (i, &tok) in tokens.iter().enumerate() {
248            Self::hash_into_vector(tok, &mut vec, 1.0);
249            if i + 1 < tokens.len() {
250                let bigram = format!("{}_{}", tok, tokens[i + 1]);
251                Self::hash_into_vector(&bigram, &mut vec, 1.4);
252            }
253        }
254
255        // 2. Subword 3-char n-grams for typo & morphology resilience
256        for &tok in &tokens {
257            if tok.len() >= 3 {
258                for j in 0..=tok.len() - 3 {
259                    let sub = &tok[j..j + 3];
260                    let sub_key = format!("sub_{}", sub);
261                    Self::hash_into_vector(&sub_key, &mut vec, 0.5);
262                }
263            }
264        }
265
266        // 3. L2 Unit Normalization
267        let sum_sq: f32 = vec.iter().map(|x| x * x).sum();
268        let norm = sum_sq.sqrt();
269        if norm > 1e-9 {
270            for val in vec.iter_mut() {
271                *val /= norm;
272            }
273        }
274
275        vec
276    }
277
278    fn hash_into_vector(token: &str, vec: &mut [f32; VECTOR_DIM], weight: f32) {
279        let digest = Md5::digest(token.as_bytes());
280        // Extract first 6 bytes as big-endian 48-bit integer
281        let h = ((digest[0] as u64) << 40)
282            | ((digest[1] as u64) << 32)
283            | ((digest[2] as u64) << 24)
284            | ((digest[3] as u64) << 16)
285            | ((digest[4] as u64) << 8)
286            | (digest[5] as u64);
287
288        let idx = (h % (VECTOR_DIM as u64)) as usize;
289        let sign = if (h >> 16) % 2 == 0 { 1.0f32 } else { -1.0f32 };
290        vec[idx] += sign * weight;
291    }
292}