1、人脸模块:人脸查询支持 向量数据库Milvus 和 SQLite

2、人脸模块:FaceNet人脸模型也支持人脸注册,查询等功能
3、人脸模块:Seetaface6 自动下载人脸库
4、人脸模块:Seetaface6解决依赖库重复下载问题
5、人脸模块:支持手动加载人脸库
6、人脸模块:人脸识别相关功能支持更多参数
This commit is contained in:
dengwenjie
2025-06-09 12:12:10 +08:00
parent ad6706f559
commit bca9462331
67 changed files with 4170 additions and 1438 deletions

View File

@@ -0,0 +1,82 @@
package cn.smartjavaai.face.vector.config;
import cn.smartjavaai.face.enums.IdStrategy;
import cn.smartjavaai.face.enums.VectorDBType;
import io.milvus.param.IndexType;
import io.milvus.param.MetricType;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Milvus配置类
* @author smartjavaai
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MilvusConfig extends VectorDBConfig {
/**
* 服务器地址
*/
private String host = "localhost";
/**
* 服务器端口
*/
private int port = 19530;
/**
* 索引类型
*/
private IndexType indexType = IndexType.IVF_FLAT;
/**
* 聚类数量用于IVF索引
*/
private int nlist = 1024;
/**
* 向量维度
*/
private int dimension;
/**
* ID策略
*/
private IdStrategy idStrategy = IdStrategy.AUTO;
/**
* 相似度计算方式
*/
private MetricType metricType;
/**
* 集合名称
*/
private String collectionName;
/**
* 是否使用内存缓存
*/
private boolean useMemoryCache = true;
/**
* 构造函数
*/
public MilvusConfig() {
setType(VectorDBType.MILVUS);
}
/**
* 构造函数
* @param host 服务器地址
* @param port 服务器端口
*/
public MilvusConfig(String host, int port) {
this();
this.host = host;
this.port = port;
}
}

View File

@@ -0,0 +1,30 @@
package cn.smartjavaai.face.vector.config;
import cn.smartjavaai.face.enums.SimilarityType;
import cn.smartjavaai.face.enums.VectorDBType;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author dwj
* @date 2025/5/31
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SQLiteConfig extends VectorDBConfig {
/**
* 数据库路径(包含文件名称)
*/
private String dbPath;
/**
* 相似度计算方式
*/
private SimilarityType similarityType;
public SQLiteConfig() {
setType(VectorDBType.SQLITE);
}
}

View File

@@ -0,0 +1,20 @@
package cn.smartjavaai.face.vector.config;
import cn.smartjavaai.face.enums.VectorDBType;
import lombok.Data;
/**
* 向量数据库基础配置
* @author dwj
*/
@Data
public abstract class VectorDBConfig {
/**
* 向量数据库类型
*/
private VectorDBType type;
}

View File

@@ -0,0 +1,48 @@
package cn.smartjavaai.face.vector.constant;
/**
* 向量数据库常量类
* @author dwj
*/
public class VectorDBConstants {
/**
* 字段名称常量
*/
public static class FieldNames {
/** ID字段名 */
public static final String ID_FIELD = "id";
/** 向量字段名 */
public static final String VECTOR_FIELD = "vector";
/** 元数据字段名 */
public static final String METADATA_FIELD = "metadata";
}
/**
* 默认配置常量
*/
public static class Defaults {
/** 默认搜索探针数 */
public static final int DEFAULT_SEARCH_PARAM_NPROBE = 10;
/** 默认向量维度 */
public static final int DEFAULT_VECTOR_DIMENSION = 512;
/** 默认元数据最大长度 */
public static final int DEFAULT_METADATA_MAX_LENGTH = 32 * 1024;
/** 默认ID字段最大长度 */
public static final int DEFAULT_ID_MAX_LENGTH = 36;
/**
* 默认集合名称
*/
public static final String DEFAULT_COLLECTION_NAME = "face";
}
/**
* 搜索参数常量
*/
public static class SearchParams {
/** 默认相似度阈值 */
public static final float DEFAULT_SIMILARITY_THRESHOLD = 0.7f;
/** 默认返回TOP-K结果数 */
public static final int DEFAULT_TOP_K = 10;
}
}

