usearch 0.18.8 → 0.19.1

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.
@@ -112,12 +112,50 @@ namespace usearch {
112
112
  using f64_t = double;
113
113
  using f32_t = float;
114
114
  using byte_t = char;
115
+ enum b1x8_t : unsigned char {};
116
+
117
+ enum class metric_kind_t : std::uint8_t {
118
+ unknown_k = 0,
119
+ // Classics:
120
+ ip_k = 'i',
121
+ cos_k = 'c',
122
+ l2sq_k = 'e',
123
+
124
+ // Custom:
125
+ pearson_k = 'p',
126
+ haversine_k = 'h',
127
+
128
+ // Sets:
129
+ jaccard_k = 'j',
130
+ hamming_k = 'b',
131
+ tanimoto_k = 't',
132
+ sorensen_k = 's',
133
+ };
134
+
135
+ enum class scalar_kind_t : std::uint8_t {
136
+ unknown_k = 0,
137
+ f64_k,
138
+ f32_k,
139
+ f16_k,
140
+ f8_k,
141
+ b1x8_k,
142
+ };
143
+
144
+ template <typename scalar_at> scalar_kind_t common_scalar_kind() noexcept {
145
+ if (std::is_same<scalar_at, f32_t>())
146
+ return scalar_kind_t::f32_k;
147
+ if (std::is_same<scalar_at, f64_t>())
148
+ return scalar_kind_t::f64_k;
149
+ if (std::is_same<scalar_at, b1x8_t>())
150
+ return scalar_kind_t::b1x8_k;
151
+ return scalar_kind_t::unknown_k;
152
+ }
115
153
 
116
154
  template <typename at> at angle_to_radians(at angle) noexcept { return angle * at(3.14159265358979323846) / at(180); }
117
155
 
118
156
  template <typename at> at square(at value) noexcept { return value * value; }
119
157
 
120
- template <std::size_t multiple_ak> inline std::size_t divide_round_up(std::size_t num) noexcept {
158
+ template <std::size_t multiple_ak> std::size_t divide_round_up(std::size_t num) noexcept {
121
159
  return (num + multiple_ak - 1) / multiple_ak;
122
160
  }
123
161
 
@@ -225,6 +263,8 @@ template <typename scalar_at = float, typename result_at = scalar_at> struct ip_
225
263
  using result_t = result_at;
226
264
  using result_type = result_t;
227
265
 
266
+ inline metric_kind_t kind() const noexcept { return metric_kind_t::ip_k; }
267
+ inline scalar_kind_t scalar_kind() const noexcept { return common_scalar_kind<scalar_t>(); }
228
268
  inline result_t operator()(view_t a, view_t b) const noexcept { return operator()(a.data(), b.data(), a.size()); }
229
269
 
230
270
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim) const noexcept {
@@ -254,6 +294,8 @@ template <typename scalar_at = float, typename result_at = scalar_at> struct cos
254
294
  using result_t = result_at;
255
295
  using result_type = result_t;
256
296
 
297
+ inline metric_kind_t kind() const noexcept { return metric_kind_t::cos_k; }
298
+ inline scalar_kind_t scalar_kind() const noexcept { return common_scalar_kind<scalar_t>(); }
257
299
  inline result_t operator()(view_t a, view_t b) const noexcept { return operator()(a.data(), b.data(), a.size()); }
258
300
 
259
301
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim) const noexcept {
@@ -269,7 +311,7 @@ template <typename scalar_at = float, typename result_at = scalar_at> struct cos
269
311
  ab += result_t(a[i]) * result_t(b[i]), //
270
312
  a2 += square<result_t>(a[i]), //
271
313
  b2 += square<result_t>(b[i]);
272
- return (ab != 0) ? (1 - ab / (std::sqrt(a2) * std::sqrt(b2))) : 0;
314
+ return (ab != 0) ? (1 - ab / (std::sqrt(a2) * std::sqrt(b2))) : 1;
273
315
  }
274
316
  };
275
317
 
@@ -283,6 +325,8 @@ template <typename scalar_at = float, typename result_at = scalar_at> struct l2s
283
325
  using result_t = result_at;
284
326
  using result_type = result_t;
285
327
 
328
+ inline metric_kind_t kind() const noexcept { return metric_kind_t::l2sq_k; }
329
+ inline scalar_kind_t scalar_kind() const noexcept { return common_scalar_kind<scalar_t>(); }
286
330
  inline result_t operator()(view_t a, view_t b) const noexcept { return operator()(a.data(), b.data(), a.size()); }
