简介:一份基于Java与Vue技术栈的向量数据库语义检索与相似文档查重系统详细项目实例,面向具备Java和Vue基础的后端工程师、系统架构师及NLP技术人员,适合学术论文查重、企业知识产权保护、网络内容监控、政务档案管理等语义比对场景。资源包为单个docx文档,体积仅81KB,却涵盖完整程序代码、数据库与GUI设计说明,以及各模块代码详解,重点覆盖BERT文本向量化、Milvus向量数据库存储与近似最近邻检索、前后端分离交互、查重阈值自适应、高亮比对报告生成等核心实现。内容按项目背景、挑战与解决方案、模型架构、代码示例等章节组织,并在此基础上补充需求分析、系统架构设计、数据库建模、API接口规范与部署运维要点,支持多格式文档解析、智能分段、安全权限控制等扩展能力,可帮助读者搭建可扩展的智能语义检索平台。已有153人学习浏览,适合高校科研、企业IT及文档数字化项目研发人员实践参考。
1. 向量数据库做语义检索与相似文档查重:这套 Java+Vue 项目究竟解决什么问题
向量数据库做语义检索与相似文档查重,是把 NLP 从“调模型”带到“能上线”最近的一条路。做知识库问答、合同比对、资料归档的团队经常撞上同一个场景:关键词搜索只认字面,同义改写之后就漏;文档量一大,靠人工一篇篇翻着查重完全不现实。这个基于 Java + Vue 的向量数据库项目,核心就一句话:把文档转成高维向量写入向量数据库,让“语义相似度”变成“向量距离”,检索和查重共用同一套索引。Java 后端负责文档入库、Embedding 接入和相似度计算,Vue 前端负责检索交互与查重结果展示。它适合正在做文本检索系统、相似文档去重或知识库项目的 Java 工程师,也适合刚接触 NLP 工程落地的同学照着复现。
2. 为什么用向量数据库:语义检索与相似文档查重共用同一块底座
2.1 关键词和 SQL LIKE 先翻车:一个字面匹配的典型漏判场景
先看一个最常见的落地场景。你有一份《企业劳动用工合规检查指引》,用户搜“员工离职经济补偿”,用 SQL LIKE 去查可能什么都查不到,而另一份标题里完全没有这些词、正文却在讲“解除劳动合同补偿金”的文档,就这样从眼前溜过去了。这就是字面匹配的天花板:它只认字符前后缀,不认语义。语义检索想解决的是“意思相近才算相关”,而不是“字面相同才返回”。
Embedding 做的事情,是把一句中文文本映射成一个几百维的浮点向量,训练目标就是让语义相近的句子在向量空间里靠得近。“离职补偿”和“解除劳动合同补偿金”之间的距离,应该远小于“离职补偿”和“公司团建”之间的距离。于是,检索问题就转化成了计算查询向量与库内文档向量的距离。这一步是质的转变:以前你维护的是倒排索引和分词词典,现在你维护的是向量空间里的坐标。
但这里有个容易被忽略的问题:文档量到了十万、百万级以后,把向量存放在普通关系表里,你没法高效回答“离我最近的 Top10 是谁”。MySQL 的 B-tree 索引对高维浮点向量几乎无能为力,全表扫一遍算距离在数据量上来以后直接不可接受。向量数据库天生就是解决这个问题的:它用 HNSW、IVF 这类近似最近邻索引,把“高维空间找邻居”从 O(N) 降到了对数级。这也是我为什么坚持在这个项目里单独引入向量数据库,而不是把向量塞回业务库凑合。
至于另一个常被拿出来对比的方向——图数据库,我也在选型时认真看过。图数据库擅长关系遍历,比如“人—组织—文档”的关联路径,但它解决不了“两篇文档没有任何共同节点,但语义高度相似”的情况。语义相似不需要显示的关系边,它天然是向量空间里的距离问题。这就是“图数据库与向量数据库对比”之后我坚定选择后者的原因。
2.2 向量数据库选型的边界:pgvector、Milvus 与 Qdrant 怎么选
市面上的向量数据库不少,真正要在 Java 项目里落地,我一般先画一张选型表,按能接受的部署成本和团队现状来定,而不是看哪个名气大:
| 选型 | 部署方式 | Java 侧接入 | 适合规模 | 运维成本 |
|---|---|---|---|---|
| pgvector | PostgreSQL 扩展 | JDBC 直接连 | 百万级向量够用 | 最低 |
| Milvus | 独立分布式服务 | 官方 Java SDK | 千万级以上 | 高 |
| Qdrant | 独立服务 | SDK 或 REST | 千万级 | 中 |
| Elasticsearch dense_vector | 依赖已有 ES | REST API | 已有 ES 体系时加分 | 中 |
如果你的团队已经有 Elasticsearch,给索引加 dense_vector 字段实现 kNN 检索是可行的,前提是你接受 ES 那套映射和分片调优。如果团队只有 Java + MySQL 存量,最稳的起步路径反而是 pgvector:它不是一个独立服务,而是 PostgreSQL 的扩展,Java 侧用 JDBC 就能操作,向量列可以和业务字段放同一张表做关联查询。我这次项目的复现默认就选 pgvector,理由是它对 Java 工程师的学习成本最低,排错路径最短。
规模上来之后,再拆 Milvus 也来得及。常见的做法是:后端先封装一层向量操作接口,库内部用 pgvector 实现,后续要换 Milvus 只替换这一层实现,上层检索服务和 Vue 页面完全不用动。这样既满足了“先跑通”,又给生产环境留了扩容余地。记住一点:选型不是选最猛的,是选你团队能养得活的。一个小团队上来就搭三节点 Milvus 集群,最后往往没人运维,索引坏了都不知道。
2.3 三条处理链路:入库、检索、查重如何共用一套向量索引
这个系统从功能上看有三大块,但底层的向量索引只有一套。第一是入库链路:原始文档经过文本提取、清洗后,调用 Embedding 模型得到向量,连同文档标题、正文、哈希一起写入数据库。第二是检索链路:用户在页面输入一句话,前端把它交给后端,后端用同一个 Embedding 模型转成向量,在向量库里做最近邻搜索,返回 TopK 结果和相似度。第三是查重链路:新文档进来时,先把全文转成向量,再拿这个向量去库里找“最像的已存文档”,相似度超过阈值就判定为重复或疑似重复。
三条链路本质上都是“文本转向量 + 算距离”,只是使用目的不同:检索是拿用户的查询向量找内容,查重是拿新文档的向量找老文档。正因为如此,项目的大部分复杂度并不在业务 CRUD,而在 Embedding 接入的一致性、向量的写入方式,以及阈值怎么校准。理解了这三条链路,后面的建表、接口和页面设计就都清晰了。前端 Vue 页面再好看,后端如果没把 TopK、阈值这些参数透传好,系统永远是纸上谈兵。
3. 后端实现:Spring Boot 接 pgvector 的建表、嵌入与查询
3.1 建库建表与实体映射:向量列和文档表到底怎么设计
先建 PostgreSQL 扩展,再创建两张表:一张存文档本身,一张存向量。分开存的原因是把向量列单独放,便于控制索引的构建频率,避免每次更新文档元数据都触发向量索引重建。
-- 启用 pgvector 扩展 CREATE EXTENSION IF NOT EXISTS vector; -- 文档主表:存标题、正文、哈希值 CREATE TABLE doc_record ( id BIGSERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, file_hash CHAR(64), created_at TIMESTAMP DEFAULT now(), updated_at TIMESTAMP DEFAULT now() ); -- 向量表:doc_id 与主表一一对应,embedding 是 768 维向量 CREATE TABLE doc_vector ( doc_id BIGINT PRIMARY KEY REFERENCES doc_record(id) ON DELETE CASCADE, embedding VECTOR(768) NOT NULL, created_at TIMESTAMP DEFAULT now() ); -- 为向量列创建 HNSW 索引 CREATE INDEX idx_doc_vector_embedding ON doc_vector USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);这里的 EMBEDDING 维度写成 768,是因为常用的开源中文 Embedding 模型如 bge-base-zh、text2vec-base-chinese 输出的都是 768 维。维度必须和模型实际输出一致,否则插入时直接报错。vector(768)括号里的数字是强约束,不是注释。
HNSW 索引参数里,m = 16表示每个节点在图中的最大连接数,数值越大召回越好,但内存也会涨;ef_construction = 64是建索引时动态候选集的大小,影响索引质量,对查询速度影响不大。这两个参数在百万级以内用默认值起步没问题,不要一上来就追求极端参数。
用 MyBatis Plus 写实体类时,业务表字段可以直接和doc_record对应;但向量表我不建议靠代码生成器反推 DDL。网上确实有“mybatisplus 根据 Java 实体类生成创建表的 sql 语句”的做法,普通字段可以,一旦遇到VECTOR(768)类型和 HNSW 索引,模板基本无能为力。我的习惯是:实体类只用来映射查询结果,建表 SQL 始终手写维护。这样最直观,也不会被工具自作主张改掉索引定义。
3.2 接入中文 Embedding 服务:把文本变成 768 维浮点数组
Java 后端并不直接加载模型,常见做法是在 Python 侧封装一个 HTTP 推理服务,加载开源中文 Embedding 模型,对外暴露一个/embed接口。Java 只负责传文本进去、拿浮点数组出来。这样切分的好处是:模型迭代不影响 Java 工程,Java 工程师也不需要在 Maven 里引入一堆 Python 不友好的深度学习依赖。
@Service @RequiredArgsConstructor public class EmbeddingService { @Value("${embedding.base-url}") private String baseUrl; private final RestTemplate restTemplate; /** * 把一段文本转成 768 维向量 */ public float[] embedText(String text) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); Map<String, Object> body = new HashMap<>(); body.put("texts", List.of(text)); body.put("truncate", true); // 超过模型最大长度时截断,避免报错 HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers); ResponseEntity<EmbeddingResponse> response = restTemplate.postForEntity(baseUrl + "/embed", request, EmbeddingResponse.class); if (response.getBody() == null || response.getBody().getVectors().isEmpty()) { throw new RuntimeException("Embedding 服务返回为空"); } return response.getBody().getVectors().get(0); } }这里有一个重要的隐藏约定:truncate必须显式传 true。中文长文档经常超过模型的最大输入长度,比如 BERT 系模型普遍是 512 个 token,超长后如果服务端不做截断,请求会直接 4xx。你在实现 EmbeddingService 时,一定要先确认部署的 Python 服务支持什么参数、超出长度后是截断还是报错,两边对齐后,再把这段 Java 代码固化下来。
Embedding 服务本身部署不复杂:下载开源模型,用 FastAPI 包一层,启动后监听本地端口即可。需要注意,模型必须固定,线上的检索和入库用的必须是同一个模型文件。你要是今天换一个模型,明天又换一个,向量空间都变了,之前入库的向量全部失效,等于整个库要重建。
3.3 检索与查重的核心接口:相似度怎么算,阈值怎么传
语义检索接口是系统的门面。它把用户查询转成向量,再用 pgvector 的余弦距离运算符<=>做排序。<=>返回的是余弦距离,范围 0 到 2,所以相似度要用1 - 余弦距离。
@Repository @RequiredArgsConstructor public class VectorSearchDao { private final JdbcTemplate jdbcTemplate; /** * 语义检索:返回与查询向量最相似的 TopK 文档 */ public List<SearchHit> semanticSearch(String query, int topK, double minSimilarity) { float[] queryVector = embeddingService.embedText(query); String vectorLiteral = toPgVectorLiteral(queryVector); String sql = """ SELECT d.id, d.title, d.content, 1 - (v.embedding <=> ?::vector) AS similarity FROM doc_vector v JOIN doc_record d ON d.id = v.doc_id WHERE 1 - (v.embedding <=> ?::vector) >= ? ORDER BY v.embedding <=> ?::vector LIMIT ? """; return jdbcTemplate.query( sql, new Object[]{vectorLiteral, minSimilarity, topK}, (rs, rowNum) -> new SearchHit( rs.getLong("id"), rs.getString("title"), rs.getString("content"), rs.getDouble("similarity") ) ); } private String toPgVectorLiteral(float[] vector) { StringBuilder sb = new StringBuilder("["); for (int i = 0; i < vector.length; i++) { if (i > 0) sb.append(","); sb.append(vector[i]); } sb.append("]"); return sb.toString(); } }?::vector是把 JDBC 参数显式转换成 pgvector 类型,不写这个强转,PostgreSQL 有时无法从字符串自动推断类型。topK 一般取 10 到 20,minSimilarity 的下限我建议由前端传入,否则后端写死一个 0.7,换一个领域语料就废了。把这些参数暴露给 Vue 页面,让使用者自己拉阈值,比你在后端拍脑袋决定要稳得多。
相似文档查重的后端逻辑,本质上就是“拿新文档的向量去库里搜索,不过滤自己”。如果只重一两篇文档,直接调 semanticSearch 然后排除自身就行。批量建库时文档多了,就要换策略:先把向量全取出来做聚类分桶,再在桶内两两比较。不要写一个双层 for 循环去比 O(N²) 对文档,我在测试时用一万篇文档跑过,全量比对要卡几分钟,线上绝对不能这么干。分桶粗筛后再做精确比对,是生产环境里最常见的查重路径。
4. Vue 前端落地:检索交互、查重页面与跨域联调
4.1 初始化 Vue 项目:环境检查与依赖安装
后端接口就绪后,前端的事情从“vue 安装及环境配置”开始。很多人在这一步就卡住了,其实是 Node 环境没配对。建议先用命令确认两件事:Node 版本要在 16 以上,npm 能正常访问仓库。
# 检查环境和版本 node -v npm -v # 用 Vite 初始化 Vue3 项目 npm create vite@latest semantic-search-frontend -- --template vue cd semantic-search-frontend # 安装依赖 npm install # 额外安装请求库 npm install axios在前后端分离的项目里,Vite 创建出来的项目默认是前端开发服务器,端口是 5173,后端 Spring Boot 默认跑在 8080。两个端口不同,就会有跨域问题。这个问题留在后面小节处理,这里先把项目跑起来,看到 Vite 的默认欢迎页,说明环境没问题了。
目录结构我一般会整理成 views(页面)、api(接口封装)、components(通用组件)三层。语义检索页和查重页是两套不同的交互,建议直接拆成两个 view。剩下通用部分,我习惯写一个api/search.js统一封装 axios 实例,这样后端地址换环境时只改一处。
4.2 语义检索页:搜索框、TopK 参数与相似度结果条
语义检索页的核心交互很简单:一个输入框,一个检索按钮,下面展示相似度倒序排列的结果列表。技术上要注意的是,在 Vue 里调用后端接口要异步处理,用 loading 状态防止用户重复点击。下面是最小可运行版本:
<script setup> import { ref } from 'vue' import { semanticSearch } from '../api/search' const keyword = ref('') const topK = ref(10) const minSimilarity = ref(0.6) const results = ref([]) const loading = ref(false) const handleSearch = async () => { if (!keyword.value.trim()) return loading.value = true try { const { data } = await semanticSearch({ query: keyword.value, topK: topK.value, minSimilarity: minSimilarity.value }) results.value = data } finally { loading.value = false } } </script> <template> <div class="search-bar"> <input v-model="keyword" placeholder="输入一句话进行语义检索" @keyup.enter="handleSearch" /> <button :disabled="loading" @click="handleSearch"> {{ loading ? '检索中...' : '检索' }} </button> </div> <ul class="result-list"> <li v-for="item in results" :key="item.id"> <h3>{{ item.title }}</h3> <p>{{ item.content }}</p> <div class="similarity-bar"> <span>相似度</span> <div class="bar"> <div class="bar-fill" :style="{ width: (item.similarity * 100) + '%' }"></div> </div> <span>{{ item.similarity.toFixed(4) }}</span> </div> </li> </ul> </template>这个页面里的 minSimilarity 默认 0.6,只是起步值。我实际项目里会给它加一个滑条,让用户自己调。为什么不能写死?因为不同 Embedding 模型产出的相似度分数分布差异很大,同一个模型在不同的领域语料上分布也不一样。0.6 在这个模型上可能什么都召回不了,换个模型可能全是误召。把阈值做成可调参数,比在后端硬编码聪明得多。topK 同理,知识库场景 10 条够用,合同查重场景可能要看 20 条。
结果列表里的相似度条用 CSS 宽度实现,item.similarity * 100是常用做法。要注意相似度可能大于 1,浮点计算误差会导致 100% 超出父容器,我给外层 div 加了 overflow hidden,这是细节,但线上页面少了它容易穿版。
4.3 相似文档查重页:分组展示与可疑重复对
查重页面和检索页面不一样,它展示的是“文档与文档之间的关系”。基本交互是:选中一个文档,点击查重,后端返回与该文档相似度超过阈值的所有文档,前端按相似度排序展示。我的页面里会给每一条结果加一个重复标记,方便人工二次确认。这里的 vue 插槽可以用来做自定义结果项扩展,比如给高分结果加红色角标,但我建议第一版先保持简单。
<script setup> import { ref } from 'vue' import { detectDuplicates } from '../api/search' const docId = ref(null) const threshold = ref(0.8) const duplicates = ref([]) const runDuplicateCheck = async () => { if (!docId.value) return const { data } = await detectDuplicates({ docId: docId.value, minSimilarity: threshold.value }) duplicates.value = data } </script> <template> <div> <label>文档 ID</label> <input v-model="docId" type="number" placeholder="输入文档 ID" /> <label>查重阈值</label> <input v-model="threshold" type="number" step="0.05" /> <button @click="runDuplicateCheck">开始查重</button> <div v-for="dup in duplicates" :key="dup.targetDocId" class="dup-item"> <span>{{ dup.sourceTitle }}</span> <span>与</span> <span>{{ dup.targetTitle }}</span> <span>相似度 {{ dup.similarity.toFixed(4) }}</span> </div> </div> </template>真正上线时,查重页面很少做“输入文档 ID”这种操作,更像是上传一个文档附件,后端解析后返回相似文档列表。我这里保留文档 ID 是为了复现方便:你可以先把两篇明显同一主题的文档入库,再拿其中一篇的 ID 去查,验证链路通不通。查重页面的核心价值是让人能直观看到相似度分数,并判断后端返回的相似文档是否合理,所以展示优先级高于样式,先把逻辑跑通,再考虑好看。
4.4 联调跨域:vite 代理和后端 CORS 的取舍
前后端端口不同,浏览器会直接拦截跨域请求。这是前后端分离项目最常见又最容易处理的坑。我推荐的做法是在 vite.config.js 里配置代理,把前端请求转发到后端,前端代码里只写相对路径,不写后端地址。
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })加上这段配置后,前端请求/api/search会被 Vite 开发服务器转发到http://localhost:8080/api/search,浏览器和前端服务同源,自然不存在跨域问题。生产环境则用 Nginx 做同样的反向代理,前后端部署在同一域名下。后端的 @CrossOrigin 注解也能解决,但既然能用代理解决,我一般不在代码里散落跨域注解,因为那样生产环境如果换域名,还得再改一遍代码。
联调时遇到最多的现象是:后端接口用 Postman 调得好好的,前端一调就被拦截。这时候你先看浏览器控制台有没有 CORS 字样,有的话先检查 vite 代理是否生效——注意 Vite 启动后要保存并重启才生效,改完等它自动重启也行。确认代理没问题后再怀疑后端。
5. 避坑记录:五个最容易让系统翻车的细节与排查方法
5.1 插入向量报 768 变 769:维度错误是向量库第一道坎
现象:执行 INSERT 时报错,大意是输入维度是 769,但表定义是 768。解决:这是我自己第一次接入 pgvector 时踩的坑。原因很隐蔽:我把 float[] 转成 Postgres 数组时,用逗号拼接后多了一个分隔符,比如数组最后一个元素后面多了一个逗号,Postgres 会把空字符串当成一个额外维度。这类问题发生在自行拼接toPgVectorLiteral时。解决方法是写完拼接逻辑后,先在数据库里执行一个小数据量的 SELECT,打印出数组长度核对。我后来直接在工具类里加了校验,拼接完再数一遍逗号数量,不对就抛异常,从源头杜绝。
另一个常见原因是 Embedding 模型换了,输出从 768 变成 384,但表结构没改。这是典型配置漂移,解决办法是把模型版本写进配置中心,每次模型升级时强制迁移向量表。
5.2 查重阈值 0.8 漏、0.6 误:阈值不是拍脑袋定的
现象:把查重阈值设为 0.8,明显是同一篇文档改写而来的内容被判为不重复;把阈值降到 0.6,大量只是沾点边的内容都被标成疑似重复。原因:不同领域语料的相似度分数分布差异很大,合同文本和新闻稿的分布完全不同,0.8 和 0.6 都没有普适性。解决:我一般会先抽 50 组人工标注过的真实重复文档对,跑一遍查重,把相似度分数全部记录下来,画出分布图——阈值就取“重复对和非重复对分区最明显”的位置,而不是猜。这个校准过程会让人很不舒服,因为你会发现,同一模型跑合同和跑技术文档,阈值能差出 0.15。
5.3 长文档直接 Embedding:检索结果“形似神不似”
现象:检索“新能源补贴政策”,返回的文档里确实有“新能源”三个字,但讲的是汽车购置,完全没提补贴。原因:长文档正文几千个字直接喂给 Embedding 模型,模型能提取的只是整体语义,细粒度主题会被稀释。解决:入库前做分块,按段落或按固定长度切块,每块单独向量化,检索时先命中子块,再聚合到父文档返回。我在项目里的参数是每块 256 到 512 个 token,块和块之间重叠 64 个 token,避免切断完整句子。分块后虽然向量数量变多,但检索精度提升非常明显。
5.4 前端 axios 跨域被浏览器拦截:CORS 的两种解法
现象:前端调用后端接口,浏览器报blocked by CORS policy,但同一个接口在 Postman 里返回正常。原因:浏览器安全策略拦截了跨域请求,Postman 不受这个限制。解决:第一优先用 Vite 代理,开发环境零侵入;生产环境用 Nginx 反代。第二才是在后端加 @CrossOrigin,但加了之后要小心,它会允许所有来源的跨域访问,如果接口部署在公网,可能有安全风险。我的建议是:CORS 配置只在联调阶段开,上线前务必收紧。
5.5 HNSW 索引参数乱调:查询变慢,问题在索引而非数据量
现象:向量数据只加了五万条,查询却越来越慢,数据库 CPU 居高不下。原因:HNSW 索引不是建了就完事,它的 ef_search 参数控制每次查询遍历的候选节点数量。有人为了召回率把它调到 200,查询变慢后还以为是数据量太大。解决:把 ef_search 从默认 40 调回 40 到 80 之间,先观察召回率是否下降得厉害,再决定要不要继续加大。我一般用查询耗时和召回率两把尺子一起量,单看召回率容易牺牲性能,单看耗时容易牺牲效果。另外,索引的 m 和 ef_construction 是建索引时的参数,改完后要重建索引,不是改个配置就生效,这个和查询参数不同,容易混淆。
6. 效果验收与进阶:用一组改写样本校准阈值,再谈 Rerank 精排
系统做完之后,最容易犯的错误是“自认为效果好”。我建议你花半天时间做一次验收:准备 20 组真实文档对,每组包含一个原始文档和一个人工改写版本,改写幅度尽量参考真实场景,比如调整语序、替换同义词、增删句子。然后一组一组跑检索和查重,统计 Top10 命中率和误报率。
拿到分布数据后,再回过头调 minSimilarity 阈值。这一步是系统的“后悔药”,它能帮你把拍脑袋定的参数改成有据可查的配置。我见过团队上线后又因为阈值不合理返工,那时文档已经灌了好几万条,重建向量索引很痛。检查环节放在上线前,能省下大量返工时间。
进阶方向主要看三块。一块是 Rerank 精排,向量检索只是粗排,返回 Top30 后再用 CrossEncoder 模型对结果逐条打分,重新排序后取 Top10,效果提升非常明显,代价是每一条都要过一遍模型,延迟高几十毫秒。第二块是长文档分块后,子块的向量聚合回文档的策略:我试过平均池化、最大池化和加权池化,平均池化最稳,最大池化容易放大噪声。第三块是查重规模化,文档到十万条之后,先做聚类分桶,再在桶内精确比对,不要再用全量两两比较。
我在第二个项目里把阈值从 0.65 调到 0.72,召回率掉了 4 个百分点,误报率却少了近一半。这个平衡点靠经验猜不出来,一定是从自己的样本里测出来的。现在做新的语义检索项目,我会把“构建人工标注样本集”写在排期里,当成和建表一样重要的事情来做。希望这篇能帮你把这个项目跑通,也替你把该踩的坑提前踩一遍。
本文还有配套的精品资源,点击获取