ES / OpenSearch 工程化实战:从搜索原理到索引设计、查询调优与项目落地

9 阅读17分钟

这篇文章不是单纯整理 ES / OpenSearch 的概念,也不是堆 DSL 语法,而是从一个后端开发者的角度,梳理我对搜索系统的理解。

我会按这条主线来写:

它解决什么问题 → 为什么需要它 → 怎么实现 → 有什么坑 → 怎么优化 → 在项目里怎么用

看完之后,至少能回答这几个问题:

  • 为什么不能一直用 MySQL 顶搜索?
  • ES / OpenSearch 到底适合解决什么问题?
  • textkeyword 怎么选?
  • queryfilter 为什么要分开?
  • 深分页为什么慢?
  • 索引设计错了怎么平滑升级?
  • 在资讯聚合、BI、AI 检索场景里怎么落地?

1. 为什么很多业务会引入 ES / OpenSearch

一开始做系统的时候,很多查询直接用 MySQL 就够了。

比如:

  • 根据 ID 查详情
  • 根据状态筛选列表
  • 根据时间排序
  • 简单模糊搜索一下标题

这些场景用 MySQL 没什么问题。

但当业务变成下面这样时,MySQL 就开始吃力了:

  • 用户想按关键词搜索标题、摘要、正文
  • 搜索结果要按相关性排序
  • 还要叠加来源、分类、语言、时间范围等筛选条件
  • 页面不只返回列表,还要展示热门分类、热门来源、趋势统计
  • 数据量越来越大,搜索体验不能明显变慢

这时候问题就变了。

它已经不是简单的“查数据”,而是:

从大量文本和结构化字段中,快速找到最相关的一批结果,并且支持筛选、排序和分析。

这正是 ES / OpenSearch 更适合解决的问题。

所以我现在更倾向于把 ES / OpenSearch 理解成:

业务系统里的检索与分析层。

它不是用来替代 MySQL 的,而是把 MySQL 不擅长的全文检索、相关性排序、复杂筛选和聚合分析拆出来,交给更合适的系统处理。

2. ES / OpenSearch 到底解决了什么问题

2.1 解决全文检索问题

MySQL 也能做模糊查询,比如:

select * from news where title like '%OpenAI%';

小数据量下没问题,但数据量一大,这种查询就很难支撑复杂搜索体验。

因为它本质上不适合做大规模全文检索。

ES / OpenSearch 的核心优势是倒排索引。

简单理解就是:

  • MySQL 更像是:文档 → 字段内容
  • 倒排索引更像是:词 → 哪些文档包含这个词

比如有几篇文章:

doc1: OpenAI releases new model
doc2: Google updates AI search
doc3: OpenAI policy changes

倒排索引大概会记录成:

OpenAI -> doc1, doc3
AI     -> doc2
policy -> doc3
search -> doc2

这样用户搜索 OpenAI 时,就不用一篇篇扫正文,而是直接找到包含这个词的文档列表。

这就是 ES / OpenSearch 做全文检索快的基础。

2.2 解决相关性排序问题

搜索不是“只要包含关键词就行”。

比如用户搜:

OpenAI policy

有几种情况:

  • 标题里命中 OpenAI policy
  • 摘要里命中 OpenAI policy
  • 正文深处出现过一次 OpenAI
  • 分类标签是 AI,但正文不相关

这些结果的相关性肯定不一样。

所以搜索系统不能只判断“有没有命中”,还要判断“命中得好不好”。

在实际业务里,通常会给不同字段设置不同权重:

  • 标题命中更重要
  • 摘要命中其次
  • 正文命中权重低一点

比如:

{
  "multi_match": {
    "query": "OpenAI policy",
    "fields": ["title^3", "summary^2", "content"]
  }
}

这里的意思就是:

  • title^3:标题权重更高
  • summary^2:摘要次之
  • content:正文正常权重

这样结果会更符合用户预期。

2.3 解决组合筛选问题

很多搜索不是只输入一个关键词。