View File

@@ -0,0 +1,594 @@
package cn.smartjavaai.face.vector.core;
import cn.smartjavaai.face.entity.FaceSearchParams;
import cn.smartjavaai.face.enums.IdStrategy;
import cn.smartjavaai.face.utils.FaceUtils;
import cn.smartjavaai.face.vector.config.MilvusConfig;
import cn.smartjavaai.face.vector.constant.VectorDBConstants;
import cn.smartjavaai.face.vector.entity.FaceVector;
import cn.smartjavaai.common.entity.FaceSearchResult;
import cn.smartjavaai.face.vector.exception.VectorDBException;
import io.milvus.client.MilvusServiceClient;
import io.milvus.grpc.*;
import io.milvus.param.*;
import io.milvus.param.collection.*;
import io.milvus.param.dml.*;
import io.milvus.param.index.CreateIndexParam;
import io.milvus.response.QueryResultsWrapper;
import io.milvus.response.SearchResultsWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
* Milvus向量数据库客户端实现
* @author dwj
*/
@Slf4j
public class MilvusClient implements VectorDBClient {
private final MilvusConfig config;
private MilvusServiceClient serviceClient;
private String collectionName;
/**
* 是否初始化完毕
*/
private boolean isInit;
public MilvusClient(MilvusConfig config) {
this.config = config;
}
@Override
public void initialize() {
try {
ConnectParam connectParam = ConnectParam.newBuilder()
.withHost(config.getHost())
.withPort(config.getPort())
.build();
serviceClient = new MilvusServiceClient(connectParam);
collectionName = StringUtils.isNotBlank(config.getCollectionName()) ? config.getCollectionName() : VectorDBConstants.Defaults.DEFAULT_COLLECTION_NAME;
createCollection(collectionName, config.getDimension());
if(config.isUseMemoryCache()){
// 加载集合到内存
loadFaceFeatures();
}
isInit = true;
} catch (Exception e) {
throw new VectorDBException("初始化Milvus客户端失败", e);
}
}
@Override
public void createCollection(String collectionName, int dimension) {
try {
if (hasCollection(collectionName)) {
log.debug("集合已存在:{}", collectionName);
return;
}
// 创建集合字段
FieldType idField = null;
switch (config.getIdStrategy()){
case AUTO://自动生成ID
idField = FieldType.newBuilder()
.withName(VectorDBConstants.FieldNames.ID_FIELD)
.withDataType(DataType.Int64)
.withPrimaryKey(true)
.withAutoID(true)
.build();
break;
case CUSTOM://自定义ID
idField = FieldType.newBuilder()
.withName(VectorDBConstants.FieldNames.ID_FIELD)
.withDataType(DataType.VarChar)
.withMaxLength(VectorDBConstants.Defaults.DEFAULT_ID_MAX_LENGTH)
.withPrimaryKey(true)
.withAutoID(false)
.build();
break;
}
FieldType vectorField = FieldType.newBuilder()
.withName(VectorDBConstants.FieldNames.VECTOR_FIELD)
.withDataType(DataType.FloatVector)
.withDimension(dimension)
.build();
FieldType metadataField = FieldType.newBuilder()
.withName(VectorDBConstants.FieldNames.METADATA_FIELD)
.withDataType(DataType.VarChar)
.withMaxLength(VectorDBConstants.Defaults.DEFAULT_METADATA_MAX_LENGTH)
.withNullable(true)//允许为空值
.build();
// 创建集合参数
CreateCollectionParam createCollectionParam = CreateCollectionParam.newBuilder()
.withCollectionName(collectionName)
.withDescription("人脸特征向量集合")
.addFieldType(idField)
.addFieldType(vectorField)
.addFieldType(metadataField)
.build();
R<RpcStatus> response = serviceClient.createCollection(createCollectionParam);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 创建集合失败:" + response.getMessage());
}
log.debug("创建集合成功");
// 创建索引
IndexType indexType = IndexType.IVF_FLAT;
if (config.getIndexType() != null) {
indexType = config.getIndexType();
}
CreateIndexParam indexParam = CreateIndexParam.newBuilder()
.withCollectionName(collectionName)
.withFieldName(VectorDBConstants.FieldNames.VECTOR_FIELD)
.withIndexType(indexType)
.withMetricType(config.getMetricType())
.withExtraParam(String.format("{\"nlist\":%d}", config.getNlist()))
.withSyncMode(Boolean.TRUE)//调用方法后等待 Milvus 执行完成
.build();
R<RpcStatus> createIndexResponse = serviceClient.createIndex(indexParam);
if (createIndexResponse.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 创建索引失败:" + createIndexResponse.getMessage());
}
log.debug("创建索引成功");
} catch (Exception e) {
throw new VectorDBException("创建Milvus集合失败", e);
}
}
@Override
public void dropCollection(String collectionName) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
if (hasCollection(collectionName)) {
R<RpcStatus> response = serviceClient.dropCollection(DropCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build());
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 删除集合失败:" + response.getMessage());
}
isInit = false;
}
} catch (Exception e) {
throw new VectorDBException("删除Milvus集合失败", e);
}
}
@Override
public boolean hasCollection(String collectionName) {
try {
R<Boolean> response = serviceClient.hasCollection(
HasCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build()
);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 查询失败:" + response.getMessage());
}
return Boolean.TRUE.equals(response.getData());
} catch (Exception e) {
throw new VectorDBException("检查Milvus集合是否保存失败", e);
}
}
@Override
public String insert(FaceVector faceVector) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
//验证
if(faceVector == null){
throw new VectorDBException("插入数据失败faceVector不能为空");
}
if(faceVector.getVector() == null || faceVector.getVector().length == 0){
throw new VectorDBException("插入数据失败vector不能为空");
}
//自定义ID
if(config.getIdStrategy() == IdStrategy.CUSTOM){
if(StringUtils.isBlank(faceVector.getId())){
throw new VectorDBException("插入数据失败ID生成策略-自定义IDid不能为空");
}
}
// 转 float[] 为 List<Float>
List<Float> vectorList = new ArrayList<>();
for (float v : faceVector.getVector()) {
vectorList.add(v);
}
List<List<Float>> vectors = Collections.singletonList(vectorList);
//List<String> metadataList = Collections.singletonList(faceVector.getMetadata());
List<String> metadataList = Optional.ofNullable(faceVector.getMetadata())
.map(Collections::singletonList)
.orElse(Collections.emptyList());
List<InsertParam.Field> fields = null;
switch (config.getIdStrategy()){
case AUTO://自动生成ID
fields = Arrays.asList(
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.VECTOR_FIELD).values(vectors).build(),
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.METADATA_FIELD).values(metadataList).build()
);
break;
case CUSTOM://自定义ID
List<String> ids = Collections.singletonList(faceVector.getId());
fields = Arrays.asList(
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.ID_FIELD).values(ids).build(),
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.VECTOR_FIELD).values(vectors).build(),
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.METADATA_FIELD).values(metadataList).build()
);
break;
}
InsertParam insertParam = InsertParam.newBuilder()
.withCollectionName(collectionName)
.withFields(fields)
.build();
R<MutationResult> response = serviceClient.insert(insertParam);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("插入失败: " + response.getMessage());
}
List<Long> autoIds = response.getData().getIDs().getIntId().getDataList();
return config.getIdStrategy() == IdStrategy.AUTO ? String.valueOf(autoIds.get(0)) : faceVector.getId();
} catch (Exception e) {
throw new VectorDBException("插入Milvus向量失败", e);
}
}
@Override
public void upsert(FaceVector faceVector) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
if(config.getIdStrategy() == IdStrategy.AUTO){
throw new VectorDBException("idStrategy为AUTO时,不支持更新操作");
}
//验证
if(faceVector == null){
throw new VectorDBException("更新数据失败faceVector不能为空");
}
if(faceVector.getVector() == null || faceVector.getVector().length == 0){
throw new VectorDBException("更新数据失败vector不能为空");
}
if(StringUtils.isBlank(faceVector.getId())){
throw new VectorDBException("更新数据失败id不能为空");
}
// 转换向量为 List<Float>
List<Float> vectorList = new ArrayList<>();
for (float v : faceVector.getVector()) {
vectorList.add(v);
}
List<List<Float>> vectors = Collections.singletonList(vectorList);
List<String> metadataList = Optional.ofNullable(faceVector.getMetadata())
.map(Collections::singletonList)
.orElse(Collections.emptyList());
// 准备字段列表
List<UpsertParam.Field> fields = new ArrayList<>();
fields.add(UpsertParam.Field.builder()
.name(VectorDBConstants.FieldNames.ID_FIELD)
.values(Collections.singletonList(faceVector.getId()))
.build());
// 添加向量和元数据字段
fields.add(UpsertParam.Field.builder()
.name(VectorDBConstants.FieldNames.VECTOR_FIELD)
.values(vectors)
.build());
fields.add(UpsertParam.Field.builder()
.name(VectorDBConstants.FieldNames.METADATA_FIELD)
.values(metadataList)
.build());
// 构建Upsert参数
UpsertParam upsertParam = UpsertParam.newBuilder()
.withCollectionName(collectionName)
.withFields(fields)
.build();
// 执行Upsert操作
R<MutationResult> response = serviceClient.upsert(upsertParam);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Upsert操作失败: " + response.getMessage());
}
} catch (Exception e) {
throw new VectorDBException("Milvus Upsert操作失败", e);
}
}
@Override
public List<String> insertBatch(List<FaceVector> faceVectors) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
List<String> ids = faceVectors.stream()
.map(FaceVector::getId)
.collect(Collectors.toList());
List<List<Float>> vectors = new ArrayList<>();
for (FaceVector fv : faceVectors) {
List<Float> list = new ArrayList<>();
for (float f : fv.getVector()) {
list.add(f);
}
vectors.add(list);
}
List<String> metadataList = faceVectors.stream()
.map(FaceVector::getMetadata)
.collect(Collectors.toList());
InsertParam insertParam = InsertParam.newBuilder()
.withCollectionName(collectionName)
.withFields(Arrays.asList(
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.ID_FIELD).values(ids).build(),
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.VECTOR_FIELD).values(vectors).build(),
InsertParam.Field.builder().name(VectorDBConstants.FieldNames.METADATA_FIELD).values(metadataList).build()
))
.build();
R<MutationResult> insertResult = serviceClient.insert(insertParam);
return ids;
} catch (Exception e) {
throw new VectorDBException("批量插入Milvus向量失败", e);
}
}
@Override
public void delete(String id) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
String expr = String.format("%s == \"%s\"", VectorDBConstants.FieldNames.ID_FIELD, id);
DeleteParam deleteParam = DeleteParam.newBuilder()
.withCollectionName(collectionName)
.withExpr(expr)
.build();
R<MutationResult> response = serviceClient.delete(deleteParam);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("删除操作失败: " + response.getMessage());
}
} catch (Exception e) {
throw new VectorDBException("删除Milvus向量失败", e);
}
}
@Override
public void deleteBatch(List<String> ids) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
// 构建IN表达式: id in ["id1", "id2", ...]
StringBuilder expr = new StringBuilder(VectorDBConstants.FieldNames.ID_FIELD + " in [");
for (int i = 0; i < ids.size(); i++) {
expr.append(ids.get(i)); // 不加引号
if (i < ids.size() - 1) {
expr.append(", ");
}
}
expr.append("]");
DeleteParam deleteParam = DeleteParam.newBuilder()
.withCollectionName(collectionName)
.withExpr(expr.toString())
.build();
R<MutationResult> response = serviceClient.delete(deleteParam);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("删除操作失败: " + response.getMessage());
}
} catch (Exception e) {
throw new VectorDBException("批量删除Milvus向量失败", e);
}
}
@Override
public List<FaceSearchResult> search(float[] queryVector, FaceSearchParams faceSearchParams) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
// 1. 包装查询向量
List<List<Float>> vectors = new ArrayList<>();
List<Float> floatList = new ArrayList<>();
for (float f : queryVector) {
floatList.add(f);
}
vectors.add(floatList);
// 2. 构造搜索参数
SearchParam searchParam = SearchParam.newBuilder()
.withCollectionName(collectionName)
.withVectorFieldName(VectorDBConstants.FieldNames.VECTOR_FIELD)
.withTopK(faceSearchParams.getTopK())
.withMetricType(config.getMetricType())
.withOutFields(Arrays.asList(VectorDBConstants.FieldNames.ID_FIELD, VectorDBConstants.FieldNames.METADATA_FIELD))
.withVectors(vectors)
.withParams("{\"nprobe\": 10}")//和nlist有关
.build();
R<SearchResults> resp = serviceClient.search(searchParam);
if (resp.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 查询失败: " + resp.getMessage());
}
SearchResults results = resp.getData();
SearchResultsWrapper wrapper = new SearchResultsWrapper(results.getResults());
// 3. 获取字段数据FieldData
List<SearchResultsWrapper.IDScore> scores = wrapper.getIDScore(0); // 默认只有一条 query 向量
List<FaceSearchResult> finalResults = new ArrayList<>();
for (int i = 0; i < scores.size(); i++) {
SearchResultsWrapper.IDScore score = scores.get(i);
float similarity = score.getScore();
if (faceSearchParams.getNormalizeSimilarity()) {
// 将分数转换为相似度
similarity = FaceUtils.convertScoreToSimilarity(config.getMetricType().name(), score.getScore());
}
if (similarity >= faceSearchParams.getThreshold()) {
// 获取 Metadata
String metadata = wrapper.getFieldData(VectorDBConstants.FieldNames.METADATA_FIELD, i).get(0).toString();
// 获取 ID
String id = wrapper.getFieldData(VectorDBConstants.FieldNames.ID_FIELD, i).get(0).toString();
finalResults.add(new FaceSearchResult(id, similarity, metadata));
}
}
return finalResults;
} catch (Exception e) {
throw new VectorDBException("搜索 Milvus 向量失败", e);
}
}
@Override
public long count(String collectionName) {
try {
if (serviceClient == null){
throw new VectorDBException("Milvus未初始化完毕");
}
R<QueryResults> response = serviceClient.query(
QueryParam.newBuilder()
.withCollectionName(collectionName)
.withOutFields(Collections.singletonList("count(*)"))
.build()
);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 查询失败msg: " + response.getMessage());
}
List<FieldData> fields = response.getData().getFieldsDataList();
if (fields.isEmpty()) {
throw new VectorDBException("Milvus 返回空字段数据");
}
FieldData countField = fields.get(0);
List<Long> countValues = countField.getScalars().getLongData().getDataList();
if (countValues.isEmpty()) {
throw new VectorDBException("Milvus count(*) 返回为空");
}
return countValues.get(0); // count(*) 查询的结果
} catch (Exception e) {
throw new VectorDBException("获取 Milvus 集合数量失败", e);
}
}
@Override
public void close() {
if (serviceClient != null) {
serviceClient.close();
isInit = false;
}
}
public void releaseCollection(String collectionName) {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
R<RpcStatus> response = serviceClient.releaseCollection(ReleaseCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build());
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus releaseCollection失败msg: " + response.getMessage());
}
}
@Override
public FaceSearchResult getById(String id) {
try {
if (!isInit){
throw new VectorDBException("Milvus未初始化完毕");
}
// 构造搜索参数
SearchParam searchParam = SearchParam.newBuilder()
.withCollectionName(collectionName)
.withExpr(VectorDBConstants.FieldNames.ID_FIELD + " == " + id)
.withOutFields(Arrays.asList(VectorDBConstants.FieldNames.ID_FIELD, VectorDBConstants.FieldNames.METADATA_FIELD))
.build();
// 5. 执行查询
R<QueryResults> response = serviceClient.query(
QueryParam.newBuilder()
.withCollectionName(collectionName)
.withExpr(VectorDBConstants.FieldNames.ID_FIELD + " == " + id)
.withOutFields(Arrays.asList(VectorDBConstants.FieldNames.ID_FIELD, VectorDBConstants.FieldNames.METADATA_FIELD))
.build()
);
// 处理响应
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("查询失败: " + response.getMessage());
}
QueryResultsWrapper wrapper = new QueryResultsWrapper(response.getData());
List<QueryResultsWrapper.RowRecord> records = wrapper.getRowRecords();
if (records.isEmpty()) {
return null;
}
// 提取第一条记录
QueryResultsWrapper.RowRecord row = records.get(0);
return new FaceSearchResult(id, 1,(String)row.get(VectorDBConstants.FieldNames.METADATA_FIELD));
} catch (Exception e) {
throw new VectorDBException("搜索 Milvus 向量失败", e);
}
}
@Override
public void loadFaceFeatures() {
// 加载集合到内存
R<RpcStatus> loadResponse = serviceClient.loadCollection(LoadCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build());
if (loadResponse.getStatus() != R.Status.Success.getCode()) {
throw new VectorDBException("Milvus 加载集合到内存失败:" + loadResponse.getMessage());
}
long count = count(collectionName);
log.debug("加载集合到内存成功,人脸数量:{}", count);
}
@Override
public void releaseFaceFeatures() {
releaseCollection(collectionName);
}
}

