> 技术文档 > 【Milvus:用Milvus实现Java向量化检索、中文全文检索,BM 25算法的归一化处理(简单、易懂)】_milvus java

【Milvus:用Milvus实现Java向量化检索、中文全文检索,BM 25算法的归一化处理(简单、易懂)】_milvus java


文章目录

    • Milvus知识
      • 什么是Milvus
      • Milvus核心价值
    • Spring Boot + Milvus 集成
      • Spring AI 集成
      • Milvus 集成
        • Milvus相关配置
        • Collection创建
    • 项目实现检索
    • 小结
    • 说明

Milvus知识

什么是Milvus

Milvus 是一个开源的​​向量数据库 ​​,专为 AI 应用设计,用于高效存储、检索和分析高维向量数据(如文本嵌入、图像特征)。在 2025 年生成式 AI 爆发的背景下,它成为现代技术栈的核心组件。

Milvus核心价值

解决 AI 的关键瓶颈​​:
​​向量相似度搜索​​:以毫秒级速度检索十亿级向量(如 ChatGPT 问答匹配、图像搜图)。
​​动态可扩展​​:支持 Kubernetes 水平扩展,适应大模型数据增长。
​​多模态支持​​:统一处理文本、图像、音视频的嵌入向量。

之前的项目是采用PostgreSQL数据库进行向量存储和检索,发现实际调用比较慢,固升级使用了Milvus
【Milvus:用Milvus实现Java向量化检索、中文全文检索,BM 25算法的归一化处理(简单、易懂)】_milvus java

官网地址:Milvus官网


Spring Boot + Milvus 集成

项目版本:springboot 3.4.2

Spring AI 集成

选取的spring-ai版本:spring-ai 1.0.0-M6

此处对于spring-ai集成不过多赘述,如需要可以自行搜索,当前项目引入spring-ai是为了生成embedding向量数据。

Milvus 集成

当前选取的milvus的java版本:milvus-sdk-java 2.5.8

<dependency> <groupId>io.milvus</groupId> <artifactId>milvus-sdk-java</artifactId> <version>2.5.8</version></dependency>

补充 - 官网当前的最新版本
【Milvus:用Milvus实现Java向量化检索、中文全文检索,BM 25算法的归一化处理(简单、易懂)】_milvus java
此处不做milvus搭建赘述,如有需要自行搜索。

