1. 搜索服务概述:搜索服务在微服务架构中的定位、Elasticsearch核心概念、NestJS与ES集成方案对比
1.1 搜索服务在微服务架构中的定位
在微服务架构中,搜索服务通常作为数据查询的专用枢纽,承担着跨服务、跨数据源的全文检索与聚合分析职责。与传统的单体应用不同,微服务架构下的数据被分散在多个独立的服务数据库中,直接跨库 JOIN 查询既不现实,也会破坏服务自治原则。搜索服务通过事件驱动或 CDC(Change Data Capture)机制,将各业务域的数据同步至 Elasticsearch 集群,对外提供统一的搜索入口。
核心职责:
- 数据汇聚:从订单、商品、用户等多个微服务同步结构化与非结构化数据。
- 索引管理:维护索引映射(Mapping)、分词器(Analyzer)与分片策略。
- 查询封装:对外暴露 RESTful / gRPC 接口,屏蔽 ES 查询 DSL 的复杂性。
- 高可用保障:通过集群部署与读写分离,确保搜索链路的 SLA。
下图展示了搜索服务在微服务架构中的典型位置:
架构建议:搜索服务应保持无状态,通过消息队列接收数据变更事件,避免与业务服务产生同步耦合。NestJS 的模块化与依赖注入特性非常适合构建这种事件驱动的搜索中间件。
1.2 Elasticsearch 核心概念
Elasticsearch 是一个基于 Lucene 的分布式搜索与分析引擎。理解以下核心概念是封装搜索服务的前提:
| 概念 | 说明 | 类比关系型数据库 |
|---|---|---|
| 索引(Index) | 存储具有相似特征的文档集合,是搜索的最小逻辑单元 | Database / Table |
| 映射(Mapping) | 定义索引中字段的数据类型、分词器、是否索引等规则 | Schema / DDL |
| 文档(Document) | JSON 格式的可被索引的基本数据单元 | Row |
| 分片(Shard) | 索引被水平拆分为多个分片,分布在集群的不同节点上 | 分区表 |
| 副本(Replica) | 每个分片的冗余拷贝,用于高可用与查询负载均衡 | 主从复制 |
| 分词器(Analyzer) | 将文本拆分为词项(Token)的组件,包含字符过滤器、分词器、Token 过滤器 | 全文索引的解析规则 |
| 查询 DSL | 基于 JSON 的查询语言,支持全文搜索、聚合、过滤、排序等 | SQL |
下图展示了 Elasticsearch 索引与分片的物理结构:
映射设计陷阱:ES 的 Mapping 一旦创建,字段类型不可修改(只能新增字段)。在生产环境中,务必在索引创建前通过 NestJS 的
@nestjs/elasticsearch 模块预先定义 Mapping,避免后期重建索引带来的数据迁移成本。
1.3 NestJS 与 Elasticsearch 集成方案对比
NestJS 生态中集成 Elasticsearch 主要有三种方案,各有适用场景:
| 方案 | 底层库 | 封装程度 | 适用场景 | 维护成本 |
|---|---|---|---|---|
| @nestjs/elasticsearch | 官方 @elastic/elasticsearch |
中等(提供 Module/Service 注入) | 标准 CRUD、简单搜索、需要官方最新特性 | 低 |
| @nestjs/bull + elasticsearch | Bull 队列 + 官方客户端 | 高(异步写入、队列缓冲) | 高并发写入、数据同步、CDC 场景 | 中 |
| 自定义 Repository 模式 | 官方客户端 + 自定义抽象层 | 高(完全控制 DSL 生成与映射管理) | 复杂查询 DSL、多索引聚合、定制化搜索服务 | 高 |
本课程选择:我们将采用自定义 Repository 模式,在
@nestjs/elasticsearch 的基础上构建 SearchRepository 抽象层。这样既能利用 NestJS 的模块化注入,又能完全掌控查询 DSL 的生成逻辑,适合生产级搜索服务的定制需求。
三种方案的代码结构对比:
// 方案一:@nestjs/elasticsearch 直接注入
@Injectable()
export class ProductSearchService {
constructor(
@Inject(ELASTICSEARCH_CLIENT)
private readonly esClient: ElasticsearchClient,
) {}
async search(keyword: string) {
return this.esClient.search({
index: 'products',
body: { query: { match: { name: keyword } } },
});
}
}
// 方案二:自定义 Repository 模式(本课程采用)
@Injectable()
export class ProductSearchRepository {
constructor(
@Inject(ELASTICSEARCH_CLIENT)
private readonly esClient: ElasticsearchClient,
) {}
async search(dto: SearchProductDto): Promise<SearchResult> {
const queryBody = this.buildQueryDSL(dto);
const response = await this.esClient.search({
index: 'products',
body: queryBody,
});
return this.mapResponse(response);
}
private buildQueryDSL(dto: SearchProductDto): Record<string, any> {
// 封装复杂的 bool / multi_match / filter 逻辑
return {
query: {
bool: {
must: dto.keyword
? [{ multi_match: { query: dto.keyword, fields: ['name', 'description'] } }]
: [{ match_all: {} }],
filter: dto.categoryId
? [{ term: { categoryId: dto.categoryId } }]
: [],
},
},
sort: [{ price: { order: dto.sortOrder || 'asc' } }],
};
}
private mapResponse(response: SearchResponse): SearchResult {
return {
total: response.hits.total.value,
items: response.hits.hits.map((hit) => ({
id: hit._id,
score: hit._score,
source: hit._source,
})),
};
}
}
选型建议:如果团队刚接触 ES,可以先从
@nestjs/elasticsearch 起步;当搜索逻辑变得复杂(多索引联合、聚合分析、自定义评分)时,再逐步迁移到自定义 Repository 模式。本课程直接以最终形态——自定义 Repository 模式——作为教学基线,让你一步到位掌握生产级封装技巧。