[TIL] Nest.js에서 ElasticSearch로 검색기능 업그레이드하기

Cherry Jin·2024년 2월 9일
2

sparta_내배캠

목록 보기
50/53
post-thumbnail

아래 내용은 엘라스틱서치 적용 후 개인적인 이해를 돕기 위해 작성한 글입니다. 틀린 내용이 있다면 알려주세요.

설치 및 실행

엘라스틱서치는 독립적인 검색 서버로, 도커 컨테이너 또는 직접 설치한 서버 상에서 실행할 수 있다. 개발 및 테스트 단계에서 도커를 활용하면 설치와 실행이 간편하다. 배포할 때는 별도의 서버에 설치하는 것이 일반적이다.

  • 도커에 대해서는 아직 많은 공부가 필요하므로, 위 방법이 최고의 선택은 아닐 수 있다.

기술적 특성

엘라스틱서치는 NoSQL 형식의 데이터베이스로 분류될 수 있다. JSON 형식의 문서를 인덱스에 저장하여 빠른 검색과 분석을 가능하게 한다. 각 문서는 고유한 ID로 식별되며, 복잡한 검색 쿼리를 통해 실시간으로 데이터를 조회할 수 있다.

엘라스틱서치 인덱싱 필요성

엘라스틱서치에서 데이터를 효율적으로 검색하고 분석하기 위해서는 인덱싱이 필수적이다. 인덱싱은 데이터를 엘라스틱서치의 구조에 맞게 변환하고 저장하는 과정으로, 이를 통해 빠른 검색 속도와 높은 성능을 달성할 수 있다. 데이터가 인덱싱되지 않은 상태에서는 원하는 정보를 신속하게 검색하거나 분석하는 것이 어렵다. 따라서, 엘라스틱서치를 사용하여 대량의 데이터를 관리하고 이를 기반으로 빠른 검색과 정확한 분석 결과를 얻고자 한다면, 적절한 인덱싱 전략을 수립하고 실행하는 것이 중요하다.

인덱싱

초기 인덱싱 이후, 추가되는 데이터에 대한 인덱싱에는 크게 두 가지 방법이 있다. 첫 번째 방법은 데이터가 생성되거나 변경될 때마다 실시간으로 인덱싱을 수행하는 것이다. 이 방법은 데이터의 변경 사항을 즉각적으로 반영할 수 있어 정보의 최신성을 유지하는 데 유리하다. 두 번째 방법은 주기적으로 데이터를 검사하여 변경 사항을 인덱싱하는 것이다. 이 방법은 시스템의 부하를 균등하게 분산시키고, 인덱싱 작업을 보다 체계적으로 관리할 수 있게 해준다. 데이터의 특성과 애플리케이션의 요구 사항에 따라 적절한 인덱싱 전략을 선택하는 것이 중요하다.

Nest.js적용과정

위 내용을 따라 내가 했던 과정은 다음과 같다.

참고 레포지토리
튜터님이 도움 주신 가이드 브랜치

1. 도커, 혹은 배포서비스에서 엘라스틱 서치 설치

  • 해당 과정은 튜터님의 도움을 받아 컨테이너를 생성했다. 이 과정은 추가 공부가 필요하다.
  • 파이널 프로젝트에 사용될 엘라스틱서치는 클라우드타입으로 배포 예정(배포된 주소를 env에 넣으면 될것!)

2. .env 파일에 다음과 같은 변수를 추가 - 로컬에서 실행시 보통 9200포트가 사용된다.

# .env 파일
ELASTICSEARCH_NODE= "http://localhost:9200"

3. Nest.js 프로젝트 내에 엘라스틱서치 인스톨 후 모듈, 서비스, 컨트롤러 생성

# 터미널에서
npm install @elastic/elasticsearch

nest g module elasticsearch
nest g service elasticsearch
nest g controller elasticsearch

4. 생성된 elasticsearch.service.ts 세팅

//elasticsearch.service.ts

import { Injectable } from '@nestjs/common';
import { ElasticsearchService } from '@nestjs/elasticsearch';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Article } from 'src/article/entities/article.entity';

@Injectable()
export class SearchService {
constructor(
	@InjectRepository(Article)
	private readonly articleRepository: Repository<Article>,
	private readonly esService: ElasticsearchService,
	) {}

튜터님이 제공해주신 깃허브 레포지토리를 참고하여 상단에 임포트 해오는 ElasticsearchService와 중복을 피하기 위해 classSearchService로 바꾸었다.

5. elasticsearch.module.ts 세팅

//elasticsearch.module.ts

import { Module } from '@nestjs/common';
import { ElasticsearchModule } from '@nestjs/elasticsearch';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SearchService } from './elasticsearch.service';
import { SearchController } from './elasticsearch.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Article } from 'src/article/entities/article.entity';

