haystack-velesdb 7.0.0__tar.gz → 8.0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haystack-velesdb
3
- Version: 7.0.0
3
+ Version: 8.0.0
4
4
  Summary: Haystack 2.x DocumentStore for VelesDB: The Local AI Memory Database.
5
5
  Author-email: VelesDB Team <contact@wiscale.fr>
6
6
  License: MIT
@@ -20,8 +20,8 @@ Requires-Python: >=3.10
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: haystack-ai>=2.0.0
23
- Requires-Dist: velesdb>=3.8.0
24
- Requires-Dist: velesdb-common>=3.8.0
23
+ Requires-Dist: velesdb>=5.1.0
24
+ Requires-Dist: velesdb-common>=8.0.0
25
25
  Provides-Extra: dev
26
26
  Requires-Dist: pytest<9.0,>=7.0; extra == "dev"
27
27
  Dynamic: license-file
@@ -160,9 +160,14 @@ integers using the first 8 bytes of SHA-256 (~9.2 × 10¹⁸ slots). For a
160
160
  is negligible for typical RAG workloads. A `ValueError` is raised at write time
161
161
  if a collision is detected between a new document and an existing one.
162
162
 
163
- **Note on `scale_score`:** When `True` (default), cosine similarity scores
164
- are normalised from `[-1, 1]` to `[0, 1]` so they behave like probabilities
165
- in downstream re-ranking.
163
+ **Note on `scale_score`:** When `True` (default), scores are higher-is-closer,
164
+ as Haystack's `DocumentJoiner` and rankers expect: cosine similarities are
165
+ normalised from `[-1, 1]` to `[0, 1]`, a `euclidean` or `hamming` distance `d`
166
+ becomes `1 / (1 + d)`, and dot and Jaccard similarities are unchanged. A fused
167
+ score (`fusion=...`) is never rescaled, but one that is still a distance
168
+ (`average`, `maximum` or `weighted` over a distance metric) is mapped to
169
+ `1 / (1 + d)`. The direction of each metric and strategy comes from
170
+ velesdb-core, through `velesdb_common.scores`.
166
171
 
167
172
  **Note on streaming ingestion:** `stream_insert()` and `write_documents_streaming()`
168
173
  forward to the underlying `velesdb.Collection.stream_insert`, which requires
@@ -132,9 +132,14 @@ integers using the first 8 bytes of SHA-256 (~9.2 × 10¹⁸ slots). For a
132
132
  is negligible for typical RAG workloads. A `ValueError` is raised at write time
133
133
  if a collision is detected between a new document and an existing one.
134
134
 
135
- **Note on `scale_score`:** When `True` (default), cosine similarity scores
136
- are normalised from `[-1, 1]` to `[0, 1]` so they behave like probabilities
137
- in downstream re-ranking.
135
+ **Note on `scale_score`:** When `True` (default), scores are higher-is-closer,
136
+ as Haystack's `DocumentJoiner` and rankers expect: cosine similarities are
137
+ normalised from `[-1, 1]` to `[0, 1]`, a `euclidean` or `hamming` distance `d`
138
+ becomes `1 / (1 + d)`, and dot and Jaccard similarities are unchanged. A fused
139
+ score (`fusion=...`) is never rescaled, but one that is still a distance
140
+ (`average`, `maximum` or `weighted` over a distance metric) is mapped to
141
+ `1 / (1 + d)`. The direction of each metric and strategy comes from
142
+ velesdb-core, through `velesdb_common.scores`.
138
143
 
139
144
  **Note on streaming ingestion:** `stream_insert()` and `write_documents_streaming()`
