diff --git a/examples/src/main/java/io/milvus/v2/TimestampExample.java b/examples/src/main/java/io/milvus/v2/TimestampExample.java new file mode 100644 index 000000000..87979763a --- /dev/null +++ b/examples/src/main/java/io/milvus/v2/TimestampExample.java @@ -0,0 +1,209 @@ +package io.milvus.v2; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.milvus.v1.CommonUtils; +import io.milvus.v2.client.ConnectConfig; +import io.milvus.v2.client.MilvusClientV2; +import io.milvus.v2.common.ConsistencyLevel; +import io.milvus.v2.common.DataType; +import io.milvus.v2.common.IndexParam; +import io.milvus.v2.service.collection.request.AddFieldReq; +import io.milvus.v2.service.collection.request.CreateCollectionReq; +import io.milvus.v2.service.collection.request.DropCollectionReq; +import io.milvus.v2.service.vector.request.*; +import io.milvus.v2.service.vector.request.data.FloatVec; +import io.milvus.v2.service.vector.request.ranker.RRFRanker; +import io.milvus.v2.service.vector.response.QueryResp; +import io.milvus.v2.service.vector.response.SearchResp; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class TimestampExample { + private static final MilvusClientV2 client; + + static { + ConnectConfig config = ConnectConfig.builder() + .uri("http://localhost:19530") + .build(); + client = new MilvusClientV2(config); + } + + private static final String COLLECTION_NAME = "java_sdk_example_timestamp_v2"; + private static final String ID_FIELD = "ID"; + + private static final String FLOAT_VECTOR_FIELD = "vector"; + private static final Integer FLOAT_VECTOR_DIM = 128; + private static final IndexParam.MetricType FLOAT_VECTOR_METRIC = IndexParam.MetricType.COSINE; + + private static final String TIMESTAMP_VECTOR_FIELD = "tsz"; + + + private static void createCollection() { + client.dropCollection(DropCollectionReq.builder() + .collectionName(COLLECTION_NAME) + .build()); + + // Create collection + CreateCollectionReq.CollectionSchema collectionSchema = CreateCollectionReq.CollectionSchema.builder() + .build(); + collectionSchema.addField(AddFieldReq.builder() + .fieldName(ID_FIELD) + .dataType(DataType.Int64) + .isPrimaryKey(Boolean.TRUE) + .build()); + collectionSchema.addField(AddFieldReq.builder() + .fieldName(FLOAT_VECTOR_FIELD) + .dataType(DataType.FloatVector) + .dimension(FLOAT_VECTOR_DIM) + .build()); + collectionSchema.addField(AddFieldReq.builder() + .fieldName(TIMESTAMP_VECTOR_FIELD) + .dataType(DataType.Timestamptz) + .build()); + + List indexes = new ArrayList<>(); + indexes.add(IndexParam.builder() + .fieldName(FLOAT_VECTOR_FIELD) + .indexType(IndexParam.IndexType.AUTOINDEX) + .metricType(FLOAT_VECTOR_METRIC) + .build()); + indexes.add(IndexParam.builder() + .fieldName(TIMESTAMP_VECTOR_FIELD) + .indexType(IndexParam.IndexType.STL_SORT) + .build()); + + CreateCollectionReq requestCreate = CreateCollectionReq.builder() + .collectionName(COLLECTION_NAME) + .collectionSchema(collectionSchema) + .indexParams(indexes) + .consistencyLevel(ConsistencyLevel.BOUNDED) + .build(); + client.createCollection(requestCreate); + System.out.println("Collection created"); + } + + private static void insertData() { + int rowCount = 10; + ZoneId zone = ZoneId.of("Asia/Shanghai"); + DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + System.out.printf("\n================= Insert with timezone: %s =================", zone); + + // Insert entities by rows + List rows = new ArrayList<>(); + Gson gson = new Gson(); + for (long i = 0L; i < rowCount; ++i) { + JsonObject row = new JsonObject(); + row.addProperty(ID_FIELD, i); + row.add(FLOAT_VECTOR_FIELD, gson.toJsonTree(CommonUtils.generateFloatVector(FLOAT_VECTOR_DIM))); + + LocalDateTime tt = LocalDateTime.of(2025, 1, 1, 0, 0, 0); + tt = tt.plusDays(i); + ZonedDateTime zt = tt.atZone(zone); + String tzFormat = zt.format(formatter); + System.out.println(tzFormat); + row.addProperty(TIMESTAMP_VECTOR_FIELD, tzFormat); + rows.add(row); + } + + client.insert(InsertReq.builder() + .collectionName(COLLECTION_NAME) + .data(rows) + .build()); + printRowCount(); + } + + private static void printRowCount() { + // Get row count, set ConsistencyLevel.STRONG to sync the data to query node so that data is visible + QueryResp countR = client.query(QueryReq.builder() + .collectionName(COLLECTION_NAME) + .outputFields(Collections.singletonList("count(*)")) + .consistencyLevel(ConsistencyLevel.STRONG) + .build()); + System.out.printf("\n%d rows persisted", (long) countR.getQueryResults().get(0).getEntity().get("count(*)")); + } + + private static void query(String timezone) { + QueryResp queryRet = client.query(QueryReq.builder() + .collectionName(COLLECTION_NAME) + .filter(ID_FIELD + " <= 3") + .timezone(timezone) + .outputFields(Collections.singletonList(TIMESTAMP_VECTOR_FIELD)) + .build()); + System.out.println("\nQuery results:"); + List records = queryRet.getQueryResults(); + for (QueryResp.QueryResult record : records) { + System.out.println(record.getEntity()); + } + } + + private static void search(String timezone) { + SearchResp searchR = client.search(SearchReq.builder() + .collectionName(COLLECTION_NAME) + .data(Collections.singletonList(new FloatVec(CommonUtils.generateFloatVector(FLOAT_VECTOR_DIM)))) + .limit(10) + .filter(ID_FIELD + " <= 3") + .timezone(timezone) + .outputFields(Collections.singletonList(TIMESTAMP_VECTOR_FIELD)) + .build()); + List> searchResults = searchR.getSearchResults(); + System.out.println("\nSearch results:"); + for (List results : searchResults) { + for (SearchResp.SearchResult result : results) { + System.out.printf("ID: %d, Score: %f, %s\n", (long) result.getId(), result.getScore(), result.getEntity().toString()); + } + } + } + + private static void hybridSearch(String timezone) { + // this is a single-route hybrid search, just demo the timezone paramter + List searchRequests = new ArrayList<>(); + searchRequests.add(AnnSearchReq.builder() + .vectorFieldName(FLOAT_VECTOR_FIELD) + .vectors(Collections.singletonList(new FloatVec(CommonUtils.generateFloatVector(FLOAT_VECTOR_DIM)))) + .limit(10) + .filter(ID_FIELD + " <= 3") + .timezone(timezone) + .build()); + + HybridSearchReq hybridSearchReq = HybridSearchReq.builder() + .collectionName(COLLECTION_NAME) + .searchRequests(searchRequests) + .functionScore(FunctionScore.builder() + .addFunction(RRFRanker.builder().k(10).build()) + .build()) + .limit(10) + .outFields(Collections.singletonList(TIMESTAMP_VECTOR_FIELD)) + .build(); + SearchResp searchResp = client.hybridSearch(hybridSearchReq); + List> searchResults = searchResp.getSearchResults(); + System.out.println("\nHybridSearch result:"); + List results = searchResults.get(0); + for (SearchResp.SearchResult result : results) { + System.out.println(result); + } + } + + public static void main(String[] args) { + createCollection(); + insertData(); + + List timezones = Arrays.asList("America/Havana", "Africa/Bangui", "Australia/Sydney"); + + for (String timezone : timezones) { + System.out.printf("\n================= Query with timezone: %s =================", timezone); + query(timezone); + search(timezone); + hybridSearch(timezone); + } + + client.close(); + } +} diff --git a/sdk-core/src/main/java/io/milvus/param/Constant.java b/sdk-core/src/main/java/io/milvus/param/Constant.java index d9a74c9b1..25b8efd4f 100644 --- a/sdk-core/src/main/java/io/milvus/param/Constant.java +++ b/sdk-core/src/main/java/io/milvus/param/Constant.java @@ -31,6 +31,7 @@ public class Constant { public static final String ARRAY_MAX_CAPACITY = "max_capacity"; public static final String TOP_K = "topk"; public static final String IGNORE_GROWING = "ignore_growing"; + public static final String TIMEZONE = "timezone"; public static final String REDUCE_STOP_FOR_BEST = "reduce_stop_for_best"; public static final String ITERATOR_FIELD = "iterator"; public static final String GROUP_BY_FIELD = "group_by_field"; diff --git a/sdk-core/src/main/java/io/milvus/v2/service/vector/request/AnnSearchReq.java b/sdk-core/src/main/java/io/milvus/v2/service/vector/request/AnnSearchReq.java index 99a0eb912..5abc123f0 100644 --- a/sdk-core/src/main/java/io/milvus/v2/service/vector/request/AnnSearchReq.java +++ b/sdk-core/src/main/java/io/milvus/v2/service/vector/request/AnnSearchReq.java @@ -35,6 +35,7 @@ public class AnnSearchReq { private List vectors; private String params; private IndexParam.MetricType metricType; + private String timezone; private AnnSearchReq(AnnSearchReqBuilder builder) { this.vectorFieldName = builder.vectorFieldName; @@ -45,6 +46,7 @@ private AnnSearchReq(AnnSearchReqBuilder builder) { this.vectors = builder.vectors; this.params = builder.params; this.metricType = builder.metricType; + this.timezone = builder.timezone; } public static AnnSearchReqBuilder builder() { @@ -123,6 +125,10 @@ public void setMetricType(IndexParam.MetricType metricType) { this.metricType = metricType; } + public String getTimezone() { + return timezone; + } + @Override public String toString() { return "AnnSearchReq{" + @@ -134,6 +140,7 @@ public String toString() { ", vectors=" + vectors + ", params='" + params + '\'' + ", metricType=" + metricType + + ", timezone='" + timezone + '\'' + '}'; } @@ -146,6 +153,7 @@ public static class AnnSearchReqBuilder { private List vectors; private String params; private IndexParam.MetricType metricType = null; + private String timezone = ""; public AnnSearchReqBuilder vectorFieldName(String vectorFieldName) { this.vectorFieldName = vectorFieldName; @@ -195,6 +203,11 @@ public AnnSearchReqBuilder metricType(IndexParam.MetricType metricType) { return this; } + public AnnSearchReqBuilder timezone(String timezone) { + this.timezone = timezone; + return this; + } + public AnnSearchReq build() { return new AnnSearchReq(this); } diff --git a/sdk-core/src/main/java/io/milvus/v2/service/vector/request/QueryReq.java b/sdk-core/src/main/java/io/milvus/v2/service/vector/request/QueryReq.java index e33ebd237..547b16bcc 100644 --- a/sdk-core/src/main/java/io/milvus/v2/service/vector/request/QueryReq.java +++ b/sdk-core/src/main/java/io/milvus/v2/service/vector/request/QueryReq.java @@ -34,6 +34,7 @@ public class QueryReq { private long offset; private long limit; private boolean ignoreGrowing; + private String timezone; // Extra parameters for query, timezone, time_fields, etc. // Make sure the value can be converted to String by String.valueOf(). @@ -63,6 +64,7 @@ private QueryReq(QueryReqBuilder builder) { this.ignoreGrowing = builder.ignoreGrowing; this.queryParams = builder.queryParams; this.filterTemplateValues = builder.filterTemplateValues; + this.timezone = builder.timezone; } public static QueryReqBuilder builder() { @@ -149,6 +151,10 @@ public void setIgnoreGrowing(boolean ignoreGrowing) { this.ignoreGrowing = ignoreGrowing; } + public String getTimezone() { + return timezone; + } + public Map getQueryParams() { return queryParams; } @@ -178,6 +184,7 @@ public String toString() { ", offset=" + offset + ", limit=" + limit + ", ignoreGrowing=" + ignoreGrowing + + ", timezone='" + timezone + '\'' + ", queryParams=" + queryParams + ", filterTemplateValues=" + filterTemplateValues + '}'; @@ -194,6 +201,7 @@ public static class QueryReqBuilder { private long offset; private long limit; private boolean ignoreGrowing; + private String timezone = ""; private Map queryParams = new HashMap<>(); private Map filterTemplateValues = new HashMap<>(); @@ -247,6 +255,11 @@ public QueryReqBuilder ignoreGrowing(boolean ignoreGrowing) { return this; } + public QueryReqBuilder timezone(String timezone) { + this.timezone = timezone; + return this; + } + public QueryReqBuilder queryParams(Map queryParams) { this.queryParams = queryParams; return this; diff --git a/sdk-core/src/main/java/io/milvus/v2/service/vector/request/SearchReq.java b/sdk-core/src/main/java/io/milvus/v2/service/vector/request/SearchReq.java index a74d64875..514d10ced 100644 --- a/sdk-core/src/main/java/io/milvus/v2/service/vector/request/SearchReq.java +++ b/sdk-core/src/main/java/io/milvus/v2/service/vector/request/SearchReq.java @@ -48,6 +48,7 @@ public class SearchReq { private Long gracefulTime; // deprecated private ConsistencyLevel consistencyLevel; private boolean ignoreGrowing; + private String timezone; private String groupByFieldName; private Integer groupSize; private Boolean strictGroupSize; @@ -92,6 +93,7 @@ private SearchReq(SearchReqBuilder builder) { this.ranker = builder.ranker; this.functionScore = builder.functionScore; this.filterTemplateValues = builder.filterTemplateValues; + this.timezone = builder.timezone; } // Getters and Setters @@ -235,6 +237,10 @@ public void setIgnoreGrowing(boolean ignoreGrowing) { this.ignoreGrowing = ignoreGrowing; } + public String getTimezone() { + return timezone; + } + public String getGroupByFieldName() { return groupByFieldName; } @@ -303,6 +309,7 @@ public String toString() { ", gracefulTime=" + gracefulTime + ", consistencyLevel=" + consistencyLevel + ", ignoreGrowing=" + ignoreGrowing + + ", timezone='" + timezone + '\'' + ", groupByFieldName='" + groupByFieldName + '\'' + ", groupSize=" + groupSize + ", strictGroupSize=" + strictGroupSize + @@ -334,6 +341,7 @@ public static class SearchReqBuilder { private Long gracefulTime = 5000L; // default value, deprecated private ConsistencyLevel consistencyLevel = null; // default value private boolean ignoreGrowing; + private String timezone = ""; private String groupByFieldName; private Integer groupSize; private Boolean strictGroupSize; @@ -433,6 +441,11 @@ public SearchReqBuilder ignoreGrowing(boolean ignoreGrowing) { return this; } + public SearchReqBuilder timezone(String timezone) { + this.timezone = timezone; + return this; + } + public SearchReqBuilder groupByFieldName(String groupByFieldName) { this.groupByFieldName = groupByFieldName; return this; diff --git a/sdk-core/src/main/java/io/milvus/v2/utils/VectorUtils.java b/sdk-core/src/main/java/io/milvus/v2/utils/VectorUtils.java index 0cad8284f..f0d71adae 100644 --- a/sdk-core/src/main/java/io/milvus/v2/utils/VectorUtils.java +++ b/sdk-core/src/main/java/io/milvus/v2/utils/VectorUtils.java @@ -114,6 +114,14 @@ public QueryRequest ConvertToGrpcQueryRequest(QueryReq request) { .setValue(String.valueOf(request.isIgnoreGrowing())) .build()); + // timezone + if (StringUtils.isNotEmpty(request.getTimezone())) { + builder.addQueryParams(KeyValuePair.newBuilder() + .setKey(Constant.TIMEZONE) + .setValue(request.getTimezone()) + .build()); + } + return builder.build(); } @@ -243,6 +251,15 @@ public SearchRequest ConvertToGrpcSearchRequest(SearchReq request) { .build()); } + // timezone + if (StringUtils.isNotEmpty(request.getTimezone())) { + builder.addSearchParams( + KeyValuePair.newBuilder() + .setKey(Constant.TIMEZONE) + .setValue(request.getTimezone()) + .build()); + } + if (request.getGroupByFieldName() != null && !request.getGroupByFieldName().isEmpty()) { builder.addSearchParams( KeyValuePair.newBuilder() @@ -471,6 +488,15 @@ public static SearchRequest convertAnnSearchParam(AnnSearchReq annSearchReq, .build()); } + // timezone + if (StringUtils.isNotEmpty(annSearchReq.getTimezone())) { + builder.addSearchParams( + KeyValuePair.newBuilder() + .setKey(Constant.TIMEZONE) + .setValue(annSearchReq.getTimezone()) + .build()); + } + // always use expression since dsl is discarded builder.setDslType(DslType.BoolExprV1); if (annSearchReq.getExpr() != null && !annSearchReq.getExpr().isEmpty()) { diff --git a/sdk-core/src/test/java/io/milvus/v2/client/MilvusClientV2Test.java b/sdk-core/src/test/java/io/milvus/v2/client/MilvusClientV2Test.java index c818631ec..18e4062dc 100644 --- a/sdk-core/src/test/java/io/milvus/v2/client/MilvusClientV2Test.java +++ b/sdk-core/src/test/java/io/milvus/v2/client/MilvusClientV2Test.java @@ -654,9 +654,9 @@ void testV2BuilderClasses() { VerifyClass(ModelRanker.class.getName(), config); VerifyClass(RRFRanker.class.getName(), config); VerifyClass(WeightedRanker.class.getName(), config); - config.assertSetter = true; - config.assertGetter = true; + config.assertSetter = false; + config.assertGetter = true; config.setIgnoredMethods(Arrays.asList("topK", "setTopK", "getTopK", "expr", "setExpr", "getExpr")); VerifyClass(AnnSearchReq.class.getName(), config); VerifyClass(HybridSearchReq.class.getName(), config);