3. NestJS与Elasticsearch集成:模块安装、配置与Client注入

在NestJS生态中集成Elasticsearch最标准的方式是使用官方提供的@nestjs/elasticsearch模块。该模块基于官方的@elastic/elasticsearch客户端进行了NestJS风格的封装,提供了声明式注入、异步配置和生命周期管理能力。本章将完整演示从安装到基础Client注入的全流程。

3.1 依赖安装与版本对齐

安装前需确认NestJS版本与Elasticsearch服务版本的兼容性。以下为推荐版本对照表:

@nestjs/elasticsearch @elastic/elasticsearch Elasticsearch Server NestJS
^10.0.0 ^8.0.0 7.x / 8.x ^9.x / ^10.x
^9.0.0 ^7.17.0 7.x ^8.x / ^9.x
版本对齐建议: 推荐使用 @nestjs/elasticsearch@10 + @elastic/elasticsearch@8 组合,支持Elasticsearch 7.x和8.x,且兼容NestJS 9/10。避免混用大版本,否则可能出现序列化或连接协议不兼容问题。

执行安装命令:

npm install @nestjs/elasticsearch @elastic/elasticsearch
# 或使用 yarn
yarn add @nestjs/elasticsearch @elastic/elasticsearch

3.2 模块注册:同步与异步配置

ElasticsearchModule 支持两种注册方式:同步注册适用于配置固定的场景;异步注册适用于需要从ConfigService或外部配置中心动态读取参数的场景。

3.2.1 同步注册(forRoot)

// app.module.ts
import { Module } from '@nestjs/common';
import { ElasticsearchModule } from '@nestjs/elasticsearch';

@Module({
  imports: [
    ElasticsearchModule.register({
      node: 'http://localhost:9200',
      auth: {
        username: 'elastic',
        password: 'changeme',
      },
      maxRetries: 3,
      requestTimeout: 60000,
    }),
  ],
})
export class AppModule {}

3.2.2 异步注册(forRootAsync)

// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ElasticsearchModule } from '@nestjs/elasticsearch';

@Module({
  imports: [
    ConfigModule.forRoot(),
    ElasticsearchModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        node: configService.get('ES_NODE'),
        auth: {
          username: configService.get('ES_USERNAME'),
          password: configService.get('ES_PASSWORD'),
        },
        maxRetries: configService.get('ES_MAX_RETRIES', 3),
        requestTimeout: configService.get('ES_TIMEOUT', 60000),
      }),
    }),
  ],
})
export class AppModule {}
生产环境推荐: 始终使用 forRootAsync 方式,将Elasticsearch连接参数托管到环境变量或配置中心,避免硬编码敏感信息。同时建议开启 sniffOnStart: true 让客户端在启动时自动发现集群节点。

3.3 基础Client注入与使用

注册模块后,通过 @InjectElasticsearchClient() 装饰器或标准 @Inject() 注入 ElasticsearchService 实例。该实例封装了底层 Client 对象,可直接调用ES API。

// search.service.ts
import { Injectable } from '@nestjs/common';
import { ElasticsearchService } from '@nestjs/elasticsearch';

@Injectable()
export class SearchService {
  constructor(
    private readonly esService: ElasticsearchService,
  ) {}

  async ping(): Promise<boolean> {
    try {
      const result = await this.esService.ping();
      return result;
    } catch (error) {
      console.error('Elasticsearch cluster unreachable', error);
      return false;
    }
  }

  async searchIndex(index: string, query: object) {
    return this.esService.search({
      index,
      body: query,
    });
  }

  async indexDocument(index: string, id: string, document: Record<string, any>) {
    return this.esService.index({
      index,
      id,
      body: document,
      refresh: 'wait_for',
    });
  }
}
注意: ElasticsearchService 的 search、index、update 等方法返回的是 ApiResponse 对象,包含 body、statusCode、headers 等属性。在NestJS控制器中返回时,建议提取 body 字段以保持响应结构简洁。

