基本搜索系统搭建 —— CQRS 架构

需求背景

从最简单的需求开始:构建一个图书信息聚合和搜索系统。

用户侧是一个图书信息展示页面,支持多条件的复杂搜索。后端需要支持:名称模糊搜索、作者名模糊搜索、出版年份、出版社、图书类型、语言、国家、地区等多维度筛选。

需求不复杂,但"多条件复杂搜索"这几个字,已经决定了我们不能走最简单的路。

核心矛盾

最直觉的做法是直接用 MySQL 查询:加几个 WHERE 条件,写几个 LIKE,上线。

但这条路走不远。MySQL 的全文检索能力有限,模糊搜索性能差,多条件组合查询在数据量大时会变得很慢。更关键的是,随着业务发展,搜索维度只会越来越多,每次新增一个搜索条件都要改 SQL、加索引,维护成本会越来越高。

所以核心矛盾是:如何在支持复杂搜索的同时,保持系统的可扩展性?

方案设计

引入 Elasticsearch 作为查询引擎,MySQL 负责存储实体数据,ES 负责支持复杂查询。两者之间采用 CQRS 架构协同工作。

CQRS(Command Query Responsibility Segregation,命令查询责任分离)的核心思想是把写操作和读操作彻底拆开。写入走 MySQL,保证事务和数据一致性;查询走 ES,发挥全文检索和复杂条件筛选的优势。两条路互不干扰,各自优化。

这个架构的代价是引入了数据最终一致性——ES 的数据是从 MySQL 异步同步过来的,会有短暂延迟。但对于搜索场景来说,这个延迟完全可以接受。

维度传统 CRUDCQRS
搜索性能普通数据库查询ES 全文检索,10x+ 提升
复杂查询支持有限强大
写性能受搜索影响独立优化,互不影响
数据一致性强一致最终一致,可接受

读写流程

写操作:

  1. 直接写入 MySQL,支持事务
  2. 通过 CDC(Change Data Capture)异步将变更同步到 ES
  3. 允许 ES 和 MySQL 之间存在短暂的弱一致性延迟

读操作:

  1. 在 ES 中根据复杂查询条件查出实体 ID 列表
  2. 根据 ID 列表从 MySQL 反查完整实体数据

注意第二步:所有数据以 MySQL 为基准,ES 只负责"找到谁",不负责"返回数据"。这样即使 ES 数据有延迟,最终展示给用户的数据也是准确的。

数据模型设计

MySQL 实体主表:

create table book_base (
  book_id       bigint primary key comment '图书ID',
  name          varchar(255) comment '图书名称',
  author        varchar(255) comment '图书作者',
  press         varchar(255) comment '出版社',
  publish_time  datetime comment '出版时间',
  type          int comment '图书类型',
  language      int comment '语言',
  country_id    int comment '国家',
  region_id     bigint comment '地区',
  introduction  text comment '图书简介' -- 非搜索项,不需要进 ES
)

Elasticsearch 文档结构:

{
  "mappings": {
    "properties": {
      "book_id":      { "type": "long" },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart",
        "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
      },
      "author": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart",
        "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
      },
      "press": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart",
        "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
      },
      "publish_time": { "type": "date", "format": "yyyy-MM-dd" },
      "type":         { "type": "integer" },
      "language":     { "type": "integer" },
      "country_id":   { "type": "integer" },
      "region_id":    { "type": "long" }
    }
  }
}

几个设计细节值得注意:introduction(图书简介)没有进 ES,因为它不是搜索项,没必要占用 ES 的存储和索引资源。文本字段同时配置了 text(用于全文检索)和 keyword(用于精确匹配和排序),这是 ES 中处理中文字段的常见做法。

这一步解决了什么

CQRS 架构搭建完成后,我们有了一个能支撑复杂搜索的基础系统。MySQL 和 ES 各司其职,写入有事务保障,查询有全文检索能力。

但这只是开始。随着业务发展,第一个问题很快就来了:有些搜索维度并不属于图书本身的属性,该怎么存?

这引出了下一篇:EAV 模型的引入。