View File

@@ -0,0 +1,266 @@
package cn.smartjavaai.face.vector.core;
import cn.hutool.core.util.IdUtil;
import cn.smartjavaai.common.config.Config;
import cn.smartjavaai.face.dao.FaceDao;
import cn.smartjavaai.face.entity.FaceSearchParams;
import cn.smartjavaai.face.utils.FaceUtils;
import cn.smartjavaai.face.utils.SimilarityUtil;
import cn.smartjavaai.face.vector.config.SQLiteConfig;
import cn.smartjavaai.face.vector.entity.FaceVector;
import cn.smartjavaai.common.entity.FaceSearchResult;
import cn.smartjavaai.face.vector.exception.VectorDBException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
@Slf4j
public class SQLiteClient implements VectorDBClient {
private final FaceDao faceDao;
private final List<FaceVector> memoryIndex = new CopyOnWriteArrayList<>();
private int featureDimension; // 维度
private final ExecutorService executor = Executors.newFixedThreadPool(4);
private SQLiteConfig config;
/**
* 是否初始化完毕
*/
private boolean isInit;
public SQLiteClient(SQLiteConfig config) {
this.config = config;
String dbPath = config.getDbPath();
//如果未指定db路径则使用默认路径
if(StringUtils.isBlank(config.getDbPath())){
dbPath = Config.getCachePath() + File.separator + "face.db";
log.debug("使用默认SQLite人脸库路径: {}", dbPath);
}
this.faceDao = FaceDao.getInstance(dbPath);
}
@Override
public void initialize() {
try {
// 加载所有特征到内存
loadAllFeaturesToMemory();
isInit = true;
log.debug("SQLiteVectorDB initialized with {} faces", memoryIndex.size());
} catch (Exception e) {
throw new VectorDBException("初始化失败", e);
}
}
// 以下方法保持接口兼容但忽略collectionName参数
@Override
public void createCollection(String collectionName, int dimension) {
this.featureDimension = dimension;
log.debug("特征维度设置为: {}", dimension);
}
@Override
public void dropCollection(String collectionName) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
clearAllData();
log.warn("所有数据已被清空");
}
@Override
public boolean hasCollection(String collectionName) {
throw new UnsupportedOperationException("Sqlite 不支持此操作");
}
@Override
public String insert(FaceVector faceVector) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
return insertBatch(Collections.singletonList(faceVector)).get(0);
}
@Override
public void upsert(FaceVector faceVector) {
// if (faceVector.getId() != null) {
// delete(faceVector.getId());
// }
insert(faceVector);
}
@Override
public List<String> insertBatch(List<FaceVector> faceVectors) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
List<String> ids = new ArrayList<>();
try {
for (FaceVector faceVector : faceVectors) {
String id = faceVector.getId() != null ?
faceVector.getId() : IdUtil.simpleUUID();
faceVector.setId(id);
// 保存到数据库
faceDao.insertOrUpdate(faceVector);
// 添加到内存索引
addToMemoryIndex(faceVector);
ids.add(id);
}
log.debug("插入了 {} 个人脸向量", faceVectors.size());
return ids;
} catch (Exception e) {
throw new VectorDBException("批量插入失败", e);
}
}
@Override
public void delete(String id) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
deleteBatch(Collections.singletonList(id));
}
@Override
public void deleteBatch(List<String> ids) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
try {
// 从数据库中删除
boolean isSuccess = faceDao.deleteFace(ids.toArray(new String[0]));
// 从内存中删除
memoryIndex.removeIf(v -> ids.contains(v.getId()));
if(!isSuccess){
throw new VectorDBException("删除失败");
}
} catch (Exception e) {
throw new VectorDBException("批量删除失败", e);
}
}
@Override
public List<FaceSearchResult> search(float[] queryVector, FaceSearchParams faceSearchParams) {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
if (memoryIndex.isEmpty()) {
return Collections.emptyList();
}
// 并行计算相似度
List<CompletableFuture<FaceSearchResult>> futures = memoryIndex.stream()
.map(vector -> CompletableFuture.supplyAsync(() -> {
float similarity = SimilarityUtil.calculate(queryVector, vector.getVector(), config.getSimilarityType(), faceSearchParams.getNormalizeSimilarity());
return similarity >= faceSearchParams.getThreshold() ?
new FaceSearchResult(vector.getId(), similarity, vector.getMetadata()) :
null;
}, executor))
.collect(Collectors.toList());
// 收集结果并过滤null
List<FaceSearchResult> allResults = futures.stream()
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 获取TopK结果
return allResults.stream()
.sorted(Comparator.comparingDouble(FaceSearchResult::getSimilarity).reversed())
.limit(faceSearchParams.getTopK())
.collect(Collectors.toList());
}
@Override
public long count(String collectionName) {
return memoryIndex.size();
}
@Override
public void close() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
// ============= 私有辅助方法 =============
private void loadAllFeaturesToMemory() {
try {
int pageSize = 1000;
int page = 0;
while (true) {
List<FaceVector> batch = faceDao.findFace(page, pageSize);
if (CollectionUtils.isEmpty(batch)) {
break;
}
for (FaceVector vector : batch) {
addToMemoryIndex(vector);
}
page++;
}
log.debug("从数据库加载了 {} 个特征向量到内存", memoryIndex.size());
} catch (Exception e) {
throw new VectorDBException("加载特征到内存失败", e);
}
}
private void addToMemoryIndex(FaceVector faceVector) {
memoryIndex.add(faceVector);
}
private void clearAllData() {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
try {
faceDao.deleteAll();
memoryIndex.clear();
} catch (Exception e) {
log.error("清空数据库失败", e);
}
}
@Override
public FaceSearchResult getById(String id) {
try {
FaceVector faceVector = faceDao.findById(id);
if(faceVector != null){
return new FaceSearchResult(faceVector.getId(), 1.0f, faceVector.getMetadata());
}
return null;
} catch (SQLException | RuntimeException | ClassNotFoundException e ) {
throw new VectorDBException("SQLite查询异常", e);
}
}
@Override
public void loadFaceFeatures() {
// 加载所有特征到内存
loadAllFeaturesToMemory();
isInit = true;
log.debug("SQLiteVectorDB load success {} faces", memoryIndex.size());
}
@Override
public void releaseFaceFeatures() {
if (!isInit){
throw new VectorDBException("人脸库未加载完毕");
}
memoryIndex.clear();
isInit = false;
}
}

