使用Milvus搭建以图搜图服务
- 介绍
- 安装Milvus
- Java调用Milvus插入、查询
- 引入Maven依赖
- 创建Milvus客户端
- 实现Milvus插入向量数据
- 实现Milvus 查询向量
- 结尾
介绍
网上相关的实现比较少,最多也只能查到Milvus,但不知道怎么使用。最后通过ChatGPT了解到了相关的使用方法,协助实现了以图搜图。
安装Milvus
先下载官网提供的milvus docker-compose文件
wget https://github.com/milvus-io/milvus/releases/download/v2.2.4/milvus-standalone-docker-compose.yml -O docker-compose.yml
修改yml文件配置,参考如下:
version: '3.5'services:etcd:container_name: milvus-etcdimage: quay.io/coreos/etcd:v3.5.5environment:- ETCD_AUTO_COMPACTION_MODE=revision- ETCD_AUTO_COMPACTION_RETENTION=1000- ETCD_QUOTA_BACKEND_BYTES=4294967296- ETCD_SNAPSHOT_COUNT=50000volumes:- /z/software/etcd:/etcdcommand: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcdminio:container_name: milvus-minioimage: minio/minio:RELEASE.2022-03-17T06-34-49Zenvironment:MINIO_ACCESS_KEY: minioadminMINIO_SECRET_KEY: minioadmin#ports:#- "9001:9001"#- "9000:9000"volumes:- /z/software/minio:/minio_datacommand: minio server /minio_data --console-address ":9001"healthcheck:test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]interval: 30stimeout: 20sretries: 3standalone:container_name: milvus-standaloneimage: milvusdb/milvus:v2.2.4command: ["milvus", "run", "standalone"]environment:ETCD_ENDPOINTS: etcd:2379MINIO_ADDRESS: minio:9000LOG_LEVEL: infovolumes:- /z/software/milvus:/var/lib/milvusports:- "19530:19530"depends_on:- "etcd"- "minio"networks:default:name: milvus-network
PS1:除了Milvus服务外,没有开放minio和etcd的端口,服务也可以互相访问
PS2:milvus默认端口为19530,可按需修改,用于服务调用,外网访问需要开放该端口
启动容器
docker-compose -f <docker-compose.yml位置> up -d
查看日志
docker logs -f --tail=200 milvus-standalone
至此,服务已经搭建完成,以下为Java实现
Java调用Milvus插入、查询
引入Maven依赖
<dependency><groupId>io.milvus</groupId><artifactId>milvus-sdk-java</artifactId>
</dependency>
<dependency><groupId>org.deeplearning4j</groupId><artifactId>deeplearning4j-core</artifactId>
</dependency>
<dependency><groupId>org.nd4j</groupId><artifactId>nd4j-native-platform</artifactId>
</dependency>
<dependency><groupId>org.deeplearning4j</groupId><artifactId>deeplearning4j-zoo</artifactId>
</dependency>
启动如果提示sfl4j 或 log4j 错误,说明本地依赖冲突需要排除。
<dependency><groupId>io.milvus</groupId><artifactId>milvus-sdk-java</artifactId><exclusions><exclusion><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j-impl</artifactId></exclusion></exclusions>
</dependency>
启动如果提示guava,缺少guava依赖
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId>
</dependency>
- milvus-sdk-java为milvus的Java SDK
- deeplearning4j为深度学习模型工具
创建Milvus客户端
public MilvusServiceClient milvusServiceClient() {return new MilvusServiceClient(ConnectParam.newBuilder().withHost(serviceHost).withPort(servicePort).build());
}
实现Milvus插入向量数据
- 步骤一:将图片转换为向量数组
// 加载ResNet50模型
ZooModel<?> zooModel = ResNet50.builder().build();
ComputationGraph pretrainedNet = (ComputationGraph) zooModel.initPretrained();
NativeImageLoader loader = new NativeImageLoader(224, 224, 3);
// 加载文件到内存,生成INDArray
INDArray image = loader.asMatrix(<File对象>);
// 加载向量
GraphVertex vertex = pretrainedNet.getVertices()[pretrainedNet.getVertices().length - 1];
INDArray features = pretrainedNet.feedForward(image, false).get(vertex.getVertexName());
float[] vector = features.toFloatVector();
- 步骤二:检查Milvus中Collection是否存在不存在则创建(Collection可以理解为数据库表)
private void checkCollection(String collectionName) {// 获取集合R<Boolean> collection = serviceClient.hasCollection(HasCollectionParam.newBuilder().withCollectionName(collectionName).build());if (!collection.getData()) {// 创建集合serviceClient.createCollection(createCollection(collectionName));}
}
/*** 以下为自定义字段,必须存在一个FloatVector类型字段,必须设置主键,没有可以用自增*/
private CreateCollectionParam createCollection(String collectionName){return CreateCollectionParam.newBuilder().withCollectionName(collectionName)// id 主键 必须有一个主键,也可以自动生成主键使用withAutoID(true).addFieldType(FieldType.newBuilder().withPrimaryKey(true).withName("id").withDataType(DataType.VarChar).withMaxLength(100).build()).addFieldType(FieldType.newBuilder().withName("name").withDataType(DataType.VarChar).withMaxLength(100).build()).addFieldType(FieldType.newBuilder().withName("url").withDataType(DataType.VarChar).withMaxLength(500).build()).addFieldType(FieldType.newBuilder().withName("vector").withDataType(DataType.FloatVector).withDimension(1000).build()).build();
}
- 步骤三:检查Milvus的Collection是否存在索引,不存在则创建,必须有一个向量索引
private void checkCollectionIndex(String collectionName) {// 查询索引 返回0代表未创建索引需要创建索引R<DescribeIndexResponse> indexResult = serviceClient.describeIndex(DescribeIndexParam.newBuilder().withCollectionName(collectionName).build());if (indexResult.getStatus() == R.Status.IndexNotExist.getCode()) {// 创建索引serviceClient.createIndex(createCollectionIndex(collectionName));}
}
privateCreateIndexParam createCollectionIndex(String collectionName) {return CreateIndexParam.newBuilder().withCollectionName(collectionName)// 需要加索引的字段名称.withFieldName("vector") .withMetricType(MetricType.IP).withSyncMode(Boolean.FALSE).withIndexType(IndexType.IVF_FLAT).withExtraParam(JSONUtil.createObj().set("nlist", "1024").toString()).build();
}
- 步骤四:向Milvus插入向量数组
List<InsertParam.Field> multiVectors = new ArrayList<>();
multiVectors.add(new InsertParam.Field("id", Collections.singletonList(IdUtil.randomId())));
multiVectors.add(new InsertParam.Field("name", Collections.singletonList("名称")));
multiVectors.add(new InsertParam.Field("url", Collections.singletonList("图片地址")));
multiVectors.add(new InsertParam.Field("vector", Collections.singletonList(Floats.asList(vector))));
R<MutationResult> insertResult = serviceClient.insert(InsertParam.newBuilder().withCollectionName(collectionName).withFields(multiVectors).build());
if (insertResult.getStatus() == R.Status.Success.getCode()) {log.info("插入向量成功! id:{},url:{},vectorSize:{}", imageDTO.getCollectionId(), imageDTO.getUrl(), vector.length);
}
- 步骤五:刷新索引
Milvus插入向量后不会立即刷新索引,所以新增后可能无法查询出。可以使用如下方法刷新索引
// 刷新索引
serviceClient.flush(FlushParam.newBuilder().addCollectionName(collectionName).withSyncFlush(false).build());
实现Milvus 查询向量
- 步骤一:将搜索的图片转换为向量
// 加载ResNet50模型
ZooModel<?> zooModel = ResNet50.builder().build();
ComputationGraph pretrainedNet = (ComputationGraph) zooModel.initPretrained();
NativeImageLoader loader = new NativeImageLoader(224, 224, 3);
// 加载文件到内存,生成INDArray
INDArray image = loader.asMatrix(<File对象>);
// 加载向量
GraphVertex vertex = pretrainedNet.getVertices()[pretrainedNet.getVertices().length - 1];
INDArray features = pretrainedNet.feedForward(image, false).get(vertex.getVertexName());
float[] vector = features.toFloatVector();
- 步骤二:将Collection加载到内存中(必须先加载内存后才可以查询)
R<RpcStatus>loadResult = serviceClient.loadCollection(LoadCollectionParam.newBuilder().withCollectionName(collectionName).withSyncLoad(true).build());
if (loadResult.getStatus() != R.Status.Success.getCode()){log.error("加载到内存失败了!ex={}", loadResult.getMessage());
}
- 步骤三:查询向量,以及相关字段信息
R<SearchResults> searchResult = serviceClient.search(SearchParam.newBuilder().withCollectionName(collectionName)// 设置返回最相似的图片数量.withTopK(10).withConsistencyLevel(ConsistencyLevelEnum.STRONG).withMetricType(MetricType.IP)// 返回的字段信息.withOutFields(Arrays.asList("id", "name", "url"))// 设置向量字段的名称.withVectorFieldName("vector").withVectors(Collections.singletonList(Floats.asList(vector)))// nprobe是指在搜索时需要遍历的最大倒排列表数,它的值越大,搜索速度越慢,但搜索精度越高// offset 偏移量,limit 每页查询数量,offset 从0开始.withParams(JSONUtil.createObj().set("nprobe", 30).set("offset", 0).set("limit", 30).toString()).build());
- 步骤四:获取响应结果
if (searchResult.getStatus() == R.Status.Success.getCode()){SearchResultsWrapper resultsWrapper = new SearchResultsWrapper(searchResult.getData().getResults());if (searchResult.getData().getResults().getIds().hasIntId()) {List<SearchResult> resultList = new ArrayList<>();for (int i = 0; i < resultsWrapper.getIDScore(0).size(); i++) {SearchResult entity = new SearchResult();entity.setId((Long)(resultsWrapper.getFieldData("id", 0).get(i)));entity.setName((String) (resultsWrapper.getFieldData("name", 0).get(i)));entity.setUrl((String) resultsWrapper.getFieldData("url", 0).get(i));resultList.add(entity);}return resultList;}
}
SearchResult:为自定义对象,用于返回给前端。
- 步骤五:释放内存
// 释放搜索加载的集合,以减少搜索完成后的内存消耗
serviceClient.releaseCollection(ReleaseCollectionParam.newBuilder().withCollectionName(collectionName).build());
结尾
目前查询有点慢,需要3~5秒,也可能是服务器性能不够,具体的优化还需要深入了解。
第一次写文章,写的不好请见谅,如有更好的方法可以评论区讨论。