Skip to content

Commit a0c797a

Browse files
fix: replace in-place dataclass mutations in document stores (#3114)
* fix: replace in-place dataclass mutations in document stores Replaces direct field assignments on Document instances with dataclasses.replace(), removing DeprecationWarnings from haystack#10650. Affected stores: - elasticsearch: doc.score in BM25 score scaling (sync + async) - opensearch: doc.score in BM25 score scaling - pinecone: doc.score in filter_documents (sync + async), document.meta in _update_meta_for_documents and _discard_invalid_meta - qdrant: document.score in score scaling Part of deepset-ai/haystack#10956 * fix(pinecone): fix PLW2901 and update tests for non-mutating _discard_invalid_meta --------- Co-authored-by: David S. Batista <dsbatista@gmail.com>
1 parent b5aa532 commit a0c797a

5 files changed

Lines changed: 52 additions & 42 deletions

File tree

integrations/elasticsearch/src/haystack_integrations/document_stores/elasticsearch/document_store.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99

1010
from collections.abc import Mapping
11+
from dataclasses import replace
1112
from typing import Any, Literal
1213

1314
import numpy as np
@@ -904,10 +905,12 @@ def _bm25_retrieval(
904905
documents = self._search_documents(**body)
905906

906907
if scale_score:
907-
for doc in documents:
908-
if doc.score is None:
909-
continue
910-
doc.score = float(1 / (1 + np.exp(-np.asarray(doc.score / BM25_SCALING_FACTOR))))
908+
documents = [
909+
replace(doc, score=float(1 / (1 + np.exp(-np.asarray(doc.score / BM25_SCALING_FACTOR)))))
910+
if doc.score is not None
911+
else doc
912+
for doc in documents
913+
]
911914

912915
return documents
913916

@@ -962,9 +965,12 @@ async def _bm25_retrieval_async(
962965
documents = await self._search_documents_async(**search_body)
963966

964967
if scale_score:
965-
for doc in documents:
966-
if doc.score is not None:
967-
doc.score = float(1 / (1 + np.exp(-(doc.score / float(BM25_SCALING_FACTOR)))))
968+
documents = [
969+
replace(doc, score=float(1 / (1 + np.exp(-(doc.score / float(BM25_SCALING_FACTOR))))))
970+
if doc.score is not None
971+
else doc
972+
for doc in documents
973+
]
968974

969975
return documents
970976

integrations/opensearch/src/haystack_integrations/document_stores/opensearch/document_store.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# ruff: noqa: FBT001, FBT002 boolean-type-hint-positional-argument and boolean-default-value-positional-argument
66

77
from collections.abc import Generator, Mapping
8+
from dataclasses import replace
89
from math import exp
910
from typing import Any, Literal
1011

@@ -940,10 +941,12 @@ def _postprocess_bm25_search_results(*, results: list[Document], scale_score: bo
940941
if not scale_score:
941942
return
942943

943-
for doc in results:
944-
if doc.score is None:
945-
continue
946-
doc.score = float(1 / (1 + exp(-(doc.score / float(BM25_SCALING_FACTOR)))))
944+
results = [
945+
replace(doc, score=float(1 / (1 + exp(-(doc.score / float(BM25_SCALING_FACTOR))))))
946+
if doc.score is not None
947+
else doc
948+
for doc in results
949+
]
947950

948951
def _bm25_retrieval(
949952
self,

integrations/pinecone/src/haystack_integrations/document_stores/pinecone/document_store.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# SPDX-License-Identifier: Apache-2.0
44

55
from copy import copy
6+
from dataclasses import replace
67
from typing import Any, Literal
78

89
from haystack import default_from_dict, default_to_dict, logging
@@ -310,8 +311,7 @@ def filter_documents(self, filters: dict[str, Any] | None = None) -> list[Docume
310311

311312
# when simply filtering, we don't want to return any scores
312313
# furthermore, we are querying with a dummy vector, so the scores are meaningless
313-
for doc in documents:
314-
doc.score = None
314+
documents = [replace(doc, score=None) for doc in documents]
315315

316316
if len(documents) == TOP_K_LIMIT:
317317
logger.warning(
@@ -336,8 +336,7 @@ async def filter_documents_async(self, filters: dict[str, Any] | None = None) ->
336336
query_embedding=self._dummy_vector, filters=filters, top_k=TOP_K_LIMIT
337337
)
338338

339-
for doc in documents:
340-
doc.score = None
339+
documents = [replace(doc, score=None) for doc in documents]
341340

342341
if len(documents) == TOP_K_LIMIT:
343342
logger.warning(
@@ -399,10 +398,8 @@ def _update_documents_metadata(documents: list[Document], meta: dict[str, Any])
399398
:param documents: List of documents to update.
400399
:param meta: Metadata fields to merge into each document's existing metadata.
401400
"""
402-
for document in documents:
403-
if document.meta is None:
404-
document.meta = {}
405-
document.meta.update(meta)
401+
for i, document in enumerate(documents):
402+
documents[i] = replace(document, meta={**(document.meta or {}), **meta})
406403

407404
def delete_by_filter(self, filters: dict[str, Any]) -> int:
408405
"""
@@ -671,7 +668,7 @@ def _convert_query_result_to_documents(self, query_result: Any) -> list[Document
671668
return documents
672669

673670
@staticmethod
674-
def _discard_invalid_meta(document: Document) -> None:
671+
def _discard_invalid_meta(document: Document) -> Document:
675672
"""
676673
Remove metadata fields with unsupported types from the document.
677674
"""
@@ -697,7 +694,9 @@ def valid_type(value: Any) -> bool:
697694
)
698695
logger.warning(msg)
699696

700-
document.meta = new_meta
697+
return replace(document, meta=new_meta)
698+
699+
return document
701700

702701
def _convert_documents_to_pinecone_format(
703702
self, documents: list[Document]
@@ -712,10 +711,9 @@ def _convert_documents_to_pinecone_format(
712711
)
713712
embedding = self._dummy_vector
714713

715-
if document.meta:
716-
self._discard_invalid_meta(document)
714+
filtered_meta = self._discard_invalid_meta(document).meta if document.meta else {}
717715

718-
metadata = dict(document.meta) if document.meta else {}
716+
metadata = dict(filtered_meta) if filtered_meta else {}
719717

720718
# we save content as metadata
721719
if document.content is not None:

integrations/pinecone/tests/test_document_store.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,13 @@ def test_discard_invalid_meta_invalid():
177177
],
178178
},
179179
)
180-
PineconeDocumentStore._discard_invalid_meta(invalid_metadata_doc)
180+
result = PineconeDocumentStore._discard_invalid_meta(invalid_metadata_doc)
181181

182-
assert invalid_metadata_doc.meta["source_id"] == "62049ba1d1e1d5ebb1f6230b0b00c5356b8706c56e0b9c36b1dfc86084cd75f0"
183-
assert invalid_metadata_doc.meta["page_number"] == 1
184-
assert invalid_metadata_doc.meta["split_id"] == 0
185-
assert invalid_metadata_doc.meta["split_idx_start"] == 0
186-
assert "_split_overlap" not in invalid_metadata_doc.meta
182+
assert result.meta["source_id"] == "62049ba1d1e1d5ebb1f6230b0b00c5356b8706c56e0b9c36b1dfc86084cd75f0"
183+
assert result.meta["page_number"] == 1
184+
assert result.meta["split_id"] == 0
185+
assert result.meta["split_idx_start"] == 0
186+
assert "_split_overlap" not in result.meta
187187

188188

189189
def test_discard_invalid_meta_valid():
@@ -194,10 +194,10 @@ def test_discard_invalid_meta_valid():
194194
"page_number": 1,
195195
},
196196
)
197-
PineconeDocumentStore._discard_invalid_meta(valid_metadata_doc)
197+
result = PineconeDocumentStore._discard_invalid_meta(valid_metadata_doc)
198198

199-
assert valid_metadata_doc.meta["source_id"] == "62049ba1d1e1d5ebb1f6230b0b00c5356b8706c56e0b9c36b1dfc86084cd75f0"
200-
assert valid_metadata_doc.meta["page_number"] == 1
199+
assert result.meta["source_id"] == "62049ba1d1e1d5ebb1f6230b0b00c5356b8706c56e0b9c36b1dfc86084cd75f0"
200+
assert result.meta["page_number"] == 1
201201

202202

203203
def test_convert_meta_to_int():

integrations/qdrant/src/haystack_integrations/document_stores/qdrant/document_store.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import inspect
22
from collections.abc import AsyncGenerator, Generator
3+
from dataclasses import replace
34
from itertools import islice
45
from typing import Any, ClassVar, cast
56

@@ -2474,15 +2475,17 @@ def _process_query_point_results(
24742475
]
24752476

24762477
if scale_score:
2477-
for document in documents:
2478-
score = document.score
2479-
if score is None:
2480-
continue
2481-
if self.similarity == "cosine":
2482-
score = (score + 1) / 2
2483-
else:
2484-
score = float(1 / (1 + exp(-score / 100)))
2485-
document.score = score
2478+
documents = [
2479+
replace(
2480+
document,
2481+
score=(document.score + 1) / 2
2482+
if self.similarity == "cosine"
2483+
else float(1 / (1 + exp(-document.score / 100))),
2484+
)
2485+
if document.score is not None
2486+
else document
2487+
for document in documents
2488+
]
24862489

24872490
return documents
24882491

0 commit comments

Comments
 (0)