ElasticSearch搜索实战
Ezzi's Blog引入ES作文章搜索的实战记录
更新于 2026.08.07
本文目录 8 节

最近在学习ElasticSearch,学完后想着先给博客接上,支持按照标题、作者、摘要、正文内容进行语义模糊搜索。
ElasticSearch支持语义搜索,对搜索的关键词进行分词后,去倒排索引中查询相关的文档,并按照相关性打分,最后返回结果。
这里说一个踩坑。从ES8开始,官方默认开启SSL访问,不支持普通的HTTP请求。如果在本地/测试环境中使用普通的HTTP访问,要在启动时加上启动参数xpack.security.enabled=false。
要引入ES,要从很多维度考虑。
多字段相关性搜索
这是ElasticSearch带来的最显著的提升,其功能强大,所以需要开发者也做好相应的工作。 为了让用户觉得搜索“聪明”,需要对不同字段设置不同的权重(Boosting)。
权重分配: 标题>摘要>正文>作者。
这里使用 multi_match 查询,配合 most_fields 模式。
{
"query": {
"multi_match": {
"query": "搜索词",
"fields": ["title^10", "summary^5", "content^1", "author^2"]
}
}
}
命中标题的文章通常比命中正文的更符合预期。将标题权重设高,能确保最相关的结果排在第一页.
高亮显示
搜索结果中的.高亮不仅仅是为了好看,更是为了告诉用户:“为什么这篇文章会被搜出来”。但当多个字段(如标题和正文)同时命中时,处理逻辑就变得至关重要。
独立高亮策略
为了避免页面显得杂乱,我采用了独立高亮策略。
- 标题: 只要命中,必须完整显示并包裹高亮标签。
- 正文: 只显示命中关键词所在的片段。
在前端呈现上,标题的高亮是主要的,而正文片段的高亮是辅助。通过 ES 的 require_field_match: false 设置,即便搜索词在某个字段权重较低,也能确保所有命中所见即所得。
关于require_field_match: false
控制 “搜索词来源字段” 与 “高亮显示字段” 之间关联性的一个开关。 在 Elasticsearch的高亮(Highlighting)逻辑中,它的默认值是
true。在默认情况下
require_field_match: true,ES会要求**只有当搜索词在该字段中被搜到时,才会在该字段进行高亮。**当设置为false时,ES的逻辑变为:只要文档命中了搜索词(不管是在哪个字段命中的),所有请求高亮的字段只要包含这个词,通通高亮。
高亮偏移处理

在搜索视图中,除了文章标题的展示外,其余内容通常受限于显示区域的大小,无法完全展示。如果只展示摘要或者文章正文的开头,而被检索到的地方在后面,就无法高亮显示,导致用户疑惑:“为什么搜到了这篇?”。因此,要把高亮的部分单独拿出来,也就是偏移处理。
为了优化体验,我引入了 fragmenter 机制:
- **分片定位(Fragmenter):**设置为
span或sentence。ES 会自动寻找包含关键词的那一段话,而不是死板地从头截取。 - **片段长度(Fragment Size):**每个高亮片段长度控制在 50 字左右,确保上下文语义完整。
Markdown纯文本化


对于HTML和Mardown格式的文本,文本中存在很多样式需要的符号。例如##、<p>等等。这些不仅影响ES的分词、搜索,还会影响前端的渲染。所以我的做法是,从数据库拉取到原始文本后,将其纯文本化。
要实现这个功能,使用Regex是可以实现的,大致代码如下。
public static String removeMarkdown(String markdown) {
if (markdown == null || markdown.isBlank()) {
return markdown;
}
// 代码块
markdown = markdown.replaceAll("(?s)```.*?```", "");
// 行内代码
markdown = markdown.replaceAll("`([^`]*)`", "$1");
// 图片
markdown = markdown.replaceAll("!\\[(.*?)\\]\\(.*?\\)", "$1");
// 链接
markdown = markdown.replaceAll("\\[(.*?)\\]\\(.*?\\)", "$1");
// 标题
markdown = markdown.replaceAll("(?m)^#{1,6}\\s+", "");
// 粗体 / 斜体
markdown = markdown.replaceAll("(\\*\\*|__)(.*?)\\1", "$2");
markdown = markdown.replaceAll("(\\*|_)(.*?)\\1", "$2");
// 删除线
markdown = markdown.replaceAll("~~(.*?)~~", "$1");
// 引用
markdown = markdown.replaceAll("(?m)^>+\\s*", "");
// 无序列表
markdown = markdown.replaceAll("(?m)^(\\s*)[-*+]\\s+", "$1");
// 有序列表
markdown = markdown.replaceAll("(?m)^(\\s*)\\d+\\.\\s+", "$1");
// 分割线
markdown = markdown.replaceAll("(?m)^(-{3,}|\\*{3,}|_{3,})$", "");
// 表格分隔行
markdown = markdown.replaceAll("(?m)^\\|?\\s*[-:]+[-| :]*\\|?$", "");
// 去掉表格竖线
markdown = markdown.replace("|", " ");
// HTML 标签
markdown = markdown.replaceAll("<[^>]+>", "");
// 多余空行
markdown = markdown.replaceAll("\\n{3,}", "\n\n");
return markdown.trim();
}
在生产环境中,不建议自己维护Regex。可以使用flexmark-java。然后
Parser parser = Parser.builder().build();
Node document = parser.parse(markdown);
TextCollectingVisitor visitor = new TextCollectingVisitor();
String text = visitor.collectAndGetText(document);
性能权衡
不引入copy_to提升查询性能
copy_to虽然能把多个字段合并成一个,但它会带来一个严重的后果:字段边界丢失。用copy_to的代价:当你把标题、摘要、正文都塞进一个all_text字段时,ES 会把它们视为一整块文本。你很难精准地告诉 ES:“如果关键词出现在标题,给 10 分;如果出现在正文,给 1 分”。不用的好处:保持字段独立,你可以利用multi_match的fields权重调节(如 title^10)。这种精细化调优对于博客搜索这种极其依赖标题权重的场景来说,远比节省那点查询开销重要。
不使用term_vectors(项向量)
用 **term_vectors** 的代价: 它会记录词频、位置、偏移量等大量元数据。开启后,索引文件的大小可能会膨胀 2 到 3 倍,且降低写入时的性能。
现在,本博客的ES引入完成,搜索功能可以体验。
评论
无需登录,审核通过后公开。只有博主可以回复。
正在加载…
已公开的评论