Skip to content

Commit 39c303a

Browse files
authored
Support AddCollectionStructField interface (#1884)
Signed-off-by: yhmo <yihua.mo@zilliz.com>
1 parent 306bef8 commit 39c303a

14 files changed

Lines changed: 669 additions & 89 deletions

File tree

examples/src/main/java/io/milvus/v2/StructExample.java

Lines changed: 155 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@
2828
import io.milvus.v2.common.ConsistencyLevel;
2929
import io.milvus.v2.common.DataType;
3030
import io.milvus.v2.common.IndexParam;
31-
import io.milvus.v2.service.collection.request.AddFieldReq;
32-
import io.milvus.v2.service.collection.request.CreateCollectionReq;
33-
import io.milvus.v2.service.collection.request.DropCollectionReq;
34-
import io.milvus.v2.service.collection.request.LoadCollectionReq;
31+
import io.milvus.v2.service.collection.request.*;
32+
import io.milvus.v2.service.collection.response.DescribeCollectionResp;
3533
import io.milvus.v2.service.index.request.CreateIndexReq;
3634
import io.milvus.v2.service.vector.request.InsertReq;
3735
import io.milvus.v2.service.vector.request.QueryReq;
@@ -43,7 +41,12 @@
4341
import io.milvus.v2.service.vector.response.QueryResp;
4442
import io.milvus.v2.service.vector.response.SearchResp;
4543

46-
import java.util.*;
44+
import java.util.ArrayList;
45+
import java.util.Arrays;
46+
import java.util.Collections;
47+
import java.util.List;
48+
import java.util.Map;
49+
import java.util.Random;
4750

4851
public class StructExample {
4952
private static final MilvusClientV2 client;
@@ -62,7 +65,12 @@ public class StructExample {
6265
private static final String CLIP_VECTOR_FIELD = "clip_embedding";
6366
private static final String DESC_FIELD = "clip_desc";
6467
private static final String DESC_VECTOR_FIELD = "description_embedding";
68+
private static final String EXTRA_STRUCT_FIELD = "metadata";
69+
private static final String RATING_FIELD = "rating";
70+
private static final String TAG_FIELD = "tag";
6571
private static final Integer VECTOR_DIM = 128;
72+
private static final Random RANDOM = new Random();
73+
private static final long EXTRA_ROW_ID = 5000L;
6674

6775
private static void createCollection() {
6876
CreateCollectionReq.CollectionSchema collectionSchema = CreateCollectionReq.CollectionSchema.builder()
@@ -77,7 +85,7 @@ private static void createCollection() {
7785
.dataType(DataType.VarChar)
7886
.maxLength(1024)
7987
.build());
80-
// define struct field schema, note that each name of sub-field must be unique in the entire collection
88+
8189
collectionSchema.addField(AddFieldReq.builder()
8290
.fieldName(STRUCT_FIELD)
8391
.description("clips of a film")
@@ -109,7 +117,6 @@ private static void createCollection() {
109117
.build())
110118
.build());
111119

112-
// define another struct field schema, note that it has a same name subfield to the STRUCT_FIELD
113120
collectionSchema.addField(AddFieldReq.builder()
114121
.fieldName("simplify_clips")
115122
.description("simplify clips")
@@ -128,13 +135,11 @@ private static void createCollection() {
128135
.collectionName(COLLECTION_NAME)
129136
.build());
130137

131-
CreateCollectionReq requestCreate = CreateCollectionReq.builder()
138+
client.createCollection(CreateCollectionReq.builder()
132139
.collectionName(COLLECTION_NAME)
133140
.collectionSchema(collectionSchema)
134-
.build();
135-
client.createCollection(requestCreate);
141+
.build());
136142

137-
// struct vector uses special index/metric type
138143
List<IndexParam> indexParams = new ArrayList<>();
139144
indexParams.add(IndexParam.builder()
140145
.fieldName(String.format("%s[%s]", STRUCT_FIELD, CLIP_VECTOR_FIELD))
@@ -162,45 +167,51 @@ private static void createCollection() {
162167
.collectionName(COLLECTION_NAME)
163168
.build());
164169
System.out.println("Collection created: " + COLLECTION_NAME);
170+
171+
describeCollection();
172+
}
173+
174+
private static void describeCollection() {
175+
DescribeCollectionResp resp = client.describeCollection(DescribeCollectionReq.builder()
176+
.collectionName(COLLECTION_NAME)
177+
.build());
178+
System.out.println(resp.getCollectionSchema());
179+
}
180+
181+
private static JsonObject buildBaseRow(long id) {
182+
JsonObject row = new JsonObject();
183+
row.addProperty(ID_FIELD, id);
184+
row.addProperty(NAME_FIELD, "film_" + id);
185+
186+
JsonArray structArr = new JsonArray();
187+
for (int k = 0; k < 5; k++) {
188+
JsonObject struct = new JsonObject();
189+
struct.addProperty(FRAME_FIELD, RANDOM.nextInt(10000));
190+
struct.add(CLIP_VECTOR_FIELD, JsonUtils.toJsonTree(CommonUtils.generateFloatVector(VECTOR_DIM)));
191+
struct.addProperty(DESC_FIELD, "clip_description_" + id);
192+
struct.add(DESC_VECTOR_FIELD, JsonUtils.toJsonTree(CommonUtils.generateFloatVector(VECTOR_DIM)));
193+
structArr.add(struct);
194+
}
195+
row.add(STRUCT_FIELD, structArr);
196+
197+
JsonArray simplifyClips = new JsonArray();
198+
for (int k = 0; k < 2; k++) {
199+
JsonObject struct = new JsonObject();
200+
struct.add(CLIP_VECTOR_FIELD, JsonUtils.toJsonTree(CommonUtils.generateFloatVector(32)));
201+
simplifyClips.add(struct);
202+
}
203+
row.add("simplify_clips", simplifyClips);
204+
return row;
165205
}
166206

167207
private static void insertData(int rowCount) {
168208
final int batchSize = 300;
169209
int insertedCount = 0;
170-
Random ran = new Random();
171210
while (insertedCount < rowCount) {
172-
int nextBatch = batchSize;
173-
int leftCount = rowCount - insertedCount;
174-
if (nextBatch > leftCount) {
175-
nextBatch = leftCount;
176-
}
211+
int nextBatch = Math.min(batchSize, rowCount - insertedCount);
177212
List<JsonObject> rows = new ArrayList<>();
178213
for (int i = 0; i < nextBatch; i++) {
179-
JsonObject row = new JsonObject();
180-
int id = insertedCount + i;
181-
row.addProperty(ID_FIELD, id);
182-
row.addProperty(NAME_FIELD, "film_" + id);
183-
JsonArray structArr = new JsonArray();
184-
for (int k = 0; k < 5; k++) {
185-
JsonObject struct = new JsonObject();
186-
struct.addProperty(FRAME_FIELD, ran.nextInt(10000));
187-
struct.add(CLIP_VECTOR_FIELD, JsonUtils.toJsonTree(CommonUtils.generateFloatVector(VECTOR_DIM)));
188-
struct.addProperty(DESC_FIELD, "clip_description_" + id);
189-
struct.add(DESC_VECTOR_FIELD, JsonUtils.toJsonTree(CommonUtils.generateFloatVector(VECTOR_DIM)));
190-
structArr.add(struct);
191-
}
192-
row.add(STRUCT_FIELD, structArr);
193-
194-
// for the "simplify_clips"
195-
structArr = new JsonArray();
196-
for (int k = 0; k < 2; k++) {
197-
JsonObject struct = new JsonObject();
198-
struct.add(CLIP_VECTOR_FIELD, JsonUtils.toJsonTree(CommonUtils.generateFloatVector(32)));
199-
structArr.add(struct);
200-
}
201-
row.add("simplify_clips", structArr);
202-
203-
rows.add(row);
214+
rows.add(buildBaseRow(insertedCount + i));
204215
}
205216

206217
InsertResp insertResp = client.insert(InsertReq.builder()
@@ -217,7 +228,6 @@ private static void insertData(int rowCount) {
217228
.consistencyLevel(ConsistencyLevel.STRONG)
218229
.build());
219230
System.out.printf("%d rows persisted\n", (long) countR.getQueryResults().get(0).getEntity().get("count(*)"));
220-
221231
}
222232

223233
private static List<QueryResp.QueryResult> query(String filter) {
@@ -238,18 +248,14 @@ private static List<QueryResp.QueryResult> query(String filter) {
238248

239249
private static void search(String annsField, List<BaseVector> searchData) {
240250
System.out.println("===================================================");
241-
String msg = String.format("Search on field '%s' in struct '%s' with nq=%d",
251+
System.out.printf("Search on field '%s' in struct '%s' with nq=%d\n",
242252
annsField, STRUCT_FIELD, searchData.size());
243-
System.out.println(msg);
244253

245-
246-
String annFullName = String.format("%s[%s]", STRUCT_FIELD, annsField);
247-
int topK = 5;
248254
SearchResp searchResp = client.search(SearchReq.builder()
249255
.collectionName(COLLECTION_NAME)
250-
.annsField(annFullName)
256+
.annsField(String.format("%s[%s]", STRUCT_FIELD, annsField))
251257
.data(searchData)
252-
.limit(topK)
258+
.limit(5)
253259
.consistencyLevel(ConsistencyLevel.BOUNDED)
254260
.outputFields(Arrays.asList(NAME_FIELD,
255261
String.format("%s[%s]", STRUCT_FIELD, FRAME_FIELD),
@@ -259,32 +265,112 @@ private static void search(String annsField, List<BaseVector> searchData) {
259265
List<List<SearchResp.SearchResult>> searchResults = searchResp.getSearchResults();
260266
for (int i = 0; i < searchResults.size(); i++) {
261267
System.out.println("Results of No." + i + " embedding list");
262-
List<SearchResp.SearchResult> results = searchResults.get(i);
263-
for (SearchResp.SearchResult result : results) {
268+
for (SearchResp.SearchResult result : searchResults.get(i)) {
264269
System.out.println(result);
265270
}
266271
}
267272
}
268273

274+
private static JsonArray buildMetadataStructArray() {
275+
JsonArray metadata = new JsonArray();
276+
277+
JsonObject first = new JsonObject();
278+
first.addProperty(RATING_FIELD, 5);
279+
first.addProperty(TAG_FIELD, "favorite");
280+
metadata.add(first);
281+
282+
JsonObject second = new JsonObject();
283+
second.addProperty(RATING_FIELD, 4);
284+
second.addProperty(TAG_FIELD, "classic");
285+
metadata.add(second);
286+
return metadata;
287+
}
288+
289+
private static void addCollectionStructField() {
290+
System.out.println("===================================================");
291+
System.out.println("Add a new struct field to the existing collection");
292+
client.addCollectionStructField(AddCollectionStructFieldReq.builder()
293+
.collectionName(COLLECTION_NAME)
294+
.fieldName(EXTRA_STRUCT_FIELD)
295+
.description("additional metadata for films")
296+
.maxCapacity(8)
297+
.addStructField(AddFieldReq.builder()
298+
.fieldName(RATING_FIELD)
299+
.dataType(DataType.Int32)
300+
.build())
301+
.addStructField(AddFieldReq.builder()
302+
.fieldName(TAG_FIELD)
303+
.dataType(DataType.VarChar)
304+
.maxLength(128)
305+
.build())
306+
.build());
307+
System.out.println("Added struct field: " + EXTRA_STRUCT_FIELD);
308+
309+
describeCollection();
310+
311+
QueryResp existingRow = client.query(QueryReq.builder()
312+
.collectionName(COLLECTION_NAME)
313+
.filter(ID_FIELD + " == 5")
314+
.consistencyLevel(ConsistencyLevel.STRONG)
315+
.outputFields(Arrays.asList(ID_FIELD, EXTRA_STRUCT_FIELD))
316+
.build());
317+
System.out.println("Existing row after addCollectionStructField() - new field is null");
318+
for (QueryResp.QueryResult result : existingRow.getQueryResults()) {
319+
System.out.println(result.getEntity());
320+
}
321+
322+
JsonObject newRow = buildBaseRow(EXTRA_ROW_ID);
323+
newRow.add(EXTRA_STRUCT_FIELD, buildMetadataStructArray());
324+
client.insert(InsertReq.builder()
325+
.collectionName(COLLECTION_NAME)
326+
.data(Collections.singletonList(newRow))
327+
.build());
328+
329+
QueryResp newRowResp = client.query(QueryReq.builder()
330+
.collectionName(COLLECTION_NAME)
331+
.filter(ID_FIELD + " == " + EXTRA_ROW_ID)
332+
.consistencyLevel(ConsistencyLevel.STRONG)
333+
.outputFields(Arrays.asList(ID_FIELD, NAME_FIELD, EXTRA_STRUCT_FIELD))
334+
.build());
335+
System.out.println("New row with the added struct field");
336+
for (QueryResp.QueryResult result : newRowResp.getQueryResults()) {
337+
System.out.println(result.getEntity());
338+
}
339+
340+
QueryResp projectedResp = client.query(QueryReq.builder()
341+
.collectionName(COLLECTION_NAME)
342+
.filter(ID_FIELD + " == " + EXTRA_ROW_ID)
343+
.consistencyLevel(ConsistencyLevel.STRONG)
344+
.outputFields(Arrays.asList(ID_FIELD, NAME_FIELD,
345+
String.format("%s[%s]", EXTRA_STRUCT_FIELD, RATING_FIELD),
346+
String.format("%s[%s]", EXTRA_STRUCT_FIELD, TAG_FIELD)))
347+
.build());
348+
System.out.println("Projected subfields from the added struct field");
349+
for (QueryResp.QueryResult result : projectedResp.getQueryResults()) {
350+
System.out.println(result.getEntity());
351+
}
352+
}
353+
269354
public static void main(String[] args) {
270-
createCollection();
271-
insertData(2000);
272-
273-
// fetch 2 rows
274-
List<QueryResp.QueryResult> results = query(ID_FIELD + " in [5, 8]");
275-
276-
// use the fetched data to search struct
277-
for (QueryResp.QueryResult result : results) {
278-
// in the insertData() method, we inserted 5 structures for each row
279-
// in query results, each struct is represented as a Map
280-
Map<String, Object> fetchedEntity = result.getEntity();
281-
List<Map<String, Object>> structs = (List<Map<String, Object>>) fetchedEntity.get(STRUCT_FIELD);
282-
EmbeddingList embList = new EmbeddingList();
283-
for (Map<String, Object> struct : structs) {
284-
List<Float> vector = (List<Float>) struct.get(CLIP_VECTOR_FIELD);
285-
embList.add(new FloatVec(vector));
355+
try {
356+
createCollection();
357+
insertData(2000);
358+
359+
List<QueryResp.QueryResult> results = query(ID_FIELD + " in [5, 8]");
360+
for (QueryResp.QueryResult result : results) {
361+
Map<String, Object> fetchedEntity = result.getEntity();
362+
List<Map<String, Object>> structs = (List<Map<String, Object>>) fetchedEntity.get(STRUCT_FIELD);
363+
EmbeddingList embList = new EmbeddingList();
364+
for (Map<String, Object> struct : structs) {
365+
List<Float> vector = (List<Float>) struct.get(CLIP_VECTOR_FIELD);
366+
embList.add(new FloatVec(vector));
367+
}
368+
search(CLIP_VECTOR_FIELD, Collections.singletonList(embList));
286369
}
287-
search(CLIP_VECTOR_FIELD, Collections.singletonList(embList));
370+
371+
addCollectionStructField();
372+
} finally {
373+
client.close();
288374
}
289375
}
290376
}

sdk-core/src/main/java/io/milvus/response/FieldDataWrapper.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,13 +460,23 @@ private List<?> getStructData(StructArrayField field, String fieldName) {
460460
// ]
461461
for (int i = 0; i < rowCount; i++) {
462462
int elementCount = 0;
463+
boolean isNullStruct = false;
463464
Map<String, List<?>> rowColumn = new HashMap<>();
464465
for (String key : columnsData.keySet()) {
465466
List<?> val = columnsData.get(key).get(i);
466467
rowColumn.put(key, val);
468+
if (val == null) {
469+
isNullStruct = true;
470+
continue;
471+
}
467472
elementCount = val.size();
468473
}
469474

475+
if (isNullStruct) {
476+
packData.add(null);
477+
continue;
478+
}
479+
470480
List<Map<String, Object>> structs = new ArrayList<>();
471481
for (int k = 0; k < elementCount; k++) {
472482
Map<String, Object> struct = new HashMap<>();

sdk-core/src/main/java/io/milvus/response/basic/RowRecordWrapper.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ protected QueryResultsWrapper.RowRecord buildRowRecord(QueryResultsWrapper.RowRe
8181
}
8282
Object value = wrapper.valueByIdx((int) index);
8383
if (wrapper.isJsonField()) {
84+
if (value == null) {
85+
if (!field.getIsDynamic()) {
86+
record.put(field.getFieldName(), null);
87+
}
88+
continue;
89+
}
90+
8491
JsonElement jsonValue = FieldDataWrapper.ParseJSONObject(value);
8592
if (!field.getIsDynamic()) {
8693
record.put(field.getFieldName(), jsonValue);

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,15 @@ public void addCollectionField(AddCollectionFieldReq request) {
489489
rpcUtils.retry(() -> collectionService.addCollectionField(this.getRpcStub(), request));
490490
}
491491

492+
/**
493+
* Add a new struct field to collection.
494+
*
495+
* @param request add new struct field request
496+
*/
497+
public void addCollectionStructField(AddCollectionStructFieldReq request) {
498+
rpcUtils.retry(() -> collectionService.addCollectionStructField(this.getRpcStub(), request));
499+
}
500+
492501
/**
493502
* Alter a field's properties.
494503
*

sdk-core/src/main/java/io/milvus/v2/service/collection/CollectionService.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,24 @@ public Void addCollectionField(MilvusServiceGrpc.MilvusServiceBlockingStub block
301301
return null;
302302
}
303303

304+
public Void addCollectionStructField(MilvusServiceGrpc.MilvusServiceBlockingStub blockingStub, AddCollectionStructFieldReq request) {
305+
String dbName = request.getDatabaseName();
306+
String collectionName = request.getCollectionName();
307+
String title = String.format("Add struct field to collection: '%s' in database: '%s'", collectionName, dbName);
308+
309+
AddCollectionStructFieldRequest.Builder builder = AddCollectionStructFieldRequest.newBuilder()
310+
.setCollectionName(collectionName)
311+
.setStructArrayFieldSchema(SchemaUtils.convertToGrpcStructFieldSchema(request.toStructFieldSchema()));
312+
if (StringUtils.isNotEmpty(dbName)) {
313+
builder.setDbName(dbName);
314+
}
315+
316+
Status response = blockingStub.addCollectionStructField(builder.build());
317+
rpcUtils.handleResponse(title, response);
318+
319+
return null;
320+
}
321+
304322
public Void alterCollectionField(MilvusServiceGrpc.MilvusServiceBlockingStub blockingStub, AlterCollectionFieldReq request) {
305323
String dbName = request.getDatabaseName();
306324
String collectionName = request.getCollectionName();

0 commit comments

Comments
 (0)