基本搜索系统搭建 —— CQRS 架构
需求背景
从最简单的需求开始:构建一个图书信息聚合和搜索系统。
用户侧是一个图书信息展示页面,支持多条件的复杂搜索。后端需要支持:名称模糊搜索、作者名模糊搜索、出版年份、出版社、图书类型、语言、国家、地区等多维度筛选。
需求不复杂,但"多条件复杂搜索"这几个字,已经决定了我们不能走最简单的路。
核心矛盾
最直觉的做法是直接用 MySQL 查询:加几个 WHERE 条件,写几个 LIKE,上线。
但这条路走不远。MySQL 的全文检索能力有限,模糊搜索性能差,多条件组合查询在数据量大时会变得很慢。更关键的是,随着业务发展,搜索维度只会越来越多,每次新增一个搜索条件都要改 SQL、加索引,维护成本会越来越高。
所以核心矛盾是:如何在支持复杂搜索的同时,保持系统的可扩展性?
方案设计
引入 Elasticsearch 作为查询引擎,MySQL 负责存储实体数据,ES 负责支持复杂查询。两者之间采用 CQRS 架构协同工作。
CQRS(Command Query Responsibility Segregation,命令查询责任分离)的核心思想是把写操作和读操作彻底拆开。写入走 MySQL,保证事务和数据一致性;查询走 ES,发挥全文检索和复杂条件筛选的优势。两条路互不干扰,各自优化。
这个架构的代价是引入了数据最终一致性——ES 的数据是从 MySQL 异步同步过来的,会有短暂延迟。但对于搜索场景来说,这个延迟完全可以接受。
| 维度 | 传统 CRUD | CQRS |
|---|---|---|
| 搜索性能 | 普通数据库查询 | ES 全文检索,10x+ 提升 |
| 复杂查询支持 | 有限 | 强大 |
| 写性能 | 受搜索影响 | 独立优化,互不影响 |
| 数据一致性 | 强一致 | 最终一致,可接受 |
读写流程
写操作:
- 直接写入 MySQL,支持事务
- 通过 CDC(Change Data Capture)异步将变更同步到 ES
- 允许 ES 和 MySQL 之间存在短暂的弱一致性延迟
读操作:
- 在 ES 中根据复杂查询条件查出实体 ID 列表
- 根据 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 模型的引入。