View File

@@ -0,0 +1,114 @@
package cn.smartjavaai.face.vector.core;
import cn.smartjavaai.face.entity.FaceSearchParams;
import cn.smartjavaai.face.vector.entity.FaceVector;
import cn.smartjavaai.common.entity.FaceSearchResult;
import cn.smartjavaai.face.vector.exception.VectorDBException;
import java.util.List;
/**
* 向量数据库客户端接口
* 定义与向量数据库交互的通用操作
* @author dwj
*/
public interface VectorDBClient extends AutoCloseable {
/**
* 初始化连接和集合
* @throws VectorDBException 初始化异常
*/
void initialize();
/**
* 创建集合
* @param collectionName 集合名称
* @param dimension 向量维度
*/
void createCollection(String collectionName, int dimension);
/**
* 删除集合
* @param collectionName 集合名称
*/
void dropCollection(String collectionName);
/**
* 检查集合是否存在
* @param collectionName 集合名称
* @return 是否存在
*/
boolean hasCollection(String collectionName);
/**
* 插入人脸向量
* @param faceVector
* @return
*/
String insert(FaceVector faceVector);
/**
* 更新或新增人脸向量
* @param faceVector
*/
void upsert(FaceVector faceVector);
/**
* 批量插入人脸向量
* @param faceVectors
* @return
*/
List<String> insertBatch(List<FaceVector> faceVectors);
/**
* 根据ID删除向量
* @param id
*/
void delete(String id);
/**
* 批量删除向量
* @param ids
*/
void deleteBatch(List<String> ids);
/**
* 搜索相似人脸
* @param queryVector
* @param faceSearchParams
* @return
*/
List<FaceSearchResult> search(float[] queryVector, FaceSearchParams faceSearchParams);
/**
* 获取集合中的向量数量
* @param collectionName 集合名称
* @return 向量数量
*/
long count(String collectionName);
/**
* 关闭连接
*/
@Override
void close();
/**
* 获取指定ID的向量
* @return
*/
FaceSearchResult getById(String id);
/**
* 加载人脸特征到内存
*/
void loadFaceFeatures();
/**
* 释放人脸特征缓存
*/
void releaseFaceFeatures();
}

