源码精读:SearchService — 6 种搜索模式的统一抽象
DefaultSearchService 是 Data Layer 中的全文搜索服务,基于 Quarkus 3.x 实现,底层使用 Doris OLAP 引擎的全文索引能力和 Redis 的 ZSET 结构。它支持 6 种搜索模式(BESTMATCH/EXACT/PREFIX/FUZZY/WILDCARD/REGEX),通过 SearchQueryBuilder 生成参数化 SQL,在多实体类型间并行搜索并合并结果。核心特性包括:并行多类型搜索(线程池 + CompletableFuture)、结果高亮(HighlightProcessor)、三种 Facet 聚合(Terms/Range/DateRange)、Redis 驱动的最近搜索和热门搜索追踪、稀疏结果时的模糊拼写建议,以及保存搜索的 CRUD。本文将深入分析其七步搜索流程、并行搜索的线程池设计、Redis ZSET 的最近/热门搜索实现、Facet 的三种分桶计算,以及自动拼写建议的回退策略。
源码精读:SearchService — 6 种搜索模式的统一抽象
“系列:S9 源码精读 · 第 8 篇 | 难度:高级 | 阅读时间:25 分钟
#TL;DR
DefaultSearchService 是 Data Layer 中的全文搜索服务,基于 Quarkus 3.x 实现,底层使用 Doris OLAP 引擎的全文索引能力和 Redis 的 ZSET 结构。它支持 6 种搜索模式(BEST_MATCH/EXACT/PREFIX/FUZZY/WILDCARD/REGEX),通过 SearchQueryBuilder 生成参数化 SQL,在多实体类型间并行搜索并合并结果。核心特性包括:并行多类型搜索(线程池 + CompletableFuture)、结果高亮(HighlightProcessor)、三种 Facet 聚合(Terms/Range/DateRange)、Redis 驱动的最近搜索和热门搜索追踪、稀疏结果时的模糊拼写建议,以及保存搜索的 CRUD。本文将深入分析其七步搜索流程、并行搜索的线程池设计、Redis ZSET 的最近/热门搜索实现、Facet 的三种分桶计算,以及自动拼写建议的回退策略。
#目录
- 整体架构与五大协作者
- 搜索主流程:searchInstances 七步法
- 并行多类型搜索:线程池 + CompletableFuture
- 6 种搜索模式
- 结果高亮:HighlightProcessor
- Facet 聚合:Terms / Range / DateRange
- Redis 最近搜索与热门搜索
- 自动补全建议:searchSuggest
- 拼写建议的模糊回退
- 保存搜索的 CRUD
- Key Takeaways
#1. 整体架构与五大协作者
@ApplicationScoped
public class DefaultSearchService implements SearchService {
private final DorisClient dorisClient;
private final RedisDataSource redisDataSource;
private final SavedSearchRepository savedSearchRepository;
private final HighlightProcessor highlightProcessor;
private final SearchQueryBuilder queryBuilder;
private final ExecutorService searchExecutor =
Executors.newFixedThreadPool(
Math.min(10, Runtime.getRuntime().availableProcessors() * 2));
}
| 协作者 | 职责 |
|---|---|
DorisClient | 执行参数化搜索 SQL |
RedisDataSource | 最近搜索/热门搜索的 ZSET 存储 |
SavedSearchRepository | 保存搜索的持久化 |
HighlightProcessor | 搜索结果名称字段高亮 |
SearchQueryBuilder | 根据搜索模式构建参数化 SQL |
#2. 搜索主流程:searchInstances 七步法
@Override
public SearchInstancesResponse searchInstances(SearchInstancesRequest request) {
long startMs = System.currentTimeMillis();
// 1. 确定搜索的实体类型
List<String> entityTypes = resolveEntityTypes(worldId, filter);
// 2. 并行执行多类型搜索,收集所有命中
List<CompletableFuture<List<SearchHit>>> futures = new ArrayList<>();
for (String entityType : entityTypes) {
futures.add(CompletableFuture.supplyAsync(() -> {
SearchQueryBuilder.BuildResult result = queryBuilder.build(
worldId, entityType, query, mode, filter, pageSize, pageToken);
List<Map<String, Object>> rows = dorisClient.executeQuery(
result.sql(), result.params().toArray());
return rows.stream().map(this::mapSearchHit).toList();
}, searchExecutor));
}
List<SearchHit> allHits = collectResults(futures);
// 3. 按分数降序排序,截断到页大小
allHits.sort(Comparator.comparingDouble(SearchHit::getScore).reversed());
if (allHits.size() > pageSize) allHits = allHits.subList(0, pageSize);
// 4. 高亮
if (request.getIncludeHighlights()) {
allHits = applyHighlights(allHits, query, preTag, postTag);
}
// 5. 计算总命中数和分页
long totalHits = computeTotalHits(worldId, entityTypes, query, mode, filter);
String nextPageToken = (currentOffset + pageSize < totalHits)
? String.valueOf(currentOffset + pageSize) : "";
// 6. 稀疏结果时的拼写建议
List<String> spellSuggestions = new ArrayList<>();
if (allHits.size() < 5 && mode != SearchMode.FUZZY) {
spellSuggestions = collectSpellSuggestions(worldId, entityTypes, query, filter);
}
// 7. Facet 聚合
List<FacetResult> facetResults = computeFacets(worldId, query, mode, filter,
request.getFacetsList());
return SearchInstancesResponse.newBuilder()
.addAllHits(allHits).setTotalHits(totalHits)
.setNextPageToken(nextPageToken)
.addAllSpellSuggestions(spellSuggestions)
.addAllFacetResults(facetResults)
.setTookMs(System.currentTimeMillis() - startMs)
.build();
}
#3. 并行多类型搜索:线程池 + CompletableFuture
private final ExecutorService searchExecutor =
Executors.newFixedThreadPool(
Math.min(10, Runtime.getRuntime().availableProcessors() * 2));
线程池大小:min(10, CPU * 2) 在高核心数机器上限制最大并行度为 10,避免对 Doris 产生过大连接压力。
@PreDestroy 优雅关闭:searchExecutor.shutdown() + 5 秒等待 + shutdownNow() 确保服务停止时不丢失进行中的搜索。
#4. 6 种搜索模式
| 模式 | SQL 策略 | 适用场景 |
|---|---|---|
| BEST_MATCH | 全文索引 + 相关性评分 | 默认搜索 |
| EXACT | = ? 精确匹配 | 精确查找 |
| PREFIX | LIKE 'query%' | 前缀搜索 |
| FUZZY | 编辑距离模糊匹配 | 容错搜索 |
| WILDCARD | LIKE '%query%' | 通配符搜索 |
| REGEX | 正则表达式匹配 | 高级搜索 |
默认模式为 BEST_MATCH(当请求未指定 SearchMode 时)。
#5. 结果高亮:HighlightProcessor
if (request.getIncludeHighlights() && query != null && !query.isBlank()) {
allHits = applyHighlights(allHits, query, preTag, postTag);
}
高亮处理器在 name 字段中标记匹配的查询词,使用可自定义的前后标签(如 <em> / </em>)。
#6. Facet 聚合:Terms / Range / DateRange
private FacetResult executeFacet(String worldId, String query, SearchMode mode,
SearchFilter filter, FacetRequest facetReq) {
FacetType type = facetReq.getType();
if (type == FacetType.FACET_TERMS) {
// GROUP BY field LIMIT size
} else if (type == FacetType.FACET_RANGE) {
// CASE WHEN field BETWEEN ... ranges
} else if (type == FacetType.FACET_DATE_RANGE) {
// DATE_FORMAT(field, granularity) GROUP BY
}
}
三种 Facet 类型:
| 类型 | 说明 | 用途 |
|---|---|---|
| FACET_TERMS | 按字段值分组计数 | 按类型、状态筛选 |
| FACET_RANGE | 按数值范围分桶 | 价格区间 |
| FACET_DATE_RANGE | 按日期粒度分桶 | 按年/月/周 |
#7. Redis 最近搜索与热门搜索
private static final String REDIS_RECENT_PREFIX = "search:recent:";
private static final String REDIS_POPULAR_PREFIX = "search:popular:";
private static final int MAX_RECENT_SIZE = 100;
- 最近搜索:
search:recent:{userId}ZSET,score = 时间戳,保留最近 100 条 - 热门搜索:
search:popular:{worldId}ZSET,score = 搜索次数
// 记录最近搜索
getSortedSetCommands().zadd(key, (double) System.currentTimeMillis(), query);
// 裁剪到最大大小
getSortedSetCommands().zremrangebyrank(key, 0L, -(MAX_RECENT_SIZE + 1));
Double-Check Locking 初始化:Redis SortedSet 命令句柄使用 volatile + 同步块的双重检查锁定模式延迟初始化。
#8. 自动补全建议:searchSuggest
@Override
public SearchSuggestResponse searchSuggest(SearchSuggestRequest request) {
List<Suggestion> suggestions = new ArrayList<>();
// 1. 实例建议(Doris 前缀搜索)
suggestions.add(Suggestion.newBuilder()
.setType(SuggestionType.INSTANCE).setScore(1.0)...);
// 2. 最近搜索关键字(Redis)
suggestions.add(Suggestion.newBuilder()
.setType(SuggestionType.KEYWORD).setScore(0.9)...);
// 3. 热门搜索关键字(Redis)
suggestions.add(Suggestion.newBuilder()
.setType(SuggestionType.KEYWORD).setScore(0.7)...);
}
三类建议按 score 排序:实例建议(1.0)> 最近关键字(0.9)> 热门关键字(0.7)。
#9. 拼写建议的模糊回退
if (allHits.size() < 5 && mode != SearchMode.FUZZY) {
spellSuggestions = collectSpellSuggestions(worldId, entityTypes, query, filter);
}
当搜索结果少于 5 条且当前模式不是 FUZZY 时,自动执行一次 FUZZY 搜索,提取前 3 个名称作为拼写建议——类似 Google 的"您是不是要搜索..."。
#10. 保存搜索的 CRUD
保存搜索功能允许用户命名和保存搜索查询以供后续复用,通过 SavedSearchRepository 持久化到数据库。
#11. Key Takeaways
- 并行多类型搜索:线程池 + CompletableFuture 并行查询多个实体类型,合并后按分数排序
- 6 种搜索模式:从精确匹配到正则表达式,覆盖全场景搜索需求
- Redis ZSET 追踪:最近搜索(per-user)和热门搜索(per-world)使用 ZSET 高效管理
- 三种 Facet:Terms/Range/DateRange 覆盖分类、数值和时间维度的聚合筛选
- 自动拼写建议:稀疏结果时自动回退到 FUZZY 搜索,提供"您是不是要搜索..."体验
- 结果高亮:支持自定义前后标签的关键词高亮
#下一篇
S9-09:MetricRegistryService — 指标计算策略路由。我们将深入指标注册服务,看它如何管理指标的注册、验证、策略推荐,以及与物化视图的自动联动。
Tags: #coomia-dip #source-code-reading #data-Layer #search #full-text #doris #redis #facets #autocomplete #spell-suggest