真实业务里,用户通常会同时加很多条件:

  • 只看英文资讯
  • 只看某几个来源
  • 只看最近 30 天
  • 只看某个分类
  • 按发布时间倒序

这时候 ES / OpenSearch 不只是做全文检索,还要做结构化筛选。

典型查询会长这样:

{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "OpenAI policy",
            "fields": ["title^3", "summary^2", "content"]
          }
        }
      ],
      "filter": [
        { "term": { "language": "en" } },
        { "terms": { "source": ["Reuters", "TechCrunch"] } },
        { "range": { "published_at": { "gte": "now-30d" } } }
      ]
    }
  }
}

这里要注意一个很重要的点:

  • 关键词搜索放在 must
  • 语言、来源、时间这些筛选条件放在 filter

因为关键词搜索需要算相关性,而语言、来源、时间只是判断是否满足条件,不需要参与打分。

2.4 解决聚合分析问题

很多人一开始以为 ES / OpenSearch 只是搜索工具,其实它还有很强的分析能力。

比如在资讯平台里,用户搜了一个关键词之后,页面除了展示文章列表,还可以展示:

  • 各来源分别有多少篇
  • 各分类分别有多少篇
  • 最近 7 天每天有多少篇
  • 哪些搜索词无结果
  • 哪些分类点击率更高

这些都可以通过聚合来做。

比如统计不同来源的数量:

{
  "size": 0,
  "aggs": {
    "source_count": {
      "terms": {
        "field": "source"
      }
    }
  }
}

这类能力对内容平台、日志分析、BI、埋点分析都很有用。

所以我理解 ES / OpenSearch 时,不会只把它当成“搜索框后面的组件”,而是把它当成一个检索与分析层。

3. 为什么不是继续用 MySQL

MySQL 很重要,但它不是万能的。

在业务系统里,我更倾向于这样分工:

主要职责
MySQL源数据、事务、强一致更新
ES / OpenSearch全文搜索、筛选、排序、聚合分析
Redis缓存、计数、限流、热点数据
MQ / Celery异步同步、削峰、任务流转

MySQL 的优势是事务和结构化数据管理。

但如果让它同时承担:

  • 大文本全文检索
  • 复杂相关性排序
  • 多字段搜索
  • 大量聚合分析
  • 高并发搜索请求

系统会越来越难维护。

所以引入 ES / OpenSearch 的核心原因不是“它更高级”,而是:

让不同系统做自己擅长的事。

MySQL 继续做源数据层,ES / OpenSearch 做检索和分析层,这样系统边界更清楚,后面优化也更有方向。

4. ES / OpenSearch 的核心实现主线

我理解 ES / OpenSearch 时,会抓这几件事:

  1. 倒排索引
  2. mapping 设计
  3. textkeyword
  4. queryfilter
  5. 排序、分页、聚合
  6. reindex 和 alias

这些比单纯背 DSL 更重要。

5. 倒排索引:为什么它适合搜索

倒排索引是 ES / OpenSearch 的基础。

普通数据库更多是围绕“记录”组织数据,而倒排索引是围绕“词”组织数据。

比如用户搜索一个关键词,ES 不需要从第一篇文章扫到最后一篇,而是直接通过关键词找到对应文档。

这也是为什么 ES / OpenSearch 很适合:

  • 文章搜索
  • 商品搜索
  • 日志检索
  • 工单搜索
  • 知识库检索
  • RAG 检索召回

不过倒排索引也带来一个点:

写入后不是绝对实时可见,而是近实时。

也就是说,文档写入之后,通常需要经过 refresh 才能被搜索到。

这点在业务设计时要注意。

如果是订单支付这种强一致场景,不能完全依赖 ES;但如果是资讯搜索、日志检索、知识库检索,近实时一般可以接受。

6. mapping 设计:搜索系统的第一步

很多 ES / OpenSearch 的问题,不是查询写得不好,而是 mapping 一开始就设计错了。

mapping 可以理解成 ES 里的“字段设计”。

它决定了:

  • 字段是什么类型
  • 是否分词
  • 能不能做精确匹配
  • 能不能排序
  • 能不能聚合
  • 查询时怎么匹配

