Skip to content

[Startup MVP recipes #15] Nest.js Get Started with Elasticsearch – local environment pointers

Docker Compose

Tips: Don’t need dedicated network for elk. Just set it up so that nestjs service can connect to the hostname of elasticsearch

elasticsearch:
  image: elasticsearch:8.4.1
  container_name: elasticsearch
  command: ['elasticsearch', '-Elogger.level=ERROR']
  environment:
    - network.host=0.0.0.0
    - discovery.type=single-node
    - cluster.name=docker-cluster
    - node.name=cluster1-node1
    - xpack.license.self_generated.type=basic
    - xpack.security.enabled=false
    - 'ES_JAVA_OPTS=-Xms512m -Xmx512m'
  ulimits:
    memlock:
      soft: -1
      hard: -1
    nofile:
      soft: 65536
      hard: 65536
  cap_add:
    - IPC_LOCK
  volumes:
    - elasticsearch-data:/usr/share/elasticsearch/data
  ports:
    - 9200:9200
    - 9300:9300

kibana:
  image: docker.elastic.co/kibana/kibana:8.4.1
  container_name: kibana
  environment:
    ELASTICSEARCH_HOSTS: http://elasticsearch:9200
    SERVER_HOSTS: 0.0.0.0
  ports:
    - '5601:5601'
  depends_on:
    - elasticsearch

volumes:
  docker-nest-postgres:
  redis:
    driver: local
  elasticsearch-data:
    driver: local

TS Elasticsearch Client Options: (we disabled TLS, auth in local environment)

{
  node: process.env.ELASTIC_URL,
}

Search Module

Tips:

  • Initialize es client using custom provider. I didn’t use Nest.js’ ES module package because it adds one more dependency to the codebase.
import { Client, ClientOptions } from '@elastic/elasticsearch';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import elasticsearchConfig from 'src/config/elasticsearch.config';

import SearchService from './search.service';

@Module({
  imports: [ConfigModule.forFeature(elasticsearchConfig)],
  providers: [
    SearchService,
    {
      provide: 'ELASTICSEARCH_CLIENT',
      useFactory: async (configService: ConfigService) => {
        const client = new Client(
          configService.get<ClientOptions>('elasticsearch'),
        );
        return client;
      },
      inject: [ConfigService],
    },
  ],
  exports: [SearchService],
})
export default class SearchModule {}

Search Service

@Injectable()
export default class SearchService {
  constructor(
    @Inject('ELASTICSEARCH_CLIENT') private readonly client: Client,
  ) {}

  async index<T>(index: string, id: number, document: T) {
    return this.client.index({
      index,
      id: id.toString(),
      document,
    });
  }

  async update<T>(index: string, id: number, document: T) {
    return this.client.update({
      index,
      id: id.toString(),
      doc: document,
    });
  }

  async delete(index: string, id: number) {
    return this.client.delete({
      index,
      id: id.toString(),
    });
  }

  async search<T>(searchRequest: SearchRequest) {
    return this.client.search<T>(searchRequest);
  }
}

MikroOrm Subscriber and Indexing to ES

A MikroOrm Entity Subscriber that syncs indices to ES:

Tips:

  • Don’t use @Subscriber()
  • Because it conflicts with Nest.js DI
  • Manually register it in constructor
import {
  EntityManager,
  EntityName,
  EventArgs,
  EventSubscriber,
} from '@mikro-orm/core';
import { Injectable } from '@nestjs/common';
import SearchService from 'src/modules/search/search.service';

import TestSearchProduct from '../entities/test-search-product.entity';

@Injectable()
export default class TestSearchProductSubscriber
  implements EventSubscriber<TestSearchProduct>
{
  private readonly index = 'test-search-product';

  constructor(
    em: EntityManager,
    private readonly searchService: SearchService,
  ) {
    em.getEventManager().registerSubscriber(this);
  }

  getSubscribedEntities(): EntityName<TestSearchProduct>[] {
    return [TestSearchProduct];
  }

  async afterCreate(args: EventArgs<TestSearchProduct>): Promise<void> {
    this.searchService.index<TestSearchProduct>(
      this.index,
      args.entity.id,
      args.entity,
    );
  }

  async afterUpdate(args: EventArgs<TestSearchProduct>): Promise<void> {
    this.searchService.update<TestSearchProduct>(
      this.index,
      args.entity.id,
      args.entity,
    );
  }

  async afterDelete(args: EventArgs<TestSearchProduct>): Promise<void> {
    this.searchService.delete(this.index, args.entity.id);
  }
}

Usage of Search Service

Tips:

  • Use TS generics
  • need to convert Date() manually
async search(match: string) {
  const response = await this.searchService.search<TestSearchProduct>({
    index: 'test-search-product',
    query: {
      match: {
        textDescription: match,
      },
    },
  });
  return response.hits.hits.map(hit => {
    return {
      ...hit._source,
      createdAt: new Date(hit._source.createdAt),
      updatedAt: new Date(hit._source.updatedAt),
    };
  });
}

Now we enabled Full Text Search powered by elasticsearch

Leave a Reply

Your email address will not be published. Required fields are marked *