Java接入向量数据库:实现文档检索与语义搜索实战
做Java后端这么多年大部分时间都在跟MySQL、Redis、Elasticsearch打交道。直到上半年接了一个文档检索的需求才发现传统的ES方案在某些场景下比如语义搜索、相似问题匹配真的是使不上劲。项目把标题定为“Java 接入向量数据库实现文档检索与语义搜索”正好把这段时间踩坑、选型、落地的过程沉淀下来希望对同样在Java技术栈里折腾向量检索的同学有点帮助。这个项目解决的问题很明确在Java服务里把非结构化的文档数据Word、PDF、Markdown、数据库文本字段等转换成向量写入向量数据库然后基于向量相似度实现语义级别的搜索。跟传统的关键词匹配不一样语义搜索能理解“苹果售后电话”和“iPhone客服热线是多少”其实是同一个意思。整理下来核心链路包括文档解析、文本切分、Embedding向量化、向量入库、相似度检索最后是Java代码接入和性能调优。这篇博文适合两类人一类是Java后端想给自己的系统加搜索或推荐能力另一类是面试前想搞明白向量数据库实战细节的候选人。整个项目从需求分析到上线跑通用了大概两周时间代码量不大但坑确实不少。1. 项目背景与整体思路拆解1.1 为什么传统方案搞不定这个需求先说需求本身系统里积累了几千篇产品文档和客服问答记录用户搜“怎么退款”希望把跟退款流程、退款时效相关的文档排在最前面。用ES或者MySQL的LIKE查询只能做字面匹配搜“怎么退款”可能查不到“申请退货后资金什么时候到账”这种文档——因为关键词完全不重叠但是语义上是高度相关的。当时我对比了几种方案方案匹配方式能理解语义吗适合场景MySQL LIKE /全文索引字符匹配否精确查询、结构化过滤Elasticsearch BM25词频逆文档频率有限同义词扩展关键词搜索、日志检索向量数据库 Embedding向量距离是语义搜索、相似推荐、问答匹配ES的BM25模型本质上是统计词频它能处理“退款”这个词但理解不了“钱没到账”和“退款慢”之间的关系。想要做到语义级别必须把文本映射到高维向量空间让语义相近的文本在空间里距离更近。这就得引入Embedding模型和向量数据库。1.2 项目整体的技术选型与链路设计整个系统从上到下拆成四层文档接入层负责读取各种格式的文档解析成纯文本。我这边主要用了Apache Tika来处理PDF、Word、HTML简单场景直接读文本文件也行。文本切分层把长文档切成合适的块Chunk。这一步非常关键直接决定检索效果。向量化层调用Embedding模型把文本块变成向量。我用的是本地部署的BGE系列模型也可以用云厂商的Embedding API。存储检索层向量数据库负责存储向量和原始文本提供相似度检索接口。Java服务通过官方SDK对接。最终的检索链路是用户输入query → query向量化 → 向量数据库ANN搜索 → 返回TopK文档块 → 可选Rerank重排 → 拼接结果返回。这个设计看起来不复杂但每一步都有不少学问。我建议拿到需求先不要急着写代码把文档格式摸清楚统计一下文本量级估算向量维度再选数据库否则后面返工的成本很高。1.3 标题背后隐藏的三个核心技术点标题里写了三个关键词Java接入、文档检索、语义搜索。对应三个核心技术点Java接入需要选对SDK搞懂连接池、超时、重试、异步写入这些客户端层面的问题。很多向量数据库是Python生态优先Java客户端要么功能不全要么文档稀烂这是最大的坑。文档检索从“搜关键词”升级到“搜含义”但也要处理中文分词、文档去重、长文档切分、切片后的上下文丢失等问题。语义搜索核心是Embedding模型的质量和索引算法的选择。模型选不好向量算得再快结果也是错的。这三个点环环相扣缺一个环节整体效果都会大打折扣。2. 向量数据库选型与Java SDK评估2.1 主流向量数据库横向对比选型阶段我调研了四款常见的向量数据库Milvus、Qdrant、Chroma、pgvector。每一款都有自己的定位适合的场景差异很大。Milvus是目前最成熟的分布式向量数据库功能全支持混合查询向量标量过滤、Collection分区、数据持久化、水平扩容。缺点是部署重依赖Etcd、MinIO、Pulsar这些组件小项目用起来杀鸡用牛刀。Java SDK是官方的milvus-sdk-java整体维护得还不错接口覆盖了大部分功能。Qdrant用Rust写的单机性能很好部署简单一个二进制文件搞定。提供Java客户端官方叫qdrant-java-client支持grpc和rest两种协议索引参数可调。如果你不想搞复杂的分布式Qdrant是首选。Chroma是轻量级方案Python生态非常顺滑适合做原型验证。Java客户端虽然存在但功能覆盖不全某些高级特性如过滤、分段用起来比较蹩脚生产环境我暂时不会选它。pgvector是PostgreSQL插件可以让你在原有业务库里直接加向量字段不需要额外维护一套存储。优点是运维简单、事务能力强、可以跟业务数据做SQL级联查缺点是向量检索性能比专业向量库差一些索引构建和查询速度在数据量大了之后会下滑。我当时的选择是Milvus主要原因一是数据量在百万级需要分布式扩展能力二是项目要求支持过滤标签比如按文档类型过滤Milvus的标量过滤配合向量检索做得比较成熟三是官方Java SDK比较稳定。如果你的数据量在几万条以内pgvector或者Qdrant单机版完全够了别盲目上重武器。2.2 Java SDK的选型要点与踩坑记录选定了Milvus之后我看了一下Java SDK的版本情况。Milvus官方提供两个Java库一个是旧的fabric8客户端不推荐已不活跃另一个是新的milvus-sdk-java在GitHub上持续维护支持Milvus 2.x的全部核心接口。Maven坐标是dependency groupIdio.milvus/groupId artifactIdmilvus-sdk-java/artifactId version2.4.0/version /dependency需要重点提一下新老SDK的API风格完全不一样。老的SDK用的是XxxParam格式一个新的SDK更贴近RESTful风格引入了一个MilvusServiceClient接口。如果你网上搜到老教程那代码基本不能用。我建议直接看官方GitHub仓库的examples目录不要看二手博客。另一个容易踩的坑是Java版本。Milvus SDK基于Java 11编译如果你项目还在Java 8要么升级到11要么考虑用HTTP调REST API绕过SDK。我见过不少团队因为这个问题卡住最后在自己的服务里包了一层OkHttp去调/api/search接口也能用但协议细节要自己处理不省心。此外SDK默认的connectTimeout和keepAlive时间都偏短高并发场景下容易出现连接复用失效、Connection reset。我后来把连接池配置调大并且开启gRPC的keepalive ping问题基本消失。下面是关键配置片段MilvusClient client new MilvusServiceClient( ConnectParam.newBuilder() .withHost(localhost) .withPort(19530) .withKeepAliveTime(10, TimeUnit.SECONDS) .withKeepAliveTimeout(3, TimeUnit.SECONDS) .withKeepAliveWithoutCalls(true) .build() );2.3 索引类型的选择HNSW还是IVF_FLAT向量检索的索引直接决定了查询速度和准确率。Milvus里常用的索引包括FLAT、IVF_FLAT、IVF_PQ、HNSW。我做了一个简单对比FLAT暴力全量计算精确但慢适合百万级以下且对速度不敏感的场景。IVF_FLAT倒排分组先聚类再搜索速度快但召回略降。适合十亿级以上的大库参数调起来麻烦。IVF_PQ乘积量化压缩向量内存占用低但会损失精度适合超大规模且内存捉襟见肘的场景。HNSW基于图的近似最近邻算法召回率高查询延迟低是中小规模百万到千万级的最佳折中。我选HNSW具体参数M16efConstruction200efSearch64。M越大图连接越密召回率高但内存占用也高efConstruction是建索引时探索的候选数影响索引质量太大建索引慢efSearch是查询时的探索宽度越大越慢但召回越好。下面是建索引和查询的Java代码JsonObject indexParams new JsonObject(); indexParams.addProperty(M, 16); indexParams.addProperty(efConstruction, 200); indexParams.addProperty(metric_type, COSINE); indexParams.addProperty(index_type, HNSW); milvusClient.createIndex( CreateIndexParam.newBuilder() .withCollectionName(document_chunks) .withFieldName(embedding) .withIndexType(IndexType.HNSW) .withMetricType(MetricType.COSINE) .withExtraParam(indexParams.toString()) .build() );实际测试下来500万向量数据、2核4G的机器上单查询延迟稳定在10毫秒以内效果还是很满意的。这里我特别强调一下MetricType建议用COSINE余弦距离对文本向量效果比对欧氏距离更好因为余弦相似度跟向量的绝对长度无关更适合Embedding向量的特性。3. 文档处理与向量化核心链路3.1 文档解析从PDF/Word到干净文本向量数据库存的不是原始文件而是切分后的文本块。首先要做的就是把各种格式的文档解析成纯文本。这一步看着简单实际很烦。PDF解析我试了两个库PDFBox和Apache Tika。PDFBox纯Java生态简单PDF好用但只要PDF里有表格、多栏排版或者扫描图片解析出来就是一堆乱序文本。Tika内部把PDFBox和OCRTesseract都包了解析效果更好但依赖较多启动时稍微慢一点。我的建议是能用Tika直接上Tika省心。Word文档相对好处理Apache POI或者Tika都行。需要注意的是一些加密文档和带宏的文档解析前要处理异常。我的代码里统一用一个DocumentParser入口Component public class DocumentParser { private final Tika tika new Tika(); public String parse(byte[] content, String filename) throws IOException { try { Metadata metadata new Metadata(); metadata.set(TikaMetadataKeys.RESOURCE_NAME_KEY, filename); return tika.parseToString(new ByteArrayInputStream(content), metadata); } catch (TikaException e) { throw new IOException(Document parsing failed: filename, e); } } }还要做一下清洗去掉多余空行、HTML标签、无意义的目录页码、图片说明等。脏文本进Embedding模型向量质量只会更差别嫌预处理麻烦。3.2 文本切分策略固定长度 vs 语义切分切分是检索效果的分水岭。块太长语义过于宽泛向量无法精确表达某个局部主题检索召回一堆“差不多”的文档块太短上下文信息不完整向量又缺乏足够的语义线索。经验值是中文场景下每个块300到500个字符比较合适同时让相邻块有50到100字符的重叠避免一句话被截断语义断裂。我有两个方案可以做切分方案一固定长度切分。简单粗暴按字数切分加重叠。适合结构不严格的文本很多开源项目比如LangChain的RecursiveCharacterTextSplitter就是基于这个思路。方案二语义切分。按段落、标题、列表边界切分最大程度保留语义完整性。我参考了LangChain的分隔符优先级思路先按标题切再按段落切最后按句子切。例如public ListString splitText(String text) { ListString chunks new ArrayList(); // 先按章节标题切 String[] sections text.split((?m)^(#\\s|第[一二三四五六七八九十]章|\\d\\.\\s)); for (String section : sections) { // 再按段落切 String[] paragraphs section.split(\\n\\s*\\n); StringBuilder current new StringBuilder(); for (String para : paragraphs) { if (current.length() para.length() 400 current.length() 200) { chunks.add(current.toString()); current.setLength(0); } current.append(para).append(\n); } if (current.length() 0) { chunks.add(current.toString()); } } return chunks; }实际效果对比方案二在知识问答场景下召回准确率明显更高尤其是技术文档这种标题层级丰富的文本。当然如果你的文档是一整段散文式的方案一也够用了。切分完还有一个细节每块要保留文档ID、标题路径、页码这些元数据后面检索结果要能定位回原文。3.3 Embedding模型的选择与向量化性能优化文本向量化的质量直接决定语义搜索的上限。模型层面我对比了三条路用云厂商的Embedding API效果好不用管部署但是有网络开销和费用数据要出内网。用本地开源的Embedding模型推荐BGE系列如bge-m3、bge-small-zh-v1.5中文效果好支持768或1024维单机部署即可。用通用多模态模型适合图文混合场景但Java生态里调用麻烦。我最终选了本地部署BGE模型向量维度1024。为什么不用768BGE官方对比实验里1024维在中文任务上效果更好而且Milvus处理1024维的向量完全没压力。Java端调用Embedding模型最简单的方式是把模型封装成一个HTTP服务也可以用Python写一个FastAPI接口Java用RestTemplate或WebClient调用。向量化是个纯计算密集的操作建议批量处理一次传一批文本比逐条调用效率高好几倍。我用的是批量接口每批32条吞吐量大概是每秒40次请求单条请求融合了多文本整体性能很可观。public ListListFloat embedBatch(ListString texts) { MapString, Object requestBody new HashMap(); requestBody.put(texts, texts); ResponseEntityEmbeddingResponse response restTemplate.postForEntity( embeddingServiceUrl, requestBody, EmbeddingResponse.class); return response.getBody().getEmbeddings(); }还有一点要注意embedding接口返回的向量在Java里通常是double[]或者float[]类型但Milvus SDK要求List 转换的时候别在循环里做太多拆箱装箱否则GC压力很大。我是把原始模型输出换成float[]数组存到内存插入时再转成List 性能好一些。4. Java接入向量数据库核心代码实现4.1 建Collection、定义Schema与写入数据先看建Collection这一步。Milvus是基于Collection组织的类似关系数据库的表。设计Schema时要考虑好主键、向量字段和标量字段。我在项目里定义一个collection叫document_chunks字段包括id主键用自增Longdoc_id文档ID标量字段用于过滤chunk_text原始文本内容标量字段embedding1024维向量字段Java创建Collection的代码如下public void createCollection() { FieldType idField FieldType.newBuilder() .setName(id) .setDataType(DataType.Int64) .setPrimaryKey(true) .setAutoID(true) .build(); FieldType docIdField FieldType.newBuilder() .setName(doc_id) .setDataType(DataType.VarChar) .setMaxLength(128) .build(); FieldType textField FieldType.newBuilder() .setName(chunk_text) .setDataType(DataType.VarChar) .setMaxLength(4096) .build(); FieldType embeddingField FieldType.newBuilder() .setName(embedding) .setDataType(DataType.FloatVector) .setDimension(1024) .build(); CreateCollectionParam createParam CreateCollectionParam.newBuilder() .withCollectionName(document_chunks) .withDescription(document semantic search chunks) .withFieldTypes(Arrays.asList(idField, docIdField, textField, embeddingField)) .build(); milvusClient.createCollection(createParam); }插入数据时按批次构建ListInsertParam.Field每批500条左右。一次性插入太多数据容易导致内存暴涨太少则RPC开销占比过高。这里直接给一个批量插入的实现public void batchInsert(ListDocumentChunk chunks) { ListInsertParam.Field fields new ArrayList(); ListLong ids new ArrayList(); ListString docIds new ArrayList(); ListString texts new ArrayList(); ListListFloat vectors new ArrayList(); for (DocumentChunk chunk : chunks) { ids.add(chunk.getId()); docIds.add(chunk.getDocId()); texts.add(chunk.getText()); vectors.add(chunk.getEmbedding()); } fields.add(new InsertParam.Field(id, ids)); fields.add(new InsertParam.Field(doc_id, docIds)); fields.add(new InsertParam.Field(chunk_text, texts)); fields.add(new InsertParam.Field(embedding, vectors)); InsertParam insertParam InsertParam.newBuilder() .withCollectionName(document_chunks) .withFields(fields) .build(); milvusClient.insert(insertParam); }这里提醒一下不同的SDK版本insert接口的字段名可能略有差异以自己引入的版本javadoc为准。4.2 查询向量化与ANN搜索实现搜索的入口是把用户的query也转成向量然后调Milvus的search接口。这一步的细节很多尤其是query向量的归一化、TopK的选择和输出字段的过滤。query向量生成复用之前的embedding服务拿到List 后直接传入SearchParam。这里我踩过一个坑BGE模型官方建议query在做embedding时先加一个指令前缀比如“为这个句子生成表示以用于检索相关文章”不加前缀的检索效果会差一截。这个细节在很多Java博客里没人提。搜索参数设置如下public ListSearchResult search(String query, String docIdFilter, int topK) { ListFloat queryVector embeddingClient.embed(query); SearchParam searchParam SearchParam.newBuilder() .withCollectionName(document_chunks) .withVectorFieldName(embedding) .withVectors(Collections.singletonList(queryVector)) .withTopK(topK) .withMetricType(MetricType.COSINE) .withParams({\ef\: 64}) .build(); if (docIdFilter ! null !docIdFilter.isEmpty()) { searchParam.withExpr(doc_id \ docIdFilter \); } SearchResult searchResult milvusClient.search(searchParam); return parseSearchResults(searchResult); }搜索完成之后Milvus会返回每条记录的ID、distance分数以及你自己要求返回的字段。要拿chunk_text必须在SearchParam里设置OutputFields否则只能拿到ID还得二次查库非常浪费性能。例如searchParam.withOutputFields(Arrays.asList(doc_id, chunk_text));4.3 从文档到语义搜索的最小可运行流程把上述模块串联起来一个最小可运行的服务流程大概是接收文档上传请求解析文本。切分文本为chunks逐条做embedding。构造字段列表批量写入Milvus。接收用户queryembedding后查Milvus。把命中的chunk_text返回给前端附带相似度分数。我项目里用Spring Boot把这几个步骤做成了两个接口POST /api/doc/upload和GET /api/search。核心逻辑加起来两百行左右关键代码不复杂难的是让embedding模型和Milvus形成稳定的数据链路。如果只跑通demo这套代码一天就能写完要做到生产可用要注意的地方太多了后面单独开一节讲。5. 文档检索效果优化与混合检索实战5.1 为什么纯向量检索依然会翻车向量检索不是银弹。我实测了三百条测试query纯向量召回在文档检索场景的F1大约在80%左右剩下的20%问题主要出在专有名词召回差比如“FTP”“熔断器”“CPU飙升”这种词汇语义上可能跟“连接不上”、“网关异常”相近但向量模型有时候抓不到精确指代关系。结果排序不稳定query包含多个条件时向量模型容易把某个强关联片段排太前忽略了整体匹配度。历史版本矛盾和相似文档堆叠一个cluster里全是讲同一主题的文档TopK容易被一篇长文档的多个chunk屠榜。解决方案是在向量检索之外叠加一层关键词检索或者BM25召回做混合检索Hybrid Search。向量负责语义召回BM25负责精确词匹配两边结果融合能显著提升准确率。5.2 用RRF算法融合向量和关键词结果主流混合检索的融合算法有RRFReciprocal Rank Fusion和加权分数融合。RRF的核心逻辑是把两条结果列表的排名倒数相加排名越高贡献越大对分数尺度不敏感不需要额外调权重。公式不复杂score(document) Σ 1 / (k rank(document))k是平滑系数一般取60。我自己实现了一个简化版RRFpublic MapString, Double rrfFusion(ListSearchResult vectorResults, ListSearchResult keywordResults) { MapString, Double scores new HashMap(); int k 60; for (int i 0; i vectorResults.size(); i) { String docId vectorResults.get(i).getDocId(); scores.merge(docId, 1.0 / (k i 1), Double::sum); } for (int i 0; i keywordResults.size(); i) { String docId keywordResults.get(i).getDocId(); scores.merge(docId, 1.0 / (k i 1), Double::sum); } return scores.entrySet().stream() .sorted(Map.Entry.String, DoublecomparingByValue().reversed()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) - e1, LinkedHashMap::new)); }融合之后你会发现纯粹的语义相似doc和关键词完全命中的doc排到了前面整体效果比单一向量检索稳很多。如果还想更精细可以在向量召回之后加一个rerank模型比如cross-encoder但这需要额外的模型推理服务而且Java侧调用成本偏高数据量大的时候先不做。5.3 标量过滤与元数据管理让检索更精准实际业务里很少只做全文语义搜索通常还要附带各种过滤条件比如“只看技术文档”“只看客服问答”“只看某个时间段的文档”。向量数据库里这类过滤建议用标量字段配合表达式完成而不是把条件拼进文本再去embedding——文本变了向量就变了这相当于把过滤逻辑塞进语义空间效果很难控制。Milvus支持直接在SearchParam里加expr表达式.withExpr(doc_type \faq\ and create_time 1735689600)把这个过滤条件下推到存储层数据库会先做标量过滤再进入向量索引搜索性能损失可控。这里要特别提一个注意点过滤字段一定要建索引Milvus对标量字段默认不建索引如果你在千万级大盘上做未索引字段的过滤查询会慢到怀疑人生。另一个元数据的实践是在chunk_text之外存储完整的父子文档映射关系。比如每一条chunk包含chunk_id和parent_id检索命中的是chunk_id但返回给用户时按parent_id做一次分组聚合把多个chunk合并成一篇完整文档再展示。这样用户体验更友好不需要看到一堆片段。6. 常见问题与排查技巧实录6.1 连接问题gRPC连接被重置、空闲超时Milvus Java SDK默认走gRPC长连接模式下容易出现空闲一段时间的Connection reset或者“UNAVAILABLE: Network closed for unknown reason”。这个问题本质上是服务端和客户端keepalive配置不对齐。排查路径先看Milvus服务端是否开了keepalive再看客户端是否禁用keepaliveWithoutCalls。Java端解决方法是.withKeepAliveWithoutCalls(true) .withKeepAliveTime(10, TimeUnit.SECONDS) .withKeepAliveTimeout(3, TimeUnit.SECONDS)如果还在用grpc的单连接高并发下也容易触发GOAWAY或too many pingsSDK上线前最好先压测不要等线上炸。6.2 数据量上来后内存过高插入的向量全部加载到内存会吃掉大量堆内存。Milvus插入接口一次性传几万条向量客户端Java进程堆内存会瞬间飙升。我遇到过一次OOM排查结果是批量插入时构造了一个超大的ArrayList一次性把几十万条向量全部放内存。解决办法控制批次大小单批次建议512条插入后释放引用用-Xmx限制堆内存并且开启GC日志观察频繁Full GC。向量数据库本身对内存的需求也不低。HNSW索引会额外占用一部分内存建议服务端内存至少是向量数据体积的2到3倍。如果你是2G内存的小机器跑500万条1024维向量基本必挂。6.3 中文检索效果差问题出在切分还是embedding中文检索效果差第一反应先别怀疑模型先检查切分。我遇到过最典型的翻车情况一个文档里包含“这是一个测试文档。测试内容如下”这种短句稀碎切分后产生大量无意义chunk这些chunk的向量几乎一样检索时严重干扰排序。解决办法是在切分时过滤掉长度小于50字符的chunk或者用简单规则把连续短句合并。如果切分没问题再检查embedding模型是否适合中文。部分英文模型处理中文很差建议使用bge-m3或text2vec-large-chinese这类中文模型。做一次简单的自测拿10条相似的query两两算余弦相似度如果相似度普遍低于0.6基本可以断定模型没选对。6.4 Java类型转换与精度问题Embedding接口返回的float数组转到Milvus的List 时可能会遇到精度损失。大部分开源模型输出的float是32位浮点Java的float恰好也是32位理论上无损耗。但如果你用double接数据再强转float可能出现精度变化。我的建议是整个链路统一用float别用double。另一个问题是同一批向量里维数不一致。embedding服务偶尔会返回dim小于1024的向量模型处理空文本时可能出现插入时Milvus会直接报“vector dimension mismatch”排查起来很隐蔽。我在embedding调用后加了一个校验if (vector.size() ! 1024) { throw new IllegalStateException(Embedding dimension mismatch: vector.size()); }6.5 常用检索参数速查表参数推荐值说明chunk_size300-500字符中文场景经验值越大越泛越小越碎chunk_overlap50-100字符防止句子被截断HNSW M16越大精度越高内存占用越大efConstruction200建索引质量太大会变慢efSearch64查询探索量小则快大则准TopK10-20语义搜索建议先召回粗集再rerankbatch_size512条插入批次过大内存爆过慢7. 从Demo到生产一点实操心得最后分享两个比较关键的实操体会。第一向量数据库不是放进去就完事的索引和数据质量决定上限。我有一次把文档清洗、切分、embedding都做完了检索效果还是不理想。后来发现是切分时把表格拆散了导致每个chunk的语义支离破碎。从那以后我给自己定了一条规矩每次做检索效果评估至少要人工抽查二十条失败case分类记录是切分问题、embedding问题还是排序问题再针对性优化。盲调参数效率极低。第二Java服务接入向量数据库本质上是在分布式系统和机器学习模型之间搭一座桥调试时要把数据流、线程池、连接池、内存占用放在一起看。有一个经验是给embedding调用单独建一个线程池设置合理的超时和失败降级——一旦embedding服务抖动不能拖垮核心查询链路。我在项目里给embedding服务配了熔断器超时300毫秒直接降级返回空结果保证搜索接口的主流程可用。项目做到上线运行整体的效果是用户搜索“微信支付失败怎么解决”能精确命中“微信支付错误码查不到账单”这类语义近似的文档搜索“申请售后”能召回“退款流程说明”和“退货地址怎么填”等关联文档。对比之前的ES关键词搜索问答准确率提升了大概30%。如果你也想在Java项目里做文档检索和语义搜索我建议先从最小链路起步别一上来就铺分布式先用Qdrant单机或pgvector跑通再根据自己的数据量决定要不要上Milvus。这个方向的技术栈还在快速演进但核心的“切分-向量化-索引-混合检索”链路短期内不会变把基本功打扎实了后面换任何数据库都只是SDK的问题。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →