搜索标签系统专题01:基本搜索系统搭建 —— CQRS 架构

需求背景

从最简单的实现开始,构建一个图书信息聚合和搜索系统。从用户交互角度,前端是一个图书信息聚合和展示的页面,支持多条件的复杂搜索功能。从后端角度来说,需要支持名称模糊搜索、作者名模糊搜索、出版年份搜索、出版社、图书类型、语言、国家、地区等搜索。

问题现状

这是一个图书信息聚合搜索系统,需要支持复杂的搜索功能。传统的数据库查询无法满足全文检索、模糊搜索、多条件组合等需求。

方案

为了支持复杂条件查询的需求,我们需要在系统中接入 ES 这个查询引擎中间件,MySQL 负责存储实体数据,ES 负责支持复杂查询,数据模型以 MySQL 为基准。

接下来的问题就是如何设计 ES 和 MySQL 的协同架构,我们选择 CQRS 架构。

CQRS(命令查询责任分离,Command Query Responsibility Segregation),也可以简单理解为读写分离架构,是一种将数据写入(命令)和数据读取(查询)完全拆分的架构模式。通过分别优化读和写流程,CQRS 能有效提升系统的性能、可扩展性与稳定性。

写入流程可专注于数据一致性和事务,而查询流程则可根据业务需求进行检索和聚合优化,支持复杂的全文搜索、条件筛选。CQRS 广泛用于高并发、数据存储和搜索需求强烈的场景,避免传统 SQL 数据的 CRUD 架构因数据一致性与高性能复杂查询需求难兼容带来的瓶颈。

CQRS 对比 传统 CRUD

维度CRUDCQRS提升
搜索性能普通数据库查询ES 全文检索十分明显
复杂查询支持有限强大质的提升
写性能受搜索影响独立优化无影响
数据一致性强一致最终一致可接受

CQRS 流程设计

写操作

  • 写操作直接写入 MySQL,支持事务
  • 变化数据异步异构到 ES,这里可以采用简单的 CDC 实现。
  • 允许 ES 和 MySQL 之间的弱一致性延迟

读操作

  • 在 ES 中根据复杂查询条件查出实体 ID 列表
  • 根据实体 ID 列表从 MySQL 中反查出实体数据,所有数据以 MySQL 为基准。

数据模型设计

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 '图书简介' -- 非搜索项
)

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"
      }
    }
  }
}