287
331
 
288
332
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim) const noexcept {
@@ -310,8 +354,13 @@ template <typename scalar_at = std::uint64_t, typename result_at = std::size_t>
310
354
  using view_t = span_gt<scalar_t const>;
311
355
  using result_t = result_at;
312
356
  using result_type = result_t;
313
- static_assert(std::is_unsigned<scalar_t>::value, "Hamming distance requires unsigned integral words");
357
+ static_assert( //
358
+ std::is_unsigned<scalar_t>::value ||
359
+ (std::is_enum<scalar_t>::value && std::is_unsigned<typename std::underlying_type<scalar_t>::type>::value),
360
+ "Hamming distance requires unsigned integral words");
314
361
 
362
+ inline metric_kind_t kind() const noexcept { return metric_kind_t::hamming_k; }
363
+ inline scalar_kind_t scalar_kind() const noexcept { return common_scalar_kind<scalar_t>(); }
315
364
  inline result_t operator()(view_t a, view_t b) const noexcept { return operator()(a.data(), b.data(), a.size()); }
316
365
 
317
366
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t words) const noexcept {
@@ -339,9 +388,14 @@ template <typename scalar_at = std::uint64_t, typename result_at = float> struct
339
388
  using view_t = span_gt<scalar_t const>;
340
389
  using result_t = result_at;
341
390
  using result_type = result_t;
342
- static_assert(std::is_unsigned<scalar_t>::value, "Tanimoto distance requires unsigned integral words");
391
+ static_assert( //
392
+ std::is_unsigned<scalar_t>::value ||
393
+ (std::is_enum<scalar_t>::value && std::is_unsigned<typename std::underlying_type<scalar_t>::type>::value),
394
+ "Tanimoto distance requires unsigned integral words");
343
395
  static_assert(std::is_floating_point<result_t>::value, "Tanimoto distance will be a fraction");
344
396
 
397
+ inline metric_kind_t kind() const noexcept { return metric_kind_t::tanimoto_k; }
398
+ inline scalar_kind_t scalar_kind() const noexcept { return common_scalar_kind<scalar_t>(); }
345
399
  inline result_t operator()(view_t a, view_t b) const noexcept { return operator()(a.data(), b.data(), a.size()); }
346
400
 
347
401
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t words) const noexcept {
@@ -371,9 +425,14 @@ template <typename scalar_at = std::uint64_t, typename result_at = float> struct
371
425
  using view_t = span_gt<scalar_t const>;
372
426
  using result_t = result_at;
373
427
  using result_type = result_t;
374
- static_assert(std::is_unsigned<scalar_t>::value, "Sorensen-Dice distance requires unsigned integral words");
428
+ static_assert( //
429
+ std::is_unsigned<scalar_t>::value ||
430
+ (std::is_enum<scalar_t>::value && std::is_unsigned<typename std::underlying_type<scalar_t>::type>::value),
431
+ "Sorensen-Dice distance requires unsigned integral words");
375
432
  static_assert(std::is_floating_point<result_t>::value, "Sorensen-Dice distance will be a fraction");
376
433
 
434
+ inline metric_kind_t kind() const noexcept { return metric_kind_t::sorensen_k; }
435
+ inline scalar_kind_t scalar_kind() const noexcept { return common_scalar_kind<scalar_t>(); }
377
436
  inline result_t operator()(view_t a, view_t b) const noexcept { return operator()(a.data(), b.data(), a.size()); }
378
437
 
379
438
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t words) const noexcept {
@@ -407,6 +466,8 @@ template <typename scalar_at = std::int32_t, typename result_at = float> struct
407
466
  using result_type = result_t;
408
467
  static_assert(!std::is_floating_point<scalar_t>::value, "Jaccard distance requires integral scalars");
409
468
 
469
+ inline metric_kind_t kind() const noexcept { return metric_kind_t::jaccard_k; }
470
+ inline scalar_kind_t scalar_kind() const noexcept { return common_scalar_kind<scalar_t>(); }
410
471
  inline result_t operator()(view_t a, view_t b) const noexcept {
411
472
  return operator()(a.data(), b.data(), a.size(), b.size());
412
473
  }
@@ -436,6 +497,8 @@ template <typename scalar_at = float, typename result_at = float> struct pearson
436
497
  using result_t = result_at;
437
498
  using result_type = result_t;
438
499
 
500
+ inline metric_kind_t kind() const noexcept { return metric_kind_t::pearson_k; }
501
+ inline scalar_kind_t scalar_kind() const noexcept { return common_scalar_kind<scalar_t>(); }
439
502
  inline result_t operator()(view_t a, view_t b) const noexcept { return operator()(a.data(), b.data(), a.size()); }
440
503
 
441
504
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim) const noexcept {
@@ -472,6 +535,9 @@ template <typename scalar_at = float, typename result_at = scalar_at> struct hav
472
535
  using result_type = result_t;
473
536
  static_assert(!std::is_integral<scalar_t>::value, "Latitude and longitude must be floating-node");
474
537
 
538
+ inline metric_kind_t kind() const noexcept { return metric_kind_t::haversine_k; }
539
+ inline scalar_kind_t scalar_kind() const noexcept { return common_scalar_kind<scalar_t>(); }
540
+
475
541
  inline result_t operator()(scalar_t const* a, scalar_t const* b) const noexcept {
476
542
  result_t lat_a = a[0], lon_a = a[1];
477
543
  result_t lat_b = b[0], lon_b = b[1];
@@ -964,9 +1030,9 @@ constexpr std::size_t default_allocator_entry_bytes() { return 64; }
964
1030
 
965
1031
  /**
966
1032
  * @brief The "magic" sequence helps infer the type of the file.
967
- * USearch indexes start with the "unumusearch" string.
1033
+ * USearch indexes start with the "usearch" string.
968
1034
  */
969
- constexpr char const* default_magic() { return "unumusearch"; }
1035
+ constexpr char const* default_magic() { return "usearch"; }
970
1036
 
971
1037
  /**
972
1038
  * @brief Configuration settings for the index construction.
@@ -1026,59 +1092,56 @@ struct search_config_t {
1026
1092
  bool exact = false;
1027
1093
  };
1028
1094
 
1029
- enum class metric_kind_t : std::uint8_t {
1030
- unknown_k = 0,
1031
- // Classics:
1032
- ip_k = 'i',
1033
- cos_k = 'c',
1034
- l2sq_k = 'e',
1035
-
1036
- // Custom:
1037
- pearson_k = 'p',
1038
- haversine_k = 'h',
1039
-
1040
- // Sets:
1041
- jaccard_k = 'j',
1042
- hamming_k = 'b',
1043
- tanimoto_k = 't',
1044
- sorensen_k = 's',
1045
- };
1046
-
1047
1095
  using file_header_t = byte_t[64];
1048
1096
 
1049
1097
  /**
1050
1098
  * @brief Serialized binary representations of the USearch index start with metadata.
1051
1099
  * Metadata is parsed into a `file_head_t`, containing the USearch package version,
1052
1100
  * and the properties of the index.
1101
+ *
1102
+ * It uses: 13 bytes for file versioning, 22 bytes for structural information = 35 bytes.
1103
+ * The following 24 bytes contain binary size of the graph, of the vectors, and the checksum,
1104
+ * leaving 5 bytes at the end vacant.
1053
1105
  */
1054
1106
  struct file_head_t {
1055
1107
 
1056
- using magic_t = char[11];
1108
+ // Versioning: 7 + 2 * 3 = 13 bytes
1109
+ using magic_t = char[7];
1057
1110
  using version_major_t = std::uint16_t;
1058
1111
  using version_minor_t = std::uint16_t;
1059
1112
  using version_patch_t = std::uint16_t;
1060
1113
 
1114
+ // Structural: 1 * 6 + 8 * 2 = 22 bytes
1061
1115
  using connectivity_t = std::uint8_t;
1062
1116
  using max_level_t = std::uint8_t;
1063
1117
  using vector_alignment_t = std::uint8_t;
1064
- using bytes_per_label_t = std::uint16_t;
1065
- using bytes_per_id_t = std::uint16_t;
1118
+ using bytes_per_label_t = std::uint8_t;
1119
+ using bytes_per_id_t = std::uint8_t;
1066
1120
  using size_t = std::uint64_t;
1067
1121
  using entry_idx_t = std::uint64_t;
1068
1122
 
1123
+ // Versioning:
1069
1124
  char const* magic;
1070
1125
  misaligned_ref_gt<version_major_t> version_major;
1071
1126
  misaligned_ref_gt<version_minor_t> version_minor;
1072
1127
  misaligned_ref_gt<version_patch_t> version_patch;
1128
+
1129
+ // Structural:
1073
1130
  misaligned_ref_gt<metric_kind_t> metric;
1074
1131
  misaligned_ref_gt<connectivity_t> connectivity;
1075
1132
  misaligned_ref_gt<max_level_t> max_level;
1076
1133
  misaligned_ref_gt<vector_alignment_t> vector_alignment;
1077
1134
  misaligned_ref_gt<bytes_per_label_t> bytes_per_label;
1078
1135
  misaligned_ref_gt<bytes_per_id_t> bytes_per_id;
1136
+ misaligned_ref_gt<scalar_kind_t> scalar_kind;
1079
1137
  misaligned_ref_gt<size_t> size;
1080
1138
  misaligned_ref_gt<entry_idx_t> entry_idx;
1081
1139
 
1140
+ // Additional:
1141
+ misaligned_ref_gt<size_t> bytes_for_graphs;
1142
+ misaligned_ref_gt<size_t> bytes_for_vectors;
1143
+ misaligned_ref_gt<size_t> bytes_checksum;
1144
+
1082
1145
  file_head_t(byte_t* ptr) noexcept
1083
1146
  : magic((char const*)exchange(ptr, ptr + sizeof(magic_t))),
1084
1147
  version_major(exchange(ptr, ptr + sizeof(version_major_t))),
@@ -1088,8 +1151,55 @@ struct file_head_t {
1088
1151
  max_level(exchange(ptr, ptr + sizeof(max_level_t))),
1089
1152
  vector_alignment(exchange(ptr, ptr + sizeof(vector_alignment_t))),
1090
1153
  bytes_per_label(exchange(ptr, ptr + sizeof(bytes_per_label_t))),
1091
- bytes_per_id(exchange(ptr, ptr + sizeof(bytes_per_id_t))), size(exchange(ptr, ptr + sizeof(size_t))),
1092
- entry_idx(exchange(ptr, ptr + sizeof(entry_idx_t))) {}
1154
+ bytes_per_id(exchange(ptr, ptr + sizeof(bytes_per_id_t))),
1155
+ scalar_kind(exchange(ptr, ptr + sizeof(scalar_kind_t))), size(exchange(ptr, ptr + sizeof(size_t))),
1156
+ entry_idx(exchange(ptr, ptr + sizeof(entry_idx_t))), bytes_for_graphs(exchange(ptr, ptr + sizeof(size_t))),
1157
+ bytes_for_vectors(exchange(ptr, ptr + sizeof(size_t))), bytes_checksum(exchange(ptr, ptr + sizeof(size_t))) {}
1158
+ };
1159
+
1160
+ struct file_head_result_t {
1161
+
1162
+ using magic_t = file_head_t::magic_t;
1163
+ using version_major_t = file_head_t::version_major_t;
1164
+ using version_minor_t = file_head_t::version_minor_t;
1165
+ using version_patch_t = file_head_t::version_patch_t;
1166
+
1167
+ using connectivity_t = file_head_t::connectivity_t;
1168
+ using max_level_t = file_head_t::max_level_t;
1169
+ using vector_alignment_t = file_head_t::vector_alignment_t;
1170
+ using bytes_per_label_t = file_head_t::bytes_per_label_t;
1171
+ using bytes_per_id_t = file_head_t::bytes_per_id_t;
1172
+ using size_t = file_head_t::size_t;
1173
+ using entry_idx_t = file_head_t::entry_idx_t;
1174
+
1175
+ // Versioning:
1176
+ version_major_t version_major;
1177
+ version_minor_t version_minor;
1178
+ version_patch_t version_patch;
1179
+
1180
+ // Structural:
1181
+ metric_kind_t metric;
1182
+ connectivity_t connectivity;
1183
+ max_level_t max_level;
1184
+ vector_alignment_t vector_alignment;
1185
+ bytes_per_label_t bytes_per_label;
1186
+ bytes_per_id_t bytes_per_id;
1187
+ scalar_kind_t scalar_kind;
1188
+ size_t size;
1189
+ entry_idx_t entry_idx;
1190
+
1191
+ // Additional:
1192
+ size_t bytes_for_graphs;
1193
+ size_t bytes_for_vectors;
1194
+ size_t bytes_checksum;
1195
+
1196
+ error_t error;
1197
+
1198
+ explicit operator bool() const noexcept { return !error; }
1199
+ file_head_result_t failed(char const* message) noexcept {
1200
+ error = message;
1201
+ return std::move(*this);
1202
+ }
1093
1203
  };
1094
1204
 
1095
1205
  static_assert(sizeof(file_header_t) == 64, "File header should be exactly 64 bytes");
@@ -1103,6 +1213,24 @@ using return_type_gt =
1103
1213
  typename std::result_of<metric_at(args_at...)>::type;
1104
1214
  #endif
1105
1215
 
1216
+ /// @brief OS-specific to wrap open file-descriptors/handles.
1217
+ #if defined(USEARCH_DEFINED_WINDOWS)
1218
+ struct viewed_file_t {
1219
+ HANDLE file_handle{};
1220
+ HANDLE mapping_handle{};
1221
+ void* ptr{};
1222
+ size_t length{};
1223
+ explicit operator bool() const noexcept { return mapping_handle != nullptr; }
1224
+ };
1225
+ #else
1226
+ struct viewed_file_t {
1227
+ int file_descriptor{};
1228
+ void* ptr{};
1229
+ size_t length{};
1230
+ explicit operator bool() const noexcept { return file_descriptor != 0; }
1231
+ };
1232
+ #endif
1233
+
1106
1234
  /**
1107
1235
  * @brief Approximate Nearest Neighbors Search index using the
1108
1236
  * Hierarchical Navigable Small World graph algorithm.
@@ -1168,18 +1296,17 @@ using return_type_gt =
1168
1296
  template <typename metric_at = ip_gt<float>, //
1169
1297
  typename label_at = std::size_t, //
1170
1298
  typename id_at = std::uint32_t, //
1171
- typename scalar_at = float, //
1172
1299
  typename allocator_at = std::allocator<char>, //
1173
1300
  typename point_allocator_at = allocator_at> //
1174
1301
  class index_gt {
1175
1302
  public:
1176
1303
  using metric_t = metric_at;
1177
- using scalar_t = scalar_at;
1178
1304
  using label_t = label_at;
1179
1305
  using id_t = id_at;
1180
1306
  using allocator_t = allocator_at;
1181
1307
  using point_allocator_t = point_allocator_at;
1182
1308
 
1309
+ using scalar_t = typename metric_t::scalar_t;
1183
1310
  using vector_view_t = span_gt<scalar_t const>;
1184
1311
  using distance_t = return_type_gt<metric_t, vector_view_t, vector_view_t>;
1185
1312
 
@@ -1390,7 +1517,7 @@ class index_gt {
1390
1517
  allocator_t allocator_{};
1391
1518
  point_allocator_t point_allocator_{};
1392
1519
  precomputed_constants_t pre_{};
1393
- int viewed_file_descriptor_{};
1520
+ viewed_file_t viewed_file_{};
1394
1521
 
1395
1522
  usearch_align_m mutable std::atomic<std::size_t> capacity_{};
1396
1523
  usearch_align_m mutable std::atomic<std::size_t> size_{};
@@ -1415,7 +1542,7 @@ class index_gt {
1415
1542
  std::size_t max_level() const noexcept { return static_cast<std::size_t>(max_level_); }
1416
1543
  index_config_t const& config() const noexcept { return config_; }
1417
1544
  index_limits_t const& limits() const noexcept { return limits_; }
1418
- bool is_immutable() const noexcept { return viewed_file_descriptor_ != 0; }
1545
+ bool is_immutable() const noexcept { return bool(viewed_file_); }
1419
1546
 
1420
1547
  /**
1421
1548
  * @section Exceptions
@@ -1424,8 +1551,8 @@ class index_gt {
1424
1551
  explicit index_gt(index_config_t config = {}, metric_t metric = {}, allocator_t allocator = {},
1425
1552
  point_allocator_t point_allocator = {}) noexcept
1426
1553
  : config_(config), limits_(0, 0), metric_(metric), allocator_(std::move(allocator)),
1427
- point_allocator_(std::move(point_allocator)), pre_(precompute_(config)), viewed_file_descriptor_(0),
1428
- size_(0u), max_level_(-1), entry_id_(0u), nodes_(nullptr), nodes_mutexes_(), contexts_(nullptr) {}
1554
+ point_allocator_(std::move(point_allocator)), pre_(precompute_(config)), size_(0u), max_level_(-1),
1555
+ entry_id_(0u), nodes_(nullptr), nodes_mutexes_(), contexts_(nullptr) {}
1429
1556
 
1430
1557
  /**
1431
1558
  * @brief Clones the structure with the same hyper-parameters, but without contents.
@@ -1453,6 +1580,7 @@ class index_gt {
1453
1580
  /**
1454
1581
  * @brief Erases all the vectors from the index.
1455
1582
  * Will change `size()` to zero, but will keep the same `capacity()`.
1583
+ * Will keep the number of available threads/contexts the same as it was.
1456
1584
  */
1457
1585
  void clear() noexcept {
1458
1586
  std::size_t n = size_;
@@ -1466,6 +1594,7 @@ class index_gt {
1466
1594
  /**
1467
1595
  * @brief Erases all the vectors from the index, also deallocating the registry.
1468
1596
  * Will change both `size()` and `capacity()` to zero.
1597
+ * Will deallocate all threads/contexts.
1469
1598
  */
1470
1599
  void reset() noexcept {
1471
1600
  clear();
@@ -1476,6 +1605,7 @@ class index_gt {
1476
1605
  context_allocator_t{}.deallocate(exchange(contexts_, nullptr), limits_.threads());
1477
1606
  limits_ = index_limits_t{0, 0};
1478
1607
  capacity_ = 0;
1608
+ reset_view_();
1479
1609
  }
1480
1610
 
1481
1611
  /**
@@ -1487,7 +1617,7 @@ class index_gt {
1487
1617
  std::swap(metric_, other.metric_);
1488
1618
  std::swap(allocator_, other.allocator_);
1489
1619
  std::swap(pre_, other.pre_);
1490
- std::swap(viewed_file_descriptor_, other.viewed_file_descriptor_);
1620
+ std::swap(viewed_file_, other.viewed_file_);
1491
1621
  std::swap(max_level_, other.max_level_);
1492
1622
  std::swap(entry_id_, other.entry_id_);
1493
1623
  std::swap(nodes_, other.nodes_);
@@ -1613,6 +1743,11 @@ class index_gt {
1613
1743
  std::size_t measurements{};
1614
1744
  error_t error{};
1615
1745
 
1746
+ // todo: this is a hack to get a handle to a failed search_result_t without changing its member types
1747
+ // given that search_result_t can represent a failed result (which implies no valid top_candidates_t &top_),
1748
+ // the top_ probably needs to be a pointer.
1749
+ inline search_result_t(index_gt const& index) noexcept
1750
+ : index_(index), top_(index.contexts_[0].top_candidates) {}
1616
1751
  inline search_result_t(search_result_t&&) = default;
1617
1752
  inline search_result_t& operator=(search_result_t&&) = default;
1618
1753
 
@@ -1854,7 +1989,7 @@ class index_gt {
1854
1989
  */
1855
1990
  std::size_t memory_usage(std::size_t allocator_entry_bytes = default_allocator_entry_bytes()) const noexcept {
1856
1991
  std::size_t total = 0;
1857
- if (viewed_file_descriptor_ == 0) {
1992
+ if (!viewed_file_) {
1858
1993
  stats_t s = stats();
1859
1994
  total += s.allocated_bytes;
1860
1995
  total += s.nodes * allocator_entry_bytes;
@@ -1895,17 +2030,23 @@ class index_gt {
1895
2030
  */
1896
2031
  serialization_result_t save(char const* file_path) const noexcept {
1897
2032
 
2033
+ // Make sure we have right to write to that file
1898
2034
  serialization_result_t result;
2035
+ std::FILE* file = std::fopen(file_path, "wb");
2036
+ if (!file)
2037
+ return result.failed(std::strerror(errno));
2038
+
2039
+ // Prepare the header with metadata
1899
2040
  file_header_t state_buffer{};
1900
2041
  std::memset(state_buffer, 0, sizeof(file_header_t));
1901
2042
  file_head_t state{state_buffer};
1902
2043
  std::memcpy(state_buffer, default_magic(), std::strlen(default_magic()));
1903
2044
 
1904
- // Check compatibility
2045
+ // Mark compatibility
1905
2046
  state.version_major = USEARCH_VERSION_MAJOR;
1906
2047
  state.version_minor = USEARCH_VERSION_MINOR;
1907
2048
  state.version_patch = USEARCH_VERSION_PATCH;
1908
- state.metric = metric_kind_t::unknown_k;
2049
+ state.metric = metric_.kind();
1909
2050
 
1910
2051
  // Describe state
1911
2052
  state.connectivity = config_.connectivity;
@@ -1913,13 +2054,25 @@ class index_gt {
1913
2054
  state.vector_alignment = config_.vector_alignment;
1914
2055
  state.bytes_per_label = sizeof(label_t);
1915
2056
  state.bytes_per_id = sizeof(id_t);
2057
+ state.scalar_kind = metric_.scalar_kind();
1916
2058
  state.size = size_;
1917
2059
  state.entry_idx = entry_id_;
1918
2060
 
1919
- std::FILE* file = std::fopen(file_path, "wb");
1920
- if (!file)
1921
- return result.failed(std::strerror(errno));
2061
+ // Augment with metadata
2062
+ std::size_t graphs_bytes = 0;
2063
+ std::size_t vectors_bytes = 0;
2064
+ for (std::size_t i = 0; i != state.size; ++i) {
2065
+ node_t node = node_with_id_(i);
2066
+ std::size_t node_bytes = node_bytes_(node);
2067
+ std::size_t node_vector_bytes = node_vector_bytes_(node);
2068
+ graphs_bytes += node_bytes - node_vector_bytes;
2069
+ vectors_bytes += node_vector_bytes;
2070
+ }
2071
+ state.bytes_for_graphs = graphs_bytes;
2072
+ state.bytes_for_vectors = vectors_bytes;
2073
+ state.bytes_checksum = 0;
1922
2074
 
2075
+ // Perform serialization
1923
2076
  auto write_chunk = [&](void* begin, std::size_t length) {
1924
2077
  std::size_t written = std::fwrite(begin, length, 1, file);
1925
2078
  if (!written) {
@@ -2027,7 +2180,7 @@ class index_gt {
2027
2180
  }
2028
2181
 
2029
2182
  std::fclose(file);
2030
- viewed_file_descriptor_ = 0;
2183
+ reset_view_();
2031
2184
  return {};
2032
2185
  }
2033
2186
 
@@ -2038,14 +2191,38 @@ class index_gt {
2038
2191
  */
2039
2192
  serialization_result_t view(char const* file_path) noexcept {
2040
2193
  serialization_result_t result;
2194
+
2041
2195
  #if defined(USEARCH_DEFINED_WINDOWS)
2042
- return result.failed("Memory-mapping is not yet available for Windows");
2196
+
2197
+ HANDLE file_handle =
2198
+ CreateFile(file_path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
2199
+ if (file_handle == INVALID_HANDLE_VALUE)
2200
+ return result.failed("Opening file failed!");
2201
+
2202
+ size_t file_length = GetFileSize(file_handle, 0);
2203
+ HANDLE mapping_handle = CreateFileMapping(file_handle, 0, PAGE_READONLY, 0, 0, 0);
2204
+ if (mapping_handle == 0) {
2205
+ CloseHandle(file_handle);
2206
+ return result.failed("Mapping file failed!");
2207
+ }
2208
+
2209
+ byte_t* file = (byte_t*)MapViewOfFile(mapping_handle, FILE_MAP_READ, 0, 0, file_length);
2210
+ if (file == 0) {
2211
+ CloseHandle(mapping_handle);
2212
+ CloseHandle(file_handle);
2213
+ return result.failed("View the map failed!");
2214
+ }
2215
+ viewed_file_.file_handle = file_handle;
2216
+ viewed_file_.mapping_handle = mapping_handle;
2217
+ viewed_file_.ptr = file;
2218
+ viewed_file_.length = file_length;
2219
+ #else
2220
+
2221
+ #if defined(USEARCH_DEFINED_LINUX)
2222
+ int descriptor = open(file_path, O_RDONLY | O_NOATIME);
2043
2223
  #else
2044
- int open_flags = O_RDONLY;
2045
- #if __linux__
2046
- open_flags |= O_NOATIME;
2224
+ int descriptor = open(file_path, O_RDONLY);
2047
2225
  #endif
2048
- int descriptor = open(file_path, open_flags);
2049
2226
 
2050
2227
  // Estimate the file size
2051
2228
  struct stat file_stat;
@@ -2061,16 +2238,20 @@ class index_gt {
2061
2238
  close(descriptor);
2062
2239
  return result.failed(std::strerror(errno));
2063
2240
  }
2241
+ viewed_file_.file_descriptor = descriptor;
2242
+ viewed_file_.ptr = file;
2243
+ viewed_file_.length = file_stat.st_size;
2244
+ #endif // Platform specific code
2064
2245
 
2065
2246
  // Read the header
2066
2247
  {
2067
2248
  file_head_t state{file};
2068
2249
  if (state.bytes_per_label != sizeof(label_t)) {
2069
- close(descriptor);
2250
+ reset_view_();
2070
2251
  return result.failed("Incompatible label type!");
2071
2252
  }
2072
2253
  if (state.bytes_per_id != sizeof(id_t)) {
2073
- close(descriptor);
2254
+ reset_view_();
2074
2255
  return result.failed("Incompatible ID type!");
2075
2256
  }
2076
2257
 
@@ -2103,14 +2284,27 @@ class index_gt {
2103
2284
  max_level_ = (std::max)(max_level_, level);
2104
2285
  }
2105
2286
 
2106
- viewed_file_descriptor_ = descriptor;
2107
2287
  return {};
2108
- #endif // POSIX
2109
2288
  }
2110
2289
 
2111
2290
  #pragma endregion
2112
2291
 
2113
2292
  private:
2293
+ void reset_view_() noexcept {
2294
+ if (!viewed_file_)
2295
+ return;
2296
+ #if defined(USEARCH_DEFINED_WINDOWS)
2297
+ UnmapViewOfFile(viewed_file_.ptr);
2298
+ CloseHandle(viewed_file_.mapping_handle);
2299
+ CloseHandle(viewed_file_.file_handle);
2300
+
2301
+ #else
2302
+ munmap(viewed_file_.ptr, viewed_file_.length);
2303
+ close(viewed_file_.file_descriptor);
2304
+ #endif
2305
+ viewed_file_ = {};
2306
+ }
2307
+
2114
2308
  inline static precomputed_constants_t precompute_(index_config_t const& config) noexcept {
2115
2309
  precomputed_constants_t pre;
2116
2310
  pre.connectivity_max_base = config.connectivity * base_level_multiple_();
@@ -2135,7 +2329,7 @@ class index_gt {
2135
2329
 
2136
2330
  void node_free_(std::size_t id) noexcept {
2137
2331
 
2138
- if (viewed_file_descriptor_ != 0)
2332
+ if (viewed_file_)
2139
2333
  return;
2140
2334
 
2141
2335
  node_t& node = nodes_[id];
@@ -2396,6 +2590,7 @@ class index_gt {
2396
2590
  void search_exact_(vector_view_t query, std::size_t count, context_t& context) const noexcept {
2397
2591
  top_candidates_t& top = context.top_candidates;
2398
2592
  top.clear();
2593
+ top.reserve(count);
2399
2594
  for (std::size_t i = 0; i != size(); ++i)
2400
2595
  top.insert({context.measure(query, node_with_id_(i)), static_cast<id_t>(i)}, count);
2401
2596
  }
@@ -2444,6 +2639,47 @@ class index_gt {
2444
2639
  }
2445
2640
  };
2446
2641
 
2642
+ /**
2643
+ * @brief Extracts metadata from pre-constructed index on disk, without loading it
2644
+ * or mapping the whole binary file.
2645
+ */
2646
+ inline file_head_result_t index_metadata(char const* file_path) noexcept {
2647
+
2648
+ file_head_result_t result;
2649
+ file_header_t state_buffer{};
2650
+ std::FILE* file = std::fopen(file_path, "rb");
2651
+ if (!file)
2652
+ return result.failed(std::strerror(errno));
2653
+
2654
+ // Read the header
2655
+ std::size_t read = std::fread(&state_buffer[0], sizeof(file_header_t), 1, file);
2656
+ std::fclose(file);
2657
+ if (!read)
2658
+ return result.failed(std::strerror(errno));
2659
+
2660
+ // Parse and validate the MIME type
2661
+ file_head_t state{state_buffer};
2662
+ if (std::strncmp(state.magic, default_magic(), std::strlen(default_magic())) != 0)
2663
+ return result.failed("Wrong MIME type!");
2664
+
2665
+ result.version_major = state.version_major;
2666
+ result.version_minor = state.version_minor;
2667
+ result.version_patch = state.version_patch;
2668
+ result.metric = state.metric;
2669
+ result.connectivity = state.connectivity;
2670
+ result.max_level = state.max_level;
2671
+ result.vector_alignment = state.vector_alignment;
2672
+ result.bytes_per_label = state.bytes_per_label;
2673
+ result.bytes_per_id = state.bytes_per_id;
2674
+ result.scalar_kind = state.scalar_kind;
2675
+ result.size = state.size;
2676
+ result.entry_idx = state.entry_idx;
2677
+ result.bytes_for_graphs = state.bytes_for_graphs;
2678
+ result.bytes_for_vectors = state.bytes_for_vectors;
2679
+ result.bytes_checksum = state.bytes_checksum;
2680
+ return result;
2681
+ }
2682
+
2447
2683
  } // namespace usearch
2448
2684
  } // namespace unum
2449
2685