所以我的理解是:

mapping 决定上限,DSL 只是发挥这个上限。

如果字段类型错了,后面查询怎么调都会很别扭。

7. text 和 keyword:最容易混,也最重要

这是 ES / OpenSearch 里最基础、也最容易踩坑的点。

7.1 text 适合全文检索

text 字段会被分词,适合:

  • 标题
  • 摘要
  • 正文
  • 描述
  • 评论内容

比如:

"title": {
  "type": "text"
}

用户搜索一个词时,ES 会根据分词后的结果去匹配文档。

7.2 keyword 适合精确匹配、排序和聚合

keyword 不会被分词,适合:

  • ID
  • 状态
  • 分类
  • 标签
  • 来源
  • 语言
  • 用户名
  • 邮箱
  • 业务编码

比如:

"source": {
  "type": "keyword"
}

这样就可以做:

  • 精确筛选
  • 排序
  • 聚合统计

比如按来源统计数量,source 就应该是 keyword

8. 一个字段有时需要 text + keyword

实际业务里,一个字段可能既要全文搜索,又要精确匹配。

比如资讯标题 title

  • 用户搜索时,希望标题参与全文检索
  • 但有时也希望按完整标题去重、聚合或排序

这时可以用 multi-fields:

"title": {
  "type": "text",
  "fields": {
    "raw": {
      "type": "keyword"
    }
  }
}

这样:

  • title 用来全文搜索
  • title.raw 用来精确匹配、排序或聚合

这类设计在搜索系统里很常见。

9. query 和 filter:一个管相关性,一个管筛选

这是查询调优里非常重要的一点。

我自己的理解是:

  • query 关心:这个文档和搜索词有多相关
  • filter 关心:这个文档是否满足条件

比如用户搜索:

OpenAI policy

这个关键词匹配需要算相关性,所以放在 query。

但下面这些条件不需要算相关性:

  • language = en
  • category = policy
  • published_at >= 最近 30 天
  • source in Reuters / TechCrunch

这些条件只需要判断满足不满足,所以更适合放 filter。

错误写法是把所有条件都丢进 query,导致每个条件都参与打分,结果查询复杂,解释也混乱。

更合理的写法是:

{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "OpenAI policy",
            "fields": ["title^3", "summary^2", "content"]
          }
        }
      ],
      "filter": [
        { "term": { "language": "en" } },
        { "term": { "category": "policy" } },
        { "range": { "published_at": { "gte": "now-30d" } } }
      ]
    }
  }
}

这个设计思路很重要。

面试里可以直接说:

关键词检索我放在 query 里做相关性计算,状态、分类、时间这类结构化条件放在 filter 里做筛选,避免无意义打分。

这句话非常实用。

10. 排序:不只是按时间倒序

很多业务搜索结果不是简单按时间排序。

常见排序方式有几种:

10.1 按相关性排序

默认搜索通常会按 _score 排。

这适合用户主动搜索关键词的场景。

10.2 按时间排序

资讯、日志、动态类场景经常需要按发布时间排序。

"sort": [
  { "published_at": "desc" }
]

10.3 相关性 + 时间排序

实际业务里,更常见的是二者结合:

"sort": [
  "_score",
  { "published_at": "desc" }
]

这样既考虑搜索相关性,也考虑内容新鲜度。

10.4 按热度排序

比如点击量、浏览量、收藏量、转发量,可以形成一个 hot_score

但热度排序要谨慎。

如果完全按热度,可能导致旧内容长期霸榜;如果完全按时间,可能导致质量不稳定。

所以真实系统里经常会做综合排序:

  • 相关性
  • 发布时间
  • 热度
  • 来源权重
  • 内容质量分

这已经接近搜索排序策略了。

11. 分页:浅分页和深分页要分开

普通列表页一般用:

{
  "from": 0,
  "size": 20
}

第一页没问题,前几页也没问题。

但如果用户一直往后翻,比如第 1000 页,就会变慢。

原因是 ES 需要先找到前面大量结果,再跳过它们,最后返回当前页。

