usearch 2.9.0 → 2.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CITATION.cff +1 -1
  2. package/CONTRIBUTING.md +36 -2
  3. package/Cargo.toml +1 -1
  4. package/README.md +12 -15
  5. package/VERSION +1 -1
  6. package/c/lib.cpp +19 -17
  7. package/c/test.c +80 -38
  8. package/c/usearch.h +15 -15
  9. package/conanfile.py +1 -1
  10. package/include/usearch/index.hpp +5 -5
  11. package/include/usearch/index_dense.hpp +8 -4
  12. package/include/usearch/index_plugins.hpp +52 -32
  13. package/javascript/types/usearch.d.ts.map +1 -1
  14. package/javascript/usearch.js +4 -1
  15. package/package.json +3 -2
  16. package/simsimd/CONTRIBUTING.md +88 -0
  17. package/simsimd/Cargo.lock +4 -3
  18. package/simsimd/Cargo.toml +10 -2
  19. package/simsimd/README.md +102 -60
  20. package/simsimd/include/simsimd/binary.h +44 -0
  21. package/simsimd/include/simsimd/complex.h +174 -0
  22. package/simsimd/include/simsimd/simsimd.h +18 -9
  23. package/simsimd/include/simsimd/spatial.h +128 -36
  24. package/simsimd/include/simsimd/types.h +3 -2
  25. package/simsimd/package-lock.json +5 -5
  26. package/simsimd/package.json +1 -1
  27. package/simsimd/rust/benches/native.rs +12 -9
  28. package/simsimd/rust/lib.c +32 -55
  29. package/simsimd/rust/lib.rs +374 -56
  30. package/stringzilla/CONTRIBUTING.md +17 -0
  31. package/stringzilla/Cargo.lock +1 -1
  32. package/stringzilla/Cargo.toml +1 -1
  33. package/stringzilla/README.md +175 -34
  34. package/stringzilla/VERSION +1 -1
  35. package/stringzilla/build.rs +77 -4
  36. package/stringzilla/c/lib.c +33 -0
  37. package/stringzilla/cli/README.md +30 -13
  38. package/stringzilla/cli/__init__.py +0 -0
  39. package/stringzilla/cli/split.py +66 -33
  40. package/stringzilla/cli/wc.py +101 -21
  41. package/stringzilla/include/stringzilla/experimental.h +122 -1
  42. package/stringzilla/include/stringzilla/stringzilla.h +262 -132
  43. package/stringzilla/include/stringzilla/stringzilla.hpp +13 -5
  44. package/stringzilla/package.json +1 -1
  45. package/stringzilla/rust/lib.rs +673 -162
  46. package/wasmer.toml +1 -1
@@ -1,50 +1,91 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
+ import argparse
3
4
  import sys
4
-
5
+ import stringzilla
5
6
  from stringzilla import File, Str
6
7
 
7
8
 
8
- def split_file(file_path, lines_per_file, output_prefix):
9
+ def parse_arguments():
10
+ parser = argparse.ArgumentParser(
11
+ description="Output pieces of FILE to PREFIXaa, PREFIXab, ...; default size is 1000 lines, and default PREFIX is 'x'."
12
+ )
13
+ parser.add_argument(
14
+ "file", nargs="?", default="-", help='File to process, "-" for standard input'
15
+ )
16
+ parser.add_argument(
17
+ "prefix", nargs="?", default="X", help='Output file prefix, default is "x"'
18
+ )
19
+ parser.add_argument(
20
+ "-l",
21
+ "--lines",
22
+ type=int,
23
+ default=1000,
24
+ help="Number of lines per output file, default is 1000",
25
+ )
26
+ parser.add_argument(
27
+ "-t",
28
+ "--separator",
29
+ default="\n",
30
+ help="Use SEP instead of newline as the record separator; '\\0' (zero) specifies the NUL character",
31
+ )
32
+ parser.add_argument(
33
+ "-n",
34
+ "--number",
35
+ type=int,
36
+ default=None,
37
+ help="Generate N output files based on size of input",
38
+ )
39
+ parser.add_argument("--version", action="version", version=stringzilla.__version__)
40
+ return parser.parse_args()
41
+
42
+
43
+ def split_file(file_path, lines_per_file, output_prefix, separator, number_of_files):
9
44
  try:
