numpy-vector-store 0.4.0__tar.gz → 0.6.0__tar.gz

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 (30) hide show
  1. numpy_vector_store-0.6.0/CHANGELOG.md +433 -0
  2. numpy_vector_store-0.6.0/MIGRATION.md +305 -0
  3. numpy_vector_store-0.6.0/PKG-INFO +512 -0
  4. numpy_vector_store-0.6.0/README.md +489 -0
  5. numpy_vector_store-0.6.0/ROADMAP.md +478 -0
  6. numpy_vector_store-0.6.0/benchmarks/README.md +91 -0
  7. numpy_vector_store-0.6.0/benchmarks/__init__.py +1 -0
  8. numpy_vector_store-0.6.0/benchmarks/benchmark.py +335 -0
  9. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/src/numpy_vector_store/__init__.py +1 -1
  10. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/src/numpy_vector_store/vector_store.py +314 -226
  11. numpy_vector_store-0.6.0/tests/fixtures/README.md +36 -0
  12. numpy_vector_store-0.6.0/tests/fixtures/vector-store-0.4.0-format-v1.npz +0 -0
  13. numpy_vector_store-0.6.0/tests/test_benchmark.py +119 -0
  14. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/tests/test_vector_store.py +678 -354
  15. numpy_vector_store-0.4.0/CHANGELOG.md +0 -160
  16. numpy_vector_store-0.4.0/MIGRATION.md +0 -169
  17. numpy_vector_store-0.4.0/PKG-INFO +0 -349
  18. numpy_vector_store-0.4.0/README.md +0 -326
  19. numpy_vector_store-0.4.0/ROADMAP.md +0 -225
  20. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/.github/FUNDING.yml +0 -0
  21. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/.github/workflows/checks.yml +0 -0
  22. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/.github/workflows/publish-pypi.yml +0 -0
  23. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/.github/workflows/publish-testpypi.yml +0 -0
  24. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/.gitignore +0 -0
  25. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/LICENSE +0 -0
  26. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/justfile +0 -0
  27. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/pyproject.toml +0 -0
  28. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/src/numpy_vector_store/py.typed +0 -0
  29. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/tests/__init__.py +0 -0
  30. {numpy_vector_store-0.4.0 → numpy_vector_store-0.6.0}/uv.lock +0 -0