也就是说:

from 越大,跳过的结果越多,成本越高。

所以一般可以这样设计:

  • 普通列表浅分页:from + size
  • 深分页 / 无限滚动:search_after
  • 后台导出大量数据:用 scroll 或其他批处理方案

对于面试来说,记住一句就够了:

浅分页可以用 from/size,深分页不要硬翻,应该用 search_after 这类游标式方案。

12. 聚合:搜索系统里的分析能力

ES / OpenSearch 的聚合能力很适合做业务分析。

比如资讯平台里可以做:

  • 热门来源
  • 热门分类
  • 每日资讯数量趋势
  • 搜索结果分布
  • 用户搜索无结果率
  • 不同分类点击率

比如按分类聚合:

{
  "size": 0,
  "aggs": {
    "category_count": {
      "terms": {
        "field": "category"
      }
    }
  }
}

这里 size: 0 表示不返回具体文档,只返回聚合结果。

这类查询很适合用于:

  • 数据看板
  • BI 分析
  • 埋点指标统计
  • 内容运营分析

不过聚合也不是越多越好。

聚合通常比普通查询更消耗 CPU 和内存,所以要围绕业务真正需要的指标设计,不要为了炫技堆很多聚合。

13. 索引升级:reindex + alias

ES / OpenSearch 有一个很重要的工程问题:

mapping 设计错了怎么办?

比如一开始把 source 建成了 text,后来发现它应该是 keyword,因为要做精确筛选和聚合。

这时通常不能简单原地改字段类型。

更常见的做法是:

  1. 新建一个索引,比如 news_v2
  2. 在新索引里设计正确的 mapping
  3. 把旧索引 news_v1 的数据 reindex 到新索引
  4. 用 alias 把线上查询从 news_v1 切到 news_v2

大概是这样:

news_v1  -> 旧索引
news_v2  -> 新索引
news     -> alias,对外统一访问

应用层只访问 news 这个 alias。

切换时把 alias 从 news_v1 指向 news_v2

这样好处是:

  • 应用层不用改索引名
  • 可以提前构建新索引
  • 切换过程更平滑
  • 出问题时也更容易回滚

这就是从“会用 ES”到“能线上落地”的区别。

14. 做 ES / OpenSearch 最容易踩的坑

14.1 把所有字符串都建成 text

这是新手很容易犯的错。

比如:

"source": {
  "type": "text"
}

如果 source 是新闻来源,比如 Reuters、TechCrunch,它更适合是 keyword

否则后面做精确筛选、排序、聚合都会很麻烦。

14.2 把所有条件都写进 query

很多筛选条件不需要相关性评分。

比如:

  • 状态
  • 分类
  • 来源
  • 语言
  • 时间范围

这些更适合放到 filter。

否则会让查询逻辑变复杂,也可能影响性能。

14.3 过度依赖 dynamic mapping

dynamic mapping 很方便,字段来了自动推断类型。

但线上系统里,如果完全依赖它,容易出现:

  • 字段类型不符合预期
  • 字段数量失控
  • 垃圾字段进入索引
  • 查询和聚合不稳定

所以核心索引最好提前设计 mapping。

14.4 深分页一直用 from/size

小页数没问题,大页数会越来越慢。

如果业务上需要无限滚动、深翻页,应该考虑 search_after

14.5 只关注“能搜到”,不关注“搜得准”

很多搜索系统上线后,最常见的问题不是查不出来,而是结果排序不合理。

比如:

  • 标题命中和正文命中权重一样
  • 新内容和旧内容没有区分
  • 高质量来源和低质量来源没有区分
  • 搜索词和业务标签没有结合

所以搜索系统不是“写个 match 就完事”,还要考虑字段权重、排序策略和业务目标。

14.6 mapping 设计错了还想硬改

字段类型一旦设计错,后面会很麻烦。

所以更推荐标准流程:

新建索引 -> reindex -> alias 切换

这也是线上系统更稳妥的做法。

15. 搜索系统怎么优化

