Add File
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2025, Agents-Flex (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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 com.agentsflex.store.milvus;
|
||||
|
||||
import com.agentsflex.core.document.Document;
|
||||
import com.agentsflex.core.store.DocumentStore;
|
||||
import com.agentsflex.core.store.SearchWrapper;
|
||||
import com.agentsflex.core.store.StoreOptions;
|
||||
import com.agentsflex.core.store.StoreResult;
|
||||
import com.agentsflex.core.util.Maps;
|
||||
import com.agentsflex.core.util.StringUtil;
|
||||
import com.agentsflex.core.util.VectorUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import io.milvus.v2.client.ConnectConfig;
|
||||
import io.milvus.v2.client.MilvusClientV2;
|
||||
import io.milvus.v2.common.ConsistencyLevel;
|
||||
import io.milvus.v2.common.DataType;
|
||||
import io.milvus.v2.common.IndexParam;
|
||||
import io.milvus.v2.exception.MilvusClientException;
|
||||
import io.milvus.v2.service.collection.request.CreateCollectionReq;
|
||||
import io.milvus.v2.service.collection.request.GetLoadStateReq;
|
||||
import io.milvus.v2.service.vector.request.*;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* MilvusVectorStore class provides an interface to interact with Milvus Vector Database.
|
||||
*/
|
||||
public class MilvusVectorStore extends DocumentStore {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MilvusVectorStore.class);
|
||||
private final MilvusClientV2 client;
|
||||
private final String defaultCollectionName;
|
||||
private final MilvusVectorStoreConfig config;
|
||||
|
||||
public MilvusVectorStore(MilvusVectorStoreConfig config) {
|
||||
ConnectConfig connectConfig = ConnectConfig.builder()
|
||||
.uri(config.getUri())
|
||||
.dbName(config.getDatabaseName())
|
||||
.token(config.getToken())
|
||||
.username(config.getUsername())
|
||||
.password(config.getPassword())
|
||||
.build();
|
||||
|
||||
this.client = new MilvusClientV2(connectConfig);
|
||||
this.defaultCollectionName = config.getDefaultCollectionName();
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoreResult storeInternal(List<Document> documents, StoreOptions options) {
|
||||
List<JSONObject> data = new ArrayList<>();
|
||||
for (Document doc : documents) {
|
||||
JSONObject dict = new JSONObject();
|
||||
dict.put("id", String.valueOf(doc.getId()));
|
||||
dict.put("content", doc.getContent());
|
||||
dict.put("vector", VectorUtil.toFloatList(doc.getVector()));
|
||||
|
||||
Map<String, Object> metadatas = doc.getMetadataMap();
|
||||
JSONObject jsonObject = JSON.parseObject(JSON.toJSONBytes(metadatas == null ? Collections.EMPTY_MAP : metadatas));
|
||||
dict.put("metadata", jsonObject);
|
||||
data.add(dict);
|
||||
}
|
||||
|
||||
String collectionName = options.getCollectionNameOrDefault(defaultCollectionName);
|
||||
InsertReq.InsertReqBuilder<?, ?> builder = InsertReq.builder();
|
||||
if (StringUtil.hasText(options.getPartitionName())) {
|
||||
builder.partitionName(options.getPartitionName());
|
||||
}
|
||||
InsertReq insertReq = builder
|
||||
.collectionName(collectionName)
|
||||
.data(data)
|
||||
.build();
|
||||
try {
|
||||
InsertResp insertResp = client.insert(insertReq);
|
||||
} catch (MilvusClientException e) {
|
||||
if (e.getMessage() != null && e.getMessage().contains("collection not found")
|
||||
&& config.isAutoCreateCollection()
|
||||
&& options.getMetadata("forInternal") == null) {
|
||||
|
||||
Boolean success = createCollection(collectionName);
|
||||
if (success != null && success) {
|
||||
//store
|
||||
options.addMetadata("forInternal", true);
|
||||
storeInternal(documents, options);
|
||||
}
|
||||
} else {
|
||||
return StoreResult.fail();
|
||||
}
|
||||
}
|
||||
|
||||
return StoreResult.successWithIds(documents);
|
||||
}
|
||||
|
||||
|
||||
private Boolean createCollection(String collectionName) {
|
||||
List<CreateCollectionReq.FieldSchema> fieldSchemaList = new ArrayList<>();
|
||||
|
||||
//id
|
||||
CreateCollectionReq.FieldSchema id = CreateCollectionReq.FieldSchema.builder()
|
||||
.name("id")
|
||||
.dataType(DataType.VarChar)
|
||||
.maxLength(36)
|
||||
.isPrimaryKey(true)
|
||||
.autoID(false)
|
||||
.build();
|
||||
fieldSchemaList.add(id);
|
||||
|
||||
//content
|
||||
CreateCollectionReq.FieldSchema content = CreateCollectionReq.FieldSchema.builder()
|
||||
.name("content")
|
||||
.dataType(DataType.VarChar)
|
||||
.maxLength(65535)
|
||||
.build();
|
||||
fieldSchemaList.add(content);
|
||||
|
||||
//metadata
|
||||
CreateCollectionReq.FieldSchema metadata = CreateCollectionReq.FieldSchema.builder()
|
||||
.name("metadata")
|
||||
.dataType(DataType.JSON)
|
||||
.build();
|
||||
fieldSchemaList.add(metadata);
|
||||
|
||||
//vector
|
||||
CreateCollectionReq.FieldSchema vector = CreateCollectionReq.FieldSchema.builder()
|
||||
.name("vector")
|
||||
.dataType(DataType.FloatVector)
|
||||
.dimension(this.getEmbeddingModel().dimensions())
|
||||
.build();
|
||||
fieldSchemaList.add(vector);
|
||||
|
||||
CreateCollectionReq.CollectionSchema collectionSchema = CreateCollectionReq.CollectionSchema
|
||||
.builder()
|
||||
.fieldSchemaList(fieldSchemaList)
|
||||
.build();
|
||||
|
||||
|
||||
List<IndexParam> indexParams = new ArrayList<>();
|
||||
IndexParam vectorIndex = IndexParam.builder().fieldName("vector")
|
||||
.indexType(IndexParam.IndexType.IVF_FLAT)
|
||||
.metricType(IndexParam.MetricType.COSINE)
|
||||
.indexName("vector")
|
||||
.extraParams(Maps.of("nlist", 1024))
|
||||
.build();
|
||||
indexParams.add(vectorIndex);
|
||||
|
||||
|
||||
CreateCollectionReq createCollectionReq = CreateCollectionReq.builder()
|
||||
.collectionName(collectionName)
|
||||
.collectionSchema(collectionSchema)
|
||||
.primaryFieldName("id")
|
||||
.vectorFieldName("vector")
|
||||
.description("Agents Flex Vector Store")
|
||||
.indexParams(indexParams)
|
||||
.build();
|
||||
|
||||
client.createCollection(createCollectionReq);
|
||||
|
||||
GetLoadStateReq quickSetupLoadStateReq = GetLoadStateReq.builder()
|
||||
.collectionName(collectionName)
|
||||
.build();
|
||||
|
||||
return client.getLoadState(quickSetupLoadStateReq);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoreResult deleteInternal(Collection<?> ids, StoreOptions options) {
|
||||
|
||||
DeleteReq.DeleteReqBuilder<?, ?> builder = DeleteReq.builder();
|
||||
if (StringUtil.hasText(options.getPartitionName())) {
|
||||
builder.partitionName(options.getPartitionName());
|
||||
}
|
||||
|
||||
DeleteReq deleteReq = builder
|
||||
.collectionName(options.getCollectionNameOrDefault(defaultCollectionName))
|
||||
.ids(new ArrayList<>(ids))
|
||||
.build();
|
||||
|
||||
try {
|
||||
client.delete(deleteReq);
|
||||
} catch (Exception e) {
|
||||
logger.error("delete document error: " + e, e);
|
||||
return StoreResult.fail();
|
||||
}
|
||||
|
||||
return StoreResult.success();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Document> searchInternal(SearchWrapper searchWrapper, StoreOptions options) {
|
||||
List<String> outputFields = searchWrapper.isOutputVector()
|
||||
? Arrays.asList("id", "vector", "content", "metadata")
|
||||
: Arrays.asList("id", "content", "metadata");
|
||||
|
||||
// 判断是否为向量查询
|
||||
if (searchWrapper.getVector() != null && searchWrapper.getVector().length > 0) {
|
||||
// 向量查询 - 使用SearchReq
|
||||
SearchReq.SearchReqBuilder<?, ?> builder = SearchReq.builder();
|
||||
if (StringUtil.hasText(options.getPartitionName())) {
|
||||
builder.partitionNames(options.getPartitionNamesOrEmpty());
|
||||
}
|
||||
|
||||
SearchReq searchReq = builder
|
||||
.collectionName(options.getCollectionNameOrDefault(defaultCollectionName))
|
||||
.consistencyLevel(ConsistencyLevel.STRONG)
|
||||
.outputFields(outputFields)
|
||||
.topK(searchWrapper.getMaxResults())
|
||||
.annsField("vector")
|
||||
.data(Collections.singletonList(VectorUtil.toFloatList(searchWrapper.getVector())))
|
||||
.filter(searchWrapper.toFilterExpression(MilvusExpressionAdaptor.DEFAULT))
|
||||
.build();
|
||||
|
||||
try {
|
||||
SearchResp resp = client.search(searchReq);
|
||||
// Parse and convert search results to Document list
|
||||
List<List<SearchResp.SearchResult>> results = resp.getSearchResults();
|
||||
List<Document> documents = new ArrayList<>();
|
||||
for (List<SearchResp.SearchResult> resultList : results) {
|
||||
for (SearchResp.SearchResult result : resultList) {
|
||||
Map<String, Object> entity = result.getEntity();
|
||||
if (entity == null || entity.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Document doc = new Document();
|
||||
doc.setId(result.getId());
|
||||
|
||||
Object vectorObj = entity.get("vector");
|
||||
if (vectorObj instanceof List) {
|
||||
//noinspection unchecked
|
||||
doc.setVector(VectorUtil.convertToVector((List<Float>) vectorObj));
|
||||
}
|
||||
|
||||
doc.setContent((String) entity.get("content"));
|
||||
|
||||
// 根据 metric 类型计算相似度
|
||||
Float distance = result.getDistance();
|
||||
if (distance != null) {
|
||||
// 根据 https://milvus.io/docs/zh/single-vector-search.md#Single-Vector-Search
|
||||
// 适用的度量类型和相应的距离范围表
|
||||
// 当 metricType 类 COSINE 时,数值越大,表示相似度越高。相似度即为 distance 值;
|
||||
|
||||
// distance 的范围是 [-1, 1], 需要统一转换为 [0, 1]
|
||||
double score = (distance + 1) / 2;
|
||||
doc.setScore(score);
|
||||
}
|
||||
|
||||
JSONObject object = (JSONObject) entity.get("metadata");
|
||||
doc.addMetadata(object);
|
||||
documents.add(doc);
|
||||
}
|
||||
}
|
||||
return documents;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error searching in Milvus", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
} else {
|
||||
// 非向量查询 - 使用QueryReq
|
||||
QueryReq.QueryReqBuilder<?, ?> builder = QueryReq.builder();
|
||||
if (StringUtil.hasText(options.getPartitionName())) {
|
||||
builder.partitionNames(options.getPartitionNamesOrEmpty());
|
||||
}
|
||||
|
||||
QueryReq queryReq = builder
|
||||
.collectionName(options.getCollectionNameOrDefault(defaultCollectionName))
|
||||
.consistencyLevel(ConsistencyLevel.STRONG)
|
||||
.outputFields(outputFields)
|
||||
.filter(searchWrapper.toFilterExpression(MilvusExpressionAdaptor.DEFAULT))
|
||||
.build();
|
||||
|
||||
try {
|
||||
QueryResp resp = client.query(queryReq);
|
||||
List<QueryResp.QueryResult> results = resp.getQueryResults();
|
||||
List<Document> documents = new ArrayList<>();
|
||||
|
||||
for (QueryResp.QueryResult result : results) {
|
||||
Map<String, Object> entity = result.getEntity();
|
||||
if (entity == null || entity.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Document doc = new Document();
|
||||
doc.setId(result.getEntity().get("id"));
|
||||
|
||||
Object vectorObj = entity.get("vector");
|
||||
if (vectorObj instanceof List) {
|
||||
//noinspection unchecked
|
||||
doc.setVector(VectorUtil.convertToVector((List<Float>) vectorObj));
|
||||
}
|
||||
|
||||
doc.setContent((String) entity.get("content"));
|
||||
|
||||
JSONObject object = (JSONObject) entity.get("metadata");
|
||||
doc.addMetadata(object);
|
||||
documents.add(doc);
|
||||
}
|
||||
return documents;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error querying in Milvus", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoreResult updateInternal(List<Document> documents, StoreOptions options) {
|
||||
if (documents == null || documents.isEmpty()) {
|
||||
return StoreResult.success();
|
||||
}
|
||||
List<JSONObject> data = new ArrayList<>();
|
||||
for (Document doc : documents) {
|
||||
JSONObject dict = new JSONObject();
|
||||
|
||||
dict.put("id", String.valueOf(doc.getId()));
|
||||
dict.put("content", doc.getContent());
|
||||
dict.put("vector", VectorUtil.toFloatList(doc.getVector()));
|
||||
|
||||
Map<String, Object> metadatas = doc.getMetadataMap();
|
||||
JSONObject jsonObject = JSON.parseObject(JSON.toJSONBytes(metadatas == null ? Collections.EMPTY_MAP : metadatas));
|
||||
dict.put("metadata", jsonObject);
|
||||
data.add(dict);
|
||||
}
|
||||
|
||||
UpsertReq upsertReq = UpsertReq.builder()
|
||||
.collectionName(options.getCollectionNameOrDefault(defaultCollectionName))
|
||||
.partitionName(options.getPartitionName())
|
||||
.data(data)
|
||||
.build();
|
||||
client.upsert(upsertReq);
|
||||
return StoreResult.successWithIds(documents);
|
||||
}
|
||||
|
||||
|
||||
public MilvusClientV2 getClient() {
|
||||
return client;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user