From 95ed825368d32ede6c8777d157dd75d7127ad31b Mon Sep 17 00:00:00 2001 From: TarunSinghChauhan Date: Tue, 25 Aug 2026 03:08:55 +0000 Subject: [PATCH 1/2] Fix score_threshold applied backwards for Recommend/Discover/Context on Euclidean/Manhattan collections In local/in-memory mode, LocalCollection.search() has an isinstance override that correctly treats Recommend (best_score/sum_scores), Discover, and Context queries as bigger-is-better (since they use sigmoid-based synthetic scores), regardless of the collection's underlying distance metric. This override was applied to the sort direction but not repeated for the score_threshold comparison a few lines below, which branched on required_order alone. On a Euclidean or Manhattan collection this caused the threshold check to use the comparison direction meant for raw distances, breaking the loop on the very first (best-scoring) point and returning zero results for any non-extreme threshold. Extracted the override into a single bigger_is_better variable used consistently by both the sort and the threshold check. Fixes #1370. --- qdrant_client/local/local_collection.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/qdrant_client/local/local_collection.py b/qdrant_client/local/local_collection.py index b21c18414..a8c993e63 100644 --- a/qdrant_client/local/local_collection.py +++ b/qdrant_client/local/local_collection.py @@ -697,7 +697,12 @@ def search( required_order = distance_to_order(distance) - if required_order == DistanceOrder.BIGGER_IS_BETTER or isinstance( + # Recommend (best_score/sum_scores), Discover, and Context queries always + # compute a sigmoid-based synthetic score where bigger is always better, + # regardless of the collection's underlying distance metric. This override + # must be applied consistently everywhere required_order is used for these + # query types - both for sort direction and for score_threshold comparison. + bigger_is_better = required_order == DistanceOrder.BIGGER_IS_BETTER or isinstance( query_vector, ( DiscoveryQuery, @@ -707,7 +712,9 @@ def search( MultiContextQuery, MultiRecoQuery, ), # sparse structures are not required, sparse always uses DOT - ): + ) + + if bigger_is_better: order = np.argsort(scores)[::-1] else: order = np.argsort(scores) @@ -727,7 +734,7 @@ def search( point_id = self.ids_inv[idx] if score_threshold is not None: - if required_order == DistanceOrder.BIGGER_IS_BETTER: + if bigger_is_better: if score < score_threshold: break else: From 9543d8f2909394c933dd0985da84125d4a09bbe2 Mon Sep 17 00:00:00 2001 From: TarunSinghChauhan Date: Tue, 25 Aug 2026 03:15:50 +0000 Subject: [PATCH 2/2] Fix datetime_utils.parse accepting truncated datetimes The hour-only-offset retry (which appends :00 to complete offsets like +01 -> +01:00) was unguarded, so it also fired for any string that failed every format in available_formats - including truncated datetimes like '2024-06-15 12' or '2024-06-15T12:30'. These would get :00 appended and accidentally match a valid format, causing local mode to silently accept input that a real Qdrant instance rejects. Guarded the retry to only fire when the string actually ends in an hour-only UTC offset ([+-]HH). Verified against all four cases from the issue: truncated hour and truncated minute now correctly return None, while the hour-only-offset and valid full date cases still parse correctly. Fixes #1349. --- qdrant_client/local/datetime_utils.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/qdrant_client/local/datetime_utils.py b/qdrant_client/local/datetime_utils.py index c2f957195..714fe5afb 100644 --- a/qdrant_client/local/datetime_utils.py +++ b/qdrant_client/local/datetime_utils.py @@ -1,3 +1,4 @@ +import re from datetime import datetime, timezone # These are the formats accepted by qdrant core @@ -46,4 +47,12 @@ def parse_available_formats(datetime_str: str) -> datetime | None: # dt examples to handle: # "2021-01-01 00:00:00.000+01" # "2021-01-01 00:00:00.000-10" - return parse_available_formats(date_str + ":00") + # This retry must only fire for an actual hour-only UTC offset at the end of + # the string (e.g. "+01" or "-10"). Without this guard, any string that fails + # every format above (including truncated datetimes like "2024-06-15 12" or + # "2024-06-15T12:30") would also get ":00" appended and could accidentally + # match a valid format, silently accepting input that qdrant core rejects. + if re.search(r"[+-]\d{2}$", date_str): + return parse_available_formats(date_str + ":00") + + return None