搜索系统设计
问题
如何设计一个全文搜索系统?
答案
架构概览
数据同步方案
| 方案 | 原理 | 延迟 | 适用场景 |
|---|---|---|---|
| 双写 | 代码中同时写 DB 和 ES | 无延迟 | 简单场景 |
| MQ 异步 | 写 DB 后发 MQ,消费者写 ES | 秒级 | 推荐方案 |
| Canal 监听 | 监听 MySQL binlog | 秒级 | 无侵入 |
| 定时任务 | 定期全量/增量同步 | 分钟级 | 兜底补偿 |
MQ 异步同步到 ES
// 商品更新后发送 MQ
public void updateProduct(Product product) {
productMapper.update(product);
rocketMQTemplate.convertAndSend("PRODUCT_SYNC", product.getId());
}
// 消费者同步到 ES
@RocketMQMessageListener(topic = "PRODUCT_SYNC")
public void syncToES(Long productId) {
Product product = productMapper.findById(productId);
ProductDoc doc = convertToDoc(product);
elasticsearchClient.index(i -> i
.index("products")
.id(String.valueOf(productId))
.document(doc)
);
}
搜索查询
ES 多条件查询 + 高亮
public SearchResult search(String keyword, int page, int size) {
SearchResponse<ProductDoc> response = elasticsearchClient.search(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.must(m -> m
.multiMatch(mm -> mm
.query(keyword)
.fields("title^3", "description") // title 权重 x3
)
)
.filter(f -> f.term(t -> t.field("status").value(1)))
)
)
.highlight(h -> h
.fields("title", hf -> hf.preTags("<em>").postTags("</em>"))
)
.from((page - 1) * size)
.size(size)
.sort(so -> so.score(sc -> sc.order(SortOrder.Desc))),
ProductDoc.class
);
return buildResult(response);
}
搜索优化
| 优化点 | 方案 |
|---|---|
| 分词 | IK 分词器(中文)、同义词词典 |
| 相关性 | boost 权重、function_score 自定义打分 |
| 性能 | 深分页用 search_after、提前终止 |
| 纠错 | suggest API、拼音分词 |
常见面试问题
Q1: ES 和 MySQL 数据不一致怎么办?
答案:
- MQ 异步同步保证最终一致性
- 定时任务全量校验补偿
- Canal 监听 binlog 无侵入同步
Q2: ES 深分页怎么处理?
答案:
from + size超过 10000 性能急剧下降- 用
search_after:基于上一页最后一条的排序值翻页 - 用
scroll(已废弃,改用 PIT + search_after)
Q3: 如何提升搜索相关性?
答案:
- 字段权重 boost(标题 > 描述)
function_score结合业务指标(销量、评分)- 同义词词典、停用词过滤
- 用户行为数据优化排序(点击率、转化率)