View File

@@ -0,0 +1,52 @@
package cn.smartjavaai.face.vector.core;
import cn.smartjavaai.face.vector.config.MilvusConfig;
import cn.smartjavaai.face.vector.config.SQLiteConfig;
import cn.smartjavaai.face.vector.config.VectorDBConfig;
import cn.smartjavaai.face.vector.exception.VectorDBException;
/**
* 向量数据库工厂类
* 用于创建不同类型的向量数据库客户端
* @author dwj
*/
public class VectorDBFactory {
private VectorDBFactory() {
// 私有构造函数,防止实例化
}
/**
* 创建向量数据库客户端
* @param config 配置信息
* @return 向量数据库客户端
* @throws VectorDBException 创建异常
*/
public static VectorDBClient createClient(VectorDBConfig config) {
if (config == null) {
throw new VectorDBException("配置不能为空");
}
VectorDBClient client;
switch (config.getType()) {
case SQLITE:
if (!(config instanceof SQLiteConfig)) {
throw new VectorDBException("SQLite类型需要SQLiteConfig配置");
}
client = new SQLiteClient((SQLiteConfig) config);
break;
case MILVUS:
if (!(config instanceof MilvusConfig)) {
throw new VectorDBException("Milvus类型需要MilvusConfig配置");
}
client = new MilvusClient((MilvusConfig) config);
break;
// 未来可以在这里添加其他向量数据库的支持
default:
throw new VectorDBException("不支持的向量数据库类型: " + config.getType());
}
return client;
}
}