10
- # 1. Memory-map the large file
11
- file_mapped = File(file_path)
12
- file_contents = Str(file_mapped)
13
-
14
- # Variables to keep track of the current position and file part number
45
+ if separator == "\\0":
46
+ separator = "\0"
47
+ if file_path == "-":
48
+ file_contents = Str(sys.stdin.read())
49
+ else:
50
+ file_mapped = File(file_path)
51
+ file_contents = Str(file_mapped)
52
+
53
+ if number_of_files is not None:
54
+ total_length = len(file_contents)
55
+ chunk_size = total_length // number_of_files
56
+ for file_part in range(number_of_files):
57
+ start = file_part * chunk_size
58
+ end = (
59
+ start + chunk_size
60
+ if file_part < number_of_files - 1
61
+ else total_length
62
+ )
63
+ current_slice = file_contents[start:end]
64
+ output_path = f"{output_prefix}{file_part}"
65
+ current_slice.write_to(output_path)
66
+ return
15
67
  current_position = 0
16
68
  file_part = 0
17
- newline_position = (
18
- -1
19
- ) # Start before file begins to find the first newline correctly
69
+ newline_position = -1
20
70
 
21
- # Loop until the end of the file
22
71
  while current_position < len(file_contents):
23
- # 2. Loop to skip `lines_per_file` lines
24
72
  for _ in range(lines_per_file):
25
- newline_position = file_contents.find("\n", newline_position + 1)
26
- if newline_position == -1: # No more newlines
73
+ newline_position = file_contents.find(separator, newline_position + 1)
74
+ if newline_position == -1:
27
75
  break
28
76
 
29
- # If no newlines were found and we're not at the start, process the rest of the file
30
77
  if newline_position == -1 and current_position < len(file_contents):
31
78
  newline_position = len(file_contents)
32
79
 
33
- # 3. Use offset_within to get the length of the current section
34
- # Assuming offset_within gives you the length from the current position
35
80
  section_length = (
36
81
  newline_position - current_position if newline_position != -1 else 0
37
82
  )
38
83
 
39
- # Extract the current section to write out
40
- if section_length > 0: # Prevent creating empty files
84
+ if section_length > 0:
41
85
  current_slice = file_contents[current_position : newline_position + 1]
42
-
43
- # 4. Save the current slice to file
44
86
  output_path = f"{output_prefix}{file_part}"
45
87
  current_slice.write_to(output_path)
46
88
 
47
- # Prepare for the next slice
48
89
  file_part += 1
49
90
  current_position = newline_position + 1
50
91
 
@@ -52,20 +93,12 @@ def split_file(file_path, lines_per_file, output_prefix):
52
93
  print(f"No such file: {file_path}")
53
94
  except Exception as e:
54
95
  print(f"An error occurred: {e}")
96
+ print("Usage example: split.py [-l LINES] [file] [prefix]")
55
97
 
56
98
 
57
99
  def main():
58
- if len(sys.argv) < 4:
59
- print(
60
- "Usage: python split_file.py <lines_per_file> <input_file> <output_prefix>"
61
- )
62
- sys.exit(1)
63
-
64
- lines_per_file = int(sys.argv[1])
65
- file_path = sys.argv[2]
66
- output_prefix = sys.argv[3]
67
-
68
- split_file(file_path, lines_per_file, output_prefix)
100
+ args = parse_arguments()
101
+ split_file(args.file, args.lines, args.prefix, args.separator, args.number)
69
102
 
70
103
 
71
104
  if __name__ == "__main__":
@@ -1,36 +1,116 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
  import sys
