ES 高级玩法大揭秘:从算分骚操作到深度分页踩坑,后端 er 速进!

咱后端天天跟数据打交道,Elasticsearch(下文简称 ES)作为 "搜索引擎天花板",光会简单的增删改查可不够。今天咱就从 "高级搜索" 切入,把算分、分页、同义词这些实战痛点掰开揉碎,保证你看完能直接抄作业!

一、ES 高级搜索:不止于 "搜得到"

先聊聊基础 ------ES 高级搜索到底高级在哪儿?普通搜索是 "你要啥我找啥",高级搜索是 "你要啥我不仅找得到,还能按你的心思排好序、筛得准"。

比如你做个电商搜索,用户搜 "性价比手机",高级搜索能做到:

  1. 优先匹配 "手机" 关键词,再关联 "性价比" 描述;

  2. 销量高、评分高的商品往前排;

  3. 还能过滤掉已下架、库存为 0 的商品。

本质上,ES 高级搜索是通过bool查询(must/should/must_not/filter)、算分机制、过滤条件组合实现的,而这一切的核心,绕不开 "算分"。

二、算分函数:让好结果 "C 位出道"

为啥搜索结果要算分?总不能把匹配的结果乱序扔给用户吧?就像外卖平台,肯定把评分高、距离近的先推给你,ES 的算分(_score)就是干这个的。

咱先盘两个最常用的算分函数:

1. TF-IDF:"提得多 = 更重要"

TF(词频):一个词在文档里出现的次数越多,分越高(比如 "手机" 在某商品描述里出现 5 次,比出现 1 次的分高);

IDF(逆文档频率):一个词在所有文档里出现得越少,分越高(比如 "曲面屏" 比 "手机" 稀有,含 "曲面屏" 的文档分更高)。

👉 通俗讲:你要找的词,在当前文档里越常见、在所有文档里越稀有,这篇文档就越匹配

2. BM25:TF-IDF 的 "升级版"

TF-IDF 有个小毛病:词频越高分越高,没上限(比如某文档里 "手机" 重复 100 次,分能上天)。BM25 就聪明多了,它会给词频设个 "天花板",超过阈值后分数增长放缓,更符合实际场景。

现在 ES 默认用的就是 BM25,如果你想改回 TF-IDF,也能在索引设置里调整,不过一般没必要~

三、算分实战:DSL 和 Java 双管齐下

光懂理论没用,咱直接上代码!算分主要有两种玩法:DSL 手动调权重,Java 代码自定义算分。

1. DSL 算分:简单粗暴调权重

比如做博客搜索,想让 "标题" 的权重比 "正文" 高 3 倍(用户更可能在标题里找关键词),DSL 这么写:

json

json 复制代码
GET /blog/_search
{
  "query": {
    "function_score": {  // 自定义算分函数
      "query": {
        "multi_match": {
          "query": "Elasticsearch",  // 搜索关键词
          "fields": ["title^3", "content"]  // ^3表示标题权重×3
        }
      },
      "boost_mode": "multiply"  // 算分模式:权重×原始分数
    }
  }
}

效果立竿见影:标题含 "Elasticsearch" 的博客,会比正文含该词的博客排得更前。

2. Java 算分:复杂场景自定义

如果需要更灵活的算分(比如结合用户等级、文章发布时间),就得用 Java 代码了。这里以 Spring Data Elasticsearch 为例:

java

运行

less 复制代码
// 1. 构建自定义算分查询
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 2. 基础查询:匹配关键词
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
        .must(QueryBuilders.matchQuery("title", "ES").boost(3.0f))  // 标题权重×3
        .must(QueryBuilders.matchQuery("content", "ES"));
// 3. 自定义算分:发布时间越近,分数越高
FunctionScoreQueryBuilder.FilterFunctionBuilder[] functions = {
    new FunctionScoreQueryBuilder.FilterFunctionBuilder(
        ScoreFunctionBuilders.fieldValueFactorFunction("publishTime")
            .modifier(FieldValueFactorFunction.Modifier.LOG1P)  // 对数增长,避免分数爆炸
            .factor(0.1f)  // 系数调整
    )
};
// 4. 组合查询
queryBuilder.withQuery(QueryBuilders.functionScoreQuery(boolQuery, functions)
        .boostMode(CombineFunction.MULTIPLY));  // 算分模式

