Prechádzať zdrojové kódy

SearchIteratorV2 (#1328)

Signed-off-by: yhmo <yihua.mo@zilliz.com>
groot 1 mesiac pred
rodič
commit
2a8feb3e8f

+ 88 - 29
examples/src/main/java/io/milvus/v2/IteratorExample.java

@@ -24,6 +24,7 @@ import com.google.gson.Gson;
 import com.google.gson.JsonObject;
 import io.milvus.orm.iterator.QueryIterator;
 import io.milvus.orm.iterator.SearchIterator;
+import io.milvus.orm.iterator.SearchIteratorV2;
 import io.milvus.response.QueryResultsWrapper;
 import io.milvus.v1.CommonUtils;
 import io.milvus.v2.client.ConnectConfig;
@@ -34,29 +35,29 @@ 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.InsertReq;
-import io.milvus.v2.service.vector.request.QueryIteratorReq;
-import io.milvus.v2.service.vector.request.QueryReq;
-import io.milvus.v2.service.vector.request.SearchIteratorReq;
+import io.milvus.v2.service.vector.request.*;
 import io.milvus.v2.service.vector.request.data.FloatVec;
 import io.milvus.v2.service.vector.response.InsertResp;
 import io.milvus.v2.service.vector.response.QueryResp;
+import io.milvus.v2.service.vector.response.SearchResp;
+import org.apache.commons.lang3.StringUtils;
 
 import java.util.*;
 
 public class IteratorExample {
+    private static final MilvusClientV2 client;
+    static {
+        client = new MilvusClientV2(ConnectConfig.builder()
+                .uri("http://localhost:19530")
+                .build());
+    }
     private static final String COLLECTION_NAME = "java_sdk_example_iterator_v2";
     private static final String ID_FIELD = "userID";
     private static final String AGE_FIELD = "userAge";
     private static final String VECTOR_FIELD = "userFace";
     private static final Integer VECTOR_DIM = 128;
 
-    public static void main(String[] args) {
-        ConnectConfig config = ConnectConfig.builder()
-                .uri("http://localhost:19530")
-                .build();
-        MilvusClientV2 client = new MilvusClientV2(config);
-
+    private static void buildCollection() {
         // Create collection
         CreateCollectionReq.CollectionSchema collectionSchema = CreateCollectionReq.CollectionSchema.builder()
                 .build();
@@ -123,21 +124,58 @@ public class IteratorExample {
                 .build());
         List<QueryResp.QueryResult> queryResults = queryResp.getQueryResults();
         System.out.printf("Inserted row count: %d\n", queryResults.get(0).getEntity().get("count(*)"));
+    }
 
-        // Search iterator
+    // Query iterator
+    private static void queryIterator(String expr, int batchSize, int offset, int limit) {
+        System.out.println("\n========== queryIterator() ==========");
+        System.out.println(String.format("expr='%s', batchSize=%d, offset=%d, limit=%d", expr, batchSize, offset, limit));
+        QueryIterator queryIterator = client.queryIterator(QueryIteratorReq.builder()
+                .collectionName(COLLECTION_NAME)
+                .expr(expr)
+                .outputFields(Lists.newArrayList(ID_FIELD, AGE_FIELD))
+                .batchSize(batchSize)
+                .offset(offset)
+                .limit(limit)
+                .consistencyLevel(ConsistencyLevel.BOUNDED)
+                .build());
+
+        System.out.println("QueryIterator results:");
+        int counter = 0;
+        while (true) {
+            List<QueryResultsWrapper.RowRecord> res = queryIterator.next();
+            if (res.isEmpty()) {
+                System.out.println("query iteration finished, close");
+                queryIterator.close();
+                break;
+            }
+
+            for (QueryResultsWrapper.RowRecord record : res) {
+                System.out.println(record);
+                counter++;
+            }
+        }
+        System.out.printf("%d query results returned%n", counter);
+    }
+
+    // Search iterator V1
+    private static void searchIteratorV1(String expr, String params, int batchSize, int topK) {
+        System.out.println("\n========== searchIteratorV1() ==========");
+        System.out.println(String.format("expr='%s', params='%s', batchSize=%d, topK=%d", expr, params, batchSize, topK));
         SearchIterator searchIterator = client.searchIterator(SearchIteratorReq.builder()
                 .collectionName(COLLECTION_NAME)
                 .outputFields(Lists.newArrayList(AGE_FIELD))
-                .batchSize(50L)
+                .batchSize(batchSize)
                 .vectorFieldName(VECTOR_FIELD)
                 .vectors(Collections.singletonList(new FloatVec(CommonUtils.generateFloatVector(VECTOR_DIM))))
-                .expr(String.format("%s > 50 && %s < 100", AGE_FIELD, AGE_FIELD))
-                .params("{\"range_filter\": 15.0, \"radius\": 20.0}")
-                .topK(300)
+                .expr(expr)
+                .params(StringUtils.isEmpty(params) ? "{}" : params)
+                .topK(topK)
                 .metricType(IndexParam.MetricType.L2)
                 .consistencyLevel(ConsistencyLevel.BOUNDED)
                 .build());
 
+        System.out.println("SearchIteratorV1 results:");
         int counter = 0;
         while (true) {
             List<QueryResultsWrapper.RowRecord> res = searchIterator.next();
@@ -153,34 +191,55 @@ public class IteratorExample {
             }
         }
         System.out.printf("%d search results returned\n%n", counter);
+    }
 
-        // Query iterator
-        QueryIterator queryIterator = client.queryIterator(QueryIteratorReq.builder()
+    // Search iterator V2
+    // In SDK v2.5.6, we provide a new search iterator implementation. SearchIteratorV2 is recommended.
+    // SearchIteratorV2 is faster than V1 by 20~30 percent, and the recall is a little better than V1.
+    private static void searchIteratorV2(String filter, Map<String, Object> params, int batchSize, int topK) {
+        System.out.println("\n========== searchIteratorV2() ==========");
+        System.out.println(String.format("expr='%s', params='%s', batchSize=%d, topK=%d",
+                filter, params==null ? "" : params.toString(), batchSize, topK));
+        SearchIteratorV2 searchIterator = client.searchIteratorV2(SearchIteratorReqV2.builder()
                 .collectionName(COLLECTION_NAME)
-                .expr(String.format("%s < 300", ID_FIELD))
-                .outputFields(Lists.newArrayList(ID_FIELD, AGE_FIELD))
-                .batchSize(50L)
-                .offset(5)
-                .limit(400)
+                .outputFields(Lists.newArrayList(AGE_FIELD))
+                .batchSize(batchSize)
+                .vectorFieldName(VECTOR_FIELD)
+                .vectors(Collections.singletonList(new FloatVec(CommonUtils.generateFloatVector(VECTOR_DIM))))
+                .filter(filter)
+                .searchParams(params==null ? new HashMap<>() : params)
+                .topK(topK)
+                .metricType(IndexParam.MetricType.L2)
                 .consistencyLevel(ConsistencyLevel.BOUNDED)
                 .build());
 
-        counter = 0;
+        System.out.println("SearchIteratorV2 results:");
+        int counter = 0;
         while (true) {
-            List<QueryResultsWrapper.RowRecord> res = queryIterator.next();
+            List<SearchResp.SearchResult> res = searchIterator.next();
             if (res.isEmpty()) {
-                System.out.println("query iteration finished, close");
-                queryIterator.close();
+                System.out.println("Search iteration finished, close");
+                searchIterator.close();
                 break;
             }
 
-            for (QueryResultsWrapper.RowRecord record : res) {
+            for (SearchResp.SearchResult record : res) {
                 System.out.println(record);
                 counter++;
             }
         }
-        System.out.printf("%d query results returned%n", counter);
+        System.out.printf("%d search results returned\n%n", counter);
+    }
 
-        client.close();
+    public static void main(String[] args) {
+        buildCollection();
+        queryIterator("userID < 300",50, 5,400);
+        searchIteratorV1("userAge > 50 &&userAge < 100", "{\"range_filter\": 15.0, \"radius\": 20.0}", 100, 500);
+        searchIteratorV1("", "", 10, 99);
+        searchIteratorV2("userAge > 10 &&userAge < 20", null, 50, 100);
+
+        Map<String,Object> extraParams = new HashMap<>();
+        extraParams.put("radius",15.0);
+        searchIteratorV2("", extraParams, 50, 100);
     }
 }

+ 19 - 0
sdk-core/src/main/java/io/milvus/orm/iterator/IteratorAdapterV2.java

@@ -1,3 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
 package io.milvus.orm.iterator;
 
 import io.milvus.common.clientenum.ConsistencyLevelEnum;

+ 19 - 0
sdk-core/src/main/java/io/milvus/orm/iterator/IteratorCache.java

@@ -1,3 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
 package io.milvus.orm.iterator;
 
 import io.milvus.response.QueryResultsWrapper;

+ 19 - 0
sdk-core/src/main/java/io/milvus/orm/iterator/SearchIterator.java

@@ -1,3 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
 package io.milvus.orm.iterator;
 
 

+ 235 - 0
sdk-core/src/main/java/io/milvus/orm/iterator/SearchIteratorV2.java

@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.milvus.orm.iterator;
+
+import io.milvus.common.utils.ExceptionUtils;
+import io.milvus.grpc.*;
+import io.milvus.param.Constant;
+import io.milvus.v2.service.collection.response.DescribeCollectionResp;
+import io.milvus.v2.service.vector.request.SearchIteratorReqV2;
+import io.milvus.v2.service.vector.request.SearchReq;
+import io.milvus.v2.service.vector.response.SearchResp;
+import io.milvus.v2.utils.ConvertUtils;
+import io.milvus.v2.utils.RpcUtils;
+import io.milvus.v2.utils.VectorUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.function.Function;
+
+import static io.milvus.param.Constant.MAX_BATCH_SIZE;
+import static io.milvus.param.Constant.UNLIMITED;
+
+public class SearchIteratorV2 {
+    private static final Logger logger = LoggerFactory.getLogger(SearchIterator.class);
+    private final MilvusServiceGrpc.MilvusServiceBlockingStub blockingStub;
+
+    private final SearchIteratorReqV2 searchIteratorReq;
+    private final int batchSize;
+
+    private Map<String, Object> searchParams;
+    private final RpcUtils rpcUtils;
+
+    private Integer leftResCnt = null;
+    private Long collectionID = null;
+    private Function<List<SearchResp.SearchResult>, List<SearchResp.SearchResult>> externalFilterFunc = null;
+    private List<SearchResp.SearchResult> cache = new ArrayList<>();
+
+    // to support V2
+    public SearchIteratorV2(SearchIteratorReqV2 searchIteratorReq,
+                            MilvusServiceGrpc.MilvusServiceBlockingStub blockingStub) {
+        this.blockingStub = blockingStub;
+        this.searchIteratorReq = searchIteratorReq;
+
+        this.batchSize = (int) searchIteratorReq.getBatchSize();
+        this.externalFilterFunc = searchIteratorReq.getExternalFilterFunc();
+        this.rpcUtils = new RpcUtils();
+
+        checkParams();
+        setupCollectionID();
+        probeForCompability();
+    }
+
+    private void checkParams() {
+        if (this.batchSize < 0) {
+            ExceptionUtils.throwUnExpectedException("Batch size cannot be less than zero");
+        } else if (this.batchSize > MAX_BATCH_SIZE) {
+            ExceptionUtils.throwUnExpectedException(String.format("Batch size cannot be larger than %d", MAX_BATCH_SIZE));
+        }
+
+        searchParams = searchIteratorReq.getSearchParams();
+        if (searchParams.containsKey(Constant.OFFSET) && (int)searchParams.get(Constant.OFFSET) > 0) {
+            ExceptionUtils.throwUnExpectedException("Offset is not supported for SearchIterator");
+        }
+
+        int rows = searchIteratorReq.getVectors().size();
+        if (rows > 1) {
+            ExceptionUtils.throwUnExpectedException("SearchIterator does not support processing multiple vectors simultaneously");
+        } else if (rows <= 0) {
+            ExceptionUtils.throwUnExpectedException("The vector data for search cannot be empty");
+        }
+
+        if (searchIteratorReq.getTopK() != UNLIMITED) {
+            this.leftResCnt = searchIteratorReq.getTopK();
+        }
+    }
+
+    private void setupCollectionID() {
+        DescribeCollectionRequest.Builder builder = DescribeCollectionRequest.newBuilder()
+                .setCollectionName(searchIteratorReq.getCollectionName());
+        if (StringUtils.isNotEmpty(searchIteratorReq.getDatabaseName())) {
+            builder.setDbName(searchIteratorReq.getDatabaseName());
+        }
+        DescribeCollectionResponse response = rpcUtils.retry(()->this.blockingStub.describeCollection(builder.build()));
+        String title = String.format("DescribeCollectionRequest collectionName:%s", searchIteratorReq.getCollectionName());
+        rpcUtils.handleResponse(title, response.getStatus());
+
+        DescribeCollectionResp respR = new ConvertUtils().convertDescCollectionResp(response);
+        this.collectionID = respR.getCollectionID();
+    }
+
+    private SearchResults executeSearch(int limit) {
+        searchParams.put("search_iter_batch_size", limit);
+        SearchReq request = SearchReq.builder()
+                .collectionName(searchIteratorReq.getCollectionName())
+                .partitionNames(searchIteratorReq.getPartitionNames())
+                .databaseName(searchIteratorReq.getDatabaseName())
+                .annsField(searchIteratorReq.getVectorFieldName())
+                .data(searchIteratorReq.getVectors())
+                .topK(limit)
+                .filter(searchIteratorReq.getFilter())
+                .consistencyLevel(searchIteratorReq.getConsistencyLevel())
+                .outputFields(searchIteratorReq.getOutputFields())
+                .roundDecimal(searchIteratorReq.getRoundDecimal())
+                .searchParams(searchParams)
+                .metricType(searchIteratorReq.getMetricType())
+                .ignoreGrowing(searchIteratorReq.isIgnoreGrowing())
+                .groupByFieldName(searchIteratorReq.getGroupByFieldName())
+                .build();
+        SearchRequest searchRequest = new VectorUtils().ConvertToGrpcSearchRequest(request);
+        SearchResults response = rpcUtils.retry(()->this.blockingStub.search(searchRequest));
+        String title = String.format("SearchRequest collectionName:%s", searchIteratorReq.getCollectionName());
+        rpcUtils.handleResponse(title, response.getStatus());
+
+        return response;
+    }
+
+    private void probeForCompability() {
+        searchParams.put("collection_id", this.collectionID);
+        searchParams.put("iterator", true);
+        searchParams.put("search_iter_v2", true);
+        searchParams.put("guarantee_timestamp", 0L);
+
+        SearchResultData resultData = executeSearch(1).getResults();
+        checkTokenExists(resultData);
+    }
+
+    private void checkTokenExists(SearchResultData resultData) {
+        String token = resultData.getSearchIteratorV2Results().getToken();
+        if (StringUtils.isEmpty(token)) {
+            ExceptionUtils.throwUnExpectedException("The server does not support Search Iterator V2." +
+                    " The search_iterator (v1) is used instead.\n" +
+                    "    Please upgrade your Milvus server version to 2.5.2 and later,\n" +
+                    "    or use a pymilvus version before 2.5.3 (excluded) to avoid this issue.");
+        }
+    }
+
+    public List<SearchResp.SearchResult> next() {
+        if (leftResCnt != null && leftResCnt <= 0) {
+            return new ArrayList<>();
+        }
+
+        if (externalFilterFunc == null) {
+            return wrapReturnRes(_next());
+        }
+
+        int targetLen = batchSize;
+        if (leftResCnt != null && leftResCnt < targetLen) {
+            targetLen = leftResCnt;
+        }
+
+        while (true) {
+            List<SearchResp.SearchResult> hits = _next();
+            if (hits == null || hits.isEmpty()) {
+                break;
+            }
+
+            if (externalFilterFunc != null) {
+                hits = externalFilterFunc.apply(hits);
+            }
+
+            cache.addAll(hits);
+            if (cache.size() >= targetLen) {
+                break;
+            }
+        }
+
+        // create a list with elements from 0 to targetLen, and remove the elements from cache
+        List<SearchResp.SearchResult> subList = cache.subList(0, targetLen);
+        List<SearchResp.SearchResult> ret = new ArrayList<>(subList);
+        subList.clear();
+        return wrapReturnRes(ret);
+    }
+
+    private List<SearchResp.SearchResult> _next() {
+        SearchResults response = executeSearch(batchSize);
+        checkTokenExists(response.getResults());
+        SearchIteratorV2Results iterInfo = response.getResults().getSearchIteratorV2Results();
+        searchParams.put("search_iter_last_bound", iterInfo.getLastBound());
+
+        if (!searchParams.containsKey("search_iter_id")) {
+            searchParams.put("search_iter_id", iterInfo.getToken());
+        }
+
+        long ts = (long)searchParams.get("guarantee_timestamp");
+        if (ts <= 0) {
+            if (response.getSessionTs() > 0) {
+                searchParams.put("guarantee_timestamp", response.getSessionTs());
+            } else {
+                logger.warn("Failed to set up mvccTs from milvus server, use client-side ts instead");
+
+                long clientTs = System.currentTimeMillis() + 1000L;
+                clientTs = clientTs << 18;
+                searchParams.put("guarantee_timestamp", clientTs);
+            }
+        }
+
+        List<List<SearchResp.SearchResult>> res = new ConvertUtils().getEntities(response);
+        return res.get(0);
+    }
+
+    private List<SearchResp.SearchResult> wrapReturnRes(List<SearchResp.SearchResult> res) {
+        if (leftResCnt == null) {
+            return res;
+        }
+
+        int currentLen = res.size();
+        if (currentLen > leftResCnt) {
+            res = res.subList(0, leftResCnt);
+        }
+        leftResCnt -= currentLen;
+        return res;
+    }
+
+    public void close() {
+    }
+}

+ 11 - 0
sdk-core/src/main/java/io/milvus/v2/client/MilvusClientV2.java

@@ -23,6 +23,7 @@ import io.grpc.ManagedChannel;
 import io.milvus.grpc.*;
 import io.milvus.orm.iterator.QueryIterator;
 import io.milvus.orm.iterator.SearchIterator;
+import io.milvus.orm.iterator.SearchIteratorV2;
 
 import io.milvus.v2.service.database.DatabaseService;
 import io.milvus.v2.service.database.request.*;
@@ -544,6 +545,16 @@ public class MilvusClientV2 {
         return rpcUtils.retry(()->vectorService.searchIterator(this.getRpcStub(), request));
     }
 
+    /**
+     * Get searchIteratorV2 based on a vector field. Use expression to do filtering before search.
+     *
+     * @param request {@link SearchIteratorReqV2}
+     * @return {status:result code, data: SearchIteratorV2}
+     */
+    public SearchIteratorV2 searchIteratorV2(SearchIteratorReqV2 request) {
+        return rpcUtils.retry(()->vectorService.searchIteratorV2(this.getRpcStub(), request));
+    }
+
     /////////////////////////////////////////////////////////////////////////////////////////////
     // Partition Operations
     /////////////////////////////////////////////////////////////////////////////////////////////

+ 5 - 0
sdk-core/src/main/java/io/milvus/v2/service/vector/VectorService.java

@@ -251,6 +251,11 @@ public class VectorService extends BaseService {
         return new SearchIterator(request, blockingStub, pkField);
     }
 
+    public SearchIteratorV2 searchIteratorV2(MilvusServiceGrpc.MilvusServiceBlockingStub blockingStub,
+                                             SearchIteratorReqV2 request) {
+        return new SearchIteratorV2(request, blockingStub);
+    }
+
     public DeleteResp delete(MilvusServiceGrpc.MilvusServiceBlockingStub blockingStub, DeleteReq request) {
         String title = String.format("DeleteRequest collectionName:%s", request.getCollectionName());
 

+ 49 - 0
sdk-core/src/main/java/io/milvus/v2/service/vector/request/SearchIteratorReqV2.java

@@ -0,0 +1,49 @@
+package io.milvus.v2.service.vector.request;
+
+import com.google.common.collect.Lists;
+import io.milvus.v2.common.ConsistencyLevel;
+import io.milvus.v2.common.IndexParam;
+import io.milvus.v2.service.vector.request.data.BaseVector;
+import io.milvus.v2.service.vector.response.SearchResp;
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.SuperBuilder;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+@Data
+@SuperBuilder
+public class SearchIteratorReqV2 {
+    private String databaseName;
+    private String collectionName;
+    @Builder.Default
+    private List<String> partitionNames = Lists.newArrayList();
+    @Builder.Default
+    private IndexParam.MetricType metricType = IndexParam.MetricType.INVALID;
+    private String vectorFieldName;
+    @Builder.Default
+    private int topK = -1;
+    @Builder.Default
+    private String filter = "";
+    @Builder.Default
+    private List<String> outputFields = Lists.newArrayList();
+    @Builder.Default
+    private List<BaseVector> vectors = Lists.newArrayList();
+    @Builder.Default
+    private int roundDecimal = -1;
+    @Builder.Default
+    private Map<String, Object> searchParams = new HashMap<>();
+    @Builder.Default
+    private ConsistencyLevel consistencyLevel = null;
+    @Builder.Default
+    private boolean ignoreGrowing = false;
+    @Builder.Default
+    private String groupByFieldName = "";
+    @Builder.Default
+    private long batchSize = 1000L;
+    @Builder.Default
+    private Function<List<SearchResp.SearchResult>, List<SearchResp.SearchResult>> externalFilterFunc = null;
+}

+ 11 - 2
sdk-core/src/main/java/io/milvus/v2/utils/VectorUtils.java

@@ -250,8 +250,17 @@ public class VectorUtils {
             });
         }
 
-        long guaranteeTimestamp = getGuaranteeTimestamp(request.getConsistencyLevel(), request.getCollectionName());
-        builder.setGuaranteeTimestamp(guaranteeTimestamp);
+        // the SearchIteratorV2 passes a guaranteeTimestamp value, no need to call getGuaranteeTimestamp()
+        if (request.getSearchParams().containsKey("iterator")) {
+            long guaranteeTimestamp = 0;
+            if (request.getSearchParams().containsKey("guarantee_timestamp")) {
+                guaranteeTimestamp = (long)request.getSearchParams().get("guarantee_timestamp");
+            }
+            builder.setGuaranteeTimestamp(guaranteeTimestamp);
+        } else {
+            long guaranteeTimestamp = getGuaranteeTimestamp(request.getConsistencyLevel(), request.getCollectionName());
+            builder.setGuaranteeTimestamp(guaranteeTimestamp);
+        }
 
         // a new parameter from v2.2.9, if user didn't specify consistency level, set this parameter to true
         if (request.getConsistencyLevel() == null) {

+ 67 - 0
sdk-core/src/test/java/io/milvus/v2/client/MilvusClientV2DockerTest.java

@@ -30,6 +30,7 @@ import io.milvus.common.utils.Float16Utils;
 import io.milvus.common.utils.JsonUtils;
 import io.milvus.orm.iterator.QueryIterator;
 import io.milvus.orm.iterator.SearchIterator;
+import io.milvus.orm.iterator.SearchIteratorV2;
 import io.milvus.param.Constant;
 import io.milvus.pool.MilvusClientV2Pool;
 import io.milvus.pool.PoolConfig;
@@ -1444,6 +1445,7 @@ class MilvusClientV2DockerTest {
             }
         }
         System.out.println(String.format("There are %d items match score between [5.0, 50.0]", counter));
+        Assertions.assertTrue(counter > 0);
 
         // query iterator
         QueryIterator queryIterator = client.queryIterator(QueryIteratorReq.builder()
@@ -1510,6 +1512,71 @@ class MilvusClientV2DockerTest {
         }
         Assertions.assertEquals(295, counter);
 
+        // search iterator V2
+        SearchIteratorV2 searchIteratorV2 = client.searchIteratorV2(SearchIteratorReqV2.builder()
+                .collectionName(randomCollectionName)
+                .outputFields(Lists.newArrayList("*"))
+                .batchSize(1000L)
+                .vectorFieldName("float_vector")
+                .filter("id >= 50")
+                .vectors(Collections.singletonList(new FloatVec(utils.generateFloatVector())))
+                .metricType(IndexParam.MetricType.L2)
+                .consistencyLevel(ConsistencyLevel.EVENTUALLY)
+                .build());
+        counter = 0;
+        while (true) {
+            List<SearchResp.SearchResult> res = searchIteratorV2.next();
+            if (res.isEmpty()) {
+                System.out.println("search iteration finished, close");
+                searchIteratorV2.close();
+                break;
+            }
+
+            for (SearchResp.SearchResult record : res) {
+                Map<String, Object> entity = record.getEntity();
+                Assertions.assertInstanceOf(Boolean.class, entity.get("bool_field"));
+                Assertions.assertInstanceOf(Integer.class, entity.get("int8_field"));
+                Assertions.assertInstanceOf(Integer.class, entity.get("int16_field"));
+                Assertions.assertInstanceOf(Integer.class, entity.get("int32_field"));
+                Assertions.assertInstanceOf(Long.class, entity.get("int64_field"));
+                Assertions.assertInstanceOf(Float.class, entity.get("float_field"));
+                Assertions.assertInstanceOf(Double.class, entity.get("double_field"));
+                Assertions.assertInstanceOf(String.class, entity.get("varchar_field"));
+                Assertions.assertInstanceOf(JsonObject.class, entity.get("json_field"));
+                Assertions.assertInstanceOf(List.class, entity.get("arr_int_field"));
+                Assertions.assertInstanceOf(List.class, entity.get("float_vector"));
+                Assertions.assertInstanceOf(ByteBuffer.class, entity.get("binary_vector"));
+                Assertions.assertInstanceOf(ByteBuffer.class, entity.get("bfloat16_vector"));
+                Assertions.assertInstanceOf(SortedMap.class, entity.get("sparse_vector"));
+
+                String varcharVal = (String)entity.get("varchar_field");
+                Assertions.assertTrue(varcharVal.startsWith("varchar_"));
+
+                long int64Val = (long)entity.get("int64_field");
+                Assertions.assertEquals(int64Val, (long)record.getId());
+                JsonObject jsonObj = (JsonObject)entity.get("json_field");
+                Assertions.assertTrue(jsonObj.has(String.format("JSON_%d", int64Val)));
+
+                List<Integer> intArr = (List<Integer>)entity.get("arr_int_field");
+                Assertions.assertTrue(intArr.size() <= 50); // max capacity 50 is defined in the baseSchema()
+
+                List<Float> floatVector = (List<Float>)entity.get("float_vector");
+                Assertions.assertEquals(DIMENSION, floatVector.size());
+
+                ByteBuffer binaryVector = (ByteBuffer)entity.get("binary_vector");
+                Assertions.assertEquals(DIMENSION, binaryVector.limit()*8);
+
+                ByteBuffer bfloat16Vector = (ByteBuffer)entity.get("bfloat16_vector");
+                Assertions.assertEquals(DIMENSION*2, bfloat16Vector.limit());
+
+                SortedMap<Long, Float> sparseVector = (SortedMap<Long, Float>)entity.get("sparse_vector");
+                Assertions.assertTrue(sparseVector.size() >= 10 && sparseVector.size() <= 20); // defined in generateSparseVector()
+
+                counter++;
+            }
+        }
+        Assertions.assertEquals((int)count - 50, counter);
+
         client.dropCollection(DropCollectionReq.builder().collectionName(randomCollectionName).build());
     }