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