@Module({
imports: [
	ElasticsearchModule.registerAsync({
		imports: [ConfigModule],
		useFactory: async (configService: ConfigService) => ({
			node: configService.get<string>('ELASTICSEARCH_NODE'),
			}),
		inject: [ConfigService],
		}),
	TypeOrmModule.forFeature([Article]),
	],
	providers: [SearchService],
	controllers: [SearchController],
})

export class SearchModule {}

프로젝트 내에서 이미 ConfigService를 통해 .env의 환경변수들을 사용하고 있었기 때문에 위와 같이 불러왔다.
imports부분의 ElasticsearchModule@nestjs/elasticsearch에서 불러오도록 하고, providers, controllers, export 의 이름은 다 바꾸어주었다.

  • app.module.ts 에 주입되는 모듈 이름도 변경해준다. SearchModule

6. 기능 구현을 테스트하기 위해 검색될 데이터들을 인덱싱 해주어야한다.

  • article 을 3개 포스팅 해준 뒤 데이터의 id, title, writer 세가지만 인덱싱했다.
//elasticsearch.service.ts

// 데이터를 인덱스(엘라스틱서치 형태로 주입)
  async indexData(indexName: string, data: any) {
    return await this.esService.index({
      index: indexName,
      body: data,
    });
  }

// 인덱스(테이블)를 생성
  async createIndex(indexName: string) {
    const { body: indexExists } = await this.esService.indices.exists({ index: indexName });
    if (!indexExists) {
      await this.esService.indices.create({ index: indexName });
    }
  }

//인덱스(테이블)를 삭제
  async deleteIndex(indexName: string) {
    const { body: indexExists } = await this.esService.indices.exists({ index: indexName });
    if (indexExists) {
      await this.esService.indices.delete({ index: indexName });
    }
  }

//기존에 있는 인덱스(테이블)를 삭제하고 새로 생성한 뒤 데이터를 넣는다.
  async indexAllArticle() {
    await this.deleteIndex('articles');
    await this.createIndex('articles');
    const articles = await this.articleRepository.find();
    const indexPromises = articles.map((article) => {
      const placeToIndex = {
        id: article.id,
        title: article.articleTitle,
        writer: article.user,
      };
      return this.indexData('articles', placeToIndex);
    });
    return Promise.all(indexPromises);
  }
  • 인덱싱은 데이터를 엘라스틱서치의 구조에 맞게 변환하고 저장하는 과정이다. 인덱싱을 위한 service의 기능을 만들었다면 컨트롤러에서 실행할 수 있는 세팅을 한다.
//elasticsearch.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { SearchService } from './elasticsearch.service';

@Controller('api/es')
export class SearchController {
constructor(private readonly searchService: SearchService) {}

@Get('/article')
async indexing() {
	const result = await this.searchService.indexAllArticle();
	console.log('인덱싱');
	return { message: '인덱싱함' };
	}
}
  • 해당 경로로 들어가서 인덱싱 되었는지 확인한다.

7. 검색기능 테스트

- 검색기능을 구현하기 위해 `elasticsearch.service.ts`에 검색기능을 만들어준다.
//elasticsearch.service.ts
  async search(index: string, query: any) {
    const hits = await this.esService.search({
      index,
      body: query,
    });
    const result = hits.body.hits.hits.map((hit) => ({
      id: hit._id,
      ...hit._source,
    }));
    return result;
  }

컨트롤러에서 사용할 수 있도록 세팅한다.

//elasticsearch.controller.ts
@Controller('api/es')
export class SearchController {
	constructor(private readonly searchService: SearchService) {}
	
  //최종 경로 http://localhost:3000/api/es/search?title=검색어
  @Get('search')
  async search(@Query('title') title: string) {
    const query = {
      query: {
        match: {
          title: {
            query: title,
            fuzziness: 1,
          },
        },
      },
    };
    return await this.searchService.search('articles', query);
  }

http://localhost:3000/api/es/search?title=검색어
이제 위 경로로 가서 검색어를 입력하면 인덱싱한 데이터가 확인된다.

배포 과정에서 생긴 문제들

  1. subscriber를 이용해 이벤트성 인덱싱을 하려고 했으나 모종의 오류로 서비스단에서 인덱싱을 진행했다.
  • 내가 저장한 id로 찾아서 수정/삭제하는게 아니라 엘라스틱서치의 index 내부 unique 아이디를 찾아 수정해야 했다. 이 부분은 다시 수정해서 섭스크라이버로 구현이 가능할 것 같다!
  1. 클라우드타입에서 배포한 엘라스틱서치와 연결하여 사용하려 했는데, 클라우드타입의 엘라스틱서치는 7 버전이어서 호환이 안되는 문제가 생겼다. npm install @elastic/elasticsearch 로 받았던 엘라스틱서치는 수정이 필요하다.
    • pakage.json 에서 다음 버전으로 변경
      "@elastic/elasticsearch": "^7.16.2",
profile
풀스택이 되버린 주니어 개발자

1개의 댓글

comment-user-thumbnail
2024년 2월 14일

선댓글 후감상입니다.

답글 달기