1use rudb_common::{Error, Result};
58
59use crate::fsst::SymbolTable;
60use crate::integer;
61use crate::lz;
62use crate::reader::Reader;
63use crate::sketch::Sketch;
64use crate::string;
65
66const FLOOR_SAMPLE_BYTES: usize = 2 * 1024;
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Strategy {
73 Independent = 0,
75 SharedTable = 1,
77 SharedDict = 2,
79}
80
81impl Strategy {
82 fn tag(self) -> u8 {
83 self as u8
84 }
85
86 fn from_tag(tag: u8) -> Result<Self> {
87 match tag {
88 0 => Ok(Self::Independent),
89 1 => Ok(Self::SharedTable),
90 2 => Ok(Self::SharedDict),
91 other => Err(Error::internal(format!("unknown column group tag {other}"))),
92 }
93 }
94
95 #[must_use]
97 pub fn name(self) -> &'static str {
98 match self {
99 Self::Independent => "INDEPENDENT",
100 Self::SharedTable => "SHARED_TABLE",
101 Self::SharedDict => "SHARED_DICT",
102 }
103 }
104}
105
106pub fn encode_group(columns: &[&[&[u8]]]) -> Result<Vec<u8>> {
117 let mut best: Option<Vec<u8>> = None;
118 for strategy in [Strategy::Independent, Strategy::SharedTable, Strategy::SharedDict] {
119 let Some(bytes) = encode_as(strategy, columns)? else {
120 continue;
121 };
122 if best.as_ref().is_none_or(|current| bytes.len() < current.len()) {
123 best = Some(bytes);
124 }
125 }
126 best.ok_or_else(|| Error::internal("no strategy applied to the column group"))
127}
128
129pub fn decode_group(bytes: &[u8]) -> Result<Vec<Vec<Vec<u8>>>> {
135 let mut reader = Reader::new(bytes);
136 let columns = decode_at(&mut reader)?;
137 if reader.remaining() != 0 {
138 return Err(Error::internal(format!(
139 "{} bytes left over after decoding a column group",
140 reader.remaining()
141 )));
142 }
143 Ok(columns)
144}
145
146pub fn strategy_sizes(columns: &[&[&[u8]]]) -> Result<Vec<(Strategy, usize)>> {
152 let mut sizes = Vec::new();
153 for strategy in [Strategy::Independent, Strategy::SharedTable, Strategy::SharedDict] {
154 if let Some(bytes) = encode_as(strategy, columns)? {
155 sizes.push((strategy, bytes.len()));
156 }
157 }
158 Ok(sizes)
159}
160
161pub fn describe(bytes: &[u8]) -> Result<String> {
167 let mut reader = Reader::new(bytes);
168 describe_at(&mut reader)
169}
170
171pub fn dictionary_groups(sketches: &[Sketch], threshold: f64) -> Result<Vec<Vec<usize>>> {
193 let mut parent: Vec<usize> = (0..sketches.len()).collect();
194 for left in 0..sketches.len() {
195 for right in (left + 1)..sketches.len() {
196 if sketches[left].jaccard(&sketches[right])? >= threshold {
197 let (a, b) = (find(&mut parent, left), find(&mut parent, right));
198 if a != b {
199 let (low, high) = if a < b { (a, b) } else { (b, a) };
202 parent[high] = low;
203 }
204 }
205 }
206 }
207 let mut groups: Vec<Vec<usize>> = Vec::new();
208 let mut roots: Vec<usize> = Vec::new();
209 for column in 0..sketches.len() {
210 let root = find(&mut parent, column);
211 match roots.iter().position(|seen| *seen == root) {
212 Some(at) => groups[at].push(column),
213 None => {
214 roots.push(root);
215 groups.push(vec![column]);
216 }
217 }
218 }
219 Ok(groups)
220}
221
222fn find(parent: &mut [usize], mut node: usize) -> usize {
223 while parent[node] != node {
224 parent[node] = parent[parent[node]];
225 node = parent[node];
226 }
227 node
228}
229
230fn encode_as(strategy: Strategy, columns: &[&[&[u8]]]) -> Result<Option<Vec<u8>>> {
231 let mut out = vec![strategy.tag()];
232 put_u32(&mut out, u32::try_from(columns.len()).map_err(|_| too_many(columns.len()))?);
233 match strategy {
234 Strategy::Independent => {
235 for column in columns {
236 out.extend_from_slice(&string::encode(column)?);
237 }
238 }
239 Strategy::SharedTable => {
240 if columns.len() < 2 {
241 return Ok(None);
242 }
243 let coded: Vec<(Vec<i64>, Vec<&[u8]>)> =
252 columns.iter().map(|column| string::front_code(column)).collect();
253 let joined: Vec<Vec<u8>> = coded
254 .iter()
255 .map(|(_, suffixes)| {
256 let mut buffer = Vec::with_capacity(suffixes.iter().map(|s| s.len()).sum());
257 for suffix in suffixes {
258 buffer.extend_from_slice(suffix);
259 }
260 buffer
261 })
262 .collect();
263 let tokens: Vec<lz::Tokens<'_>> =
264 joined.iter().map(|bytes| lz::tokens_of(bytes)).collect();
265 let literals: Vec<&[&[u8]]> =
266 tokens.iter().map(|token| token.literals.as_slice()).collect();
267 let table = SymbolTable::train(&shared_sample(&literals));
268 if table.is_empty() {
269 return Ok(None);
270 }
271 table.serialize(&mut out);
272 for (index, (prefixes, suffixes)) in coded.iter().enumerate() {
273 put_u32(
274 &mut out,
275 u32::try_from(prefixes.len()).map_err(|_| too_many(prefixes.len()))?,
276 );
277 out.extend_from_slice(&integer::encode(prefixes)?);
278 let sizes: Vec<i64> = suffixes.iter().map(|value| value.len() as i64).collect();
279 out.extend_from_slice(&integer::encode(&sizes)?);
280 out.extend_from_slice(&integer::encode(&tokens[index].lengths)?);
281 out.extend_from_slice(&integer::encode(&tokens[index].offsets)?);
282 let mut compressed = Vec::new();
283 let mut lengths = Vec::with_capacity(tokens[index].literals.len());
284 for run in &tokens[index].literals {
285 let before = compressed.len();
286 table.compress(run, &mut compressed);
287 lengths.push((compressed.len() - before) as i64);
288 }
289 out.extend_from_slice(&integer::encode(&lengths)?);
290 out.extend_from_slice(&compressed);
291 }
292 }
293 Strategy::SharedDict => {
294 if columns.len() < 2 {
295 return Ok(None);
296 }
297 let dictionary = union_values(columns);
298 let total: usize = columns.iter().map(|column| column.len()).sum();
299 if dictionary.is_empty() || dictionary.len() >= total {
302 return Ok(None);
303 }
304 let entries: Vec<&[u8]> = dictionary.iter().map(Vec::as_slice).collect();
305 out.extend_from_slice(&string::encode(&entries)?);
306 for column in columns {
307 let codes = codes_over(column, &dictionary);
308 out.extend_from_slice(&integer::encode(&codes)?);
309 }
310 }
311 }
312 Ok(Some(out))
313}
314
315fn decode_at(reader: &mut Reader<'_>) -> Result<Vec<Vec<Vec<u8>>>> {
316 let strategy = Strategy::from_tag(reader.u8()?)?;
317 let count = reader.u32()? as usize;
318 let mut columns = Vec::with_capacity(count.min(1024));
319 match strategy {
320 Strategy::Independent => {
321 for _ in 0..count {
322 let (values, used) = string::decode_prefix(reader.rest())?;
323 reader.skip(used)?;
324 columns.push(values);
325 }
326 }
327 Strategy::SharedTable => {
328 let (table, used) = SymbolTable::deserialize(reader.rest())?;
329 reader.skip(used)?;
330 for _ in 0..count {
331 let rows = reader.u32()? as usize;
332 let (prefixes, used) = integer::decode_prefix(reader.rest())?;
333 reader.skip(used)?;
334 let (sizes, used) = integer::decode_prefix(reader.rest())?;
335 reader.skip(used)?;
336 let (matched, used) = integer::decode_prefix(reader.rest())?;
337 reader.skip(used)?;
338 let (offsets, used) = integer::decode_prefix(reader.rest())?;
339 reader.skip(used)?;
340 let (lengths, used) = integer::decode_prefix(reader.rest())?;
341 reader.skip(used)?;
342 if sizes.len() != rows || prefixes.len() != rows {
343 return Err(Error::internal(format!(
344 "a column says it holds {rows} values and has {} prefixes and {} sizes",
345 prefixes.len(),
346 sizes.len()
347 )));
348 }
349 let mut literals = Vec::with_capacity(lengths.len());
350 for length in lengths {
351 let length = usize::try_from(length)
352 .map_err(|_| Error::internal("a negative compressed length"))?;
353 let compressed = reader.bytes(length)?;
354 let mut run = Vec::new();
355 table.decompress(compressed, &mut run)?;
356 literals.push(run);
357 }
358 let mut total = 0usize;
359 for size in &sizes {
360 total += usize::try_from(*size)
361 .map_err(|_| Error::internal("a negative value length"))?;
362 }
363 let bytes = lz::rebuild(&literals, &matched, &offsets, total)?;
364 if bytes.len() != total {
365 return Err(Error::internal(format!(
366 "a matched column group rebuilt {} bytes and its values need {total}",
367 bytes.len()
368 )));
369 }
370 let mut suffixes = Vec::with_capacity(rows);
371 let mut at = 0;
372 for size in &sizes {
373 let width = usize::try_from(*size).unwrap_or(0);
374 suffixes.push(bytes[at..at + width].to_vec());
375 at += width;
376 }
377 columns.push(string::front_decode(&prefixes, suffixes)?);
378 }
379 }
380 Strategy::SharedDict => {
381 let (dictionary, used) = string::decode_prefix(reader.rest())?;
382 reader.skip(used)?;
383 for _ in 0..count {
384 let (codes, used) = integer::decode_prefix(reader.rest())?;
385 reader.skip(used)?;
386 let mut values = Vec::with_capacity(codes.len());
387 for code in codes {
388 let entry = usize::try_from(code)
389 .ok()
390 .and_then(|index| dictionary.get(index))
391 .ok_or_else(|| {
392 Error::internal(format!("code {code} is not in the shared dictionary"))
393 })?;
394 values.push(entry.clone());
395 }
396 columns.push(values);
397 }
398 }
399 }
400 Ok(columns)
401}
402
403fn describe_at(reader: &mut Reader<'_>) -> Result<String> {
404 let strategy = Strategy::from_tag(reader.u8()?)?;
405 let count = reader.u32()? as usize;
406 let mut parts = Vec::with_capacity(count.min(1024));
407 let head = match strategy {
408 Strategy::Independent => {
409 for _ in 0..count {
410 let (text, used) = string::describe_prefix(reader.rest())?;
411 reader.skip(used)?;
412 parts.push(text);
413 }
414 "INDEPENDENT".to_string()
415 }
416 Strategy::SharedTable => {
417 let (table, used) = SymbolTable::deserialize(reader.rest())?;
418 reader.skip(used)?;
419 for _ in 0..count {
420 let rows = reader.u32()? as usize;
421 let (prefixes, used) = integer::describe_prefix(reader.rest())?;
422 reader.skip(used)?;
423 let (sizes, used) = integer::describe_prefix(reader.rest())?;
424 reader.skip(used)?;
425 if rows == 0 && !sizes.is_empty() {
426 return Err(Error::internal("a column group disagrees with itself"));
427 }
428 let (matched, used) = integer::describe_prefix(reader.rest())?;
429 reader.skip(used)?;
430 let (offsets, used) = integer::describe_prefix(reader.rest())?;
431 reader.skip(used)?;
432 let (text, _) = integer::describe_prefix(reader.rest())?;
435 let (lengths, used) = integer::decode_prefix(reader.rest())?;
436 reader.skip(used)?;
437 let bytes: i64 = lengths.iter().sum();
438 reader.skip(usize::try_from(bytes).map_err(|_| {
439 Error::internal("a column group has a negative compressed size")
440 })?)?;
441 parts.push(format!("FRONT({prefixes}, LZ({sizes}, {matched}, {offsets}, {text}))"));
442 }
443 format!("SHARED_TABLE[{}]", table.len())
444 }
445 Strategy::SharedDict => {
446 let (text, used) = string::describe_prefix(reader.rest())?;
447 reader.skip(used)?;
448 for _ in 0..count {
449 let (codes, used) = integer::describe_prefix(reader.rest())?;
450 reader.skip(used)?;
451 parts.push(codes);
452 }
453 format!("SHARED_DICT({text})")
454 }
455 };
456 Ok(format!("{head}({})", parts.join(", ")))
457}
458
459fn shared_sample<'a>(columns: &[&[&'a [u8]]]) -> Vec<&'a [u8]> {
474 let sizes: Vec<usize> =
475 columns.iter().map(|column| column.iter().map(|value| value.len()).sum()).collect();
476 let total: usize = sizes.iter().sum();
477 let floor = FLOOR_SAMPLE_BYTES;
478 let mut sample = Vec::new();
479 for (column, bytes) in columns.iter().zip(&sizes) {
480 let share = if total == 0 {
481 floor
482 } else {
483 (string::SAMPLE_BYTES as u128 * *bytes as u128 / total as u128) as usize
484 };
485 sample.extend(string::sample_bytes_of(column, share.max(floor)));
486 }
487 sample
488}
489
490fn union_values(columns: &[&[&[u8]]]) -> Vec<Vec<u8>> {
492 let mut values: Vec<Vec<u8>> =
493 columns.iter().flat_map(|column| column.iter().map(|value| value.to_vec())).collect();
494 values.sort_unstable();
495 values.dedup();
496 values
497}
498
499fn codes_over(values: &[&[u8]], dictionary: &[Vec<u8>]) -> Vec<i64> {
500 values
501 .iter()
502 .map(|value| {
503 dictionary
504 .binary_search_by(|entry| entry.as_slice().cmp(value))
505 .expect("the dictionary is the union of the columns in this group")
506 as i64
507 })
508 .collect()
509}
510
511fn too_many(count: usize) -> Error {
512 Error::internal(format!("a column group of {count} is larger than the format allows"))
513}
514
515fn put_u32(out: &mut Vec<u8>, value: u32) {
516 out.extend_from_slice(&value.to_le_bytes());
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 fn urls(host: &str, count: usize, from: usize) -> Vec<Vec<u8>> {
526 let paths = ["/index.html", "/catalog/item", "/search", "/user/profile/settings"];
527 (from..from + count)
528 .map(|index| {
529 let path = paths[(index / 3) % paths.len()];
530 format!("http://{host}{path}?session={}&ref=google", index * 7).into_bytes()
531 })
532 .collect()
533 }
534
535 fn borrow(values: &[Vec<u8>]) -> Vec<&[u8]> {
536 values.iter().map(Vec::as_slice).collect()
537 }
538
539 fn group<'a>(columns: &'a [Vec<&'a [u8]>]) -> Vec<&'a [&'a [u8]]> {
540 columns.iter().map(Vec::as_slice).collect()
541 }
542
543 fn round_trip(columns: &[&[&[u8]]]) -> Vec<u8> {
544 let bytes = encode_group(columns).unwrap();
545 let back = decode_group(&bytes).unwrap();
546 assert_eq!(back.len(), columns.len());
547 for (decoded, original) in back.iter().zip(columns) {
548 assert_eq!(decoded.len(), original.len(), "{}", describe(&bytes).unwrap());
549 for (left, right) in decoded.iter().zip(*original) {
550 assert_eq!(left.as_slice(), *right, "{}", describe(&bytes).unwrap());
551 }
552 }
553 bytes
554 }
555
556 fn strategy_of(bytes: &[u8]) -> Strategy {
557 Strategy::from_tag(bytes[0]).unwrap()
558 }
559
560 fn size_of(sizes: &[(Strategy, usize)], strategy: Strategy) -> usize {
561 sizes
562 .iter()
563 .find(|(kind, _)| *kind == strategy)
564 .map(|(_, size)| *size)
565 .unwrap_or_else(|| panic!("{} did not apply", strategy.name()))
566 }
567
568 #[test]
569 fn two_columns_from_the_same_universe_share_a_dictionary() {
570 let left = urls("www.example.com", 20_000, 0);
573 let right = urls("www.example.com", 20_000, 5_000);
574 let columns = [borrow(&left), borrow(&right)];
575 let group = group(&columns);
576 let bytes = round_trip(&group);
577 assert_eq!(strategy_of(&bytes), Strategy::SharedDict);
578 let sizes = strategy_sizes(&group).unwrap();
579 let independent = size_of(&sizes, Strategy::Independent);
580 assert!(
583 bytes.len() * 4 < independent * 3,
584 "{} against {independent} independent",
585 bytes.len()
586 );
587 }
588
589 #[test]
590 fn two_columns_of_the_same_alphabet_share_a_symbol_table() {
591 let left = urls("www.example.com", 4_000, 0);
616 let right = urls("news.other.example.org", 4_000, 500_000);
617 let columns = [borrow(&left), borrow(&right)];
618 let group = group(&columns);
619 let bytes = round_trip(&group);
620 let sizes = strategy_sizes(&group).unwrap();
621 let shared = size_of(&sizes, Strategy::SharedTable);
622 let independent = size_of(&sizes, Strategy::Independent);
623 assert!(shared < independent, "{shared} against {independent} independent");
624 assert_eq!(strategy_of(&bytes), Strategy::SharedTable);
625 }
626
627 #[test]
628 fn a_small_column_gains_most_from_a_shared_table() {
629 let big = urls("www.example.com", 20_000, 0);
632 let small = urls("www.example.com", 40, 900_000);
633 let columns = [borrow(&big), borrow(&small)];
634 let sizes = strategy_sizes(&group(&columns)).unwrap();
635 let shared = size_of(&sizes, Strategy::SharedTable);
636 let independent = size_of(&sizes, Strategy::Independent);
637 assert!(shared < independent, "group {shared} against {independent} apart");
638 }
639
640 #[test]
641 fn unrelated_columns_are_left_alone() {
642 let urls = urls("www.example.com", 4_000, 0);
643 let numbers: Vec<Vec<u8>> = (0..4_000)
644 .map(|index| format!("{:016x}", index * 2_654_435_761u64).into_bytes())
645 .collect();
646 let columns = [borrow(&urls), borrow(&numbers)];
647 let group = group(&columns);
648 let bytes = round_trip(&group);
649 assert_eq!(strategy_of(&bytes), Strategy::Independent);
650 }
651
652 #[test]
653 fn a_group_of_one_is_the_column_on_its_own() {
654 let column = urls("www.example.com", 2_000, 0);
655 let columns = [borrow(&column)];
656 let bytes = round_trip(&group(&columns));
657 assert_eq!(strategy_of(&bytes), Strategy::Independent);
658 assert_eq!(bytes.len(), 5 + string::encode(&borrow(&column)).unwrap().len());
659 }
660
661 #[test]
662 fn an_empty_group_round_trips() {
663 let bytes = round_trip(&[]);
664 assert_eq!(decode_group(&bytes).unwrap().len(), 0);
665 }
666
667 #[test]
668 fn columns_do_not_have_to_be_the_same_length() {
669 let left = urls("www.example.com", 3_000, 0);
670 let right = urls("www.example.com", 700, 1_000);
671 let columns = [borrow(&left), borrow(&right)];
672 round_trip(&group(&columns));
673 }
674
675 #[test]
676 fn an_empty_column_in_a_group_round_trips() {
677 let left = urls("www.example.com", 1_000, 0);
678 let empty: Vec<Vec<u8>> = Vec::new();
679 let columns = [borrow(&left), borrow(&empty)];
680 round_trip(&group(&columns));
681 }
682
683 #[test]
684 fn every_strategy_that_applies_decodes_to_the_input() {
685 let left = urls("www.example.com", 3_000, 0);
686 let right = urls("www.example.com", 3_000, 1_000);
687 let columns = [borrow(&left), borrow(&right)];
688 let group = group(&columns);
689 for strategy in [Strategy::Independent, Strategy::SharedTable, Strategy::SharedDict] {
690 let bytes = encode_as(strategy, &group).unwrap().unwrap();
691 let back = decode_group(&bytes).unwrap();
692 assert_eq!(back[0].len(), left.len(), "{}", strategy.name());
693 assert_eq!(back[1][7], right[7], "{}", strategy.name());
694 }
695 }
696
697 #[test]
698 fn the_chooser_picks_the_smallest_strategy() {
699 let left = urls("www.example.com", 2_000, 0);
700 let right = urls("www.example.com", 2_000, 1_000);
701 let columns = [borrow(&left), borrow(&right)];
702 let group = group(&columns);
703 let chosen = encode_group(&group).unwrap();
704 for (_, size) in strategy_sizes(&group).unwrap() {
705 assert!(chosen.len() <= size);
706 }
707 }
708
709 #[test]
710 fn describe_says_what_every_column_came_out_as() {
711 let left = urls("www.example.com", 2_000, 0);
712 let right = urls("www.example.com", 2_000, 1_000);
713 let columns = [borrow(&left), borrow(&right)];
714 let group = group(&columns);
715 for strategy in [Strategy::Independent, Strategy::SharedTable, Strategy::SharedDict] {
716 let bytes = encode_as(strategy, &group).unwrap().unwrap();
717 let shape = describe(&bytes).unwrap();
718 assert!(shape.starts_with(strategy.name()), "{shape}");
719 assert!(shape.contains(", "), "{shape}");
720 }
721 }
722
723 #[test]
724 fn a_truncated_group_is_an_error_and_not_a_panic() {
725 let left = urls("www.example.com", 40, 0);
726 let right = urls("www.example.com", 40, 20);
727 let columns = [borrow(&left), borrow(&right)];
728 let group = group(&columns);
729 for strategy in [Strategy::Independent, Strategy::SharedTable, Strategy::SharedDict] {
730 let bytes = encode_as(strategy, &group).unwrap().unwrap();
731 for len in 0..bytes.len() {
732 assert!(
733 decode_group(&bytes[..len]).is_err(),
734 "{} decoded at {len} bytes",
735 strategy.name()
736 );
737 }
738 }
739 }
740
741 #[test]
742 fn trailing_bytes_are_an_error() {
743 let column = urls("www.example.com", 10, 0);
744 let columns = [borrow(&column)];
745 let mut bytes = encode_group(&group(&columns)).unwrap();
746 bytes.push(0);
747 let error = decode_group(&bytes).unwrap_err();
748 assert!(error.message().contains("left over"), "{error}");
749 }
750
751 #[test]
752 fn an_unknown_tag_is_an_error() {
753 let error = decode_group(&[9, 0, 0, 0, 0]).unwrap_err();
754 assert!(error.message().contains("unknown column group tag"), "{error}");
755 }
756
757 #[test]
758 fn a_code_outside_the_shared_dictionary_is_an_error() {
759 let mut bytes = vec![Strategy::SharedDict.tag()];
760 put_u32(&mut bytes, 1);
761 bytes.extend_from_slice(&string::encode(&[b"one".as_slice()]).unwrap());
762 bytes.extend_from_slice(&integer::encode(&[4]).unwrap());
763 let error = decode_group(&bytes).unwrap_err();
764 assert!(error.message().contains("not in the shared dictionary"), "{error}");
765 }
766
767 #[test]
768 fn overlapping_columns_are_grouped_and_the_rest_are_not() {
769 let first = urls("www.example.com", 20_000, 0);
770 let second = urls("news.other.example.org", 20_000, 0);
771 let third = urls("www.example.com", 20_000, 4_000);
772 let fourth = urls("news.other.example.org", 20_000, 4_000);
773 let sketches: Vec<Sketch> = [&first, &second, &third, &fourth]
774 .iter()
775 .map(|column| Sketch::of(&borrow(column)))
776 .collect();
777 let groups = dictionary_groups(&sketches, 0.5).unwrap();
778 assert_eq!(groups, vec![vec![0, 2], vec![1, 3]]);
779 }
780
781 #[test]
782 fn a_column_that_overlaps_nothing_is_a_group_of_one() {
783 let sketches: Vec<Sketch> = (0..4)
784 .map(|index| Sketch::of(&borrow(&urls("www.example.com", 5_000, index * 100_000))))
785 .collect();
786 let groups = dictionary_groups(&sketches, 0.5).unwrap();
787 assert_eq!(groups, vec![vec![0], vec![1], vec![2], vec![3]]);
788 }
789
790 #[test]
791 fn grouping_is_transitive_and_does_not_need_every_pair_to_overlap() {
792 let a = urls("www.example.com", 20_000, 0);
796 let b = urls("www.example.com", 20_000, 10_000);
797 let c = urls("www.example.com", 20_000, 20_000);
798 let sketches: Vec<Sketch> =
799 [&a, &b, &c].iter().map(|column| Sketch::of(&borrow(column))).collect();
800 assert!(sketches[0].jaccard(&sketches[2]).unwrap() < 0.01);
801 let groups = dictionary_groups(&sketches, 0.3).unwrap();
802 assert_eq!(groups, vec![vec![0, 1, 2]]);
803 }
804
805 #[test]
806 fn grouping_needs_sketches_of_the_same_size() {
807 let sketches = [Sketch::new(16).unwrap(), Sketch::new(32).unwrap()];
808 assert!(dictionary_groups(&sketches, 0.5).is_err());
809 }
810}