ibm-db-haystack 0.3.0__tar.gz → 0.4.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 (20) hide show
  1. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/CHANGELOG.md +16 -0
  2. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/PKG-INFO +1 -1
  3. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/src/haystack_integrations/document_stores/ibm_db/document_store.py +17 -5
  4. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/tests/test_document_store.py +181 -0
  5. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/.gitignore +0 -0
  6. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/LICENSE.txt +0 -0
  7. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/README.md +0 -0
  8. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/docker-compose.yml +0 -0
  9. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/pydoc/config_docusaurus.yml +0 -0
  10. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/pyproject.toml +0 -0
  11. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/src/haystack_integrations/components/retrievers/ibm_db/__init__.py +0 -0
  12. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/src/haystack_integrations/components/retrievers/ibm_db/embedding_retriever.py +0 -0
  13. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/src/haystack_integrations/components/retrievers/py.typed +0 -0
  14. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/src/haystack_integrations/document_stores/ibm_db/__init__.py +0 -0
  15. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/src/haystack_integrations/document_stores/ibm_db/filters.py +0 -0
  16. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/src/haystack_integrations/document_stores/py.typed +0 -0
  17. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/tests/__init__.py +0 -0
  18. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/tests/conftest.py +0 -0
  19. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/tests/test_embedding_retriever.py +0 -0
  20. {ibm_db_haystack-0.3.0 → ibm_db_haystack-0.4.0}/tests/test_filters.py +0 -0
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/ibm_db-v0.3.0] - 2026-09-01
4
+
5
+ ### 🐛 Bug Fixes
6
+
7
+ - `IBMDB2DocumentStore` search_term + pagination (#3689)
8
+ - IBMDB adding optional parameter `filters` to `get_metadata_field_unique_values()` (#3739)
9
+
10
+ ### 🧪 Testing
11
+
12
+ - `IBMDBDocumentStore` removing duplicated tests already covered by haystack Mixin tests (#3779)
13
+
14
+ ### 🧹 Chores
15
+
16
+ - Re-standardize READMEs (#3768)
17
+
18
+
3
19
  ## [integrations/ibm_db-v0.2.0] - 2026-07-27
4
20
 
5
21
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ibm-db-haystack
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Haystack integration for IBM Db2
5
5
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/ibm_db#readme
6
6
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
@@ -5,14 +5,13 @@
5
5
  """IBM Db2 Document Store for Haystack."""
6
6
 
7
7
  import json
8
- import logging
9
8
  import threading
10
9
  from collections.abc import Iterator
11
10
  from contextlib import contextmanager, suppress
12
11
  from typing import Any, Literal
13
12
 
14
13
  import ibm_db_dbi # type: ignore[import-untyped]
15
- from haystack import default_from_dict, default_to_dict
14
+ from haystack import default_from_dict, default_to_dict, logging
16
15
  from haystack.dataclasses import Document
17
16
  from haystack.document_stores.errors import DocumentStoreError, DuplicateDocumentError
18
17
  from haystack.document_stores.types import DuplicatePolicy
@@ -22,6 +21,8 @@ from .filters import FilterTranslator
22
21
 
23
22
  logger = logging.getLogger(__name__)
24
23
 
24
+ VALID_DISTANCE_METRICS = frozenset({"COSINE", "DOT", "EUCLIDEAN", "EUCLIDEAN_SQUARED", "HAMMING", "MANHATTAN"})
25
+
25
26
 
26
27
  def _parse_embedding(embedding: Any) -> list[float] | None:
27
28
  """
@@ -103,7 +104,14 @@ class IBMDb2DocumentStore:
103
104
  connection_options: dict[str, Any] | None = None,
104
105
  table_name: str = "haystack_documents",
105
106
  embedding_dim: int = 768,
106
- distance_metric: Literal["EUCLIDEAN", "COSINE", "MANHATTAN"] = "COSINE",
107
+ distance_metric: Literal[
108
+ "COSINE",
109
+ "DOT",
110
+ "EUCLIDEAN",
111
+ "EUCLIDEAN_SQUARED",
112
+ "HAMMING",
113
+ "MANHATTAN",
114
+ ] = "COSINE",
107
115
  recreate_table: bool = False,
108
116
  ):
109
117
  """
@@ -121,7 +129,8 @@ class IBMDb2DocumentStore:
121
129
  :param connection_options: Additional connection options as dict (optional)
122
130
  :param table_name: Name of the table to store documents (default: "haystack_documents")
123
131
  :param embedding_dim: Dimension of embedding vectors (default: 768)
124
- :param distance_metric: Distance metric for similarity search (default: "COSINE")
132
+ :param distance_metric: Distance metric for similarity search (default: "COSINE"). Supported metrics in Db2:
133
+ "COSINE", "DOT", "EUCLIDEAN", "EUCLIDEAN_SQUARED", "HAMMING", "MANHATTAN".
125
134
  :param recreate_table: If True, drop and recreate the table (default: False)
126
135
  """
127
136
  self.database = database
@@ -136,6 +145,9 @@ class IBMDb2DocumentStore:
136
145
  self.connection_options = connection_options
137
146
  self.table_name = table_name
138
147
  self.embedding_dim = embedding_dim
148
+ if distance_metric not in VALID_DISTANCE_METRICS:
149
+ msg = f"Invalid distance_metric '{distance_metric}'. Must be one of {sorted(VALID_DISTANCE_METRICS)}."
150
+ raise ValueError(msg)
139
151
  self.distance_metric = distance_metric
140
152
  self.recreate_table = recreate_table
141
153
 
@@ -269,7 +281,7 @@ class IBMDb2DocumentStore:
269
281
  try:
270
282
  cur.execute(create_sql)
271
283
  conn.commit()
272
- logger.info(f"Created table {self.table_name}")
284
+ logger.info("Created table {table_name}", table_name=self.table_name)
273
285
  except Exception:
274
286
  conn.rollback()
275
287
  # If it still fails, raise the error
@@ -5,12 +5,14 @@
5
5
  """Integration tests for IBM DB2 Document Store using Haystack mixin tests."""
6
6
 
7
7
  import math
8
+ import sys
8
9
  from datetime import datetime, timedelta, timezone
9
10
  from unittest.mock import MagicMock, Mock
10
11
 
11
12
  import pytest
12
13
  from haystack.dataclasses import Document
13
14
  from haystack.document_stores.errors import DocumentStoreError, DuplicateDocumentError
15
+ from haystack.document_stores.types import DuplicatePolicy
14
16
  from haystack.testing.document_store import (
15
17
  CountDocumentsByFilterTest,
16
18
  CountDocumentsTest,
@@ -239,6 +241,120 @@ class TestDocumentStore(
239
241
  assert [doc.id for doc in results] == ["a", "c"]
240
242
  assert all(doc.score is not None for doc in results)
241
243
 
244
+ @pytest.mark.parametrize(
245
+ "metric",
246
+ ["COSINE", "DOT", "EUCLIDEAN", "EUCLIDEAN_SQUARED", "HAMMING", "MANHATTAN"],
247
+ )
248
+ def test_all_distance_metrics_return_ranked_results(self, metric: str, connection_config):
249
+ """
250
+ Verify that _embedding_retrieval works correctly for each of the 6 Db2
251
+ distance metrics: correct ranking, finite scores, top_k respected.
252
+
253
+ A fresh store with embedding_dim=4 is created for each metric so the
254
+ test is independent of the default `document_store` fixture.
255
+ All Db2 distance metrics return scores ordered ASC (smaller = more similar).
256
+ """
257
+ table_name = f"haystack_metric_{metric.lower()}_{sys.version_info.major}_{sys.version_info.minor}"
258
+ store = IBMDb2DocumentStore(
259
+ **connection_config,
260
+ table_name=table_name,
261
+ embedding_dim=4,
262
+ distance_metric=metric,
263
+ recreate_table=True,
264
+ )
265
+
266
+ try:
267
+ # Embeddings chosen so ranking is unambiguous for every metric:
268
+ # most_similar == query → distance 0 (or near 0) for all metrics
269
+ # second_best is close → small distance
270
+ # least_similar is far → large distance
271
+ query = [1.0, 1.0, 1.0, 1.0]
272
+ most_similar = [1.0, 1.0, 1.0, 1.0]
273
+ second_best = [1.0, 1.0, 0.5, 0.5]
274
+ least_similar = [0.0, 0.0, 0.0, 1.0]
275
+
276
+ docs = [
277
+ Document(content="most_similar", embedding=most_similar),
278
+ Document(content="second_best", embedding=second_best),
279
+ Document(content="least_similar", embedding=least_similar),
280
+ ]
281
+ store.write_documents(docs, policy=DuplicatePolicy.OVERWRITE)
282
+
283
+ results = store._embedding_retrieval(query_embedding=query, top_k=2)
284
+
285
+ # Exactly top_k documents returned
286
+ assert len(results) == 2, f"[{metric}] Expected 2 results, got {len(results)}"
287
+
288
+ # most_similar must be in the top-2; least_similar must be excluded
289
+ contents = [r.content for r in results]
290
+ assert "most_similar" in contents, f"[{metric}] 'most_similar' missing from top-2: {contents}"
291
+ assert "least_similar" not in contents, f"[{metric}] 'least_similar' appeared in top-2: {contents}"
292
+
293
+ # Scores must be finite floats and ordered ASC (ORDER BY score ASC)
294
+ for doc in results:
295
+ assert doc.score is not None, f"[{metric}] score is None"
296
+ assert math.isfinite(doc.score), f"[{metric}] non-finite score: {doc.score}"
297
+ assert results[0].content == "most_similar", (
298
+ f"[{metric}] Expected 'most_similar' at rank-1, got '{results[0].content}' "
299
+ f"(scores: {[r.score for r in results]})"
300
+ )
301
+ assert results[0].score <= results[1].score, (
302
+ f"[{metric}] Score ordering wrong: {results[0].score} > {results[1].score}"
303
+ )
304
+
305
+ finally:
306
+ try:
307
+ conn = store._get_connection()
308
+ with conn.cursor() as cur:
309
+ cur.execute(f"DROP TABLE {store.table_name}")
310
+ conn.commit()
311
+ except Exception:
312
+ pass
313
+ store.close()
314
+
315
+ @pytest.mark.parametrize(
316
+ "metric",
317
+ ["COSINE", "DOT", "EUCLIDEAN", "EUCLIDEAN_SQUARED", "HAMMING", "MANHATTAN"],
318
+ )
319
+ def test_all_distance_metrics_respect_filters(self, metric: str, connection_config):
320
+ """
321
+ Verify that metadata filters work correctly alongside each distance metric.
322
+ """
323
+ table_name = f"haystack_metric_flt_{metric.lower()}_{sys.version_info.major}_{sys.version_info.minor}"
324
+ store = IBMDb2DocumentStore(
325
+ **connection_config,
326
+ table_name=table_name,
327
+ embedding_dim=4,
328
+ distance_metric=metric,
329
+ recreate_table=True,
330
+ )
331
+
332
+ try:
333
+ query = [1.0, 1.0, 1.0, 1.0]
334
+ docs = [
335
+ Document(content="in_1", embedding=[1.0, 1.0, 1.0, 1.0], meta={"cat": "A"}),
336
+ Document(content="in_2", embedding=[0.9, 0.9, 0.9, 0.9], meta={"cat": "A"}),
337
+ Document(content="out", embedding=[1.0, 1.0, 1.0, 1.0], meta={"cat": "B"}),
338
+ ]
339
+ store.write_documents(docs, policy=DuplicatePolicy.OVERWRITE)
340
+
341
+ filters = {"field": "meta.cat", "operator": "==", "value": "A"}
342
+ results = store._embedding_retrieval(query_embedding=query, top_k=5, filters=filters)
343
+
344
+ assert len(results) == 2, f"[{metric}] Expected 2 filtered results, got {len(results)}"
345
+ for doc in results:
346
+ assert doc.meta.get("cat") == "A", f"[{metric}] Doc with wrong category returned: {doc.meta}"
347
+
348
+ finally:
349
+ try:
350
+ conn = store._get_connection()
351
+ with conn.cursor() as cur:
352
+ cur.execute(f"DROP TABLE {store.table_name}")
353
+ conn.commit()
354
+ except Exception:
355
+ pass
356
+ store.close()
357
+
242
358
  def test_get_metadata_field_unique_values_pagination(self, document_store: IBMDb2DocumentStore):
243
359
  docs = [
244
360
  Document(content=f"doc {i}", meta={"category": c}) for i, c in enumerate(["A", "B", "A", "C", "B", "A"])
@@ -541,6 +657,47 @@ class TestIBMDb2DocumentStoreUnit:
541
657
  assert isinstance(new_store.username, Secret)
542
658
  assert isinstance(new_store.password, Secret)
543
659
 
660
+ @pytest.mark.parametrize(
661
+ "metric",
662
+ [
663
+ "COSINE",
664
+ "DOT",
665
+ "EUCLIDEAN",
666
+ "EUCLIDEAN_SQUARED",
667
+ "HAMMING",
668
+ "MANHATTAN",
669
+ ],
670
+ )
671
+ def test_supported_distance_metrics(self, metric, monkeypatch):
672
+ """Test that all 6 supported Db2 distance metrics can be configured and serialized."""
673
+ monkeypatch.setenv("DB2_USERNAME", "db2inst1")
674
+ monkeypatch.setenv("DB2_PASSWORD", "Passw0rd123!")
675
+ store = IBMDb2DocumentStore(
676
+ database="testdb",
677
+ hostname="localhost",
678
+ username=Secret.from_env_var("DB2_USERNAME"),
679
+ password=Secret.from_env_var("DB2_PASSWORD"),
680
+ distance_metric=metric,
681
+ )
682
+ assert store.distance_metric == metric
683
+ data = store.to_dict()
684
+ assert data["init_parameters"]["distance_metric"] == metric
685
+ restored = IBMDb2DocumentStore.from_dict(data)
686
+ assert restored.distance_metric == metric
687
+
688
+ def test_invalid_distance_metric_raises(self, monkeypatch):
689
+ """Test that an invalid distance_metric raises ValueError at construction time."""
690
+ monkeypatch.setenv("DB2_USERNAME", "db2inst1")
691
+ monkeypatch.setenv("DB2_PASSWORD", "Passw0rd123!")
692
+ with pytest.raises(ValueError, match="Invalid distance_metric"):
693
+ IBMDb2DocumentStore(
694
+ database="testdb",
695
+ hostname="localhost",
696
+ username=Secret.from_env_var("DB2_USERNAME"),
697
+ password=Secret.from_env_var("DB2_PASSWORD"),
698
+ distance_metric="L2", # type: ignore[arg-type]
699
+ )
700
+
544
701
  def test_to_row_with_none_metadata(self, unit_store):
545
702
  """Test _to_row with None metadata."""
546
703
  doc = Document(id="1", content="test", meta=None, embedding=[0.1] * 768)
@@ -638,6 +795,30 @@ class TestIBMDb2DocumentStoreUnit:
638
795
  executed_sql = cur.execute.call_args.args[0]
639
796
  assert "AND embedding IS NOT NULL" in executed_sql
640
797
 
798
+ @pytest.mark.parametrize(
799
+ "metric",
800
+ [
801
+ "COSINE",
802
+ "DOT",
803
+ "EUCLIDEAN",
804
+ "EUCLIDEAN_SQUARED",
805
+ "HAMMING",
806
+ "MANHATTAN",
807
+ ],
808
+ )
809
+ def test_embedding_retrieval_uses_configured_metric(self, mocked_store, metric):
810
+ store, _, cur = mocked_store
811
+ store.distance_metric = metric
812
+ cur.fetchall.return_value = []
813
+
814
+ store._embedding_retrieval([0.1, 0.2], top_k=5)
815
+
816
+ executed_sql = cur.execute.call_args.args[0]
817
+ expected_fragment = (
818
+ f"VECTOR_DISTANCE(embedding, VECTOR(CAST(? AS CLOB(100000)), {store.embedding_dim}, FLOAT32), {metric})"
819
+ )
820
+ assert expected_fragment in executed_sql
821
+
641
822
  @pytest.mark.parametrize(
642
823
  "fetch_error",
643
824
  [