|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one |
| 3 | + * or more contributor license agreements. See the NOTICE file |
| 4 | + * distributed with this work for additional information |
| 5 | + * regarding copyright ownership. The ASF licenses this file |
| 6 | + * to you under the Apache License, Version 2.0 (the |
| 7 | + * "License"); you may not use this file except in compliance |
| 8 | + * with the License. You may obtain a copy of the License at |
| 9 | + * |
| 10 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | + * |
| 12 | + * Unless required by applicable law or agreed to in writing, |
| 13 | + * software distributed under the License is distributed on an |
| 14 | + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | + * KIND, either express or implied. See the License for the |
| 16 | + * specific language governing permissions and limitations |
| 17 | + * under the License. |
| 18 | + */ |
| 19 | +package io.milvus.v2.bulkwriter; |
| 20 | + |
| 21 | +import com.google.gson.Gson; |
| 22 | +import com.google.gson.JsonArray; |
| 23 | +import com.google.gson.JsonObject; |
| 24 | +import io.milvus.bulkwriter.RemoteBulkWriter; |
| 25 | +import io.milvus.bulkwriter.RemoteBulkWriterParam; |
| 26 | +import io.milvus.bulkwriter.common.clientenum.BulkFileType; |
| 27 | +import io.milvus.bulkwriter.common.clientenum.CloudStorage; |
| 28 | +import io.milvus.bulkwriter.connect.AzureConnectParam; |
| 29 | +import io.milvus.bulkwriter.connect.S3ConnectParam; |
| 30 | +import io.milvus.bulkwriter.connect.StorageConnectParam; |
| 31 | +import io.milvus.bulkwriter.request.describe.MilvusDescribeImportRequest; |
| 32 | +import io.milvus.bulkwriter.request.import_.MilvusImportRequest; |
| 33 | +import io.milvus.bulkwriter.restful.BulkImportUtils; |
| 34 | +import io.milvus.v1.CommonUtils; |
| 35 | +import io.milvus.v2.client.ConnectConfig; |
| 36 | +import io.milvus.v2.client.MilvusClientV2; |
| 37 | +import io.milvus.v2.common.ConsistencyLevel; |
| 38 | +import io.milvus.v2.common.DataType; |
| 39 | +import io.milvus.v2.service.collection.request.AddFieldReq; |
| 40 | +import io.milvus.v2.service.collection.request.CreateCollectionReq; |
| 41 | +import io.milvus.v2.service.collection.request.DropCollectionReq; |
| 42 | +import io.milvus.v2.service.collection.request.LoadCollectionReq; |
| 43 | +import io.milvus.v2.service.collection.request.RefreshLoadReq; |
| 44 | +import io.milvus.v2.service.database.request.CreateDatabaseReq; |
| 45 | +import io.milvus.v2.service.database.response.ListDatabasesResp; |
| 46 | +import io.milvus.v2.service.vector.request.QueryReq; |
| 47 | +import io.milvus.v2.service.vector.response.QueryResp; |
| 48 | + |
| 49 | +import java.io.IOException; |
| 50 | +import java.util.ArrayList; |
| 51 | +import java.util.Arrays; |
| 52 | +import java.util.List; |
| 53 | +import java.util.Map; |
| 54 | +import java.util.concurrent.TimeUnit; |
| 55 | + |
| 56 | +public class BulkWriterStructExample { |
| 57 | + public static final String HOST = "127.0.0.1"; |
| 58 | + public static final Integer PORT = 19530; |
| 59 | + public static final String USER_NAME = "user.name"; |
| 60 | + public static final String PASSWORD = "password"; |
| 61 | + |
| 62 | + public static class StorageConsts { |
| 63 | + public static final CloudStorage cloudStorage = CloudStorage.MINIO; |
| 64 | + public static final String STORAGE_ENDPOINT = cloudStorage.getEndpoint("http://127.0.0.1:9000"); |
| 65 | + public static final String STORAGE_BUCKET = "a-bucket"; |
| 66 | + public static final String STORAGE_ACCESS_KEY = "minioadmin"; |
| 67 | + public static final String STORAGE_SECRET_KEY = "minioadmin"; |
| 68 | + public static final String STORAGE_REGION = ""; |
| 69 | + public static final String AZURE_CONTAINER_NAME = "azure.container.name"; |
| 70 | + public static final String AZURE_ACCOUNT_NAME = "azure.account.name"; |
| 71 | + public static final String AZURE_ACCOUNT_KEY = "azure.account.key"; |
| 72 | + } |
| 73 | + |
| 74 | + private static final Gson GSON = new Gson(); |
| 75 | + private static final String DATABASE_NAME = "java_sdk_db"; |
| 76 | + private static final String COLLECTION_NAME = "java_sdk_bulkwriter_struct_v2"; |
| 77 | + private static final int DIM = 16; |
| 78 | + private static final BulkFileType FILE_TYPE = BulkFileType.PARQUET; |
| 79 | + private static MilvusClientV2 milvusClient; |
| 80 | + |
| 81 | + public static void main(String[] args) throws Exception { |
| 82 | + createConnection(); |
| 83 | + CreateCollectionReq.CollectionSchema schema = buildSchema(); |
| 84 | + List<JsonObject> rows = buildRows(5); |
| 85 | + createCollection(schema); |
| 86 | + List<List<String>> batchFiles = writeRows(schema, rows); |
| 87 | + callBulkInsert(batchFiles); |
| 88 | + verifyImport(rows); |
| 89 | + } |
| 90 | + |
| 91 | + private static void createConnection() { |
| 92 | + String url = String.format("http://%s:%s", HOST, PORT); |
| 93 | + milvusClient = new MilvusClientV2(ConnectConfig.builder() |
| 94 | + .uri(url) |
| 95 | + .username(USER_NAME) |
| 96 | + .password(PASSWORD) |
| 97 | + .build()); |
| 98 | + |
| 99 | + ListDatabasesResp dbs = milvusClient.listDatabases(); |
| 100 | + if (!dbs.getDatabaseNames().contains(DATABASE_NAME)) { |
| 101 | + milvusClient.createDatabase(CreateDatabaseReq.builder() |
| 102 | + .databaseName(DATABASE_NAME) |
| 103 | + .build()); |
| 104 | + } |
| 105 | + try { |
| 106 | + milvusClient.useDatabase(DATABASE_NAME); |
| 107 | + } catch (Exception e) { |
| 108 | + System.out.println("Unable to switch database, error: " + e); |
| 109 | + } |
| 110 | + } |
| 111 | + |
| 112 | + private static CreateCollectionReq.CollectionSchema buildSchema() { |
| 113 | + CreateCollectionReq.CollectionSchema schema = CreateCollectionReq.CollectionSchema.builder() |
| 114 | + .build(); |
| 115 | + schema.addField(AddFieldReq.builder() |
| 116 | + .fieldName("id") |
| 117 | + .dataType(DataType.Int64) |
| 118 | + .isPrimaryKey(true) |
| 119 | + .autoID(false) |
| 120 | + .build()); |
| 121 | + schema.addField(AddFieldReq.builder() |
| 122 | + .fieldName("struct_field") |
| 123 | + .dataType(DataType.Array) |
| 124 | + .elementType(DataType.Struct) |
| 125 | + .maxCapacity(8) |
| 126 | + .addStructField(AddFieldReq.builder() |
| 127 | + .fieldName("st_name") |
| 128 | + .dataType(DataType.VarChar) |
| 129 | + .maxLength(128) |
| 130 | + .build()) |
| 131 | + .addStructField(AddFieldReq.builder() |
| 132 | + .fieldName("st_float_vector") |
| 133 | + .dataType(DataType.FloatVector) |
| 134 | + .dimension(DIM) |
| 135 | + .build()) |
| 136 | + .addStructField(AddFieldReq.builder() |
| 137 | + .fieldName("st_binary_vector") |
| 138 | + .dataType(DataType.BinaryVector) |
| 139 | + .dimension(DIM) |
| 140 | + .build()) |
| 141 | + .addStructField(AddFieldReq.builder() |
| 142 | + .fieldName("st_float16_vector") |
| 143 | + .dataType(DataType.Float16Vector) |
| 144 | + .dimension(DIM) |
| 145 | + .build()) |
| 146 | + .addStructField(AddFieldReq.builder() |
| 147 | + .fieldName("st_int8_vector") |
| 148 | + .dataType(DataType.Int8Vector) |
| 149 | + .dimension(DIM) |
| 150 | + .build()) |
| 151 | + .build()); |
| 152 | + return schema; |
| 153 | + } |
| 154 | + |
| 155 | + private static List<JsonObject> buildRows(int rowCount) { |
| 156 | + List<JsonObject> rows = new ArrayList<>(); |
| 157 | + for (int i = 0; i < rowCount; i++) { |
| 158 | + JsonObject row = new JsonObject(); |
| 159 | + row.addProperty("id", i); |
| 160 | + |
| 161 | + JsonArray structArray = new JsonArray(); |
| 162 | + for (int j = 0; j < i % 3 + 1; j++) { |
| 163 | + structArray.add(buildStruct(i, j)); |
| 164 | + } |
| 165 | + row.add("struct_field", structArray); |
| 166 | + rows.add(row); |
| 167 | + } |
| 168 | + return rows; |
| 169 | + } |
| 170 | + |
| 171 | + private static JsonObject buildStruct(int rowId, int structId) { |
| 172 | + JsonObject struct = new JsonObject(); |
| 173 | + struct.addProperty("st_name", String.format("row_%d_struct_%d", rowId, structId)); |
| 174 | + |
| 175 | + List<Float> floatVector = CommonUtils.generateFloatVector(DIM); |
| 176 | + List<Float> float16Vector = CommonUtils.generateFloatVector(DIM); |
| 177 | + struct.add("st_float_vector", GSON.toJsonTree(floatVector)); |
| 178 | + struct.add("st_binary_vector", GSON.toJsonTree(CommonUtils.generateBinaryVector(DIM).array())); |
| 179 | + struct.add("st_float16_vector", GSON.toJsonTree(CommonUtils.encodeFloat16Vector(float16Vector, false).array())); |
| 180 | + struct.add("st_int8_vector", GSON.toJsonTree(CommonUtils.generateInt8Vector(DIM).array())); |
| 181 | + return struct; |
| 182 | + } |
| 183 | + |
| 184 | + private static List<List<String>> writeRows(CreateCollectionReq.CollectionSchema schema, List<JsonObject> rows) throws Exception { |
| 185 | + try (RemoteBulkWriter remoteBulkWriter = buildRemoteBulkWriter(schema)) { |
| 186 | + for (JsonObject row : rows) { |
| 187 | + remoteBulkWriter.appendRow(row); |
| 188 | + } |
| 189 | + System.out.printf("%s rows appended%n", remoteBulkWriter.getTotalRowCount()); |
| 190 | + remoteBulkWriter.commit(false); |
| 191 | + System.out.printf("Uploaded files: %s%n", remoteBulkWriter.getBatchFiles()); |
| 192 | + return remoteBulkWriter.getBatchFiles(); |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + private static RemoteBulkWriter buildRemoteBulkWriter(CreateCollectionReq.CollectionSchema schema) throws IOException { |
| 197 | + RemoteBulkWriterParam bulkWriterParam = RemoteBulkWriterParam.newBuilder() |
| 198 | + .withCollectionSchema(schema) |
| 199 | + .withRemotePath("bulk_data/struct_vector_example") |
| 200 | + .withFileType(FILE_TYPE) |
| 201 | + .withChunkSize(128 * 1024 * 1024) |
| 202 | + .withConnectParam(buildStorageConnectParam()) |
| 203 | + .build(); |
| 204 | + return new RemoteBulkWriter(bulkWriterParam); |
| 205 | + } |
| 206 | + |
| 207 | + private static StorageConnectParam buildStorageConnectParam() { |
| 208 | + if (CloudStorage.isAzCloud(StorageConsts.cloudStorage.getCloudName())) { |
| 209 | + String connectionStr = "DefaultEndpointsProtocol=https;AccountName=" + StorageConsts.AZURE_ACCOUNT_NAME |
| 210 | + + ";AccountKey=" + StorageConsts.AZURE_ACCOUNT_KEY |
| 211 | + + ";EndpointSuffix=core.windows.net"; |
| 212 | + return AzureConnectParam.newBuilder() |
| 213 | + .withConnStr(connectionStr) |
| 214 | + .withContainerName(StorageConsts.AZURE_CONTAINER_NAME) |
| 215 | + .build(); |
| 216 | + } |
| 217 | + |
| 218 | + return S3ConnectParam.newBuilder() |
| 219 | + .withEndpoint(StorageConsts.STORAGE_ENDPOINT) |
| 220 | + .withCloudName(StorageConsts.cloudStorage.getCloudName()) |
| 221 | + .withBucketName(StorageConsts.STORAGE_BUCKET) |
| 222 | + .withAccessKey(StorageConsts.STORAGE_ACCESS_KEY) |
| 223 | + .withSecretKey(StorageConsts.STORAGE_SECRET_KEY) |
| 224 | + .withRegion(StorageConsts.STORAGE_REGION) |
| 225 | + .build(); |
| 226 | + } |
| 227 | + |
| 228 | + private static void createCollection(CreateCollectionReq.CollectionSchema schema) { |
| 229 | + milvusClient.dropCollection(DropCollectionReq.builder() |
| 230 | + .collectionName(COLLECTION_NAME) |
| 231 | + .databaseName(DATABASE_NAME) |
| 232 | + .build()); |
| 233 | + milvusClient.createCollection(CreateCollectionReq.builder() |
| 234 | + .collectionName(COLLECTION_NAME) |
| 235 | + .databaseName(DATABASE_NAME) |
| 236 | + .collectionSchema(schema) |
| 237 | + .consistencyLevel(ConsistencyLevel.BOUNDED) |
| 238 | + .build()); |
| 239 | + System.out.printf("Collection %s created%n", COLLECTION_NAME); |
| 240 | + } |
| 241 | + |
| 242 | + private static void callBulkInsert(List<List<String>> batchFiles) throws InterruptedException { |
| 243 | + String url = String.format("http://%s:%s", HOST, PORT); |
| 244 | + MilvusImportRequest request = MilvusImportRequest.builder() |
| 245 | + .collectionName(COLLECTION_NAME) |
| 246 | + .dbName(DATABASE_NAME) |
| 247 | + .files(batchFiles) |
| 248 | + .apiKey(USER_NAME + ":" + PASSWORD) |
| 249 | + .build(); |
| 250 | + String importResult = BulkImportUtils.bulkImport(url, request); |
| 251 | + System.out.println(importResult); |
| 252 | + |
| 253 | + String jobId = GSON.fromJson(importResult, JsonObject.class) |
| 254 | + .getAsJsonObject("data") |
| 255 | + .get("jobId") |
| 256 | + .getAsString(); |
| 257 | + |
| 258 | + while (true) { |
| 259 | + TimeUnit.SECONDS.sleep(5); |
| 260 | + String progressResult = BulkImportUtils.getImportProgress(url, MilvusDescribeImportRequest.builder() |
| 261 | + .jobId(jobId) |
| 262 | + .apiKey(USER_NAME + ":" + PASSWORD) |
| 263 | + .build()); |
| 264 | + JsonObject progress = GSON.fromJson(progressResult, JsonObject.class); |
| 265 | + String state = progress.getAsJsonObject("data").get("state").getAsString(); |
| 266 | + if ("Failed".equals(state)) { |
| 267 | + String reason = progress.getAsJsonObject("data").get("reason").getAsString(); |
| 268 | + throw new RuntimeException(String.format("Import job %s failed: %s", jobId, reason)); |
| 269 | + } |
| 270 | + if ("Completed".equals(state)) { |
| 271 | + System.out.printf("Import job %s completed%n", jobId); |
| 272 | + break; |
| 273 | + } |
| 274 | + System.out.printf("Import job %s is running, state=%s%n", jobId, state); |
| 275 | + } |
| 276 | + } |
| 277 | + |
| 278 | + private static void verifyImport(List<JsonObject> rows) { |
| 279 | + milvusClient.loadCollection(LoadCollectionReq.builder() |
| 280 | + .collectionName(COLLECTION_NAME) |
| 281 | + .databaseName(DATABASE_NAME) |
| 282 | + .build()); |
| 283 | + milvusClient.refreshLoad(RefreshLoadReq.builder() |
| 284 | + .collectionName(COLLECTION_NAME) |
| 285 | + .databaseName(DATABASE_NAME) |
| 286 | + .build()); |
| 287 | + |
| 288 | + List<Long> ids = Arrays.asList(0L, (long) rows.size() - 1); |
| 289 | + QueryResp queryResp = milvusClient.query(QueryReq.builder() |
| 290 | + .collectionName(COLLECTION_NAME) |
| 291 | + .databaseName(DATABASE_NAME) |
| 292 | + .filter(String.format("id in %s", ids)) |
| 293 | + .outputFields(Arrays.asList("*")) |
| 294 | + .consistencyLevel(ConsistencyLevel.STRONG) |
| 295 | + .build()); |
| 296 | + |
| 297 | + List<QueryResp.QueryResult> results = queryResp.getQueryResults(); |
| 298 | + if (results.size() != ids.size()) { |
| 299 | + throw new RuntimeException("Unexpected query result count"); |
| 300 | + } |
| 301 | + |
| 302 | + for (QueryResp.QueryResult result : results) { |
| 303 | + Map<String, Object> entity = result.getEntity(); |
| 304 | + long id = (Long) entity.get("id"); |
| 305 | + JsonArray expectedStructs = rows.get((int) id).get("struct_field").getAsJsonArray(); |
| 306 | + Object structField = entity.get("struct_field"); |
| 307 | + if (!(structField instanceof List<?>)) { |
| 308 | + throw new RuntimeException("struct_field should be returned as a list"); |
| 309 | + } |
| 310 | + |
| 311 | + List<?> fetchedStructs = (List<?>) structField; |
| 312 | + if (fetchedStructs.size() != expectedStructs.size()) { |
| 313 | + throw new RuntimeException("Struct element count is unmatched"); |
| 314 | + } |
| 315 | + |
| 316 | + for (int i = 0; i < fetchedStructs.size(); i++) { |
| 317 | + if (!(fetchedStructs.get(i) instanceof Map<?, ?>)) { |
| 318 | + throw new RuntimeException("Struct element should be returned as a map"); |
| 319 | + } |
| 320 | + |
| 321 | + Map<?, ?> fetchedStruct = (Map<?, ?>) fetchedStructs.get(i); |
| 322 | + JsonObject expectedStruct = expectedStructs.get(i).getAsJsonObject(); |
| 323 | + String expectedName = expectedStruct.get("st_name").getAsString(); |
| 324 | + if (!expectedName.equals(fetchedStruct.get("st_name"))) { |
| 325 | + throw new RuntimeException("Struct field st_name is unmatched"); |
| 326 | + } |
| 327 | + assertVectorFieldExists(fetchedStruct, "st_float_vector"); |
| 328 | + assertVectorFieldExists(fetchedStruct, "st_binary_vector"); |
| 329 | + assertVectorFieldExists(fetchedStruct, "st_float16_vector"); |
| 330 | + assertVectorFieldExists(fetchedStruct, "st_int8_vector"); |
| 331 | + } |
| 332 | + |
| 333 | + System.out.printf("Verified row %d: %s%n", id, entity); |
| 334 | + } |
| 335 | + System.out.println("RemoteBulkWriter struct vector import is correct!"); |
| 336 | + } |
| 337 | + |
| 338 | + private static void assertVectorFieldExists(Map<?, ?> struct, String fieldName) { |
| 339 | + Object value = struct.get(fieldName); |
| 340 | + if (value == null) { |
| 341 | + throw new RuntimeException(String.format("Struct vector field '%s' is missing", fieldName)); |
| 342 | + } |
| 343 | + } |
| 344 | +} |
0 commit comments