Browse Source

Show output fields for SearchREsult/QueryResult (#523)

Signed-off-by: groot <yihua.mo@zilliz.com>
groot 2 years ago
parent
commit
85273fa292

+ 2 - 2
docker-compose.yml

@@ -31,7 +31,7 @@ services:
 
 
   standalone:
   standalone:
     container_name: milvus-javasdk-test-standalone
     container_name: milvus-javasdk-test-standalone
-    image: milvusdb/milvus:v2.2.5
+    image: milvusdb/milvus:v2.2.9
     command: ["milvus", "run", "standalone"]
     command: ["milvus", "run", "standalone"]
     environment:
     environment:
       ETCD_ENDPOINTS: etcd:2379
       ETCD_ENDPOINTS: etcd:2379
@@ -75,7 +75,7 @@ services:
 
 
   standaloneslave:
   standaloneslave:
     container_name: milvus-javasdk-test-slave-standalone
     container_name: milvus-javasdk-test-slave-standalone
-    image: milvusdb/milvus:v2.2.5
+    image: milvusdb/milvus:v2.2.9
     command: ["milvus", "run", "standalone"]
     command: ["milvus", "run", "standalone"]
     environment:
     environment:
       ETCD_ENDPOINTS: etcdslave:2379
       ETCD_ENDPOINTS: etcdslave:2379

+ 18 - 17
src/main/java/io/milvus/param/ParamUtils.java

@@ -202,7 +202,6 @@ public class ParamUtils {
     public static InsertRequest convertInsertParam(@NonNull InsertParam requestParam,
     public static InsertRequest convertInsertParam(@NonNull InsertParam requestParam,
                                                    DescCollResponseWrapper wrapper) {
                                                    DescCollResponseWrapper wrapper) {
         String collectionName = requestParam.getCollectionName();
         String collectionName = requestParam.getCollectionName();
-        String partitionName = requestParam.getPartitionName();
 
 
         // gen insert request
         // gen insert request
         MsgBase msgBase = MsgBase.newBuilder().setMsgType(MsgType.Insert).build();
         MsgBase msgBase = MsgBase.newBuilder().setMsgType(MsgType.Insert).build();
@@ -219,6 +218,24 @@ public class ParamUtils {
         return insertBuilder.build();
         return insertBuilder.build();
     }
     }
     private static void fillFieldsData(InsertParam requestParam, DescCollResponseWrapper wrapper, InsertRequest.Builder insertBuilder) {
     private static void fillFieldsData(InsertParam requestParam, DescCollResponseWrapper wrapper, InsertRequest.Builder insertBuilder) {
+        // set partition name only when there is no partition key field
+        String partitionName = requestParam.getPartitionName();
+        boolean isPartitionKeyEnabled = false;
+        for (FieldType fieldType : wrapper.getFields()) {
+            if (fieldType.isPartitionKey()) {
+                isPartitionKeyEnabled = true;
+            }
+        }
+        if (isPartitionKeyEnabled) {
+            if (partitionName != null && !partitionName.isEmpty()) {
+                String msg = "Collection " + requestParam.getCollectionName() + " has partition key, not allow to specify partition name";
+                throw new ParamException(msg);
+            }
+        } else if (partitionName != null) {
+            insertBuilder.setPartitionName(partitionName);
+        }
+
+        // convert insert data
         List<InsertParam.Field> columnFields = requestParam.getFields();
         List<InsertParam.Field> columnFields = requestParam.getFields();
         List<JSONObject> rowFields = requestParam.getRows();
         List<JSONObject> rowFields = requestParam.getRows();
 
 
@@ -232,13 +249,7 @@ public class ParamUtils {
     private static void checkAndSetColumnData(InsertParam requestParam, List<FieldType> fieldTypes, InsertRequest.Builder insertBuilder, List<InsertParam.Field> fields) {
     private static void checkAndSetColumnData(InsertParam requestParam, List<FieldType> fieldTypes, InsertRequest.Builder insertBuilder, List<InsertParam.Field> fields) {
         // gen fieldData
         // gen fieldData
         // make sure the field order must be consisted with collection schema
         // make sure the field order must be consisted with collection schema
-        String partitionName = requestParam.getPartitionName();
-        boolean isPartitionKeyEnabled = false;
         for (FieldType fieldType : fieldTypes) {
         for (FieldType fieldType : fieldTypes) {
-            if (fieldType.isPartitionKey()) {
-                isPartitionKeyEnabled = true;
-            }
-
             boolean found = false;
             boolean found = false;
             for (InsertParam.Field field : fields) {
             for (InsertParam.Field field : fields) {
                 if (field.getName().equals(fieldType.getName())) {
                 if (field.getName().equals(fieldType.getName())) {
@@ -259,16 +270,6 @@ public class ParamUtils {
                 throw new ParamException(msg);
                 throw new ParamException(msg);
             }
             }
         }
         }
-
-        // set partition name only when there is no partition key field
-        if (isPartitionKeyEnabled) {
-            if (partitionName != null && !partitionName.isEmpty()) {
-                String msg = "Collection " + requestParam.getCollectionName() + " has partition key, not allow to specify partition name";
-                throw new ParamException(msg);
-            }
-        } else if (partitionName != null) {
-            insertBuilder.setPartitionName(partitionName);
-        }
     }
     }
 
 
     private static void checkAndSetRowData(DescCollResponseWrapper wrapper, InsertRequest.Builder insertBuilder, List<JSONObject> rows) {
     private static void checkAndSetRowData(DescCollResponseWrapper wrapper, InsertRequest.Builder insertBuilder, List<JSONObject> rows) {

+ 21 - 0
src/main/java/io/milvus/param/dml/InsertParam.java

@@ -201,6 +201,27 @@ public class InsertParam {
             }
             }
         }
         }
     }
     }
+
+    /**
+     * Constructs a <code>String</code> by {@link InsertParam} instance.
+     *
+     * @return <code>String</code>
+     */
+    @Override
+    public String toString() {
+        String baseStr = "InsertParam{" +
+                "collectionName='" + collectionName + '\'' +
+                ", partitionName='" + partitionName + '\'' +
+                ", rowCount=" + rowCount;
+        if (!CollectionUtils.isEmpty(fields)) {
+            return baseStr +
+                    ", columnFields+" + fields +
+                    '}';
+        } else {
+            return baseStr + '}';
+        }
+    }
+
     /**
     /**
      * Internal class for insert data.
      * Internal class for insert data.
      * If dataType is Bool, values is List of Boolean;
      * If dataType is Bool, values is List of Boolean;

+ 8 - 7
src/main/java/io/milvus/response/FieldDataWrapper.java

@@ -2,6 +2,7 @@ package io.milvus.response;
 
 
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.JSONObject;
 import com.google.protobuf.ProtocolStringList;
 import com.google.protobuf.ProtocolStringList;
+import io.milvus.exception.ParamException;
 import io.milvus.grpc.DataType;
 import io.milvus.grpc.DataType;
 import io.milvus.grpc.FieldData;
 import io.milvus.grpc.FieldData;
 import io.milvus.exception.IllegalResponseException;
 import io.milvus.exception.IllegalResponseException;
@@ -174,7 +175,7 @@ public class FieldDataWrapper {
         }
         }
     }
     }
 
 
-    public Integer getAsInt(int index, String paramName) {
+    public Integer getAsInt(int index, String paramName) throws IllegalResponseException {
         if (isJsonField()) {
         if (isJsonField()) {
             String result = getAsString(index, paramName);
             String result = getAsString(index, paramName);
             return result == null ? null : Integer.parseInt(result);
             return result == null ? null : Integer.parseInt(result);
@@ -182,7 +183,7 @@ public class FieldDataWrapper {
         throw new IllegalResponseException("Only JSON type support this operation");
         throw new IllegalResponseException("Only JSON type support this operation");
     }
     }
 
 
-    public String getAsString(int index, String paramName) {
+    public String getAsString(int index, String paramName) throws IllegalResponseException {
         if (isJsonField()) {
         if (isJsonField()) {
             JSONObject jsonObject = parseObjectData(index);
             JSONObject jsonObject = parseObjectData(index);
             return jsonObject.getString(paramName);
             return jsonObject.getString(paramName);
@@ -190,7 +191,7 @@ public class FieldDataWrapper {
         throw new IllegalResponseException("Only JSON type support this operation");
         throw new IllegalResponseException("Only JSON type support this operation");
     }
     }
 
 
-    public Boolean getAsBool(int index, String paramName) {
+    public Boolean getAsBool(int index, String paramName) throws IllegalResponseException {
         if (isJsonField()) {
         if (isJsonField()) {
             String result = getAsString(index, paramName);
             String result = getAsString(index, paramName);
             return result == null ? null : Boolean.parseBoolean(result);
             return result == null ? null : Boolean.parseBoolean(result);
@@ -198,7 +199,7 @@ public class FieldDataWrapper {
         throw new IllegalResponseException("Only JSON type support this operation");
         throw new IllegalResponseException("Only JSON type support this operation");
     }
     }
 
 
-    public Double getAsDouble(int index, String paramName) {
+    public Double getAsDouble(int index, String paramName) throws IllegalResponseException {
         if (isJsonField()) {
         if (isJsonField()) {
             String result = getAsString(index, paramName);
             String result = getAsString(index, paramName);
             return result == null ? null : Double.parseDouble(result);
             return result == null ? null : Double.parseDouble(result);
@@ -206,7 +207,7 @@ public class FieldDataWrapper {
         throw new IllegalResponseException("Only JSON type support this operation");
         throw new IllegalResponseException("Only JSON type support this operation");
     }
     }
 
 
-    public Object get(int index, String paramName) throws Exception {
+    public Object get(int index, String paramName) throws IllegalResponseException {
         if (isJsonField()) {
         if (isJsonField()) {
             JSONObject jsonObject = parseObjectData(index);
             JSONObject jsonObject = parseObjectData(index);
             return jsonObject.get(paramName);
             return jsonObject.get(paramName);
@@ -214,9 +215,9 @@ public class FieldDataWrapper {
         throw new IllegalResponseException("Only JSON type support this operation");
         throw new IllegalResponseException("Only JSON type support this operation");
     }
     }
 
 
-    private Object valueByIdx(int index) {
+    public Object valueByIdx(int index) throws ParamException {
         if (index < 0 || index >= getFieldData().size()) {
         if (index < 0 || index >= getFieldData().size()) {
-            throw new IllegalResponseException("index out of range");
+            throw new ParamException("index out of range");
         }
         }
         return getFieldData().get(index);
         return getFieldData().get(index);
     }
     }

+ 151 - 1
src/main/java/io/milvus/response/QueryResultsWrapper.java

@@ -1,11 +1,16 @@
 package io.milvus.response;
 package io.milvus.response;
 
 
+import com.alibaba.fastjson.JSONObject;
+import io.milvus.exception.IllegalResponseException;
 import io.milvus.exception.ParamException;
 import io.milvus.exception.ParamException;
 import io.milvus.grpc.*;
 import io.milvus.grpc.*;
 
 
+import io.milvus.param.dml.InsertParam;
+import kotlin.text.UStringsKt;
+import lombok.Getter;
 import lombok.NonNull;
 import lombok.NonNull;
 
 
-import java.util.List;
+import java.util.*;
 
 
 /**
 /**
  * Utility class to wrap response of <code>query</code> interface.
  * Utility class to wrap response of <code>query</code> interface.
@@ -35,6 +40,12 @@ public class QueryResultsWrapper {
         throw new ParamException("The field name doesn't exist");
         throw new ParamException("The field name doesn't exist");
     }
     }
 
 
+    /**
+     * Get the dynamic field. Only available when a collection's dynamic field is enabled.
+     * Throws {@link ParamException} if the dynamic field doesn't exist.
+     *
+     * @return {@link FieldDataWrapper}
+     */
     public FieldDataWrapper getDynamicWrapper() throws ParamException {
     public FieldDataWrapper getDynamicWrapper() throws ParamException {
         List<FieldData> fields = results.getFieldsDataList();
         List<FieldData> fields = results.getFieldsDataList();
         for (FieldData field : fields) {
         for (FieldData field : fields) {
@@ -45,4 +56,143 @@ public class QueryResultsWrapper {
 
 
         throw new ParamException("The dynamic field doesn't exist");
         throw new ParamException("The dynamic field doesn't exist");
     }
     }
+
+    /**
+     * Gets the row count of a query result.
+     *
+     * @return <code>long</code> row count of the query result
+     */
+    public long getRowCount() {
+        List<FieldData> fields = results.getFieldsDataList();
+        for (FieldData field : fields) {
+            FieldDataWrapper wrapper = new FieldDataWrapper(field);
+            return wrapper.getRowCount();
+        }
+
+        return 0L;
+    }
+
+    /**
+     * Gets a row record from query result.
+     *  Throws {@link ParamException} if the index is illegal.
+     *
+     * @return <code>RowRecord</code> a row record of the query result
+     */
+    public RowRecord getRowRecord(long index) throws ParamException {
+        List<String> outputFields = results.getOutputFieldsList();
+        List<FieldData> fields = results.getFieldsDataList();
+
+        RowRecord record = new RowRecord();
+        for (String outputKey : outputFields) {
+            boolean isField = false;
+            for (FieldData field : fields) {
+                if (outputKey.equals(field.getFieldName())) {
+                    FieldDataWrapper wrapper = new FieldDataWrapper(field);
+                    if (index < 0 || index >= wrapper.getRowCount()) {
+                        throw new ParamException("Index out of range");
+                    }
+                    Object value = wrapper.valueByIdx((int)index);
+                    if (wrapper.isJsonField()) {
+                        record.put(field.getFieldName(), JSONObject.parseObject(new String((byte[])value)));
+                    } else {
+                        record.put(field.getFieldName(), value);
+                    }
+                    isField = true;
+                    break;
+                }
+            }
+
+            // if the output field is not a field name, fetch it from dynamic field
+            if (!isField) {
+                FieldDataWrapper dynamicField = getDynamicWrapper();
+                if (dynamicField != null) {
+                    Object obj = dynamicField.get((int)index, outputKey);
+                    if (obj != null) {
+                        record.put(outputKey, obj);
+                    }
+                }
+            }
+        }
+
+        return record;
+    }
+
+    /**
+     * Gets row records list from query result.
+     *
+     * @return <code>List<RowRecord></code> a row records list of the query result
+     */
+    public List<RowRecord> getRowRecords() {
+        long rowCount = getRowCount();
+        List<RowRecord> records = new ArrayList<>();
+        for (long i = 0; i < rowCount; i++) {
+            RowRecord record = getRowRecord(i);
+            records.add(record);
+        }
+
+        return records;
+    }
+
+    /**
+     * Internal-use class to wrap response of <code>query</code> interface.
+     */
+    @Getter
+    public static final class RowRecord {
+        Map<String, Object> fieldValues = new HashMap<>();
+
+        public RowRecord() {
+        }
+
+        public boolean put(String keyName, Object obj) {
+            if (fieldValues.containsKey(keyName)) {
+                return false;
+            }
+            fieldValues.put(keyName, obj);
+
+            return true;
+        }
+
+        /**
+         * Get a value by a key name. If the key name is a field name, return the value of this field.
+         * If the key name is in dynamic field, return the value from the dynamic field.
+         * Throws {@link ParamException} if the key name doesn't exist.
+         *
+         * @return {@link FieldDataWrapper}
+         */
+        public Object get(String keyName) throws ParamException {
+            if (fieldValues.isEmpty()) {
+                throw new ParamException("This record is empty");
+            }
+
+            Object obj = fieldValues.get(keyName);
+            if (obj == null) {
+                // find the value from dynamic field
+                Object meta = fieldValues.get("$meta");
+                if (meta != null) {
+                    JSONObject jsonMata = (JSONObject)meta;
+                    Object innerObj = jsonMata.get(keyName);
+                    if (innerObj != null) {
+                        return innerObj;
+                    }
+                }
+                throw new ParamException("The key name is not found");
+            }
+
+            return obj;
+        }
+
+        /**
+         * Constructs a <code>String</code> by {@link QueryResultsWrapper.RowRecord} instance.
+         *
+         * @return <code>String</code>
+         */
+        @Override
+        public String toString() {
+            List<String> pairs = new ArrayList<>();
+            fieldValues.forEach((keyName, fieldValue) -> {
+                pairs.add(keyName + ":" + fieldValue.toString());
+            });
+            return pairs.toString();
+        }
+    }
 }
 }

+ 97 - 6
src/main/java/io/milvus/response/SearchResultsWrapper.java

@@ -1,5 +1,6 @@
 package io.milvus.response;
 package io.milvus.response;
 
 
+import com.alibaba.fastjson.JSONObject;
 import io.milvus.exception.IllegalResponseException;
 import io.milvus.exception.IllegalResponseException;
 import io.milvus.exception.ParamException;
 import io.milvus.exception.ParamException;
 import io.milvus.grpc.*;
 import io.milvus.grpc.*;
@@ -7,7 +8,9 @@ import lombok.Getter;
 import lombok.NonNull;
 import lombok.NonNull;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 
 
 /**
 /**
  * Utility class to wrap response of <code>search</code> interface.
  * Utility class to wrap response of <code>search</code> interface.
@@ -88,8 +91,9 @@ public class SearchResultsWrapper {
             throw new IllegalResponseException("Result scores count is wrong");
             throw new IllegalResponseException("Result scores count is wrong");
         }
         }
 
 
-        List<IDScore> idScore = new ArrayList<>();
+        List<IDScore> idScores = new ArrayList<>();
 
 
+        // set id and distance
         IDs ids = results.getIds();
         IDs ids = results.getIds();
         if (ids.hasIntId()) {
         if (ids.hasIntId()) {
             LongArray longIDs = ids.getIntId();
             LongArray longIDs = ids.getIntId();
@@ -98,7 +102,7 @@ public class SearchResultsWrapper {
             }
             }
 
 
             for (int n = 0; n < k; ++n) {
             for (int n = 0; n < k; ++n) {
-                idScore.add(new IDScore("", longIDs.getData((int)offset + n), results.getScores((int)offset + n)));
+                idScores.add(new IDScore("", longIDs.getData((int)offset + n), results.getScores((int)offset + n)));
             }
             }
         } else if (ids.hasStrId()) {
         } else if (ids.hasStrId()) {
             StringArray strIDs = ids.getStrId();
             StringArray strIDs = ids.getStrId();
@@ -107,13 +111,57 @@ public class SearchResultsWrapper {
             }
             }
 
 
             for (int n = 0; n < k; ++n) {
             for (int n = 0; n < k; ++n) {
-                idScore.add(new IDScore(strIDs.getData((int)offset + n), 0, results.getScores((int)offset + n)));
+                idScores.add(new IDScore(strIDs.getData((int)offset + n), 0, results.getScores((int)offset + n)));
             }
             }
         } else {
         } else {
             throw new IllegalResponseException("Result ids is illegal");
             throw new IllegalResponseException("Result ids is illegal");
         }
         }
 
 
-        return idScore;
+        // set output fields
+        List<String> outputFields = results.getOutputFieldsList();
+        List<FieldData> fields = results.getFieldsDataList();
+        if (fields.isEmpty()) {
+            return idScores;
+        }
+
+        for (String outputKey : outputFields) {
+            boolean isField = false;
+            FieldDataWrapper dynamicField = null;
+            for (FieldData field : fields) {
+                if (field.getIsDynamic()) {
+                    dynamicField = new FieldDataWrapper(field);
+                }
+                if (outputKey.equals(field.getFieldName())) {
+                    FieldDataWrapper wrapper = new FieldDataWrapper(field);
+                    for (int n = 0; n < k; ++n) {
+                        if ((offset + n) >= wrapper.getRowCount()) {
+                            throw new ParamException("Illegal values length of output fields");
+                        }
+
+                        Object value = wrapper.valueByIdx((int)offset + n);
+                        if (wrapper.isJsonField()) {
+                            idScores.get(n).put(field.getFieldName(), JSONObject.parseObject(new String((byte[])value)));
+                        } else {
+                            idScores.get(n).put(field.getFieldName(), value);
+                        }
+                    }
+
+                    isField = true;
+                    break;
+                }
+            }
+
+            // if the output field is not a field name, fetch it from dynamic field
+            if (!isField && dynamicField != null) {
+                for (int n = 0; n < k; ++n) {
+                    Object obj = dynamicField.get((int)offset + n, outputKey);
+                    if (obj != null) {
+                        idScores.get(n).put(outputKey, obj);
+                    }
+                }
+            }
+        }
+        return idScores;
     }
     }
 
 
     @Getter
     @Getter
@@ -158,6 +206,7 @@ public class SearchResultsWrapper {
         private final String strID;
         private final String strID;
         private final long longID;
         private final long longID;
         private final float score;
         private final float score;
+        Map<String, Object> fieldValues = new HashMap<>();
 
 
         public IDScore(String strID, long longID, float score) {
         public IDScore(String strID, long longID, float score) {
             this.strID = strID;
             this.strID = strID;
@@ -165,12 +214,54 @@ public class SearchResultsWrapper {
             this.score = score;
             this.score = score;
         }
         }
 
 
+        public boolean put(String keyName, Object obj) {
+            if (fieldValues.containsKey(keyName)) {
+                return false;
+            }
+            fieldValues.put(keyName, obj);
+
+            return true;
+        }
+
+        /**
+         * Get a value by a key name. If the key name is a field name, return the value of this field.
+         * If the key name is in dynamic field, return the value from the dynamic field.
+         * Throws {@link ParamException} if the key name doesn't exist.
+         *
+         * @return {@link FieldDataWrapper}
+         */
+        public Object get(String keyName) throws ParamException {
+            if (fieldValues.isEmpty()) {
+                throw new ParamException("This record is empty");
+            }
+
+            Object obj = fieldValues.get(keyName);
+            if (obj == null) {
+                // find the value from dynamic field
+                Object meta = fieldValues.get("$meta");
+                if (meta != null) {
+                    JSONObject jsonMata = (JSONObject)meta;
+                    Object innerObj = jsonMata.get(keyName);
+                    if (innerObj != null) {
+                        return innerObj;
+                    }
+                }
+            }
+
+            return obj;
+        }
+
         @Override
         @Override
         public String toString() {
         public String toString() {
+            List<String> pairs = new ArrayList<>();
+            fieldValues.forEach((keyName, fieldValue) -> {
+                pairs.add(keyName + ":" + fieldValue.toString());
+            });
+
             if (strID.isEmpty()) {
             if (strID.isEmpty()) {
-                return "(ID: " + getLongID() + " Score: " + getScore() + ")";
+                return "(ID: " + getLongID() + " Score: " + getScore() + " OutputFields: " + pairs + ")";
             } else {
             } else {
-                return "(ID: '" + getStrID() + "' Score: " + getScore() + ")";
+                return "(ID: '" + getStrID() + "' Score: " + getScore()+ " OutputFields: " + pairs + ")";
             }
             }
         }
         }
     }
     }

+ 194 - 0
src/test/java/io/milvus/client/MilvusClientDockerTest.java

@@ -19,6 +19,8 @@
 
 
 package io.milvus.client;
 package io.milvus.client;
 
 
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListenableFuture;
 import io.milvus.grpc.*;
 import io.milvus.grpc.*;
 import io.milvus.param.*;
 import io.milvus.param.*;
@@ -1258,4 +1260,196 @@ class MilvusClientDockerTest {
 
 
         client.dropCollection(DropCollectionParam.newBuilder().withCollectionName(randomCollectionName).build());
         client.dropCollection(DropCollectionParam.newBuilder().withCollectionName(randomCollectionName).build());
     }
     }
+
+    @Test
+    void testDynamicField() {
+        String randomCollectionName = generator.generate(10);
+
+        // collection schema
+        String field1Name = "id_field";
+        String field2Name = "vec_field";
+        String field3Name = "json_field";
+        List<FieldType> fieldsSchema = new ArrayList<>();
+        fieldsSchema.add(FieldType.newBuilder()
+                .withPrimaryKey(true)
+                .withAutoID(false)
+                .withDataType(DataType.Int64)
+                .withName(field1Name)
+                .withDescription("identity")
+                .build());
+
+        fieldsSchema.add(FieldType.newBuilder()
+                .withDataType(DataType.FloatVector)
+                .withName(field2Name)
+                .withDescription("face")
+                .withDimension(dimension)
+                .build());
+
+        fieldsSchema.add(FieldType.newBuilder()
+                .withDataType(DataType.JSON)
+                .withName(field3Name)
+                .withDescription("info")
+                .build());
+
+        // create collection
+        CreateCollectionParam createParam = CreateCollectionParam.newBuilder()
+                .withCollectionName(randomCollectionName)
+                .withFieldTypes(fieldsSchema)
+                .withEnableDynamicField(true)
+                .build();
+
+        R<RpcStatus> createR = client.createCollection(createParam);
+        assertEquals(R.Status.Success.getCode(), createR.getStatus().intValue());
+
+        R<DescribeCollectionResponse> response = client.describeCollection(DescribeCollectionParam.newBuilder()
+                .withCollectionName(randomCollectionName)
+                .build());
+
+        DescCollResponseWrapper desc = new DescCollResponseWrapper(response.getData());
+        System.out.println(desc.toString());
+
+        // create index
+        CreateIndexParam indexParam = CreateIndexParam.newBuilder()
+                .withCollectionName(randomCollectionName)
+                .withFieldName(field2Name)
+                .withIndexName("abv")
+                .withIndexType(IndexType.FLAT)
+                .withMetricType(MetricType.L2)
+                .withExtraParam("{}")
+                .build();
+
+        R<RpcStatus> createIndexR = client.createIndex(indexParam);
+        assertEquals(R.Status.Success.getCode(), createIndexR.getStatus().intValue());
+
+        // load collection
+        R<RpcStatus> loadR = client.loadCollection(LoadCollectionParam.newBuilder()
+                .withCollectionName(randomCollectionName)
+                .build());
+        assertEquals(R.Status.Success.getCode(), loadR.getStatus().intValue());
+
+        int rowCount = 10;
+        // insert data by row-based
+        List<JSONObject> rows = new ArrayList<>();
+        for (long i = 0L; i < rowCount; ++i) {
+            JSONObject row = new JSONObject();
+            row.put(field1Name, i);
+            row.put(field2Name, generateFloatVectors(1).get(0));
+
+            // JSON field
+            JSONObject info = new JSONObject();
+            info.put("row-based-info", i);
+            row.put(field3Name, info);
+
+            // extra meta is automatically stored in dynamic field
+            row.put("extra_meta", i % 3 == 0);
+            row.put(generator.generate(5), 100);
+
+            rows.add(row);
+        }
+
+        InsertParam insertRowParam = InsertParam.newBuilder()
+                .withCollectionName(randomCollectionName)
+                .withRows(rows)
+                .build();
+
+        R<MutationResult> insertRowResp = client.insert(insertRowParam);
+        assertEquals(R.Status.Success.getCode(), insertRowResp.getStatus().intValue());
+        System.out.println(rowCount + " rows inserted");
+
+        // insert data by column-based
+        List<Long> ids = new ArrayList<>();
+        List<JSONObject> infos = new ArrayList<>();
+        for (long i = 0L; i < rowCount; ++i) {
+            ids.add(rowCount + i);
+            JSONObject obj = new JSONObject();
+            obj.put("column-based-info", i);
+            obj.put(generator.generate(5), i);
+            infos.add(obj);
+        }
+        List<List<Float>> vectors = generateFloatVectors(rowCount);
+
+        List<InsertParam.Field> fieldsInsert = new ArrayList<>();
+        fieldsInsert.add(new InsertParam.Field(field1Name, ids));
+        fieldsInsert.add(new InsertParam.Field(field2Name, vectors));
+        fieldsInsert.add(new InsertParam.Field(field3Name, infos));
+
+        InsertParam insertColumnsParam = InsertParam.newBuilder()
+                .withCollectionName(randomCollectionName)
+                .withFields(fieldsInsert)
+                .build();
+
+        R<MutationResult> insertColumnResp = client.insert(insertColumnsParam);
+        assertEquals(R.Status.Success.getCode(), insertColumnResp.getStatus().intValue());
+        System.out.println(rowCount + " rows inserted");
+
+        // get collection statistics
+        R<GetCollectionStatisticsResponse> statR = client.getCollectionStatistics(GetCollectionStatisticsParam
+                .newBuilder()
+                .withCollectionName(randomCollectionName)
+                .withFlush(true)
+                .build());
+        assertEquals(R.Status.Success.getCode(), statR.getStatus().intValue());
+
+        GetCollStatResponseWrapper stat = new GetCollStatResponseWrapper(statR.getData());
+        System.out.println("Collection row count: " + stat.getRowCount());
+
+        // retrieve rows
+        String expr = "extra_meta == true";
+        List<String> outputFields = Arrays.asList(field3Name, "extra_meta");
+        QueryParam queryParam = QueryParam.newBuilder()
+                .withCollectionName(randomCollectionName)
+                .withExpr(expr)
+                .withOutFields(outputFields)
+                .build();
+
+        R<QueryResults> queryR = client.query(queryParam);
+        assertEquals(R.Status.Success.getCode(), queryR.getStatus().intValue());
+
+        QueryResultsWrapper queryResultsWrapper = new QueryResultsWrapper(queryR.getData());
+        List<QueryResultsWrapper.RowRecord> records = queryResultsWrapper.getRowRecords();
+        System.out.println("Query results:");
+        for (QueryResultsWrapper.RowRecord record:records) {
+            System.out.println(record);
+            Object extraMeta = record.get("extra_meta");
+            if (extraMeta != null) {
+                System.out.println("'extra_meta' is from dynamic field, value: " + extraMeta);
+            }
+        }
+
+        // search
+        List<List<Float>> targetVectors = generateFloatVectors(2);
+        int topK = 5;
+        SearchParam searchParam = SearchParam.newBuilder()
+                .withCollectionName(randomCollectionName)
+                .withMetricType(MetricType.L2)
+                .withTopK(topK)
+                .withVectors(targetVectors)
+                .withVectorFieldName(field2Name)
+                .withParams("{}")
+                .withOutFields(outputFields)
+                .build();
+
+        R<SearchResults> searchR = client.search(searchParam);
+        assertEquals(R.Status.Success.getCode(), searchR.getStatus().intValue());
+
+        // verify the search result
+        SearchResultsWrapper results = new SearchResultsWrapper(searchR.getData().getResults());
+        for (int i = 0; i < targetVectors.size(); ++i) {
+            List<SearchResultsWrapper.IDScore> scores = results.getIDScore(i);
+            System.out.println("The result of No." + i + " target vector:");
+            for (SearchResultsWrapper.IDScore score:scores) {
+                System.out.println(score);
+                Object extraMeta = score.get("extra_meta");
+                if (extraMeta != null) {
+                    System.out.println("'extra_meta' is from dynamic field, value: " + extraMeta);
+                }
+            }
+        }
+
+        // drop collection
+        R<RpcStatus> dropR = client.dropCollection(DropCollectionParam.newBuilder()
+                .withCollectionName(randomCollectionName)
+                .build());
+        assertEquals(R.Status.Success.getCode(), dropR.getStatus().intValue());
+    }
 }
 }