NestJS에서 초성 검색 기능 구현하기

controlZ·2023년 10월 16일
1
post-thumbnail

🐶 : 검색어 입력 시 초성만 입력해도 검색이 되게 가능한가여?

사소한 기능이지만, 초성만 입력해도 초성을 포함하는 단어들이 검색되면 그렇게 편할 수가 없다.

저번부터 하려다가 계속 안되가지고.. 반쯤 포기했다가(쭈글) 오늘 각잡고 무조건 구현하고야 만다는 마인드로 구현해보았다.

개발 환경
NestJS + TypeORM + MariaDB

Before

기존 코드

async findFruits(
    transactionManager: EntityManager,
    fruitName: string,
  ): Promise<Fruit[]> {
    return await transactionManager.findBy(Fruit, {
      fruitName: Like(`%${fruitName}%`),
    });
  }

기존 코드의 문제점
기존엔 TypeORM 문서에 나와있는 find 메서드의 여러 옵션 중 Like 연산자를 사용한 방법을 사용했다. 하지만 이 방법은 초성만을 입력받았을 때 초성이 포함된 단어를 DB에서 찾아오지 못했다. 예를들어 '바나나'를 검색하고 싶어서 'ㅂ' 혹은 'ㅂㄴㄴ'을 입력했다면, '바나나'가 아닌 빈 배열을 return했다. 위의 코드로 '바나나'를 찾고싶다면, 적어도 '바'를 입력해야만 했다.

After

위대한 Google에 초성 검색을 두드려보자 라이브러리 하나를 발견할 수 있었다.

바로 Hangul.js라는 라이브러리 !

Hangul.js
Hangul.js는 한글로 이루어진 문장의 자음과 모음을 분리하는 자바스크립트 라이브러리입니다. 자모 분리 또는 초성 검색에 사용할 수 있습니다.

라이브러리를 보며 어떻게 활용할 수 있을까 고민하던 중 눈에 들어온 부분이 있었다 👀 👀

Hangul.js 라이브러리에 있는 disassemble 메서드의 2번째 인자로 true를 전달하게 되면, 글자마자 독립된 배열을 만들 수 있다 ···
ex) 바나나 → [['ㅂ', 'ㅏ'], ['ㄴ', 'ㅏ'], ['ㄴ', 'ㅏ']]

이 메서드를 사용하면, 각 배열의 첫 번째 요소들이 초성에 해당하게 된다. 배열을 순회하며 각 글자의 초성들을 합쳐 하나의 문자열로 만들어준 다음, 이를 검색어에 있는 초성과 비교하면 될 것 같지 않은가 💡

이 아이디어를 그대로 코드로 작성해보았다.

역시나 문제가 발생했다. 절대 그냥 잘 되는 법이 없다.
(개발자 특 오히려 한번에 되면 이상해함)

🚨문제 상황🚨
검색어가 초성으로 이루어진 경우, DB에 있는 단어들이 잘 검색되었다. 하지만 초성 검색이 아닌 경우('바나나'의 '바'만 입력한 검색), 기존처럼 단어 중 일부 글자만 입력했을 때 검색이 안되었다.

이럴땐 당황하지 않고, 침착하게 잠시 또 생각을 해보면 된다.

보완
검색어가 초성인 경우와 초성이 아닌경우를 나눠, 초성 검색인 경우엔 위에 아이디어를 적용해주고. 초성 검색이 아닌 경우엔 기존의 코드를 적용시켜주면 된다.

검색어가 초성인지 아닌지를 어떻게 따지냐고 물을 수도 있을 것 같다. 나의 경우엔, Hangul.js에 있는 isConsonant 메서드를 사용해서 검색어 문자열이 모두 자음으로 이루어져있는지 확인했다.

최종 코드

async findFruitsWithChosung(
    fruitName: string,
  ): Promise<Fruit[]> {
    return await this.dataSource.transaction(
      async (entity_manager: EntityManager) => {
        
        if (!Hangeul.isConsonant(fruitName)) {
          return await this.fruitRepository.findFruits(
            entity_manager,
            fruitName,
          );
        }
        
        const fruitsList =
          await this.fruitRepository.findAllFruit(
            entity_manager,
          );

        const matchingFruits: Fruit[] = [];

        for (const fruit of fruitList) {
          const disassembledArray = Hangeul.disassemble(
            fruit.fruitName,
            true,
          );
          
          const chosung = disassembledArray.map((arr) => arr[0]).join('');

          if (chosung.includes(fruitName)) {
            matchingFruits.push(fruit);
          }
        }
        
        return matchingFruits;
      },
    );
  }

이제 초성만 입력해도 초성을 포함하는 단어 모두를 찾아올 수 있다 😉😉

0개의 댓글