@@ -0,0 +1,433 @@
1
+ # Changelog
2
+
3
+ This changelog records user-visible changes to NumPy Vector Store. Earlier
4
+ release notes remain available on the
5
+ [GitHub releases page](https://github.com/tvanreenen/numpy-vector-store/releases).
6
+
7
+ ## 0.6.0 - 2026-08-21
8
+
9
+ This release makes the existing API's input and failure contracts predictable
10
+ before 1.0. It closes cases where the same invalid call could behave
11
+ differently on an empty and populated store, where Python booleans could act as
12
+ row indexes or counts, and where loosely coerced configuration could change
13
+ meaning after persistence. It also turns the project's performance claims and
14
+ format-version-1 compatibility promise into reproducible regression evidence.
15
+
16
+ Valid, documented 0.5 usage continues to work unchanged. Version 0.6 does not
17
+ add a new feature family, archive format, runtime dependency, or public
18
+ exception hierarchy.
19
+
20
+ ### API at a glance
21
+
22
+ The complete workflow remains centered on `VectorStore` and `VectorHit`:
23
+
24
+ ```python
25
+ store = VectorStore(dimensions=1536, normalize=True)
26
+ store.add(vectors, metadata)
27
+
28
+ hits = store.cosine_search(query, top_k=10)
29
+ hits = store.dot_search(query, top_k=10, min_value=0.5)
30
+ hits = store.euclidean_search(query, top_k=10, within_rows=row_indexes)
31
+
32
+ row = store.get(0)
33
+ store.clear()
34
+
35
+ store.save("vectors.npz")
36
+ store.save()
37
+
38
+ loaded = VectorStore.open("vectors.npz")
39
+ loaded.reload()
40
+ ```
41
+
42
+ The ownership, amortized ingestion, deterministic tie ordering, explicit
43
+ persistence lifecycle, and trusted-local-file boundary established in 0.5 are
44
+ unchanged.
45
+
46
+ ### Predictable scalar inputs
47
+
48
+ - Accept Python integers, NumPy integer scalars, and other values implementing
49
+ the standard integer-index protocol for `dimensions`, `top_k`, and
50
+ `get(index)`, then convert them to a Python `int`.
51
+ - Reject Python and NumPy booleans where integer semantics are required.
52
+ `top_k=True` no longer means one, and `get(True)` no longer invokes NumPy
53
+ boolean indexing.
54
+ - Continue requiring positive `dimensions` and `top_k` values. A valid integer
55
+ outside the stored row range still makes `get()` return `None`.
56
+ - Accept Python and NumPy booleans for `normalize`, then retain a canonical
57
+ Python `bool` in memory and persistence.
58
+ - Accept finite Python and NumPy integer or floating-point scalars for
59
+ `min_value` and `max_value`. Reject booleans, strings, complex values,
60
+ arrays, and non-finite thresholds before search.
61
+
62
+ An inappropriate scalar type now raises `TypeError`, while a supported type
63
+ with an invalid value raises `ValueError`. Row bounds continue using
64
+ `IndexError` where row selectors require an existing row. Exact error wording
65
+ remains explanatory rather than a compatibility guarantee.
66
+
67
+ ### Search checks independent of store state
68
+
69
+ - Validate `within_rows` before returning an empty result. It must be a
70
+ one-dimensional sequence of unique integer row indexes, without booleans,
71
+ and every index must be within the current store.
72
+ - Reject duplicate filtered indexes instead of returning the same stored row
73
+ more than once.
74
+ - Apply the same zero-query rules before the empty-store shortcut. Cosine
75
+ search always requires a non-zero query. Dot-product and Euclidean searches
76
+ require one when the store normalizes vectors, while raw stores continue to
77
+ accept a zero query for those two metrics.
78
+ - Keep selector validation vectorized for native NumPy integer arrays and avoid
79
+ changing process-global warning filters while validating Python sequences.
80
+
81
+ These changes affect preconditions, not ranking. Valid 0.5 searches retain the
82
+ same metric calculations, thresholds, deterministic tie handling, filtered-row
83
+ semantics, and `VectorHit` results.
84
+
85
+ ### Persistence inputs and compatibility boundaries
86
+
87
+ - Accept strings and string-returning path-like objects for `open()`,
88
+ `save(path)`, and bound archive paths. Reject an explicitly empty path with
89
+ `ValueError` and an inappropriate path type, including bytes paths, with
90
+ `TypeError`.
91
+ - Keep `save()` with no argument distinct from `save("")`: omission means
92
+ "save to the current binding," while an empty string is an invalid path.
93
+ - Preserve extension resolution. A path without `.npz` still resolves to the
94
+ same archive for saving, opening, and reloading.
95
+ - Preserve owning exceptions at the persistence boundary. Filesystem failures
96
+ use the relevant `OSError` subclass, malformed archive schemas use
97
+ `ValueError`, and NumPy, pickle, or application metadata-loading failures are
98
+ not hidden inside a package-specific wrapper.
99
+
100
+ Archive format version 1 is unchanged. The compatibility suite now includes an
101
+ archive produced by the published 0.4.0 package on Python 3.11 with NumPy
102
+ 1.23.2 and verifies that the current reader recovers its configuration, vectors,
103
+ and metadata. This is a forward-reading promise for the self-describing format,
104
+ not support for older unversioned two-array archives or a promise that an old
105
+ package can read an arbitrary future format. Persistence remains a trusted-file
106
+ feature because opaque metadata uses NumPy's pickle-backed object arrays.
107
+
108
+ ### Reproducible performance evidence
109
+
110
+ - Add repository benchmark commands for public exact search and ingestion.
111
+ Each command records the workload, seeded input digests, complete timing
112
+ samples, median, Python and NumPy versions, Git state, timer, thread-related
113
+ environment variables, and NumPy build configuration as JSON.
114
+ - Measure prepared inputs and documented public operations rather than random
115
+ input generation. Search records complete query batches; ingestion records a
116
+ fresh store and every `add()` call.
117
+ - Add structural regression checks for geometric capacity reuse, partial
118
+ top-k selection, and unfiltered access to the stored vector matrix without a
119
+ preliminary full-matrix copy.
120
+ - Keep wall-clock limits out of shared CI, where runner load would make failures
121
+ noisy and machine-specific.
122
+
123
+ The README now reports one aligned grid at 1,000, 10,000, and 100,000 rows for
124
+ 384, 1,536, and 3,072 dimensions, measured on a 24 GB Apple M4 Mac mini. It
125
+ also makes the intended scale explicit: this project is for small-to-medium,
126
+ in-process exact search. The 100,000-row measurements are an upper reference,
127
+ not a target for indefinite scaling; routinely million-row workloads generally
128
+ need an indexed or service-backed system.
129
+
130
+ ### Runtime compatibility and upgrade notes
131
+
132
+ - Continue supporting Python 3.11 through 3.14 and NumPy 1.23.2 or newer.
133
+ - Continue exercising every supported Python version in CI, including a
134
+ dedicated Python 3.11 job with the minimum NumPy version.
135
+ - Keep the runtime wheel limited to the package. Benchmark tooling and its
136
+ guide are included in the source distribution without adding runtime
137
+ dependencies.
138
+ - Require no archive conversion when upgrading from 0.5; format version 1 is
139
+ still the only supported archive format.
140
+
141
+ Applications using documented 0.5 input types should not need code changes.
142
+ Review call sites that pass booleans where integers are expected, fractional
143
+ result counts, string or boolean thresholds, duplicate or malformed
144
+ `within_rows` values, zero queries that were only attempted against empty
145
+ stores, or explicitly empty persistence paths. Those accidentally accepted or
146
+ state-dependent cases now fail at the public boundary with consistent standard
147
+ exceptions.
148
+
149
+ ## 0.5.0 - 2026-08-21
150
+
151
+ This release gives `VectorStore` clear ownership of its configuration and row
152
+ storage, makes repeated additions scale without recopying the complete store on
153
+ every call, and defines deterministic ordering for equal search values. It also
154
+ finishes the persistence transition announced in 0.4: the explicit
155
+ create/open/save/reload lifecycle is now the only persistence API.
156
+
157
+ ### API at a glance
158
+
159
+ The core workflow remains small:
160
+
161
+ ```python
162
+ store = VectorStore(dimensions=1536, normalize=True)
163
+ store.add(vectors, metadata)
164
+
165
+ hits = store.cosine_search(query, top_k=10)
166
+ row = store.get(0)
167
+
168
+ store.save("vectors.npz")
169
+ store.save()
170
+
171
+ loaded = VectorStore.open("vectors.npz")
172
+ loaded.reload()
173
+ ```
174
+
175
+ `VectorStore` and `VectorHit` remain the only public classes. Version 0.5 does
176
+ not add a document wrapper, builder, snapshot object, metadata query language,
177
+ or another persistence abstraction.
178
+
179
+ ### Store-owned configuration and rows
180
+
181
+ - Make `dimensions`, `normalize`, and `file_path` read-only properties. The
182
+ constructor owns configuration, while `open(path)` and a successful
183
+ `save(path)` own archive binding changes.
184
+ - Return zero-copy, non-writeable active-row views from `vectors` and
185
+ `metadata`. Direct item assignment and ordinary attempts to enable writes are
186
+ rejected.
187
+ - Keep inspection views moment-in-time. Code holding a view across `add()`,
188
+ `clear()`, or `reload()` must request a new one to inspect current rows.
189
+ - Return an independent `float32` vector copy from `get(index)`, so changing a
190
+ retrieved vector cannot change normalized storage or a later search.
191
+ - Preserve opaque metadata payloads by reference. The read-only metadata view
192
+ protects row alignment, not the contents of a caller-owned dict, list,
193
+ dataclass, or other application object.
194
+
195
+ The views prevent accidental mutation through the supported API; they are not
196
+ tamper-proof snapshots. Deliberately reaching backing storage through `.base`,
197
+ private attributes, `ctypes`, or similar escape hatches remains unsupported.
198
+ Call `.copy()` when code needs an independently mutable array.
199
+
200
+ ### Amortized repeated additions
201
+
202
+ - Replace whole-store concatenation on every noninitial `add()` with private
203
+ contiguous vector and metadata capacity plus an active row count.
204
+ - Reuse spare rows when a new batch fits. When it does not, grow vector and
205
+ metadata storage together and copy active rows once.
206
+ - Keep spare capacity out of `len()`, inspection, search, `within_rows`,
207
+ retrieval, and saved archives.
208
+ - Make `clear()` return the store to empty arrays and drop the store's retained
209
+ capacity. A caller-held older NumPy view may still keep its previous buffer
210
+ alive until that view is released.
211
+
212
+ The `add(vectors, metadata)` signature, insertion order, validation,
213
+ normalization, and opaque metadata behavior do not change. Passing a batch is
214
+ still useful when the application already has one, but repeated small
215
+ additions no longer move every earlier row on each call.
216
+
217
+ ### Deterministic search ties
218
+
219
+ - Continue ordering cosine and dot-product results from larger values to
220
+ smaller values and Euclidean results from smaller distances to larger ones.
221
+ - Break exact computed-value ties by ascending original store row index.
222
+ - Apply the row-index tie break when choosing which rows cross the `top_k`
223
+ boundary, not only when ordering an already selected subset.
224
+ - Use original store indexes for filtered searches, so shuffling the same
225
+ `within_rows` values does not change tied results.
226
+ - Preserve partial top-k selection rather than replacing it with a full-store
227
+ sort.
228
+
229
+ Only exactly equal computed values use the row-index tie break. Close but
230
+ unequal values remain ordered by their metric value.
231
+
232
+ ### Final persistence lifecycle
233
+
234
+ The 0.4 compatibility window is now closed:
235
+
236
+ - Remove constructor `file_path=`. Create an in-memory store, then call
237
+ `save(path)` to write and bind it.
238
+ - Remove instance `load()`. Use `VectorStore.open(path)` to construct a store
239
+ from an archive and `reload()` to refresh a bound store.
240
+ - Remove context-manager persistence. Call `save()` explicitly where the
241
+ application intends to persist state.
242
+ - Remove the reader for unversioned archives containing only `vectors` and
243
+ `metadata`.
244
+
245
+ Archive format version 1 is unchanged. Applications that already use the 0.4
246
+ `open()`, `save(path)`, `save()`, and `reload()` lifecycle need no persistence
247
+ changes. An older unversioned archive must be converted with 0.4 using its
248
+ original dimensions and normalization mode, or recreated from source data,
249
+ before upgrading. See the [persistence migration guide](MIGRATION.md) for the
250
+ side-by-side replacements and conversion procedure.
251
+
252
+ ### Thread safety and persistence boundaries
253
+
254
+ - Support concurrent search, `get()`, and inspection on one instance only
255
+ while its state and shared metadata payloads remain unchanged.
256
+ - Require application-level synchronization for every access when any thread
257
+ may call `add()`, `clear()`, `reload()`, or `save()`, or mutate shared
258
+ metadata.
259
+ - Keep atomic archive replacement as a destination-visibility guarantee, not a
260
+ store snapshot, file lock, or multi-writer coordination system.
261
+
262
+ Separate store instances writing the same path can still replace one another;
263
+ applications with multiple writers must serialize them. Metadata persistence
264
+ continues to use NumPy's pickle-backed object arrays, so archives remain trusted
265
+ input and must not be opened from untrusted or unverifiable sources.
266
+
267
+ ### Runtime compatibility and upgrade notes
268
+
269
+ - Continue supporting Python 3.11 through 3.14 and NumPy 1.23.2 or newer.
270
+ - Continue exercising every supported Python version in CI, with a dedicated
271
+ minimum-NumPy job on Python 3.11.
272
+ - Keep archive format version 1 readable and writable without a file migration.
273
+ - Expect `AttributeError` from code that assigns public configuration or row
274
+ arrays, and different ordering from code that relied on incidental NumPy
275
+ partition order for exact ties.
276
+ - Expect a migration before upgrading code that still uses constructor
277
+ `file_path=`, instance `load()`, context-manager persistence, or an
278
+ unversioned two-array archive.
279
+
280
+ ## 0.4.0 - 2026-08-09
281
+
282
+ This release makes persistence explicit, self-describing, and safer to update.
283
+ The earlier archive format stored vectors and metadata but omitted the settings
284
+ needed to interpret those vectors correctly. Version 0.4 records that
285
+ configuration in every new archive and introduces a lifecycle that clearly
286
+ separates creating, opening, saving, and reloading a store.
287
+
288
+ ### Explicit persistence lifecycle
289
+
290
+ - Add `VectorStore.open(path)` to construct a store from a versioned archive.
291
+ The archive supplies its own dimensions and normalization mode, so callers no
292
+ longer need to repeat configuration that may be wrong.
293
+ - Let `save(path)` perform the first save or a Save As operation and bind that
294
+ destination. Later `save()` calls update the bound archive.
295
+ - Add `reload()` as a deliberate refresh from disk. It always attempts to read
296
+ the bound archive and leaves current in-memory state unchanged if reading or
297
+ validation fails.
298
+ - Keep creating a new in-memory store separate from opening one on disk. This
299
+ makes file access and persistence boundaries visible in application code.
300
+
301
+ ### Versioned, self-describing archives
302
+
303
+ - Write archive format version 1 with `format_version`, `dimensions`,
304
+ `normalize`, `vectors`, and `metadata` fields.
305
+ - Validate the complete archive before changing live store state, including
306
+ field names, scalar configuration, array dtypes and shapes, row counts,
307
+ finite vector values, and normalized-store zero-vector rules.
308
+ - Reject unsupported format versions and malformed archives clearly rather
309
+ than inferring missing configuration or partially applying valid fields.
310
+ - Continue preserving each metadata item as one opaque row payload.
311
+
312
+ ### Safer archive replacement
313
+
314
+ - Write each save to a uniquely named temporary archive in the destination
315
+ directory, close it, and then replace the destination with `os.replace`.
316
+ - Preserve the previous complete archive when writing or replacement fails and
317
+ clean up temporary files after failures.
318
+ - Bind a new Save As destination only after its archive has been written
319
+ successfully.
320
+
321
+ This provides an atomic visibility boundary: a reader opening the destination
322
+ sees the previous complete archive or the new complete archive instead of a
323
+ partially written file. It does not provide file locking, multi-writer
324
+ coordination, or a universal power-loss durability guarantee.
325
+
326
+ ### Short migration window
327
+
328
+ - Keep constructor `file_path=`, instance `load()`, and direct context-manager
329
+ persistence for version 0.4 with `FutureWarning`. They will be removed in
330
+ 0.5.
331
+ - Make the deprecated context manager save only after a successful block. If
332
+ the block raises, it does not save or suppress the exception.
333
+ - Keep a configuration-aware reader for older archives containing only
334
+ `vectors` and `metadata`. Loading one warns, and its next save rewrites it as
335
+ format version 1.
336
+ - Intentionally make `open()` reject an unversioned archive because that file
337
+ cannot report its original dimensions or normalization semantics.
338
+ - Add a dedicated [persistence migration guide](MIGRATION.md) with side-by-side
339
+ API replacements and one-time legacy archive conversion instructions.
340
+
341
+ The legacy API and unversioned archive reader are removed in 0.5. Applications
342
+ should migrate an old archive once with 0.4 or recreate it from source data;
343
+ indefinite compatibility with the incomplete two-array format is not planned.
344
+
345
+ ### Runtime compatibility
346
+
347
+ - Support Python 3.11 through 3.14. Python 3.10 remains supported by the 0.3
348
+ release series but is not supported by 0.4.
349
+ - Raise the minimum NumPy version from 1.21.3 to 1.23.2, the earliest release
350
+ that supports Python 3.11.
351
+ - Exercise every supported Python version in CI and test NumPy 1.23.2 in a
352
+ dedicated minimum-dependency job.
353
+
354
+ ### Upgrade notes and boundaries
355
+
356
+ - Search, insertion, retrieval, clearing, normalization, and metadata behavior
357
+ are unchanged from 0.3.2.
358
+ - Code using `VectorStore.open()`, `save(path)`, `save()`, and `reload()` is on
359
+ the persistence API intended for 0.5.
360
+ - Code using a transitional entry point continues to work in 0.4 but emits a
361
+ warning so the required 0.5 migration is visible during testing.
362
+ - Metadata still uses NumPy's pickle-backed object-array loading. Only open
363
+ archives produced by your application or another trusted source.
364
+ - Mutable public state, repeated-add performance, deterministic tie ordering,
365
+ and a formal thread-safety contract remain planned for later releases.
366
+
367
+ ## 0.3.2 - 2026-07-27
368
+
369
+ This reliability and performance patch makes existing vector storage, search,
370
+ metadata, and persistence behavior safer and more predictable. It does not
371
+ intentionally break valid existing usage or change the `.npz` archive format.
372
+
373
+ ### Numerical reliability
374
+
375
+ - Reject vectors, queries, and search thresholds that contain non-finite values
376
+ or cannot remain finite when represented as `float32`. Invalid input now
377
+ fails before it can corrupt stored state or ranking.
378
+ - Calculate norms and raw metric intermediates with `float64` where `float32`
379
+ could overflow or underflow. Large and very small finite vectors can now be
380
+ normalized and compared reliably.
381
+ - Allow zero vectors in stores created with `normalize=False`, where they are
382
+ valid for dot-product and Euclidean search.
383
+ - Raise a clear error if a raw cosine search includes a zero vector, because
384
+ cosine similarity is undefined for that row.
385
+ - Avoid duplicate full-size `float64` buffers when calculating raw Euclidean
386
+ distance.
387
+
388
+ ### Persistence
389
+
390
+ - Resolve a path without an `.npz` suffix to the same archive for both saving
391
+ and loading. For example, `file_path="vectors"` consistently uses
392
+ `vectors.npz`.
393
+ - Allow `load()` to be retried when the persistence file did not exist during
394
+ an earlier attempt.
395
+ - Reset load state in `clear()` so a subsequent explicit `load()` can restore
396
+ the saved rows.
397
+ - Keep repeated `load()` calls idempotent after a successful load.
398
+
399
+ ### Metadata
400
+
401
+ - Preserve each item in the outer metadata sequence as one opaque row payload.
402
+ Tuples and lists are no longer mistaken for extra NumPy array dimensions.
403
+ - Support dictionary, dataclass, tuple, list, string, integer, and other scalar
404
+ payloads consistently through insertion, search results, saving, and loading.
405
+ - Continue rejecting explicitly multidimensional NumPy metadata arrays rather
406
+ than silently flattening ambiguous input.
407
+
408
+ ### Search memory use
409
+
410
+ - Search the stored vector matrix directly when `within_rows` is omitted,
411
+ avoiding an unnecessary full-matrix copy on every unfiltered query.
412
+ - Preserve original store indexes and metadata when `within_rows` selects a
413
+ filtered subset.
414
+ - Document that filtered searches allocate a temporary matrix proportional to
415
+ the selected row count and vector dimensions.
416
+
417
+ ### Compatibility and validation
418
+
419
+ - Test Python 3.10 through 3.14 in GitHub Actions.
420
+ - Test the minimum supported NumPy version in a dedicated Python 3.10 job.
421
+ - Raise the minimum NumPy requirement from 1.20 to 1.21.3 so it is compatible
422
+ with the oldest supported Python version.
423
+ - Require linting, formatting, type checking, and the full Python test matrix
424
+ before publishing to PyPI.
425
+
426
+ ### Upgrade notes
427
+
428
+ - No public method signatures or persisted field names changed.
429
+ - Existing trusted `.npz` archives with `vectors` and `metadata` remain
430
+ readable.
431
+ - Environments using NumPy 1.20 must upgrade to NumPy 1.21.3 or newer.
432
+ - Inputs that previously produced `nan`, `inf`, or unreliable rankings now
433
+ raise `ValueError` instead.