// 5. 执行查询
SearchHits<Blog> hits = elasticsearchRestTemplate.search(
    queryBuilder.build(), Blog.class);

这段代码实现了 "关键词匹配 + 发布时间加权",新发布的优质文章会更靠前,很适合博客、新闻类场景。

四、深度分页:后端 er 的 "翻页噩梦"?

聊完算分,咱来踩个大坑 ------ 深度分页。比如用户想翻到第 1000 页,你以为 ES 会直接返回第 1000 页的数据?错了!它的逻辑能让你 "卡到怀疑人生"。

1. 单节点分页:简单但有限

单节点下,ES 分页用的是from+size(from 是起始位置,size 是每页条数),逻辑很直白:

  1. 从所有数据里筛选出匹配的结果;

  2. 把结果排序后,跳过前from条,取size条。

比如from=1000,size=10,ES 要先查 1010 条数据,再扔了前 1000 条,只返回 10 条。这时候问题来了:from越大,ES 要处理的数据越多,性能越差。

2. 集群分页:坑更大!

集群环境下,ES 有多个分片(shard),分页逻辑更复杂:

  1. 假设集群有 3 个分片,你要查from=1000,size=10

  2. 每个分片都会查 1010 条数据(因为不知道其他分片的数据排序),然后返回给协调节点(coordinator);

  3. 协调节点把 3×1010=3030 条数据合并、排序,再跳过 1000 条,取 10 条。

你想想:from=10000的时候,每个分片要返回 10010 条,3 个分片就是 30030 条,协调节点处理这么多数据,不崩才怪!这就是为啥 ES 官方不推荐from+size用于深度分页(一般from超过 1000 就会报警)。

五、深度分页救星:ES 给的方案 + 实战技巧

别慌!ES 早想到了这个问题,给了两种解决方案,咱再结合实际开发补两个技巧。

1. 方案一:Scroll(滚动查询)------ 适合批量导出

Scroll 就像给数据 "拍快照",第一次查询时生成一个快照 ID(scroll_id),后续用这个 ID 分批拉取数据,不用每次重新排序。

适用场景:批量导出数据(比如每天凌晨同步 ES 数据到 MySQL),不适合实时翻页(快照不会更新,新增数据查不到)。

Java 代码示例(Spring Data ES):

java

运行

scss 复制代码
// 1. 初始化滚动查询,保存1分钟快照
NativeSearchQuery query = new NativeSearchQueryBuilder()
        .withQuery(QueryBuilders.matchAllQuery())
        .withScroll(TimeValue.timeValueMinutes(1))  // 快照有效期1分钟
        .withPageable(PageRequest.of(0, 1000))  // 每次拉1000条
        .build();

// 2. 第一次查询,获取scroll_id和第一页数据
SearchHits<Blog> scrollHits = elasticsearchRestTemplate.search(query, Blog.class);
String scrollId = scrollHits.getScrollId();

// 3. 循环拉取后续数据,直到没有结果
while (scrollHits.getTotalHits() > 0) {
    // 处理当前页数据
    List<Blog> blogs = scrollHits.stream()
            .map(SearchHit::getContent)
            .collect(Collectors.toList());
    
    // 用scroll_id拉取下一页
    NativeSearchQuery scrollQuery = new NativeSearchQueryBuilder()
            .withScrollId(scrollId)
            .withScroll(TimeValue.timeValueMinutes(1))
            .build();
    scrollHits = elasticsearchRestTemplate.search(scrollQuery, Blog.class);
    scrollId = scrollHits.getScrollId();
}

// 4. 最后释放scroll(避免内存泄漏)
elasticsearchRestTemplate.clearScroll(scrollId);

2. 方案二:Search After------ 适合实时翻页

Search After 是 "接力式" 分页:用前一页最后一条数据的 "唯一排序字段"(比如id+publishTime)作为条件,拉取下一页,不用计算from

适用场景:实时翻页(比如电商商品列表),缺点是不能跳页(只能上一页 / 下一页)。

DSL 示例:

json