15.1 先优化索引设计,再优化 DSL

很多时候查询慢,不是因为 DSL 写得不够花,而是字段设计错了。

比如:

  • 应该是 keyword 的字段建成了 text
  • 需要排序聚合的字段没有设计好
  • 字段过多,mapping 失控
  • 没有区分全文字段和结构化字段

所以第一步应该先看 mapping。

15.2 query 和 filter 分工清楚

搜索词走 query,结构化条件走 filter。

比如:

  • title / summary / content:走全文 query
  • source / category / language / time:走 filter

这能让查询逻辑更清楚,也避免无意义打分。

15.3 字段权重要符合业务直觉

一般来说:

  • 标题命中 > 摘要命中 > 正文命中
  • 新内容在资讯场景里通常更重要
  • 权威来源可能需要更高权重
  • 热度可以参与排序,但不能完全支配排序

搜索排序要服务业务,不只是技术问题。

15.4 分页方式要和场景匹配

普通列表页:

from + size

无限滚动或深分页:

search_after

大批量导出:

批处理方案

不同场景不要用同一套分页方式硬扛。

15.5 聚合围绕指标设计

聚合不是越多越好。

应该围绕业务指标设计,比如:

  • PV / UV
  • 点击率
  • 无结果率
  • 热门分类
  • 热门来源
  • 搜索词分布

这些指标能反过来帮助产品和运营判断:

  • 用户在搜什么
  • 哪些内容更受欢迎
  • 哪些搜索没有满足用户需求
  • 哪些分类需要补充内容

16. 面试里可以怎么讲

如果面试官问:

你项目里为什么用 ES / OpenSearch?

可以这样答:

在资讯聚合项目里,MySQL 主要负责源数据存储和业务更新,但用户搜索场景里需要全文检索、来源/分类/语言/时间筛选、相关性排序和聚合分析,这些不是 MySQL 最擅长的。所以我会把 ES / OpenSearch 作为单独的检索与分析层。索引设计上,标题、摘要、正文用 text,来源、分类、语言用 keyword,发布时间用 date;查询时关键词走 query,结构化条件走 filter;结果页除了返回列表,还可以返回来源和分类的聚合结果。

如果继续追问:

query 和 filter 为什么要分开?

可以答:

query 负责相关性,比如关键词在标题、摘要、正文里的匹配程度;filter 负责条件筛选,比如语言、分类、时间范围。这些条件不需要参与打分,放 filter 里更清晰,也能避免无意义的相关性计算。

如果再追问:

深分页怎么处理?

可以答:

普通列表页可以用 from/size,但深分页会越来越慢,因为需要跳过大量前面的结果。更深的翻页或者无限滚动,我会改成 search_after 这类游标式分页方案。

如果追问:

mapping 设计错了怎么办?

可以答:

一般不会直接硬改旧字段类型,而是新建一个正确 mapping 的新索引,通过 reindex 把数据迁过去,再用 alias 切换线上读写索引,这样对业务影响更小,也方便回滚。

17. 总结

学 ES / OpenSearch,最重要的不是一上来背很多 DSL。

我觉得更重要的是先建立这条主线:

业务为什么需要搜索和分析层?
哪些数据适合放进索引?
字段类型怎么设计?
查询里哪些条件要算相关性,哪些只是过滤?
排序、分页、聚合怎么支撑产品体验?
索引变更怎么平滑升级?

把这些问题想清楚之后,ES / OpenSearch 就不是一个孤立的中间件知识点,而是一套可以挂到真实项目里的工程能力。

对内容平台来说,它支撑的是搜索体验和内容分析。

对 BI 系统来说,它可以补充搜索式数据发现和低延迟聚合能力。

对 AI 应用来说,它也可以作为 RAG / Agent 检索层的一部分,和向量检索、结构化过滤一起配合。

所以我最后对它的理解是:

ES / OpenSearch 不是替代数据库,而是在业务系统中承担检索与分析职责。真正的重点不是会写几个查询语法,而是能不能从业务目标出发,设计好索引、查询、分页、聚合和升级方案。