Milvus相关配置
@Configurationpublic class MilvusConfig { @Value(\"${milvus.url:localhost:19530}\") private String url; /** * 创建Milvus客户端实例 * * @return MilvusClientV2实例 */ @Bean public MilvusClientV2 milvusClientV2() { final ConnectConfig connectConfig = ConnectConfig.builder() .uri(url) .build(); return new MilvusClientV2(connectConfig); }}
Collection创建

重点!:

这里需要注意的是,为了实现向量化检索全文检索,需要创建两个不同的collection集合!!!

此处我是将配置Milvus客户端集合结构索引参数融合在一起了

代码展示:

import io.milvus.common.clientenum.FunctionType;import io.milvus.v2.client.ConnectConfig;import io.milvus.v2.client.MilvusClientV2;import io.milvus.v2.common.DataType;import io.milvus.v2.common.IndexParam;import io.milvus.v2.service.collection.request.AddFieldReq;import io.milvus.v2.service.collection.request.CreateCollectionReq;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.stereotype.Component;import java.util.*;/** * Milvus配置类 * 用于配置Milvus客户端、集合结构和索引参数 * 支持向量检索和全文检索两种模式 */@Slf4j@Data@Component@ConfigurationProperties(prefix = \"milvus\")public class MilvusConfig { /** * Milvus服务器地址 */ @Value(\"${milvus.url:localhost:19530}\") private String url; /** * 向量检索集合名称 */ @Value(\"${milvus.collection.vector.name:vector_name}\") private String vectorCollectionName; /** * 全文检索集合名称 */ @Value(\"${milvus.collection.text.name:text_name}\") private String textCollectionName; /** * 向量维度 */ @Value(\"${milvus.collection.dimension:1024}\") private int dimension; /** * 创建Milvus客户端实例 * * @return MilvusClientV2实例 */ @Bean public MilvusClientV2 milvusClientV2() { final ConnectConfig connectConfig = ConnectConfig.builder() .uri(url) .build(); return new MilvusClientV2(connectConfig); } /** * 创建向量检索集合的Schema * id主键自增 * @return 向量集合Schema */ public CreateCollectionReq.CollectionSchema createVectorCollectionSchema() { final CreateCollectionReq.CollectionSchema schema = CreateCollectionReq.CollectionSchema.builder() .build(); schema.addField(AddFieldReq.builder() .fieldName(\"id\") .dataType(DataType.Int64) .isPrimaryKey(true) .autoID(true) .build()); schema.addField(AddFieldReq.builder() .fieldName(\"embedding\") .dataType(DataType.FloatVector) .dimension(dimension) .build()); return schema; } /** * 创建全文检索集合的Schema,id主键自增 * 配置了中文分词器和BM25函数 * * @return 文本集合Schema */ public CreateCollectionReq.CollectionSchema createTextCollectionSchema() { final CreateCollectionReq.CollectionSchema schema = CreateCollectionReq.CollectionSchema.builder() .build(); schema.addField(AddFieldReq.builder() .fieldName(\"id\") .dataType(DataType.Int64) .isPrimaryKey(true) .autoID(true) .build()); schema.addField(AddFieldReq.builder() .fieldName(\"text\") .dataType(DataType.VarChar) .maxLength(4000) .enableAnalyzer(true) .analyzerParams(Map.of(\"type\", \"chinese\")) .build()); schema.addField(AddFieldReq.builder() .fieldName(\"sparse\") .dataType(DataType.SparseFloatVector) .build()); schema.addFunction(CreateCollectionReq.Function.builder() .functionType(FunctionType.BM25) .name(\"text_bm25_emb\") .inputFieldNames(Collections.singletonList(\"text\")) .outputFieldNames(Collections.singletonList(\"SPARSE\")) .build()); return schema; } /** * 创建向量检索的索引参数 * 使用HNSW索引类型,余弦相似度度量方式 * 配置了M=8和efConstruction=64的HNSW参数 * * @return 向量索引参数列表 */ public List<IndexParam> createVectorIndex() { final List<IndexParam> indexes = new ArrayList<>(); final Map<String, Object> extraParams = new HashMap<>(); extraParams.put(\"M\", 8); extraParams.put(\"efConstruction\", 64); indexes.add(IndexParam.builder() .fieldName(\"embedding\") .indexType(IndexParam.IndexType.HNSW) .metricType(IndexParam.MetricType.COSINE) .extraParams(extraParams) .build()); return indexes; } /** * 创建全文检索的索引参数 * 使用AUTOINDEX索引类型,BM25相似度度量方式 * * @return 文本索引参数列表 */ public List<IndexParam> createTextIndex() { final List<IndexParam> indexes = new ArrayList<>(); indexes.add(IndexParam.builder() .fieldName(\"sparse\") .indexType(IndexParam.IndexType.AUTOINDEX) .metricType(IndexParam.MetricType.BM25) .build()); return indexes; }}

官网的全文检索:全文检索

官网没有明显标注 中文 全文检索 怎么配置,默认英文
此处的重点就是创建 text 字段时增加 .analyzerParams(Map.of(\"type\", \"chinese\")) 配置


项目实现检索

插入数据

当前是在插入数据时校验collection是否存在,不存在则创建

@Resource private MilvusClientV2 client; @Resource private MilvusConfig milvusConfig; @Resource private Gson gson; private final String vectorCollectionName; private final String textCollectionName; public MilvusDalServiceImpl(final MilvusConfig milvusConfig) { this.vectorCollectionName = milvusConfig.getVectorCollectionName(); this.textCollectionName = milvusConfig.getTextCollectionName(); } public void insertData(final float[] embedding, final String content) { try { createCollection(); // 插入向量数据 final Map<String, Object> vectorData = new HashMap<>(); vectorData.put(\"embedding\", embedding); final JsonObject vectorJson = gson.toJsonTree(vectorData).getAsJsonObject(); client.insert(InsertReq.builder()  .collectionName(vectorCollectionName)  .data(Collections.singletonList(vectorJson))  .build()); // 插入文本数据(无需插入spare的值,milvus会自动生成) final Map<String, Object> textData = new HashMap<>(); textData.put(\"text\", content); final JsonObject textJson = gson.toJsonTree(textData).getAsJsonObject(); client.insert(InsertReq.builder()  .collectionName(textCollectionName)  .data(Collections.singletonList(textJson))  .build()); } catch (final Exception e) { log.error(\"Error inserting data: {}\", e.getMessage(), e); throw new SystemRunTimeException(\"Failed to insert data\", e); } } public void createCollection() { try { // 创建向量检索 Collection if (!hasCollection(vectorCollectionName)) { final CreateCollectionReq vectorCollectionReq = CreateCollectionReq.builder() .collectionName(vectorCollectionName) .collectionSchema(milvusConfig.createVectorCollectionSchema()) .indexParams(milvusConfig.createVectorIndex()) .build(); client.createCollection(vectorCollectionReq); log.info(\"Successfully created vector collection: {}\", vectorCollectionName); } // 创建全文检索 Collection if (!hasCollection(textCollectionName)) { final CreateCollectionReq textCollectionReq = CreateCollectionReq.builder() .collectionName(textCollectionName) .collectionSchema(milvusConfig.createTextCollectionSchema()) .indexParams(milvusConfig.createTextIndex()) .build(); client.createCollection(textCollectionReq); log.info(\"Successfully created text collection: {}\", textCollectionName); } } catch (Exception e) { log.error(\"Error creating collections: {}\", e.getMessage(), e); throw new SystemRunTimeException(\"Failed to create collections\", e); } } /** * 检查集合是否存在 * * @param collectionName 集合名称 * @return 是否存在 */ private boolean hasCollection(final String collectionName) { return client.hasCollection(HasCollectionReq.builder() .collectionName(collectionName) .build()); }

向量检索

向量化数据生成 spring-ai

float[] embedding = openAiEmbeddingModel.embed(\"文本内容\")

检索代码:

 public SearchResp searchSimilarVectors(final float[] embedding, final float similarity, final int limit) { final Map<String, Object> searchParams = new HashMap<>(); // 相似度阈值[0-1] searchParams.put(\"radius\", similarity); final SearchReq searchReq = SearchReq.builder() .collectionName(vectorCollectionName) .annsField(\"embedding\") .data(Collections.singletonList(new FloatVec(embedding))) .searchParams(searchParams) .topK(limit) .build(); return client.search(searchReq); }

全文检索

BM 25 算法的相似度阈值是0到无限(最大分数​​:理论无上限)

 public SearchResp fullTextSearch(final String searchContent, final float similarity, final int limit) { final Map<String, Object> searchParams = new HashMap<>(); // 相似度阈值[0-~] searchParams.put(\"radius\", similarity); final SearchReq searchReq = SearchReq.builder() .collectionName(textCollectionName) .annsField(\"sparse\") .data(Collections.singletonList(new EmbeddedText(param.getSearchContent()))) .searchParams(searchParams) .topK(limit) .build(); return client.search(searchReq); }

皆可增加.outputFields(Collections.singletonList(\"id\"))返回参数

归一化处理

因为向量化检索全文检索的相似度阈值不同,为了前端界面统一的[0-1]阈值,那么则需要做归一化处理

一:入参阈值处理

向量化和全文检索的searchParams可以统一处理,更改为.searchParams(buildBaseSearchParams(similarity, true))

 /** * 构建基础搜索参数 * * @param similarity 相似度参数 * @param isVector 是否为向量搜索 * @return 基础搜索参数 */ private Map<String, Object> buildBaseSearchParams(final float similarity, final boolean isVector) { final Map<String, Object> searchParams = new HashMap<>(); // 向量检索直接使用[0,1]区间,全文检索映射到[0,10]区间 final float normalizedSimilarity = isVector ? similarity : // 向量检索:直接使用[0,1] similarity * 10; // 全文检索:映射到[0,10] searchParams.put(\"radius\", normalizedSimilarity); return searchParams; }

全文检索的映射可根据自己的项目更改,一般是10,100

二:返回的score归一化

返回参数SearchResp 里包含score以及设置的返回参数,例如:id

处理下检索结果

 /** * 处理搜索结果,提取ID和score的映射 * * @param searchResp 搜索结果响应 * @return ID和score的映射 */ private Map<String, Float> processSearchResults(final SearchResp searchResp) { if (searchResp != null && searchResp.getSearchResults() != null) { return searchResp.getSearchResults().stream()  .flatMap(List::stream)  .collect(Collectors.toMap( result -> result.getId().toString(), SearchResp.SearchResult::getScore, (v1, v2) -> Math.max(v1, v2) // 对于重复的id,保留最大分数  )); } return Collections.emptyMap(); }

归一化处理

 /** * 归一化分数 * * @param scores 原始分数 * @param isVector 是否为向量搜索 * @return 归一化后的分数 */ private Map<String, Float> normalizeScores(final Map<String, Float> scores, final boolean isVector) { if (scores == null || scores.isEmpty()) { return new HashMap<>(); } // 向量检索直接使用原始分数,全文检索从[0,10]映射到[0,1] return scores.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> isVector ?  entry.getValue() : // 向量检索:直接使用原始分数  entry.getValue() / 10, // 全文检索:从[0,10]映射到[0,1] (v1, v2) -> v1 )); }

小结

可根据向量化检索全文检索,合并成混合检索
关键:配置中文的全文检索

效果展示:
【Milvus:用Milvus实现Java向量化检索、中文全文检索,BM 25算法的归一化处理(简单、易懂)】_milvus java


说明

文中如有疑问欢迎讨论、指正,互相学习,感谢关注。