1use super::entangled::{hash_u64, EntangledHVec, DEFAULT_RHO_DENOM};
42use serde::{Deserialize, Serialize};
43use std::cell::RefCell;
44
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
46pub enum TextEncoder {
47 #[default]
48 #[serde(rename = "multiscale-words-v1")]
49 MultiscaleWordsV1,
50 #[serde(rename = "multiscale-capped-union-v1")]
52 MultiscaleCappedUnionV1,
53 #[serde(rename = "local-embed-v1")]
56 LocalEmbedV1,
57 #[serde(rename = "multiscale-words-v2")]
59 MultiscaleWordsV2,
60}
61
62impl TextEncoder {
63 pub fn name(self) -> &'static str {
64 match self {
65 Self::MultiscaleWordsV1 => "multiscale-words-v1",
66 Self::MultiscaleCappedUnionV1 => "multiscale-capped-union-v1",
67 Self::LocalEmbedV1 => "local-embed-v1",
68 Self::MultiscaleWordsV2 => "multiscale-words-v2",
69 }
70 }
71
72 pub fn is_embedding(self) -> bool {
74 self == Self::LocalEmbedV1
75 }
76
77 pub(crate) fn encode(self, text: &str, dim: usize) -> EntangledHVec {
78 assert!(
79 !self.is_embedding(),
80 "embedding encoders are applied to embeddings by the engine, not to text"
81 );
82 ENCODE_STATE.with(|state| encode_cached(text, dim, self, &mut state.borrow_mut()))
83 }
84
85 #[doc(hidden)]
88 pub fn encode_text(self, text: &str, dim: usize) -> EntangledHVec {
89 self.encode(text, dim)
90 }
91
92 #[doc(hidden)]
96 pub fn encode_reference(self, text: &str, dim: usize) -> EntangledHVec {
97 assert!(
98 !self.is_embedding(),
99 "embedding encoders are applied to embeddings by the engine, not to text"
100 );
101 encode_reference(text, dim, self)
102 }
103
104 fn bundle(self, vectors: &[EntangledHVec]) -> EntangledHVec {
105 match self {
106 Self::MultiscaleWordsV1 | Self::LocalEmbedV1 => EntangledHVec::bundle(vectors),
107 Self::MultiscaleCappedUnionV1 => EntangledHVec::bundle_bloom(vectors),
108 Self::MultiscaleWordsV2 => {
109 let dim = vectors.first().map_or(0, |v| v.dim);
110 let mut counts = std::collections::BTreeMap::<u32, usize>::new();
111 for vector in vectors {
112 for &index in vector.indices() {
113 *counts.entry(index).or_default() += 1;
114 }
115 }
116 let threshold = vectors.len().div_ceil(2);
117 let mut selected: Vec<_> = counts
118 .into_iter()
119 .filter(|&(_, count)| count >= threshold)
120 .collect();
121 selected.sort_unstable_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
122 selected.truncate((dim / DEFAULT_RHO_DENOM).max(1));
123 let mut indices: Vec<_> = selected.into_iter().map(|(index, _)| index).collect();
124 indices.sort_unstable();
125 EntangledHVec::from_indices(indices, dim)
126 }
127 }
128 }
129
130 fn bundles_by_union(self) -> bool {
132 self == Self::MultiscaleCappedUnionV1
133 }
134}
135
136impl std::str::FromStr for TextEncoder {
137 type Err = anyhow::Error;
138
139 fn from_str(name: &str) -> Result<Self, Self::Err> {
140 match name {
141 "multiscale-words-v1" => Ok(Self::MultiscaleWordsV1),
142 "multiscale-words-v2" => Ok(Self::MultiscaleWordsV2),
143 "multiscale-capped-union-v1" => Ok(Self::MultiscaleCappedUnionV1),
144 "local-embed-v1" => Ok(Self::LocalEmbedV1),
145 _ => anyhow::bail!("unknown text encoder {name:?}"),
146 }
147 }
148}
149
150const CHAR_NGRAM_SIZES: [usize; 4] = [2, 3, 4, 5];
174
175const MAX_CHAR_NGRAM: usize = 5;
177const _: () = assert!(CHAR_NGRAM_SIZES[CHAR_NGRAM_SIZES.len() - 1] == MAX_CHAR_NGRAM);
178
179const LEVEL_CHAR: u64 = 0x01;
181const LEVEL_WORD: u64 = 0x02;
182const LEVEL_PHRASE: u64 = 0x03;
183
184const TERM_ARENA_BUDGET: usize = 1 << 20;
187
188const NGRAM_ARENA_BUDGET: usize = 1 << 21;
190
191const COUNTING_RATIO: usize = 8;
194
195const SPARSE_COUNTING_MAX_DIM: usize = 1 << 16;
198
199pub fn encode_text_internal(text: &str, dim: usize) -> EntangledHVec {
200 TextEncoder::MultiscaleWordsV1.encode(text, dim)
201}
202
203thread_local! {
204 static ENCODE_STATE: RefCell<EncodeState> = RefCell::new(EncodeState::default());
205}
206
207#[derive(Clone, Copy)]
209struct Span {
210 start: usize,
211 len: usize,
212}
213
214impl Span {
215 fn range(self) -> std::ops::Range<usize> {
216 self.start..self.start + self.len
217 }
218}
219
220#[derive(Clone, Copy, PartialEq, Eq, Hash)]
223struct NgramKey {
224 n: u8,
225 chars: [char; MAX_CHAR_NGRAM],
226}
227
228#[derive(Default)]
232struct EncodeState {
233 dim: usize,
234 terms: crate::core::hash::FastMap<u64, Span>,
236 term_arena: Vec<u32>,
237 ngrams: crate::core::hash::FastMap<NgramKey, Span>,
240 ngram_arena: Vec<u32>,
241 chars: Vec<char>,
242 flat: Vec<u32>,
244 acc: Vec<u32>,
245 acc_next: Vec<u32>,
246 rotated: Vec<u32>,
247 counts: Vec<u32>,
248 selected: Vec<(u32, u32)>,
249}
250
251impl EncodeState {
252 fn prepare(&mut self, dim: usize) {
254 if self.dim != dim {
255 self.dim = dim;
256 self.terms.clear();
257 self.term_arena.clear();
258 self.ngrams.clear();
259 self.ngram_arena.clear();
260 }
261 }
262
263 fn ngram_span(&mut self, n: usize, window: &[char]) -> Span {
265 let mut key = NgramKey {
266 n: n as u8,
267 chars: ['\0'; MAX_CHAR_NGRAM],
268 };
269 key.chars[..n].copy_from_slice(window);
270 if let Some(&span) = self.ngrams.get(&key) {
271 return span;
272 }
273 let dim = self.dim;
274 let first = term_span(
275 &mut self.terms,
276 &mut self.term_arena,
277 dim,
278 seeded(LEVEL_CHAR, n as u64, window[0] as u64),
279 );
280 self.acc.clear();
282 self.acc.extend_from_slice(&self.term_arena[first.range()]);
283 for (i, &c) in window.iter().enumerate().skip(1) {
284 let span = term_span(
285 &mut self.terms,
286 &mut self.term_arena,
287 dim,
288 seeded(LEVEL_CHAR, n as u64, c as u64),
289 );
290 rotate_sorted(
291 &self.term_arena[span.range()],
292 (i % dim) as u32,
293 dim as u32,
294 &mut self.rotated,
295 );
296 symmetric_difference(&self.acc, &self.rotated, &mut self.acc_next);
297 std::mem::swap(&mut self.acc, &mut self.acc_next);
298 }
299 if self.ngram_arena.len() + self.acc.len() > NGRAM_ARENA_BUDGET {
300 self.ngrams.clear();
301 self.ngram_arena.clear();
302 }
303 let span = Span {
304 start: self.ngram_arena.len(),
305 len: self.acc.len(),
306 };
307 self.ngram_arena.extend_from_slice(&self.acc);
308 self.ngrams.insert(key, span);
309 span
310 }
311
312 fn reset_counts(&mut self, dim: usize) {
314 if self.counts.len() != dim {
315 self.counts.clear();
316 self.counts.resize(dim, 0);
317 }
318 debug_assert!(self.counts.iter().all(|&c| c == 0));
319 }
320
321 fn bundle_flat(&mut self, count: usize, encoder: TextEncoder) -> EntangledHVec {
324 let dim = self.dim;
325 if self.flat.is_empty() {
326 return EntangledHVec::from_indices(Vec::new(), dim);
327 }
328 let union = encoder.bundles_by_union();
329 let threshold = if union { 1 } else { (count as u32).div_ceil(2) };
330 self.selected.clear();
331 let dense = dim <= self.flat.len().saturating_mul(COUNTING_RATIO);
332 if !union && self.flat.len() < dim && (dense || dim <= SPARSE_COUNTING_MAX_DIM) {
333 self.reset_counts(dim);
339 for &idx in &self.flat {
340 self.counts[idx as usize] += 1;
341 }
342 for &idx in &self.flat {
343 let n = std::mem::take(&mut self.counts[idx as usize]);
344 if n >= threshold {
345 self.selected.push((idx, n));
346 }
347 }
348 self.selected.sort_unstable_by_key(|&(idx, _)| idx);
349 } else if dense {
350 self.reset_counts(dim);
351 for &idx in &self.flat {
352 self.counts[idx as usize] += 1;
353 }
354 for (idx, slot) in self.counts.iter_mut().enumerate() {
355 let n = std::mem::take(slot);
356 if n >= threshold {
357 self.selected.push((idx as u32, n));
358 }
359 }
360 } else {
361 self.flat.sort_unstable();
362 let mut current = self.flat[0];
363 let mut n: u32 = 1;
364 for &idx in &self.flat[1..] {
365 if idx == current {
366 n += 1;
367 } else {
368 if n >= threshold {
369 self.selected.push((current, n));
370 }
371 current = idx;
372 n = 1;
373 }
374 }
375 if n >= threshold {
376 self.selected.push((current, n));
377 }
378 }
379 if union {
380 let indices: Vec<u32> = self.selected.iter().map(|&(idx, _)| idx).collect();
381 return EntangledHVec::from_indices(indices, dim);
382 }
383 let target_count = (dim / DEFAULT_RHO_DENOM).max(1);
384 if self.selected.len() > target_count {
386 self.selected
387 .select_nth_unstable_by(target_count - 1, |a, b| {
388 let frequency = b.1.cmp(&a.1);
389 if encoder == TextEncoder::MultiscaleWordsV2 {
390 frequency.then_with(|| a.0.cmp(&b.0))
391 } else {
392 frequency
393 }
394 });
395 self.selected.truncate(target_count);
396 }
397 let mut indices: Vec<u32> = self.selected.iter().map(|&(idx, _)| idx).collect();
398 indices.sort_unstable();
399 EntangledHVec::from_indices(indices, dim)
400 }
401}
402
403fn term_span(
406 terms: &mut crate::core::hash::FastMap<u64, Span>,
407 arena: &mut Vec<u32>,
408 dim: usize,
409 seed: u64,
410) -> Span {
411 if let Some(&span) = terms.get(&seed) {
412 return span;
413 }
414 let vector = EntangledHVec::new_deterministic(dim, seed);
415 let indices = vector.indices();
416 if arena.len() + indices.len() > TERM_ARENA_BUDGET {
417 terms.clear();
418 arena.clear();
419 }
420 let span = Span {
421 start: arena.len(),
422 len: indices.len(),
423 };
424 arena.extend_from_slice(indices);
425 terms.insert(seed, span);
426 span
427}
428
429fn rotate_sorted(base: &[u32], shift: u32, dim: u32, out: &mut Vec<u32>) {
433 out.clear();
434 if shift == 0 {
435 out.extend_from_slice(base);
436 return;
437 }
438 let wrap = dim - shift;
439 let split = base.partition_point(|&idx| idx < wrap);
440 out.extend(base[split..].iter().map(|&idx| idx - wrap));
441 out.extend(base[..split].iter().map(|&idx| idx + shift));
442}
443
444fn symmetric_difference(a: &[u32], b: &[u32], out: &mut Vec<u32>) {
446 out.clear();
447 let (mut i, mut j) = (0, 0);
448 while i < a.len() && j < b.len() {
449 match a[i].cmp(&b[j]) {
450 std::cmp::Ordering::Less => {
451 out.push(a[i]);
452 i += 1;
453 }
454 std::cmp::Ordering::Greater => {
455 out.push(b[j]);
456 j += 1;
457 }
458 std::cmp::Ordering::Equal => {
459 i += 1;
460 j += 1;
461 }
462 }
463 }
464 out.extend_from_slice(&a[i..]);
465 out.extend_from_slice(&b[j..]);
466}
467
468fn encode_cached(
471 text: &str,
472 dim: usize,
473 encoder: TextEncoder,
474 state: &mut EncodeState,
475) -> EntangledHVec {
476 let text = text.trim();
477 if text.is_empty() {
478 return EntangledHVec::from_indices(Vec::new(), dim);
479 }
480 state.prepare(dim);
481 state.chars.clear();
482 state.chars.extend(text.chars());
483 let words: Vec<&str> = text.split_whitespace().collect();
484 let mut level_bundles: Vec<EntangledHVec> = Vec::with_capacity(CHAR_NGRAM_SIZES.len() + 2);
485
486 for &n in &CHAR_NGRAM_SIZES {
488 let windows = match state.chars.len().checked_sub(n) {
489 Some(rest) => rest + 1,
490 None => continue,
491 };
492 state.flat.clear();
493 for start in 0..windows {
494 let mut window = ['\0'; MAX_CHAR_NGRAM];
495 window[..n].copy_from_slice(&state.chars[start..start + n]);
496 let span = state.ngram_span(n, &window[..n]);
497 state
498 .flat
499 .extend_from_slice(&state.ngram_arena[span.range()]);
500 }
501 level_bundles.push(state.bundle_flat(windows, encoder));
502 }
503
504 let d = dim as u32;
505
506 if !words.is_empty() {
508 state.flat.clear();
509 for (pos, word) in words.iter().enumerate() {
510 let span = term_span(
511 &mut state.terms,
512 &mut state.term_arena,
513 dim,
514 hash_str_seed(word, LEVEL_WORD),
515 );
516 let shift = (pos % dim) as u32;
517 state.flat.extend(
518 state.term_arena[span.range()]
519 .iter()
520 .map(|&idx| (idx + shift) % d),
521 );
522 }
523 level_bundles.push(state.bundle_flat(words.len(), encoder));
524 }
525
526 if words.len() >= 2 {
528 state.flat.clear();
529 for (pos, pair) in words.windows(2).enumerate() {
530 let left = term_span(
531 &mut state.terms,
532 &mut state.term_arena,
533 dim,
534 hash_str_seed(pair[0], LEVEL_PHRASE),
535 );
536 state.acc_next.clear();
538 state
539 .acc_next
540 .extend_from_slice(&state.term_arena[left.range()]);
541 let right = term_span(
542 &mut state.terms,
543 &mut state.term_arena,
544 dim,
545 hash_str_seed(pair[1], LEVEL_PHRASE),
546 );
547 rotate_sorted(
548 &state.term_arena[right.range()],
549 (1 % dim) as u32,
550 d,
551 &mut state.rotated,
552 );
553 symmetric_difference(&state.acc_next, &state.rotated, &mut state.acc);
554 let shift = (pos % dim) as u32;
555 state
556 .flat
557 .extend(state.acc.iter().map(|&idx| (idx + shift) % d));
558 }
559 level_bundles.push(state.bundle_flat(words.len() - 1, encoder));
560 }
561
562 if level_bundles.is_empty() {
563 return EntangledHVec::new_deterministic(dim, seeded(LEVEL_CHAR, 1, state.chars[0] as u64));
564 }
565 combine_levels(&level_bundles, dim)
566}
567
568fn encode_reference(text: &str, dim: usize, encoder: TextEncoder) -> EntangledHVec {
570 let text = text.trim();
571 if text.is_empty() {
572 return EntangledHVec::from_indices(vec![], dim);
573 }
574
575 let chars: Vec<char> = text.chars().collect();
576 let words: Vec<&str> = text.split_whitespace().collect();
577
578 let mut level_bundles: Vec<EntangledHVec> = Vec::new();
581
582 for &n in &CHAR_NGRAM_SIZES {
584 if chars.len() < n {
585 continue;
586 }
587 let ngrams: Vec<EntangledHVec> = chars
588 .windows(n)
589 .map(|window| {
590 let mut chunk = EntangledHVec::new_deterministic(
591 dim,
592 seeded(LEVEL_CHAR, n as u64, window[0] as u64),
593 )
594 .permute(0);
595 for (i, &c) in window.iter().enumerate().skip(1) {
596 let next = EntangledHVec::new_deterministic(
597 dim,
598 seeded(LEVEL_CHAR, n as u64, c as u64),
599 )
600 .permute(i);
601 chunk = chunk.bind(&next);
602 }
603 chunk
604 })
605 .collect();
606 level_bundles.push(encoder.bundle(&ngrams));
607 }
608
609 if !words.is_empty() {
611 let word_vecs: Vec<EntangledHVec> = words
612 .iter()
613 .enumerate()
614 .map(|(pos, word)| {
615 let word_hash = hash_str_seed(word, LEVEL_WORD);
616 EntangledHVec::new_deterministic(dim, word_hash).permute(pos)
617 })
618 .collect();
619 level_bundles.push(encoder.bundle(&word_vecs));
620 }
621
622 if words.len() >= 2 {
624 let phrase_vecs: Vec<EntangledHVec> = words
625 .windows(2)
626 .enumerate()
627 .map(|(pos, pair)| {
628 let h0 = hash_str_seed(pair[0], LEVEL_PHRASE);
629 let h1 = hash_str_seed(pair[1], LEVEL_PHRASE);
630 let v0 = EntangledHVec::new_deterministic(dim, h0).permute(0);
631 let v1 = EntangledHVec::new_deterministic(dim, h1).permute(1);
632 v0.bind(&v1).permute(pos)
633 })
634 .collect();
635 level_bundles.push(encoder.bundle(&phrase_vecs));
636 }
637
638 if level_bundles.is_empty() {
639 return EntangledHVec::new_deterministic(dim, seeded(LEVEL_CHAR, 1, chars[0] as u64));
640 }
641 combine_levels(&level_bundles, dim)
642}
643
644fn combine_levels(level_bundles: &[EntangledHVec], dim: usize) -> EntangledHVec {
649 let active_count = (dim / DEFAULT_RHO_DENOM).max(1);
650 let mut freq: crate::core::hash::FastMap<u32, u32> =
651 crate::core::hash::FastMap::with_capacity_and_hasher(active_count * 4, Default::default());
652 for bundle in level_bundles {
653 for &idx in bundle.indices() {
654 *freq.entry(idx).or_insert(0) += 1;
655 }
656 }
657
658 let mut scored: Vec<(u32, u32)> = freq.into_iter().collect();
659 if scored.len() > active_count {
660 scored.select_nth_unstable_by(active_count - 1, |a, b| {
661 b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0))
662 });
663 scored.truncate(active_count);
664 }
665
666 let mut indices: Vec<u32> = scored.into_iter().map(|(idx, _)| idx).collect();
667 indices.sort_unstable();
668 EntangledHVec::from_indices(indices, dim)
669}
670
671fn seeded(level: u64, scale: u64, item: u64) -> u64 {
672 hash_u64(hash_u64(level, scale), item)
673}
674
675pub(crate) fn hash_str_seed(s: &str, level: u64) -> u64 {
676 let mut h = level;
677 for &byte in s.as_bytes() {
678 h = hash_u64(h, byte as u64);
679 }
680 h
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 #[test]
688 fn words_v2_majority_uses_frequency_then_coordinate_independent_of_input_order() {
689 for dim in [256, 65_537] {
690 for repeated in [false, true] {
691 let target = (dim / DEFAULT_RHO_DENOM).max(1);
692 let mut even: Vec<_> = (0..target + 2).map(|i| (2 * i) as u32).collect();
693 let mut odd: Vec<_> = (0..target + 2).map(|i| (2 * i + 1) as u32).collect();
694 if repeated {
695 even.push(dim as u32 - 1);
696 odd.push(dim as u32 - 1);
697 }
698 let first = EntangledHVec::from_indices(even, dim);
699 let second = EntangledHVec::from_indices(odd, dim);
700 let vectors = vec![first, second];
701 let expected = TextEncoder::MultiscaleWordsV2.bundle(&vectors);
702 let mut correct: Vec<_> = (0..target - usize::from(repeated))
703 .map(|i| i as u32)
704 .collect();
705 if repeated {
706 correct.push(dim as u32 - 1);
707 }
708 assert_eq!(expected.indices(), correct);
709 for reverse in [false, true] {
710 let mut state = EncodeState {
711 dim,
712 ..Default::default()
713 };
714 state.flat = vectors
715 .iter()
716 .flat_map(|v| v.indices().iter().copied())
717 .collect();
718 if reverse {
719 state.flat.reverse();
720 }
721 assert_eq!(
722 state
723 .bundle_flat(vectors.len(), TextEncoder::MultiscaleWordsV2)
724 .indices(),
725 expected.indices()
726 );
727 }
728 }
729 }
730 }
731
732 #[test]
733 fn test_empty_input() {
734 let v = encode_text_internal("", 16384);
735 assert!(v.indices().is_empty());
736 }
737
738 #[test]
739 fn test_morphological_similarity() {
740 let dim = 16384;
741 let cat = encode_text_internal("cat", dim);
742 let cats = encode_text_internal("cats", dim);
743 let dog = encode_text_internal("dog", dim);
744 let sim_cat_cats = cat.similarity(&cats);
745 let sim_cat_dog = cat.similarity(&dog);
746 assert!(
747 sim_cat_cats > sim_cat_dog,
748 "cat/cats ({:.4}) should be more similar than cat/dog ({:.4})",
749 sim_cat_cats,
750 sim_cat_dog
751 );
752 }
753
754 #[test]
755 fn test_word_identity_matters() {
756 let dim = 16384;
757 let v1 = encode_text_internal("the quick brown fox", dim);
758 let v2 = encode_text_internal("the quick brown fox", dim);
759 let v3 = encode_text_internal("completely different words here", dim);
760 assert!(
761 (v1.similarity(&v2) - 1.0).abs() < f64::EPSILON,
762 "Identical text must match"
763 );
764 assert!(
765 v1.similarity(&v3) < 0.3,
766 "Unrelated text should have low similarity"
767 );
768 }
769
770 #[test]
771 fn test_word_overlap_similarity() {
772 let dim = 16384;
773 let v1 = encode_text_internal("the cat sat on the mat", dim);
774 let v2 = encode_text_internal("the dog sat on the mat", dim);
775 let v3 = encode_text_internal("quantum physics is fascinating", dim);
776 let sim_close = v1.similarity(&v2);
777 let sim_far = v1.similarity(&v3);
778 assert!(
779 sim_close > sim_far,
780 "Sentences sharing most words ({:.4}) should be more similar than unrelated ({:.4})",
781 sim_close,
782 sim_far
783 );
784 }
785
786 #[test]
787 fn test_multi_scale_captures_short_text() {
788 let dim = 16384;
789 assert!(!encode_text_internal("x", dim).indices().is_empty());
790 let v = encode_text_internal("hi", dim);
791 assert!(!v.indices().is_empty());
792 let v2 = encode_text_internal("his", dim);
794 let v3 = encode_text_internal("xyz", dim);
795 assert!(
796 v.similarity(&v2) > v.similarity(&v3),
797 "hi/his should be more similar than hi/xyz"
798 );
799 }
800
801 #[test]
802 fn rotation_matches_permute() {
803 for dim in [1usize, 2, 255, 256, 1024] {
804 for seed in 0..32u64 {
805 let base = EntangledHVec::new_deterministic(dim, seed);
806 for shift in 0..dim.min(7) {
807 let mut out = Vec::new();
808 rotate_sorted(base.indices(), shift as u32, dim as u32, &mut out);
809 assert_eq!(
810 out,
811 base.permute(shift).indices(),
812 "dim {dim} shift {shift}"
813 );
814 }
815 }
816 }
817 }
818
819 #[test]
820 fn memoized_path_matches_reference_at_density_boundaries() {
821 let texts = [
822 "x",
823 "hi",
824 "aa",
825 "aaaa aaaa aaaa",
826 "the cat sat on the mat",
827 "Café records explain backups. Restore deleted documents from verified snapshots.",
828 "ünïcödé text with punctuation!!! and NUMBERS 12345, repeated repeated repeated words",
829 "a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p",
830 ];
831 for encoder in [
832 TextEncoder::MultiscaleWordsV1,
833 TextEncoder::MultiscaleWordsV2,
834 TextEncoder::MultiscaleCappedUnionV1,
835 ] {
836 for dim in [
838 1usize, 2, 3, 255, 256, 257, 1024, 4096, 10_000, 16384, 65535, 65536, 65537,
839 ] {
840 for text in texts {
841 let reference = encoder.encode_reference(text, dim);
842 let cached = encoder.encode(text, dim);
843 assert_eq!(cached.dim, reference.dim);
844 assert_eq!(
845 cached.indices(),
846 reference.indices(),
847 "{} dim {dim} text {text:?}",
848 encoder.name()
849 );
850 assert_eq!(encoder.encode(text, dim).indices(), reference.indices());
852 }
853 }
854 }
855 }
856}