4
-
4
+ import argparse
5
+ import stringzilla
5
6
  from stringzilla import File, Str
6
7
 
7
8
 
8
- def wc(file_path):
9
- try:
10
- mapped_file = File(file_path)
11
- mapped_bytes = Str(mapped_file)
12
- line_count = mapped_bytes.count("\n")
13
- word_count = mapped_bytes.count(" ")
14
- char_count = mapped_bytes.__len__()
9
+ def parse_arguments():
10
+ parser = argparse.ArgumentParser(
11
+ description="Print newline, word, and byte counts for each FILE, and a total line if more than one FILE is \
12
+ specified. A word is a non-zero-length sequence of acters delimited by white space."
13
+ )
14
+ parser.add_argument("files", nargs="*", default=["-"], help="Files to process")
15
+ parser.add_argument(
16
+ "-c", "--bytes", action="store_true", help="print the byte counts"
17
+ )
18
+ parser.add_argument(
19
+ "-m", "--chars", action="store_true", help="print the character counts"
20
+ )
21
+ parser.add_argument(
22
+ "-l", "--lines", action="store_true", help="print the newline counts"
23
+ )
24
+ parser.add_argument(
25
+ "-L",
26
+ "--max-line-length",
27
+ action="store_true",
28
+ help="print the maximum display width",
29
+ )
30
+ parser.add_argument(
31
+ "-w", "--words", action="store_true", help="print the word counts"
32
+ )
33
+ parser.add_argument("--version", action="version", version=stringzilla.__version__)
34
+ return parser.parse_args()
35
+
36
+
37
+ def wc(file_path, args):
38
+ if file_path == "-": # read from stdin
39
+ content = sys.stdin.read()
40
+ mapped_bytes = Str(content)
41
+ else:
42
+ try:
43
+ mapped_file = File(file_path)
44
+ mapped_bytes = Str(mapped_file)
45
+ except RuntimeError: # File gives a RuntimeError if the file does not exist
46
+ return f"No such file: {file_path}", False
47
+
48
+ line_count = mapped_bytes.count("\n")
49
+ word_count = mapped_bytes.count(" ") + 1
50
+ char_count = mapped_bytes.__len__()
51
+ counts = {
52
+ "line_count": line_count,
53
+ "word_count": word_count,
54
+ "char_count": char_count,
55
+ }
56
+
57
+ if args.max_line_length:
58
+ max_line_length = max(len(line) for line in str(mapped_bytes).split("\n"))
59
+ counts["max_line_length"] = max_line_length
15
60
 
16
- return line_count, word_count, char_count
17
- except FileNotFoundError:
18
- return f"No such file: {file_path}"
61
+ if args.bytes or args.chars:
62
+ byte_count = char_count # assume 1 char = 1 byte
63
+ counts["byte_count"] = byte_count
64
+
65
+ return counts, True
66
+
67
+
68
+ def format_output(counts, args):
69
+ selected_counts = []
70
+ if args.lines:
71
+ selected_counts.append(counts["line_count"])
72
+ if args.words:
73
+ selected_counts.append(counts["word_count"])
74
+ if args.chars:
75
+ selected_counts.append(counts["char_count"])
76
+ if args.bytes:
77
+ selected_counts.append(counts.get("byte_count", counts["char_count"]))
78
+ if args.max_line_length:
79
+ selected_counts.append(counts.get("max_line_length", 0))
80
+
81
+ if not any([args.lines, args.words, args.chars, args.bytes, args.max_line_length]):
82
+ selected_counts = [
83
+ counts["line_count"],
84
+ counts["word_count"],
85
+ counts["char_count"],
86
+ ]
87
+
88
+ return " ".join(str(count) for count in selected_counts)
19
89
 
20
90
 
21
91
  def main():
22
- if len(sys.argv) < 2:
23
- print("Usage: python wc.py <file>")
24
- sys.exit(1)
92
+ args = parse_arguments()
93
+ total_counts = {
94
+ "line_count": 0,
95
+ "word_count": 0,
96
+ "char_count": 0,
97
+ "max_line_length": 0,
98
+ "byte_count": 0,
99
+ }
25
100
 
