mirror of
https://github.com/qdrant/qdrant-client.git
synced 2026-09-21 13:37:55 -05:00
fix: fix start from with date object
This commit is contained in:
@@ -3646,7 +3646,9 @@ class RestToGrpc:
|
||||
return grpc.StartFrom(integer=model)
|
||||
if isinstance(model, float):
|
||||
return grpc.StartFrom(float=model)
|
||||
if isinstance(model, datetime):
|
||||
if isinstance(model, date):
|
||||
# covers datetime too, which is a subclass of date. convert_datetime turns a
|
||||
# bare date into midnight UTC on that day.
|
||||
ts = cls.convert_datetime(model)
|
||||
return grpc.StartFrom(timestamp=ts)
|
||||
if isinstance(model, str):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timezone
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from qdrant_client.http.models import OrderValue
|
||||
from qdrant_client.local.datetime_utils import parse
|
||||
@@ -24,7 +24,7 @@ def datetime_to_microseconds(dt: datetime) -> int:
|
||||
return delta.days * MICROS_PER_DAY + delta.seconds * MICROS_PER_SECOND + delta.microseconds
|
||||
|
||||
|
||||
def to_order_value(value: str | datetime | OrderValue | None) -> OrderValue | None:
|
||||
def to_order_value(value: str | date | datetime | OrderValue | None) -> OrderValue | None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
@@ -37,6 +37,11 @@ def to_order_value(value: str | datetime | OrderValue | None) -> OrderValue | No
|
||||
if isinstance(value, datetime):
|
||||
return datetime_to_microseconds(value)
|
||||
|
||||
if isinstance(value, date):
|
||||
# Must stay below the datetime branch: datetime is a subclass of date. Midnight is
|
||||
# naive, so `datetime_to_microseconds` reads it as UTC, like core reads "%Y-%m-%d".
|
||||
return datetime_to_microseconds(datetime.combine(value, datetime.min.time()))
|
||||
|
||||
if isinstance(value, str):
|
||||
dt = parse(value)
|
||||
if dt is not None:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from datetime import datetime, timedelta, timezone, tzinfo
|
||||
from datetime import date, datetime, timedelta, timezone, tzinfo
|
||||
|
||||
import pytest
|
||||
|
||||
from qdrant_client.local.datetime_utils import parse
|
||||
from qdrant_client.local.order_by import datetime_to_microseconds
|
||||
from qdrant_client.local.order_by import datetime_to_microseconds, to_order_value
|
||||
|
||||
|
||||
@pytest.mark.parametrize( # type: ignore
|
||||
@@ -144,3 +144,11 @@ def test_tzinfo_without_an_offset_counts_as_naive() -> None:
|
||||
assert datetime_to_microseconds(
|
||||
datetime(2024, 6, 15, 12, 30, 45, tzinfo=NoOffset())
|
||||
) == datetime_to_microseconds(datetime(2024, 6, 15, 12, 30, 45, tzinfo=timezone.utc))
|
||||
|
||||
|
||||
def test_to_order_value_reads_a_bare_date_as_utc_midnight() -> None:
|
||||
"""A `date` is a member of the StartFrom union, and means the same instant as the
|
||||
"%Y-%m-%d" string REST serializes it to. Local midnight would shift the window, and is
|
||||
only visible on a client outside UTC."""
|
||||
assert to_order_value(date(2021, 1, 1)) == 1609459200000000 # 2021-01-01T00:00:00Z
|
||||
assert to_order_value("2021-01-01") == 1609459200000000
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import date, datetime
|
||||
|
||||
from qdrant_client import models
|
||||
from qdrant_client.client_base import QdrantBase
|
||||
@@ -220,3 +220,55 @@ def test_scroll_from_naive_datetime() -> None:
|
||||
|
||||
compare_client_results(grpc_client, http_client, scroll_from_naive_datetime)
|
||||
compare_client_results(local_client, http_client, scroll_from_naive_datetime)
|
||||
|
||||
|
||||
DATE_START_FROM = date(2024, 6, 15)
|
||||
|
||||
DATED_POINTS = [
|
||||
models.PointStruct(id=1, vector=[], payload={"date_field": "2024-06-13T12:00:00+00:00"}),
|
||||
models.PointStruct(id=2, vector=[], payload={"date_field": "2024-06-14T23:59:59+00:00"}),
|
||||
# exactly on the boundary: start_from is inclusive
|
||||
models.PointStruct(id=3, vector=[], payload={"date_field": "2024-06-15T00:00:00+00:00"}),
|
||||
models.PointStruct(id=4, vector=[], payload={"date_field": "2024-06-15T08:30:00+00:00"}),
|
||||
models.PointStruct(id=5, vector=[], payload={"date_field": "2024-06-17T00:00:00+00:00"}),
|
||||
]
|
||||
|
||||
|
||||
def scroll_from_date(client: QdrantBase, direction: models.Direction) -> list[models.Record]:
|
||||
records, next_page = client.scroll(
|
||||
collection_name=COLLECTION_NAME,
|
||||
limit=10,
|
||||
order_by=models.OrderBy(key="date_field", direction=direction, start_from=DATE_START_FROM),
|
||||
with_payload=True,
|
||||
)
|
||||
|
||||
assert next_page is None
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def test_scroll_order_by_date_start_from() -> None:
|
||||
"""A `date` start_from means midnight UTC on that day to local, REST and gRPC alike.
|
||||
|
||||
Local mode used to drop it and scroll the whole collection, and gRPC used to raise.
|
||||
"""
|
||||
local_client = init_local()
|
||||
init_client(local_client, DATED_POINTS, vectors_config={})
|
||||
|
||||
http_client = init_remote()
|
||||
init_client(http_client, DATED_POINTS, vectors_config={})
|
||||
http_client.create_payload_index(
|
||||
COLLECTION_NAME, "date_field", models.PayloadSchemaType.DATETIME, wait=True
|
||||
)
|
||||
|
||||
grpc_client = init_remote(prefer_grpc=True)
|
||||
|
||||
for direction, expected_ids in [
|
||||
(models.Direction.ASC, [3, 4, 5]),
|
||||
(models.Direction.DESC, [3, 2, 1]),
|
||||
]:
|
||||
# a dropped start_from returns all five points, which congruence alone would miss
|
||||
assert [record.id for record in scroll_from_date(http_client, direction)] == expected_ids
|
||||
|
||||
compare_client_results(grpc_client, http_client, scroll_from_date, direction=direction)
|
||||
compare_client_results(local_client, http_client, scroll_from_date, direction=direction)
|
||||
|
||||
Reference in New Issue
Block a user