3.4 连接架构与Client生命周期

下图展示了NestJS应用与Elasticsearch集群之间的连接架构,以及Client对象的生命周期管理:

NestJS Elasticsearch Client 连接架构与生命周期 NestJS Application AppModule (ElasticsearchModule.forRootAsync) SearchService (注入 ElasticsearchService) ElasticsearchService (Client 实例) 生命周期:onModuleInit → 连接池建立 onApplicationShutdown → 连接池关闭 Elasticsearch Cluster Node 1 (master) Node 2 (data) Node 3 (data) Node 4 (ingest) 索引分片:P0(主) / R0(副本) P1(主) / R1(副本) Client 自动嗅探节点列表 负载均衡到可用数据节点

3.5 多Client实例与连接隔离

在微服务架构或多租户场景中,可能需要连接多个Elasticsearch集群。此时可使用 ElasticsearchModule.forRoot 的 clientName 选项创建命名Client:

// multi-cluster.module.ts
import { Module } from '@nestjs/common';
import { ElasticsearchModule } from '@nestjs/elasticsearch';

@Module({
  imports: [
    ElasticsearchModule.register({
      clientName: 'logs-cluster',
      node: 'http://logs-es:9200',
    }),
    ElasticsearchModule.register({
      clientName: 'search-cluster',
      node: 'http://search-es:9200',
    }),
  ],
})
export class MultiClusterModule {}

注入时通过 @InjectElasticsearchClient('logs-cluster') 指定具体Client:

import { Injectable } from '@nestjs/common';
import { ElasticsearchService } from '@nestjs/elasticsearch';

@Injectable()
export class LogService {
  constructor(
    @InjectElasticsearchClient('logs-cluster')
    private readonly logsClient: ElasticsearchService,
  ) {}

  async writeLog(entry: any) {
    return this.logsClient.index({
      index: 'app-logs',
      body: entry,
    });
  }
}

3.6 连接健康检查与重试策略

建议在应用启动后执行一次连接健康检查,确保Elasticsearch集群可用。以下为推荐的健康检查实现:

// health.service.ts
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
import { ElasticsearchService } from '@nestjs/elasticsearch';

@Injectable()
export class HealthService implements OnModuleInit {
  private readonly logger = new Logger(HealthService.name);

  constructor(private readonly esService: ElasticsearchService) {}

  async onModuleInit() {
    await this.checkConnection();
  }

  async checkConnection(): Promise<boolean> {
    try {
      const info = await this.esService.info();
      this.logger.log(`Connected to Elasticsearch cluster: ${info.body.cluster_name}`);
      this.logger.log(`Version: ${info.body.version.number}`);
      return true;
    } catch (error) {
      this.logger.error('Failed to connect to Elasticsearch', error.message);
      // 可根据业务决定是否抛出异常阻止应用启动
      return false;
    }
  }
}
重试策略配置: 在 ElasticsearchModule.register 中可配置 maxRetries(请求重试次数)和 requestTimeout(请求超时毫秒数)。对于写入密集型场景,建议设置 maxRetries: 5 并开启 sniffOnConnectionFault: true,让客户端在连接故障时自动重新发现可用节点。

3.7 常见问题与避坑指南

  • 连接超时: 检查Elasticsearch服务是否启动,防火墙是否开放9200端口。使用 curl http://localhost:9200 验证。
  • 认证失败: 确认用户名密码正确,Elasticsearch 8.x 默认开启安全认证,需在 elasticsearch.yml 中配置或使用API Key。
  • 版本不兼容: @elastic/elasticsearch@8 默认使用HTTP/2协议,若Elasticsearch 7.x 未开启HTTP/2,需在配置中设置 protocol: 'http'。
  • Client未注入: 确保 ElasticsearchModule 在正确的模块中注册,且服务类所在的模块导入了该模块。
  • 连接池泄漏: 避免在每次请求中手动创建 Client 实例,始终使用NestJS注入的单例Client。