json 复制代码
// 第一页:正常查询,返回最后一条数据的sort字段
GET /product/_search
{
  "query": { "match": { "category": "手机" } },
  "sort": [
    { "sales": "desc" },  // 按销量降序
    { "_id": "asc" }      // _id唯一,避免排序冲突
  ],
  "size": 10,
  "from": 0
}

// 假设第一页最后一条数据的sort是 [1000, "123456"]
// 第二页:用search_after接力
GET /product/_search
{
  "query": { "match": { "category": "手机" } },
  "sort": [
    { "sales": "desc" },
    { "_id": "asc" }
  ],
  "size": 10,
  "search_after": [1000, "123456"]  // 前一页最后一条的sort值
}

3. 实际开发技巧

  • 限制分页深度:比如最多只允许翻 50 页,超过提示 "请缩小搜索范围"(用户真的会翻 100 页吗?大概率不会);
  • 用 "跳转页" 代替 "连续翻页":比如让用户输入页码跳转时,先检查页码是否超过阈值,超过则用 Search After + 缓存处理;
  • 冷热数据分离:把老数据(比如 3 个月前的订单)迁移到冷节点,减少热数据分片的查询压力。

六、同义词:让 ES "听懂" 你的潜台词

最后聊聊同义词 ------ 用户搜 "智能机",你想让 "手机" 也出现在结果里;搜 "电脑",想包含 "笔记本""台式机",这时候就得靠同义词。

1. 第一步:配置同义词文件

先在 ES 的config目录下新建synonyms.txt,写好同义词组(用英文逗号分隔):

txt

复制代码
智能机,手机
电脑,笔记本,台式机
java,爪哇

2. 第二步:定义同义词分词器

创建索引时,自定义分词器,把同义词功能加进去:

json

json 复制代码
PUT /product
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_synonym_analyzer": {  // 自定义同义词分词器
          "type": "custom",
          "tokenizer": "ik_max_word",  // 用IK分词(中文必备)
          "filter": ["synonym"]  // 启用同义词过滤
        }
      },
      "filter": {
        "synonym": {  // 同义词过滤器配置
          "type": "synonym",
          "synonyms_path": "synonyms.txt"  // 同义词文件路径
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_synonym_analyzer",  // 字段使用同义词分词器
        "search_analyzer": "my_synonym_analyzer"
      }
    }
  }
}

3. 测试效果

现在搜 "智能机",ES 会自动把 "手机" 也当成关键词匹配:

json

bash 复制代码
GET /product/_search
{
  "query": {
    "match": { "name": "智能机" }
  }
}

返回结果里,含 "智能机" 和 "手机" 的商品都会出现,用户体验直接拉满!

结尾:ES 实战永无止境

今天咱把 ES 高级搜索、算分、分页、同义词这些核心知识点过了一遍,都是后端开发中高频用到的技能。其实 ES 还有很多玩法,比如聚合查询、索引优化、性能调优,后续咱再慢慢聊。

你在 ES 开发中还遇到过哪些坑?比如算分不准、分页超时、同义词不生效?评论区聊聊你的解决方案,咱一起抱团避坑!

相关推荐
江团1io02 小时前
深入解析MVCC:多版本并发控制的原理与实现
java·经验分享·mysql
树码小子2 小时前
Java网络编程:(socket API编程:UDP协议的 socket API -- 回显程序的服务器端程序的编写)
java·网络·udp
Python私教2 小时前
Django全栈班v1.04 Python基础语法 20250912 上午
后端·python·django
君宝2 小时前
Linux ALSA架构:PCM_OPEN流程 (二)
java·linux·c++
云深麋鹿2 小时前
数据链路层总结
java·网络
fire-flyer2 小时前
响应式客户端 WebClient详解
java·spring·reactor
北执南念2 小时前
基于 Spring 的策略模式框架,用于根据不同的类的标识获取对应的处理器实例
java·spring·策略模式
王道长服务器 | 亚马逊云2 小时前
一个迁移案例:从传统 IDC 到 AWS 的真实对比
java·spring boot·git·云计算·github·dubbo·aws
island13142 小时前
【C++框架#5】Elasticsearch 安装和使用
开发语言·c++·elasticsearch