View File

@@ -0,0 +1,68 @@
package cn.smartjavaai.face.vector.entity;
import lombok.Data;
import java.util.UUID;
/**
* 人脸向量实体类
* @author smartjavaai
*/
@Data
public class FaceVector {
/**
* 向量ID
*/
private String id;
/**
* 人脸特征向量
*/
private float[] vector;
/**
* 元数据可以存储人脸相关的其他信息JSON格式
*/
private String metadata;
/**
* 默认构造函数
*/
public FaceVector() {
this.id = UUID.randomUUID().toString();
}
/**
* 构造函数
* @param vector 人脸特征向量
*/
public FaceVector(float[] vector) {
this();
this.vector = vector;
}
/**
* 构造函数
* @param vector 人脸特征向量
* @param metadata 元数据
*/
public FaceVector(float[] vector, String metadata) {
this();
this.vector = vector;
this.metadata = metadata;
}
/**
* 构造函数
* @param id 向量ID
* @param vector 人脸特征向量
* @param metadata 元数据
*/
public FaceVector(String id, float[] vector, String metadata) {
this.id = id;
this.vector = vector;
this.metadata = metadata;
}
}

View File

@@ -0,0 +1,26 @@
package cn.smartjavaai.face.vector.exception;
/**
* 向量数据库异常
* @author smartjavaai
*/
public class VectorDBException extends RuntimeException {
/**
* 构造函数
* @param message 异常信息
*/
public VectorDBException(String message) {
super(message);
}
/**
* 构造函数
* @param message 异常信息
* @param cause 原始异常
*/
public VectorDBException(String message, Throwable cause) {
super(message, cause);
}
}