chroma-haystack 4.2.0__tar.gz → 4.3.1__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 (60) hide show
  1. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/CHANGELOG.md +45 -0
  2. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/PKG-INFO +4 -3
  3. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/pyproject.toml +17 -4
  4. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/src/haystack_integrations/components/retrievers/chroma/retriever.py +6 -2
  5. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/src/haystack_integrations/document_stores/chroma/document_store.py +110 -59
  6. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/src/haystack_integrations/document_stores/chroma/filters.py +5 -5
  7. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/src/haystack_integrations/document_stores/chroma/utils.py +2 -1
  8. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/tests/test_document_store.py +198 -179
  9. chroma_haystack-4.3.1/tests/test_document_store_async.py +217 -0
  10. chroma_haystack-4.3.1/tests/test_filters.py +46 -0
  11. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/tests/test_retriever.py +72 -0
  12. chroma_haystack-4.2.0/tests/test_document_store_async.py +0 -444
  13. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/.gitignore +0 -0
  14. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/LICENSE +0 -0
  15. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/README.md +0 -0
  16. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_01.txt +0 -0
  17. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_02.txt +0 -0
  18. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_03.txt +0 -0
  19. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_04.txt +0 -0
  20. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_05.txt +0 -0
  21. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_06.txt +0 -0
  22. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_07.txt +0 -0
  23. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_08.txt +0 -0
  24. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_09.txt +0 -0
  25. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_10.txt +0 -0
  26. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_11.txt +0 -0
  27. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_12.txt +0 -0
  28. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_20.txt +0 -0
  29. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_21.txt +0 -0
  30. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_22.txt +0 -0
  31. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_23.txt +0 -0
  32. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_24.txt +0 -0
  33. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_25.txt +0 -0
  34. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_26.txt +0 -0
  35. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_27.txt +0 -0
  36. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_28.txt +0 -0
  37. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_29.txt +0 -0
  38. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_30.txt +0 -0
  39. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_31.txt +0 -0
  40. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_32.txt +0 -0
  41. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_40.txt +0 -0
  42. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_41.txt +0 -0
  43. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_42.txt +0 -0
  44. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_43.txt +0 -0
  45. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_44.txt +0 -0
  46. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_45.txt +0 -0
  47. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_46.txt +0 -0
  48. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_50.txt +0 -0
  49. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_51.txt +0 -0
  50. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_52.txt +0 -0
  51. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/data/usr_90.txt +0 -0
  52. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/example/example.py +0 -0
  53. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/pydoc/config_docusaurus.yml +0 -0
  54. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/src/haystack_integrations/components/retrievers/chroma/__init__.py +0 -0
  55. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/src/haystack_integrations/components/retrievers/py.typed +0 -0
  56. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/src/haystack_integrations/document_stores/chroma/__init__.py +0 -0
  57. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/src/haystack_integrations/document_stores/chroma/errors.py +0 -0
  58. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/src/haystack_integrations/document_stores/py.typed +0 -0
  59. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/tests/__init__.py +0 -0
  60. {chroma_haystack-4.2.0 → chroma_haystack-4.3.1}/tests/conftest.py +0 -0
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/chroma-v4.3.0] - 2026-04-29
4
+
5
+ ### 🚀 Features
6
+
7
+ - *(chroma)* Support DuplicatePolicy in write_documents and use async mixin tests (#3245)
8
+
9
+ ### 🚜 Refactor
10
+
11
+ - Weaviate, chroma, elasticsearch, opensearch, azure_ai_search use `_normalize_metadata_field_name` from haystack.utils (#2953)
12
+
13
+ ### 🧪 Testing
14
+
15
+ - `ChromaDocumentStore` use Mixin tests (#3026)
16
+ - Track test coverage for all integrations (#3065)
17
+ - Better categorize some Document Stores tests (#3085)
18
+ - Chroma - add unit tests (#3175)
19
+
20
+ ### 🧹 Chores
21
+
22
+ - Add missing -> None return type annotations to chroma __init__ methods (#2976)
23
+ - Enforce ruff docstring rules (D102/D103/D205/D209/D213/D417/D419) in first 10 integrations (#3008)
24
+ - Increase lower pins for 3.14 support in some integrations + test with 3.14 (#3033)
25
+
26
+
27
+ ## [integrations/chroma-v4.2.0] - 2026-03-11
28
+
29
+ ### 🚀 Features
30
+
31
+ - Add support for metadata that contains lists of supported types (#2877)
32
+
33
+ ### 📚 Documentation
34
+
35
+ - Simplify pydoc configs (#2855)
36
+
37
+ ### 🧪 Testing
38
+
39
+ - Replacing each `DocumentStore` specific tests and used the generalised ones from `haystack.testing.document_store` (#2812)
40
+ - Chroma - remove tests for invalid Settings (now ignored) (#2935)
41
+
42
+ ### 🧹 Chores
43
+
44
+ - Remove unused allow-direct-references (#2866)
45
+ - Standardize author mentions (#2897)
46
+
47
+
3
48
  ## [integrations/chroma-v4.1.1] - 2026-02-10
4
49
 
5
50
  ### 📚 Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chroma-haystack
3
- Version: 4.2.0
3
+ Version: 4.3.1
4
4
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/chroma#readme
5
5
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
6
6
  Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/chroma
@@ -14,11 +14,12 @@ Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
17
18
  Classifier: Programming Language :: Python :: Implementation :: CPython
18
19
  Classifier: Programming Language :: Python :: Implementation :: PyPy
19
20
  Requires-Python: >=3.10
20
- Requires-Dist: chromadb>=1.5.0
21
- Requires-Dist: haystack-ai>=2.24.0
21
+ Requires-Dist: chromadb>=1.5.4
22
+ Requires-Dist: haystack-ai>=2.28.0
22
23
  Description-Content-Type: text/markdown
23
24
 
24
25
  # chroma-haystack
@@ -19,12 +19,13 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3.11",
20
20
  "Programming Language :: Python :: 3.12",
21
21
  "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
22
23
  "Programming Language :: Python :: Implementation :: CPython",
23
24
  "Programming Language :: Python :: Implementation :: PyPy",
24
25
  ]
25
26
  dependencies = [
26
- "haystack-ai>=2.24.0",
27
- "chromadb>=1.5.0"
27
+ "haystack-ai>=2.28.0",
28
+ "chromadb>=1.5.4"
28
29
  ]
29
30
 
30
31
  [project.urls]
@@ -66,7 +67,8 @@ dependencies = [
66
67
  unit = 'pytest -m "not integration" {args:tests}'
67
68
  integration = 'pytest -m "integration" {args:tests}'
68
69
  all = 'pytest {args:tests}'
69
- cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x {args:tests}'
70
+ unit-cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x -m "not integration" {args:tests}'
71
+ integration-cov-append-retry = 'pytest --cov=haystack_integrations --cov-append --reruns 3 --reruns-delay 30 -x -m "integration" {args:tests}'
70
72
 
71
73
  types = "mypy -p haystack_integrations.components.retrievers.chroma -p haystack_integrations.document_stores.chroma {args}"
72
74
 
@@ -83,9 +85,17 @@ line-length = 120
83
85
  [tool.ruff.lint]
84
86
  select = [
85
87
  "A",
88
+ "ANN",
86
89
  "ARG",
87
90
  "B",
88
91
  "C",
92
+ "D102", # Missing docstring in public method
93
+ "D103", # Missing docstring in public function
94
+ "D205", # 1 blank line required between summary line and description
95
+ "D209", # Closing triple quotes go to new line
96
+ "D213", # summary lines must be positioned on the second physical line of the docstring
97
+ "D417", # Missing argument descriptions in the docstring
98
+ "D419", # Docstring is empty
89
99
  "DTZ",
90
100
  "E",
91
101
  "EM",
@@ -111,6 +121,8 @@ select = [
111
121
  ignore = [
112
122
  # Allow non-abstract empty methods in abstract base classes
113
123
  "B027",
124
+ # Allow `Any` - used legitimately for **kwargs and dynamic metadata values
125
+ "ANN401",
114
126
  # Allow boolean positional values in function calls, like `dict.get(... True)`
115
127
  "FBT003",
116
128
  # Ignore checks for possible passwords
@@ -138,12 +150,13 @@ ban-relative-imports = "parents"
138
150
 
139
151
  [tool.ruff.lint.per-file-ignores]
140
152
  # Tests can use magic values, assertions, and relative imports
141
- "tests/**/*" = ["PLR2004", "S101", "TID252"]
153
+ "tests/**/*" = ["PLR2004", "S101", "TID252", "D", "ANN"]
142
154
  "example/**/*" = ["T201"]
143
155
 
144
156
  [tool.coverage.run]
145
157
  source = ["haystack_integrations"]
146
158
  branch = true
159
+ relative_files = true
147
160
  parallel = false
148
161
 
149
162
 
@@ -51,8 +51,10 @@ class ChromaQueryTextRetriever:
51
51
  filters: dict[str, Any] | None = None,
52
52
  top_k: int = 10,
53
53
  filter_policy: str | FilterPolicy = FilterPolicy.REPLACE,
54
- ):
54
+ ) -> None:
55
55
  """
56
+ Initialize the ChromaQueryTextRetriever.
57
+
56
58
  :param document_store: an instance of `ChromaDocumentStore`.
57
59
  :param filters: filters to narrow down the search space.
58
60
  :param top_k: the maximum number of documents to retrieve.
@@ -164,8 +166,10 @@ class ChromaEmbeddingRetriever:
164
166
  filters: dict[str, Any] | None = None,
165
167
  top_k: int = 10,
166
168
  filter_policy: str | FilterPolicy = FilterPolicy.REPLACE,
167
- ):
169
+ ) -> None:
168
170
  """
171
+ Initialize the ChromaEmbeddingRetriever.
172
+
169
173
  :param document_store: an instance of `ChromaDocumentStore`.
170
174
  :param filters: filters to narrow down the search space.
171
175
  :param top_k: the maximum number of documents to retrieve.
@@ -11,8 +11,9 @@ from chromadb.api.types import GetResult, Metadata, OneOrMany, QueryResult
11
11
  from chromadb.config import Settings
12
12
  from haystack import default_from_dict, default_to_dict, logging
13
13
  from haystack.dataclasses import Document
14
- from haystack.document_stores.errors import DocumentStoreError
14
+ from haystack.document_stores.errors import DocumentStoreError, DuplicateDocumentError
15
15
  from haystack.document_stores.types import DuplicatePolicy
16
+ from haystack.utils.misc import _normalize_metadata_field_name
16
17
 
17
18
  from .filters import _convert_filters
18
19
  from .utils import get_embedding_function
@@ -45,9 +46,10 @@ class ChromaDocumentStore:
45
46
  metadata: dict | None = None,
46
47
  client_settings: dict[str, Any] | None = None,
47
48
  **embedding_function_params: Any,
48
- ):
49
+ ) -> None:
49
50
  """
50
51
  Creates a new ChromaDocumentStore instance.
52
+
51
53
  It is meant to be connected to a Chroma collection.
52
54
 
53
55
  Note: for the component to be part of a serializable pipeline, the __init__
@@ -102,7 +104,7 @@ class ChromaDocumentStore:
102
104
  self._collection: chromadb.Collection | None = None
103
105
  self._async_collection: AsyncCollection | None = None
104
106
 
105
- def _ensure_initialized(self):
107
+ def _ensure_initialized(self) -> None:
106
108
  if not self._collection:
107
109
  # Create the client instance
108
110
  if self._persist_path and (self._host or self._port is not None):
@@ -138,9 +140,10 @@ class ChromaDocumentStore:
138
140
 
139
141
  self._client = client # store client for potential future use
140
142
 
141
- self._metadata = self._metadata or {}
142
- if "hnsw:space" not in self._metadata:
143
- self._metadata["hnsw:space"] = self._distance_function
143
+ # Build the collection metadata locally so `self._metadata` stays exactly as the user passed it.
144
+ # This keeps `to_dict()` deterministic and avoids mutating a user-supplied dict in place.
145
+ collection_metadata = dict(self._metadata) if self._metadata else {}
146
+ collection_metadata.setdefault("hnsw:space", self._distance_function)
144
147
 
145
148
  existing_collection_names = [c.name for c in client.list_collections()]
146
149
  if self._collection_name in existing_collection_names:
@@ -149,18 +152,18 @@ class ChromaDocumentStore:
149
152
  embedding_function=self._embedding_func,
150
153
  )
151
154
 
152
- if self._metadata != self._collection.metadata:
155
+ if collection_metadata != self._collection.metadata:
153
156
  logger.warning(
154
157
  "Collection already exists. The `distance_function` and `metadata` parameters will be ignored."
155
158
  )
156
159
  else:
157
160
  self._collection = client.create_collection(
158
161
  name=self._collection_name,
159
- metadata=self._metadata,
162
+ metadata=collection_metadata,
160
163
  embedding_function=self._embedding_func,
161
164
  )
162
165
 
163
- async def _ensure_initialized_async(self):
166
+ async def _ensure_initialized_async(self) -> None:
164
167
  if not self._async_collection:
165
168
  if self._host is None or self._port is None:
166
169
  error_message = (
@@ -186,9 +189,10 @@ class ChromaDocumentStore:
186
189
 
187
190
  self._async_client = client # store client for potential future use
188
191
 
189
- self._metadata = self._metadata or {}
190
- if "hnsw:space" not in self._metadata:
191
- self._metadata["hnsw:space"] = self._distance_function
192
+ # Build the collection metadata locally so `self._metadata` stays exactly as the user passed it.
193
+ # This keeps `to_dict()` deterministic and avoids mutating a user-supplied dict in place.
194
+ collection_metadata = dict(self._metadata) if self._metadata else {}
195
+ collection_metadata.setdefault("hnsw:space", self._distance_function)
192
196
 
193
197
  collection = await client.list_collections()
194
198
  existing_collection_names = [c.name for c in collection]
@@ -198,14 +202,14 @@ class ChromaDocumentStore:
198
202
  embedding_function=self._embedding_func,
199
203
  )
200
204
 
201
- if self._metadata != self._async_collection.metadata:
205
+ if collection_metadata != self._async_collection.metadata:
202
206
  logger.warning(
203
207
  "Collection already exists. The `distance_function` and `metadata` parameters will be ignored."
204
208
  )
205
209
  else:
206
210
  self._async_collection = await client.create_collection(
207
211
  name=self._collection_name,
208
- metadata=self._metadata,
212
+ metadata=collection_metadata,
209
213
  embedding_function=self._embedding_func,
210
214
  )
211
215
 
@@ -242,16 +246,6 @@ class ChromaDocumentStore:
242
246
  "include": ["embeddings", "documents", "metadatas", "distances"],
243
247
  }
244
248
 
245
- @staticmethod
246
- def _normalize_metadata_field_name(metadata_field: str) -> str:
247
- """
248
- Normalizes a metadata field name by removing the "meta." prefix if present.
249
-
250
- :param metadata_field: The metadata field name to normalize.
251
- :returns: The normalized field name without "meta." prefix.
252
- """
253
- return metadata_field[5:] if metadata_field.startswith("meta.") else metadata_field
254
-
255
249
  @staticmethod
256
250
  def _infer_type_from_value(value: Any) -> str:
257
251
  """
@@ -341,7 +335,7 @@ class ChromaDocumentStore:
341
335
  for meta in metadatas:
342
336
  if meta and field_name in meta:
343
337
  val = meta.get(field_name)
344
- if isinstance(val, (str, int, float)):
338
+ if isinstance(val, str | int | float):
345
339
  values.append(val)
346
340
 
347
341
  if not values:
@@ -581,62 +575,123 @@ class ChromaDocumentStore:
581
575
  def write_documents(
582
576
  self,
583
577
  documents: list[Document],
584
- policy: DuplicatePolicy = DuplicatePolicy.FAIL,
578
+ policy: DuplicatePolicy = DuplicatePolicy.NONE,
585
579
  ) -> int:
586
580
  """
587
- Writes (or overwrites) documents into the store.
581
+ Writes documents into the store.
588
582
 
589
583
  :param documents:
590
584
  A list of documents to write into the document store.
591
585
  :param policy:
592
- Not supported at the moment.
586
+ How to handle documents whose `id` already exists in the store:
587
+ - `NONE` (default): treated as `FAIL`.
588
+ - `OVERWRITE`: replace the existing document.
589
+ - `SKIP`: keep the existing document and skip the new one.
590
+ - `FAIL`: raise `DuplicateDocumentError`.
593
591
 
594
592
  :raises ValueError:
595
593
  When input is not valid.
594
+ :raises DuplicateDocumentError:
595
+ When `policy` is `FAIL` (or `NONE`) and any document `id` already exists.
596
596
 
597
597
  :returns:
598
- The number of documents written
598
+ The number of documents written.
599
599
  """
600
600
  self._ensure_initialized()
601
601
  assert self._collection is not None
602
602
 
603
- for doc in documents:
604
- data = ChromaDocumentStore._convert_document_to_chroma(doc)
605
- if data is not None:
606
- self._collection.add(**data)
603
+ if policy == DuplicatePolicy.NONE:
604
+ policy = DuplicatePolicy.FAIL
605
+
606
+ chroma_payloads: list[dict[str, Any]] = [
607
+ p for p in (ChromaDocumentStore._convert_document_to_chroma(doc) for doc in documents) if p is not None
608
+ ]
609
+ if not chroma_payloads:
610
+ return 0
611
+
612
+ if policy in (DuplicatePolicy.FAIL, DuplicatePolicy.SKIP):
613
+ existing_ids = set(self._collection.get(ids=[p["ids"][0] for p in chroma_payloads])["ids"])
614
+ payloads_to_write = self._apply_duplicate_policy(chroma_payloads, existing_ids, policy)
615
+ else:
616
+ payloads_to_write = chroma_payloads
617
+
618
+ for payload in payloads_to_write:
619
+ if policy == DuplicatePolicy.OVERWRITE:
620
+ self._collection.upsert(**payload)
621
+ else:
622
+ self._collection.add(**payload)
607
623
 
608
- return len(documents)
624
+ return len(payloads_to_write)
609
625
 
610
626
  async def write_documents_async(
611
627
  self,
612
628
  documents: list[Document],
613
- policy: DuplicatePolicy = DuplicatePolicy.FAIL,
629
+ policy: DuplicatePolicy = DuplicatePolicy.NONE,
614
630
  ) -> int:
615
631
  """
616
- Asynchronously writes (or overwrites) documents into the store.
632
+ Asynchronously writes documents into the store.
617
633
 
618
634
  Asynchronous methods are only supported for HTTP connections.
619
635
 
620
636
  :param documents:
621
637
  A list of documents to write into the document store.
622
638
  :param policy:
623
- Not supported at the moment.
639
+ How to handle documents whose `id` already exists in the store:
640
+ - `NONE` (default): treated as `FAIL`.
641
+ - `OVERWRITE`: replace the existing document.
642
+ - `SKIP`: keep the existing document and skip the new one.
643
+ - `FAIL`: raise `DuplicateDocumentError`.
624
644
 
625
645
  :raises ValueError:
626
646
  When input is not valid.
647
+ :raises DuplicateDocumentError:
648
+ When `policy` is `FAIL` (or `NONE`) and any document `id` already exists.
627
649
 
628
650
  :returns:
629
- The number of documents written
651
+ The number of documents written.
630
652
  """
631
653
  await self._ensure_initialized_async()
632
654
  assert self._async_collection is not None
633
655
 
634
- for doc in documents:
635
- data = ChromaDocumentStore._convert_document_to_chroma(doc)
636
- if data is not None:
637
- await self._async_collection.add(**data)
656
+ if policy == DuplicatePolicy.NONE:
657
+ policy = DuplicatePolicy.FAIL
658
+
659
+ chroma_payloads: list[dict[str, Any]] = [
660
+ p for p in (ChromaDocumentStore._convert_document_to_chroma(doc) for doc in documents) if p is not None
661
+ ]
662
+ if not chroma_payloads:
663
+ return 0
664
+
665
+ if policy in (DuplicatePolicy.FAIL, DuplicatePolicy.SKIP):
666
+ existing = await self._async_collection.get(ids=[p["ids"][0] for p in chroma_payloads])
667
+ existing_ids = set(existing["ids"])
668
+ payloads_to_write = self._apply_duplicate_policy(chroma_payloads, existing_ids, policy)
669
+ else:
670
+ payloads_to_write = chroma_payloads
638
671
 
639
- return len(documents)
672
+ for payload in payloads_to_write:
673
+ if policy == DuplicatePolicy.OVERWRITE:
674
+ await self._async_collection.upsert(**payload)
675
+ else:
676
+ await self._async_collection.add(**payload)
677
+
678
+ return len(payloads_to_write)
679
+
680
+ @staticmethod
681
+ def _apply_duplicate_policy(
682
+ payloads: list[dict[str, Any]],
683
+ existing_ids: set[str],
684
+ policy: DuplicatePolicy,
685
+ ) -> list[dict[str, Any]]:
686
+ if policy == DuplicatePolicy.FAIL:
687
+ duplicates = [p["ids"][0] for p in payloads if p["ids"][0] in existing_ids]
688
+ if duplicates:
689
+ msg = f"Documents with ids {duplicates} already exist in the document store."
690
+ raise DuplicateDocumentError(msg)
691
+ return payloads
692
+ if policy == DuplicatePolicy.SKIP:
693
+ return [p for p in payloads if p["ids"][0] not in existing_ids]
694
+ return payloads
640
695
 
641
696
  def delete_documents(self, document_ids: list[str]) -> None:
642
697
  """
@@ -1008,8 +1063,7 @@ class ChromaDocumentStore:
1008
1063
  filters: dict[str, Any] | None = None,
1009
1064
  ) -> list[list[Document]]:
1010
1065
  """
1011
- Asynchronously perform vector search on the stored document, pass the embeddings of the queries instead of
1012
- their text.
1066
+ Asynchronously perform vector search using query embeddings instead of text.
1013
1067
 
1014
1068
  Asynchronous methods are only supported for HTTP connections.
1015
1069
 
@@ -1074,8 +1128,7 @@ class ChromaDocumentStore:
1074
1128
 
1075
1129
  def count_unique_metadata_by_filter(self, filters: dict[str, Any], metadata_fields: list[str]) -> dict[str, int]:
1076
1130
  """
1077
- Returns the number of unique values for each specified metadata field
1078
- of the documents that match the provided filters.
1131
+ Return unique value counts for metadata fields of documents matching the provided filters.
1079
1132
 
1080
1133
  :param filters: The filters to apply to count documents.
1081
1134
  For filter syntax, see [Haystack metadata filtering](https://docs.haystack.deepset.ai/docs/metadata-filtering)
@@ -1087,7 +1140,7 @@ class ChromaDocumentStore:
1087
1140
  self._ensure_initialized()
1088
1141
  assert self._collection is not None
1089
1142
 
1090
- normalized_fields = [self._normalize_metadata_field_name(field) for field in metadata_fields]
1143
+ normalized_fields = [_normalize_metadata_field_name(field) for field in metadata_fields]
1091
1144
 
1092
1145
  kwargs = ChromaDocumentStore._prepare_get_kwargs(filters)
1093
1146
  kwargs["include"] = ["metadatas"]
@@ -1099,8 +1152,7 @@ class ChromaDocumentStore:
1099
1152
  self, filters: dict[str, Any], metadata_fields: list[str]
1100
1153
  ) -> dict[str, int]:
1101
1154
  """
1102
- Asynchronously returns the number of unique values for each specified metadata field
1103
- of the documents that match the provided filters.
1155
+ Asynchronously return unique value counts for metadata fields of documents matching the provided filters.
1104
1156
 
1105
1157
  Asynchronous methods are only supported for HTTP connections.
1106
1158
 
@@ -1114,7 +1166,7 @@ class ChromaDocumentStore:
1114
1166
  await self._ensure_initialized_async()
1115
1167
  assert self._async_collection is not None
1116
1168
 
1117
- normalized_fields = [self._normalize_metadata_field_name(field) for field in metadata_fields]
1169
+ normalized_fields = [_normalize_metadata_field_name(field) for field in metadata_fields]
1118
1170
 
1119
1171
  kwargs = ChromaDocumentStore._prepare_get_kwargs(filters)
1120
1172
  kwargs["include"] = ["metadatas"]
@@ -1205,7 +1257,7 @@ class ChromaDocumentStore:
1205
1257
  self._ensure_initialized()
1206
1258
  assert self._collection is not None
1207
1259
 
1208
- field_name = self._normalize_metadata_field_name(metadata_field)
1260
+ field_name = _normalize_metadata_field_name(metadata_field)
1209
1261
 
1210
1262
  result = self._collection.get(include=["metadatas"])
1211
1263
  return self._compute_field_min_max(result.get("metadatas", []), field_name)
@@ -1229,7 +1281,7 @@ class ChromaDocumentStore:
1229
1281
  await self._ensure_initialized_async()
1230
1282
  assert self._async_collection is not None
1231
1283
 
1232
- field_name = self._normalize_metadata_field_name(metadata_field)
1284
+ field_name = _normalize_metadata_field_name(metadata_field)
1233
1285
 
1234
1286
  result = await self._async_collection.get(include=["metadatas"])
1235
1287
  return self._compute_field_min_max(result.get("metadatas", []), field_name)
@@ -1242,8 +1294,7 @@ class ChromaDocumentStore:
1242
1294
  size: int = 10,
1243
1295
  ) -> tuple[list[str], int]:
1244
1296
  """
1245
- Returns unique values for a metadata field, optionally filtered by
1246
- a search term in the content field, with pagination support.
1297
+ Return unique metadata field values, optionally filtered by a content search term, with pagination.
1247
1298
 
1248
1299
  :param metadata_field: The metadata field to get unique values for.
1249
1300
  Can include or omit the "meta." prefix.
@@ -1256,7 +1307,7 @@ class ChromaDocumentStore:
1256
1307
  self._ensure_initialized()
1257
1308
  assert self._collection is not None
1258
1309
 
1259
- field_name = self._normalize_metadata_field_name(metadata_field)
1310
+ field_name = _normalize_metadata_field_name(metadata_field)
1260
1311
 
1261
1312
  kwargs: dict[str, Any] = {"include": ["metadatas"]}
1262
1313
  if search_term:
@@ -1273,8 +1324,7 @@ class ChromaDocumentStore:
1273
1324
  size: int = 10,
1274
1325
  ) -> tuple[list[str], int]:
1275
1326
  """
1276
- Asynchronously returns unique values for a metadata field, optionally filtered by
1277
- a search term in the content field, with pagination support.
1327
+ Asynchronously return unique metadata field values, optionally filtered by content, with pagination.
1278
1328
 
1279
1329
  Asynchronous methods are only supported for HTTP connections.
1280
1330
 
@@ -1289,7 +1339,7 @@ class ChromaDocumentStore:
1289
1339
  await self._ensure_initialized_async()
1290
1340
  assert self._async_collection is not None
1291
1341
 
1292
- field_name = self._normalize_metadata_field_name(metadata_field)
1342
+ field_name = _normalize_metadata_field_name(metadata_field)
1293
1343
 
1294
1344
  kwargs: dict[str, Any] = {"include": ["metadatas"]}
1295
1345
  if search_term:
@@ -1325,6 +1375,7 @@ class ChromaDocumentStore:
1325
1375
  host=self._host,
1326
1376
  port=self._port,
1327
1377
  distance_function=self._distance_function,
1378
+ metadata=self._metadata,
1328
1379
  client_settings=self._client_settings,
1329
1380
  **self._embedding_function_params,
1330
1381
  )
@@ -44,8 +44,10 @@ class ChromaFilter:
44
44
 
45
45
  def _convert_filters(filters: dict[str, Any]) -> ChromaFilter:
46
46
  """
47
- Converts Haystack filters into a format compatible with Chroma, separating them into ids, metadata filters,
48
- and content filters to be passed to chroma as ids, where, and where_document clauses respectively.
47
+ Converts Haystack filters into a format compatible with Chroma.
48
+
49
+ Separates filters into ids, metadata filters, and content filters to be passed to chroma as ids, where,
50
+ and where_document clauses respectively.
49
51
  """
50
52
 
51
53
  ids = []
@@ -103,9 +105,7 @@ def _convert_filter_clause(filters: dict[str, Any]) -> dict[str, Any]:
103
105
 
104
106
  def _create_where_document_filter(field: str, value: dict[Any, Any]) -> dict[str, Any]:
105
107
  """
106
- Method to check if given haystack filter is a document filter
107
- and converts it to Chroma-compatible where_document filter.
108
-
108
+ Check if a given Haystack filter is a document filter and convert it to a Chroma-compatible where_document filter.
109
109
  """
110
110
  where_document: dict[str, list[Any]] = defaultdict(list)
111
111
 
@@ -37,7 +37,8 @@ FUNCTION_REGISTRY = {
37
37
 
38
38
 
39
39
  def get_embedding_function(function_name: str, **kwargs: Any) -> EmbeddingFunction:
40
- """Load an embedding function by name.
40
+ """
41
+ Load an embedding function by name.
41
42
 
42
43
  :param function_name: the name of the embedding function.
43
44
  :param kwargs: additional arguments to pass to the embedding function.