26
- file_path = sys.argv[1]
27
- counts = wc(file_path)
101
+ for file_path in args.files:
102
+ counts, success = wc(file_path, args)
103
+ if success:
104
+ for key in total_counts.keys():
105
+ total_counts[key] += counts.get(key, 0)
106
+ output = format_output(counts, args) + f" {file_path}"
107
+ print(output)
108
+ else:
109
+ print(counts)
28
110
 
29
- if isinstance(counts, tuple):
30
- line_count, word_count, char_count = counts
31
- print(f"{line_count} {word_count} {char_count} {file_path}")
32
- else:
33
- print(counts)
111
+ if len(args.files) > 1:
112
+ total_output = format_output(total_counts, args) + " total"
113
+ print(total_output)
34
114
 
35
115
 
36
116
  if __name__ == "__main__":
@@ -368,6 +368,127 @@ sz_u512_vec_t sz_inclusive_min(sz_i32_t previous, sz_error_cost_t gap, sz_u512_v
368
368
 
369
369
  #if SZ_USE_ARM_NEON
370
370
 
371
+ SZ_PUBLIC sz_cptr_t sz_find_neon_too_smart(sz_cptr_t h, sz_size_t h_length, sz_cptr_t n, sz_size_t n_length) {
372
+
373
+ // This almost never fires, but it's better to be safe than sorry.
374
+ if (h_length < n_length || !n_length) return SZ_NULL_CHAR;
375
+ if (n_length == 1) return sz_find_byte_neon(h, h_length, n);
376
+
377
+ // Scan through the string.
378
+ // Assuming how tiny the Arm NEON registers are, we should avoid internal branches at all costs.
379
+ // That's why, for smaller needles, we use different loops.
380
+ if (n_length == 2) {
381
+ // This is a common case. Aside from ASCII bigrams, it's also the most common case for UTF-16,
382
+ // or any UTF8 content in Cyrillic, Greek, Armenian, Hebrew, Arabic, Coptic, Syriac, Thaana,
383
+ // N'Ko writing system of West-African nations, and, of course, Latin scripts.
384
+ // Dealing with 16-bit values, we can check 15 possible offsets in a single loop iteration.
385
+ // For that we are going to keep 2 registers populated with haystack data.
386
+ // First - bigrams at even offsets - 0, 2, 4, 6, 8, 10, 12, 14.
387
+ // Second - bigrams at odd offsets - 1, 3, 5, 7, 9, 11, 13. One less than the first one.
388
+ sz_u64_t matches;
389
+ sz_u128_vec_t h_even_vec, h_odd_vec, n_vec, interleave_mask_vec, matches_vec;
390
+ // Broadcast needle characters into SIMD registers.
391
+ n_vec.u16x8 = vdupq_n_u16(sz_u16_load(n).u16);
392
+ interleave_mask_vec.u16x8 = vdupq_n_u16(0x00FFu);
393
+ for (; h_length >= 16; h += 15, h_length -= 15) {
394
+ h_even_vec.u8x16 = vld1q_u8((sz_u8_t const *)h);
395
+ h_odd_vec.u8x16 = vextq_u8(h_even_vec.u8x16, /* can be any noise: */ h_even_vec.u8x16, 1);
396
+ // We can now compare both 16-bit arrays with the needle.
397
+ // The result of each comparison will also be 16 bits long.
398
+ // Then - we blend!
399
+ // For odd offsets we are gonna take the bottom 8 bits, and for even - the top ones!
400
+ matches_vec.u8x16 =
401
+ vbslq_u8(interleave_mask_vec.u8x16, vreinterpretq_u8_u16(vceqq_u16(h_even_vec.u16x8, n_vec.u16x8)),
402
+ vreinterpretq_u8_u16(vceqq_u16(h_odd_vec.u16x8, n_vec.u16x8)));
403
+ matches = vreinterpretq_u8_u4(matches_vec.u8x16);
404
+ if (matches) return h + sz_u64_ctz(matches) / 4;
405
+ }
406
+ }
407
+ else if (n_length == 3) {
408
+ // Comparing 24-bit values is a bumer. Being lazy, I went with a simple design.
409
+ // Instead of keeping one register per haystack offset, I keep a register per needle character.
410
+ sz_u64_t matches;
411
+ sz_u128_vec_t h_vec, n_first_vec, n_second_vec, n_third_vec, matches_vec;
412
+ // Broadcast needle characters into SIMD registers.
413
+ n_first_vec.u8x16 = vld1q_dup_u8((sz_u8_t const *)&n[0]);
414
+ n_second_vec.u8x16 = vld1q_dup_u8((sz_u8_t const *)&n[1]);
415
+ n_third_vec.u8x16 = vld1q_dup_u8((sz_u8_t const *)&n[2]);
416
+ for (; h_length >= 16; h += 14, h_length -= 14) {
417
+ h_vec.u8x16 = vld1q_u8((sz_u8_t const *)h);
418
+ // Let's compare the first character.
419
+ matches_vec.u8x16 = vceqq_u8(h_vec.u8x16, n_first_vec.u8x16);
420
+ // Let's compare the second one, shift the equality indicators left by 8 bits, and blend.
421
+ matches_vec.u8x16 =
422
+ vandq_u8(matches_vec.u8x16, vextq_u8(vceqq_u8(h_vec.u8x16, n_second_vec.u8x16), vdupq_n_u8(0), 1));
423
+ // Let's compare the third one, shift the equality indicators left by 16 bits, and blend.
424
+ matches_vec.u8x16 =
425
+ vandq_u8(matches_vec.u8x16, vextq_u8(vceqq_u8(h_vec.u8x16, n_third_vec.u8x16), vdupq_n_u8(0), 2));
426
+ // Now reduce bytes to nibbles, and check for matches.
427
+ matches = vreinterpretq_u8_u4(matches_vec.u8x16);
428
+ if (matches) return h + sz_u64_ctz(matches) / 4;
429
+ }
430
+ }
431
+ else if (n_length == 4) {
432
+ // This is a common case not only for ASCII 4-grams, but also UTF-32 content,
433
+ // emojis, Chinese, and many other east-Asian languages.
434
+ // Dealing with 32-bit values, we can analyze 13 offsets at once.
435
+ sz_u128_vec_t h_first_vec, h_second_vec, h_third_vec, h_fourth_vec, interleave_2mask_vec, interleave_4mask_vec,
436
+ n_vec, matches_vec;
437
+ sz_u64_t matches;
438
+ // Broadcast needle characters into SIMD registers.
439
+ n_vec.u32x4 = vdupq_n_u32(sz_u32_load(n).u32);
440
+ interleave_2mask_vec.u16x8 = vdupq_n_u16(0x00FFu);
441
+ interleave_4mask_vec.u32x4 = vdupq_n_u32(0x0000FFFFu);
442
+ for (; h_length >= 16; h += 13, h_length -= 13) {
443
+ h_first_vec.u8x16 = vld1q_u8((sz_u8_t const *)h);
444
+ h_second_vec.u8x16 = vextq_u8(h_first_vec.u8x16, /* can be any noise: */ h_first_vec.u8x16, 1);
445
+ h_third_vec.u8x16 = vextq_u8(h_first_vec.u8x16, /* can be any noise: */ h_first_vec.u8x16, 2);
446
+ h_fourth_vec.u8x16 = vextq_u8(h_first_vec.u8x16, /* can be any noise: */ h_first_vec.u8x16, 3);
447
+ // We can now compare all four arrays of 32-bit values with the needle.
448
+ // The result of each comparison will also be 32 bits long.
449
+ // Then - we blend!
450
+ matches_vec.u8x16 = vbslq_u8(
451
+ interleave_4mask_vec.u8x16,
452
+ vbslq_u8(interleave_2mask_vec.u8x16, vreinterpretq_u8_u32(vceqq_u32(h_first_vec.u32x4, n_vec.u32x4)),
453
+ vreinterpretq_u8_u32(vceqq_u32(h_second_vec.u32x4, n_vec.u32x4))),
454
+ vbslq_u8(interleave_2mask_vec.u8x16, vreinterpretq_u8_u32(vceqq_u32(h_third_vec.u32x4, n_vec.u32x4)),
455
+ vreinterpretq_u8_u32(vceqq_u32(h_fourth_vec.u32x4, n_vec.u32x4))));
456
+ matches = vreinterpretq_u8_u4(matches_vec.u8x16);
457
+ if (matches) return h + sz_u64_ctz(matches) / 4;
458
+ }
459
+ }
460
+ else {
461
+ // Pick the parts of the needle that are worth comparing.
462
+ sz_size_t offset_first, offset_mid, offset_last;
463
+ _sz_locate_needle_anomalies(n, n_length, &offset_first, &offset_mid, &offset_last);
464
+ // Broadcast those characters into SIMD registers.
465
+ sz_u64_t matches;
466
+ sz_u128_vec_t h_first_vec, h_mid_vec, h_last_vec, n_first_vec, n_mid_vec, n_last_vec, matches_vec;
467
+ n_first_vec.u8x16 = vld1q_dup_u8((sz_u8_t const *)&n[offset_first]);
468
+ n_mid_vec.u8x16 = vld1q_dup_u8((sz_u8_t const *)&n[offset_mid]);
469
+ n_last_vec.u8x16 = vld1q_dup_u8((sz_u8_t const *)&n[offset_last]);
470
+ // Walk through the string.
471
+ for (; h_length >= n_length + 16; h += 16, h_length -= 16) {
472
+ h_first_vec.u8x16 = vld1q_u8((sz_u8_t const *)(h + offset_first));
473
+ h_mid_vec.u8x16 = vld1q_u8((sz_u8_t const *)(h + offset_mid));
474
+ h_last_vec.u8x16 = vld1q_u8((sz_u8_t const *)(h + offset_last));
475
+ matches_vec.u8x16 = vandq_u8( //
476
+ vandq_u8( //
477
+ vceqq_u8(h_first_vec.u8x16, n_first_vec.u8x16), //
478
+ vceqq_u8(h_mid_vec.u8x16, n_mid_vec.u8x16)),
479
+ vceqq_u8(h_last_vec.u8x16, n_last_vec.u8x16));
480
+ matches = vreinterpretq_u8_u4(matches_vec.u8x16);
481
+ while (matches) {
482
+ int potential_offset = sz_u64_ctz(matches) / 4;
483
+ if (sz_equal(h + potential_offset, n, n_length)) return h + potential_offset;
484
+ matches &= matches - 1;
485
+ }
486
+ }
487
+ }
488
+
489
+ return sz_find_serial(h, h_length, n, n_length);
490
+ }
491
+
371
492
  SZ_INTERNAL void interleave_uint32x4_to_uint64x2(uint32x4_t in_low, uint32x4_t in_high, uint64x2_t *out_first_second,
372
493
  uint64x2_t *out_third_fourth) {
373
494
  // Interleave elements
@@ -588,7 +709,7 @@ SZ_PUBLIC void sz_hashes_neon_reusing_loads(sz_cptr_t start, sz_size_t length, s
588
709
  }
589
710
 
590
711
  SZ_PUBLIC void sz_hashes_neon_readahead(sz_cptr_t start, sz_size_t length, sz_size_t window_length, sz_size_t step,
591
- sz_hash_callback_t callback, void *callback_handle) {
712
+ sz_hash_callback_t callback, void *callback_handle) {
592
713
 
593
714
  if (length < window_length || !window_length) return;
594
715
  if (length < 2 * window_length) {