设计模式与搜索引擎:适配器集成搜索源、策略模式选择搜索算法、模板方法定义搜索流程
搜索引擎这东西,说起来大家都用过。但真要自己动手写一个聚合搜索系统,坑还是挺多的。我几年前做过一个企业内部的知识库搜索平台,当时要对接好几个不同的数据源——有 Elasticsearch、有数据库全文索引、还有第三方的 API 接口。每个源的查询方式都不一样,返回的数据结构也五花八门。
那时候我就想,能不能用设计模式把这套东西理清楚?后来还真让我找到了一个比较舒服的组合方式:适配器模式管接入,策略模式管算法,模板方法模式管流程。今天就跟大家聊聊这套打法。
一、适配器模式:统一搜索源的接入方式
先说说最头疼的问题——搜索源不统一。
你想想看,Elasticsearch 用的是 RESTful API,传 JSON 查询体;MySQL 的全文索引得拼 SQL 语句;第三方的搜索服务可能又要求 XML 格式。每个源都有自己的脾气,调用方要是直接跟它们打交道,代码里全是 if-else,维护起来简直要命。
适配器模式就是干这个的。它把不同搜索源的接口,统一成我们想要的「标准搜索接口」。
我举个例子。假设我们定义了一个标准接口:
// 统一搜索源接口
public interface SearchSource {
SearchResult search(SearchRequest request);
}
然后针对 Elasticsearch,我们写一个适配器:
public class ElasticsearchAdapter implements SearchSource {
private ElasticsearchClient client;
public ElasticsearchAdapter(ElasticsearchClient client) {
this.client = client;
}
@Override
public SearchResult search(SearchRequest request) {
// 将统一的 SearchRequest 转换成 ES 的查询 DSL
String esQuery = buildEsQuery(request);
String response = client.query(esQuery);
// 将 ES 返回的结果转换成统一的 SearchResult
return parseEsResponse(response);
}
private String buildEsQuery(SearchRequest request) {
// 构建 ES 查询 JSON
return "{ \"query\": { \"match\": { \"content\": \""
+ request.getKeyword() + "\" } } }";
}
private SearchResult parseEsResponse(String response) {
// 解析 ES 返回的 JSON,转换成统一格式
return new SearchResult(/* ... */);
}
}
同样,MySQL 全文索引也可以写一个适配器:
public class MysqlFulltextAdapter implements SearchSource {
private JdbcTemplate jdbcTemplate;
public MysqlFulltextAdapter(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public SearchResult search(SearchRequest request) {
String sql = "SELECT * FROM documents WHERE MATCH(content) AGAINST(? IN BOOLEAN MODE)";
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, request.getKeyword());
return convertToSearchResult(rows);
}
}
这样一来,调用方根本不用关心背后是 ES 还是 MySQL,它只需要对着 SearchSource 接口编程就行。这就是适配器模式的价值——让不兼容的东西变得兼容。
二、策略模式:灵活切换搜索算法
搜索源统一了,但搜索算法呢?
不同的场景可能需要不同的搜索策略。比如精确匹配、模糊搜索、语义搜索,甚至混合搜索。这些算法之间可以互相替换,而且未来还可能增加新的算法。
策略模式正好派上用场。它把算法封装成独立的策略类,让算法可以独立于使用它的客户端变化。
我们定义一个搜索算法接口:
public interface SearchStrategy {
List<SearchResult> execute(List<SearchSource> sources, SearchRequest request);
}
然后实现几种具体的策略:
// 精确匹配策略
public class ExactMatchStrategy implements SearchStrategy {
@Override
public List<SearchResult> execute(List<SearchSource> sources, SearchRequest request) {
List<SearchResult> results = new ArrayList<>();
for (SearchSource source : sources) {
SearchResult result = source.search(request);
// 精确匹配:只保留完全匹配的结果
result.getItems().removeIf(item -> !item.getTitle().equals(request.getKeyword()));
results.add(result);
}
return results;
}
}
// 模糊搜索策略
public class FuzzySearchStrategy implements SearchStrategy {
@Override
public List<SearchResult> execute(List<SearchSource> sources, SearchRequest request) {
List<SearchResult> results = new ArrayList<>();
for (SearchSource source : sources) {
// 模糊搜索:使用通配符或编辑距离
SearchRequest fuzzyRequest = new SearchRequest(request.getKeyword(), true);
results.add(source.search(fuzzyRequest));
}
return results;
}
}
// 混合搜索策略(加权合并多个源的结果)
public class HybridSearchStrategy implements SearchStrategy {
@Override
public List<SearchResult> execute(List<SearchSource> sources, SearchRequest request) {
List<SearchResult> allResults = new ArrayList<>();
for (SearchSource source : sources) {
allResults.add(source.search(request));
}
// 按权重合并、去重、排序
return mergeAndRank(allResults);
}
}
使用的时候,客户端只需要选择策略:
SearchStrategy strategy = new FuzzySearchStrategy();
List<SearchResult> results = strategy.execute(sources, request);
三、模板方法模式:固定搜索流程,允许子类定制
搜索源和搜索算法都搞定了,但整个搜索流程其实是有固定套路的:
- 接收请求
- 预处理(如分词、过滤敏感词)
- 选择搜索源
- 执行搜索
- 后处理(如排序、分页、高亮)
- 返回结果
这个流程的骨架是固定的,但某些步骤可以有不同的实现。比如预处理阶段,有的系统需要做中文分词,有的不需要。后处理阶段,有的要按时间排序,有的要按相关性排序。
模板方法模式就是干这个的——在父类中定义流程骨架,把可变的部分留给子类去实现。
public abstract class AbstractSearchEngine {
// 模板方法:定义搜索流程
public final SearchResponse search(SearchRequest request) {
// 1. 预处理
SearchRequest processedRequest = preProcess(request);
// 2. 选择搜索源
List<SearchSource> sources = selectSources(processedRequest);
// 3. 选择搜索策略
SearchStrategy strategy = selectStrategy(processedRequest);
// 4. 执行搜索
List<SearchResult> rawResults = strategy.execute(sources, processedRequest);
// 5. 后处理
SearchResponse response = postProcess(rawResults);
return response;
}
// 子类可覆盖的钩子方法
protected SearchRequest preProcess(SearchRequest request) {
// 默认实现:不做任何处理
return request;
}
protected abstract List<SearchSource> selectSources(SearchRequest request);
protected abstract SearchStrategy selectStrategy(SearchRequest request);
protected SearchResponse postProcess(List<SearchResult> results) {
// 默认实现:简单合并
return new SearchResponse(results);
}
}
然后我们可以实现具体的搜索引擎:
public class DocumentSearchEngine extends AbstractSearchEngine {
@Override
protected SearchRequest preProcess(SearchRequest request) {
// 文档搜索需要做中文分词
String segmented = ChineseSegmenter.segment(request.getKeyword());
return new SearchRequest(segmented, request.isFuzzy());
}
@Override
protected List<SearchSource> selectSources(SearchRequest request) {
// 文档搜索只查 ES 和数据库
return Arrays.asList(esSource, dbSource);
}
@Override
protected SearchStrategy selectStrategy(SearchRequest request) {
// 文档搜索用模糊匹配
return new FuzzySearchStrategy();
}
@Override
protected SearchResponse postProcess(List<SearchResult> results) {
// 按文档更新时间排序
results.sort((a, b) -> b.getUpdateTime().compareTo(a.getUpdateTime()));
return new SearchResponse(results);
}
}
你看,模板方法把流程固定了,但每个步骤都可以灵活定制。这就是它的魅力——复用骨架,定制细节。
四、三种模式如何协同工作
这三种模式不是孤立的,它们在实际系统中是协同工作的。我画了一张图来说明它们的关系:
从这张图可以看得很清楚:
- 模板方法在最上层,定义了「预处理 → 选源 → 选策略 → 执行 → 后处理」这个固定流程。
- 适配器模式在中间层,把各种异构的搜索源包装成统一的 SearchSource 接口。
- 策略模式也在中间层,封装了不同的搜索算法,让模板方法可以灵活选择。
三者的关系是:模板方法「用」适配器和策略,适配器「管」接入,策略「管」算法,各司其职。
五、避坑指南
最后分享几个我踩过的坑:
嗯,这套组合打法我后来在好几个项目里复用,效果都不错。设计模式这东西,单独看每个都挺简单,但组合起来用,才是真正的功力所在。