140
145
  forward to the underlying `velesdb.Collection.stream_insert`, which requires
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "haystack-velesdb"
7
- version = "7.0.0"
7
+ version = "8.0.0"
8
8
  description = "Haystack 2.x DocumentStore for VelesDB: The Local AI Memory Database."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -31,8 +31,8 @@ classifiers = [
31
31
  requires-python = ">=3.10"
32
32
  dependencies = [
33
33
  "haystack-ai>=2.0.0",
34
- "velesdb>=3.8.0",
35
- "velesdb-common>=3.8.0",
34
+ "velesdb>=5.1.0",
35
+ "velesdb-common>=8.0.0",
36
36
  ]
37
37
 
38
38
  [project.optional-dependencies]
@@ -4,4 +4,4 @@ from haystack_velesdb.document_store import VelesDBDocumentStore
4
4
  from haystack_velesdb.retriever import VelesDBEmbeddingRetriever
5
5
 
6
6
  __all__ = ["VelesDBDocumentStore", "VelesDBEmbeddingRetriever"]
7
- __version__ = "7.0.0"
7
+ __version__ = "8.0.0"
@@ -17,6 +17,11 @@ import velesdb
17
17
  from velesdb_common.collection_admin import CollectionAdminMixin
18
18
  from velesdb_common.fusion import build_fusion_strategy
19
19
  from velesdb_common.ids import stable_hash_id
20
+ from velesdb_common.scores import (
21
+ fused_higher_is_better,
22
+ score_to_similarity,
23
+ to_similarity,
24
+ )
20
25
  from velesdb_common.security import (
21
26
  validate_collection_name,
22
27
  validate_metric,
@@ -225,8 +230,38 @@ def _doc_to_point(doc: Document, sparse_vector: Optional[dict] = None) -> dict:
225
230
  return point
226
231
 
227
232
 
233
+ def _doc_score(
234
+ raw: Optional[float],
235
+ *,
236
+ scale_score: bool,
237
+ metric: str,
238
+ higher_is_better: Optional[bool],
239
+ ) -> Optional[float]:
240
+ """The ``Document.score`` of a VelesDB result score.
241
+
242
+ A fused score (``higher_is_better`` given, as core states it for the
243
+ strategy and metric) is always made higher-is-closer: Haystack ranks and
244
+ joins documents by descending score. A plain search score is scaled when
245
+ *scale_score* is set: cosine from ``[-1, 1]`` to ``[0, 1]``, a euclidean
246
+ or hamming distance ``d`` to ``1 / (1 + d)``, dot and Jaccard unchanged.
247
+ """
248
+ if raw is None:
249
+ return None
250
+ if higher_is_better is not None:
251
+ return to_similarity(raw, higher_is_better)
252
+ if not scale_score:
253
+ return raw
254
+ if metric == "cosine":
255
+ return (raw + 1.0) / 2.0
256
+ return score_to_similarity(metric, raw)
257
+
258
+
228
259
  def _result_to_doc(
229
- result: dict, *, scale_score: bool = False, metric: str = "cosine"
260
+ result: dict,
261
+ *,
262
+ scale_score: bool = False,
263
+ metric: str = "cosine",
264
+ higher_is_better: Optional[bool] = None,
230
265
  ) -> Document:
231
266
  """Convert a VelesDB search or scroll result to a Haystack Document.
232
267
 
@@ -256,13 +291,12 @@ def _result_to_doc(
256
291
  )
257
292
  content = payload.get("content")
258
293
  meta = {k: v for k, v in payload.items() if k not in _RESERVED_PAYLOAD_KEYS}
259
- raw_score: Optional[float] = result.get("score")
260
- if scale_score and raw_score is not None and metric == "cosine":
261
- # Normalise cosine similarity from [-1, 1] to [0, 1].
262
- # Only meaningful for cosine; l2 and dot scores have different ranges.
263
- score: Optional[float] = (raw_score + 1.0) / 2.0
264
- else:
265
- score = raw_score
294
+ score = _doc_score(
295
+ result.get("score"),
296
+ scale_score=scale_score,
297
+ metric=metric,
298
+ higher_is_better=higher_is_better,
299
+ )
266
300
  return Document(id=doc_id, content=content, meta=meta, score=score)
267
301
 
268
302
 
@@ -705,11 +739,13 @@ class VelesDBDocumentStore(CollectionAdminMixin):
705
739
  filters: Optional Haystack 2.x filter dict to restrict the search
706
740
  space. Translated to VelesDB native shape before being
707
741
  forwarded; ``meta.<key>`` is stripped to ``<key>``.
708
- scale_score: When ``True`` and ``metric="cosine"``, scores are
709
- normalised from ``[-1, 1]`` to ``[0, 1]``. Ignored for other
710
- metrics, where raw scores are returned unchanged. Score
711
- scaling does not apply to fused (``fusion``) results, whose
712
- scores come from the fusion strategy rather than the metric.
742
+ scale_score: When ``True``, scores are higher-is-closer: cosine
743
+ normalised from ``[-1, 1]`` to ``[0, 1]``, a ``euclidean`` or
744
+ ``hamming`` distance ``d`` mapped to ``1 / (1 + d)``, dot and
745
+ Jaccard unchanged. When ``False``, raw scores are returned. A
746
+ fused (``fusion``) score is never rescaled, but a distance it
747
+ keeps (``average``, ``maximum`` or ``weighted`` over a distance
748
+ metric) is mapped to ``1 / (1 + d)`` either way.
713
749
  fusion: Optional fusion strategy name applied to the ranking —
714
750
  one of ``"average"``, ``"maximum"``, ``"rrf"``,
715
751
  ``"weighted"``, ``"relative_score"`` / ``"rsf"``. When set,
@@ -734,7 +770,7 @@ class VelesDBDocumentStore(CollectionAdminMixin):
734
770
  results: List[dict] = self._get_collection().search_request(
735
771
  velesdb.SearchOptions(
736
772
  vector=query_embedding,
737
- top_k=top_k,
773
+ k=top_k,
738
774
  filter=veles_filter,
739
775
  )
740
776
  )
@@ -766,9 +802,14 @@ class VelesDBDocumentStore(CollectionAdminMixin):
766
802
  top_k=top_k,
767
803
  fusion=strategy,
768
804
  )
769
- # Fused scores are strategy-derived, not metric similarities, so the
770
- # cosine [-1, 1] -> [0, 1] rescaling is intentionally not applied.
771
- return [_result_to_doc(r, metric=self._metric) for r in results]
805
+ # Fused scores are strategy-derived, not metric similarities: no
806
+ # cosine rescaling, but a score-space strategy over a distance metric
807
+ # still returns distances, which are made higher-is-closer.
808
+ higher_is_better = fused_higher_is_better(strategy, fusion, self._metric)
809
+ return [
810
+ _result_to_doc(r, metric=self._metric, higher_is_better=higher_is_better)
811
+ for r in results
812
+ ]
772
813
 
773
814
  # ------------------------------------------------------------------
774
815
  # Haystack pipeline serialisation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haystack-velesdb
3
- Version: 7.0.0
3
+ Version: 8.0.0
4
4
  Summary: Haystack 2.x DocumentStore for VelesDB: The Local AI Memory Database.
5
5
  Author-email: VelesDB Team <contact@wiscale.fr>
6
6
  License: MIT
@@ -20,8 +20,8 @@ Requires-Python: >=3.10
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: haystack-ai>=2.0.0
23
- Requires-Dist: velesdb>=3.8.0
24
- Requires-Dist: velesdb-common>=3.8.0
23
+ Requires-Dist: velesdb>=5.1.0
24
+ Requires-Dist: velesdb-common>=8.0.0
25
25
  Provides-Extra: dev
26
26
  Requires-Dist: pytest<9.0,>=7.0; extra == "dev"
27
27
  Dynamic: license-file
@@ -160,9 +160,14 @@ integers using the first 8 bytes of SHA-256 (~9.2 × 10¹⁸ slots). For a
160
160
  is negligible for typical RAG workloads. A `ValueError` is raised at write time
161
161
  if a collision is detected between a new document and an existing one.
162
162
 
163
- **Note on `scale_score`:** When `True` (default), cosine similarity scores
164
- are normalised from `[-1, 1]` to `[0, 1]` so they behave like probabilities
165
- in downstream re-ranking.
163
+ **Note on `scale_score`:** When `True` (default), scores are higher-is-closer,
164
+ as Haystack's `DocumentJoiner` and rankers expect: cosine similarities are
165
+ normalised from `[-1, 1]` to `[0, 1]`, a `euclidean` or `hamming` distance `d`
166
+ becomes `1 / (1 + d)`, and dot and Jaccard similarities are unchanged. A fused
167
+ score (`fusion=...`) is never rescaled, but one that is still a distance
168
+ (`average`, `maximum` or `weighted` over a distance metric) is mapped to
169
+ `1 / (1 + d)`. The direction of each metric and strategy comes from
170
+ velesdb-core, through `velesdb_common.scores`.
166
171
 
167
172
  **Note on streaming ingestion:** `stream_insert()` and `write_documents_streaming()`
168
173
  forward to the underlying `velesdb.Collection.stream_insert`, which requires
@@ -1,6 +1,6 @@
1
1
  haystack-ai>=2.0.0
2
- velesdb>=3.8.0
3
- velesdb-common>=3.8.0
2
+ velesdb>=5.1.0
3
+ velesdb-common>=8.0.0
4
4
 
5
5
  [dev]
6
6
  pytest<9.0,>=7.0
@@ -123,14 +123,14 @@ class _FakeSearchOptions:
123
123
  vector: Any = None,
124
124
  *,
125
125
  sparse_vector: Any = None,
126
- top_k: int = 10,
126
+ k: int = 10,
127
127
  filter: Any = None, # pylint: disable=redefined-builtin
128
128
  sparse_index_name: Any = None,
129
129
  include_vectors: bool = False,
130
130
  ) -> None:
131
131
  self.vector = vector
132
132
  self.sparse_vector = sparse_vector
133
- self.top_k = top_k
133
+ self.k = k
134
134
  self.filter = filter
135
135
  self.sparse_index_name = sparse_index_name
136
136
  self.include_vectors = include_vectors
@@ -165,7 +165,7 @@ class _FakeCollection:
165
165
 
166
166
  def search_request(self, opts: Any) -> list:
167
167
  """Canonical search entry point — delegate to the legacy `search`."""
168
- return self.search(opts.vector, top_k=opts.top_k, filter=opts.filter)
168
+ return self.search(opts.vector, top_k=opts.k, filter=opts.filter)
169
169
 
170
170
  def multi_query_search(
171
171
  self,
@@ -321,6 +321,18 @@ def _load_module() -> types.ModuleType:
321
321
  sys.modules["velesdb_common.collection_admin"] = vc_admin
322
322
  _admin_spec.loader.exec_module(vc_admin)
323
323
 
324
+ # The REAL velesdb_common.scores too: it imports velesdb lazily-guarded
325
+ # and, against this fake binding, uses its literal fallbacks, so the
326
+ # store's score direction is the canonical one.
327
+ _scores_path = (
328
+ Path(__file__).resolve().parents[2] / "common" / "src" / "velesdb_common" / "scores.py"
329
+ )
330
+ _scores_spec = importlib.util.spec_from_file_location("velesdb_common.scores", _scores_path)
331
+ assert _scores_spec and _scores_spec.loader
332
+ vc_scores = importlib.util.module_from_spec(_scores_spec)
333
+ sys.modules["velesdb_common.scores"] = vc_scores
334
+ _scores_spec.loader.exec_module(vc_scores)
335
+
324
336
  vc_sec = types.ModuleType("velesdb_common.security")
325
337
  vc_sec.validate_path = _passthrough # type: ignore[attr-defined]
326
338
  vc_sec.validate_collection_name = _passthrough # type: ignore[attr-defined]
@@ -397,12 +409,30 @@ def test_filter_documents_returns_all_when_none() -> None:
397
409
  assert len(store.filter_documents()) == 2
398
410
 
399
411
 
400
- def test_scale_score_not_applied_for_non_cosine_metric() -> None:
412
+ def test_scale_score_maps_a_distance_to_a_higher_is_closer_score() -> None:
401
413
  store = _MOD.VelesDBDocumentStore(path="/tmp/hs", collection_name="t_score_nc", metric="euclidean")
402
414
  store.write_documents([Document(id="z", content="raw", embedding=[1.0])])
403
415
  scaled = store.embedding_retrieval([1.0], scale_score=True)
404
- # For euclidean metric scale_score should be a no-op — raw score returned.
405
- assert scaled[0].score == 0.9
416
+ raw = store.embedding_retrieval([1.0], scale_score=False)
417
+ # A euclidean score is a distance: DocumentJoiner sorts descending, so the
418
+ # scaled score must grow as the distance shrinks.
419
+ assert scaled[0].score == 1.0 / (1.0 + 0.9)
420
+ assert raw[0].score == 0.9
421
+
422
+
423
+ def test_scale_score_keeps_a_dot_similarity() -> None:
424
+ store = _MOD.VelesDBDocumentStore(path="/tmp/hs", collection_name="t_score_dot", metric="dot")
425
+ store.write_documents([Document(id="d", content="dot", embedding=[1.0])])
426
+ assert store.embedding_retrieval([1.0], scale_score=True)[0].score == 0.9
427
+
428
+
429
+ def test_score_space_fusion_over_a_distance_is_higher_is_closer() -> None:
430
+ store = _MOD.VelesDBDocumentStore(path="/tmp/hs", collection_name="t_fused_l2", metric="euclidean")
431
+ store.write_documents([Document(id="f", content="fused", embedding=[1.0])])
432
+ averaged = store.embedding_retrieval([1.0], fusion="average")
433
+ ranked = store.embedding_retrieval([1.0], fusion="rrf")
434
+ assert averaged[0].score == 1.0 / (1.0 + 0.9) # a fused distance, mapped
435
+ assert ranked[0].score == 0.9 # RRF is already higher-is-better
406
436
 
407
437
 
408
438
  def test_scroll_limit_is_respected() -> None: