실무에서 mongoDB slow 쿼리를 해결해보려고 했던 경험(2) - Elasticsearch

seunghoking.·2020년 10월 7일
1

mongodb

목록 보기
2/2

지난 글에서...

여러가지 상황에 직면

  1. mongoDB에서 정규식으로 찾아 검색 진행.

    → 정규식은 인덱스로 찾지 않고 전문을 검색해버림 굉장히 느림

  2. mongoDB의 text index를 통해 정규식이 아닌 $text, $search 쿼리를 사용하여 검색 진행.

    1) 1번에 비해 속도는 개선 되었으나 mongoDB의 text index는 정확히 검색한 단어와 일치하는 것(포함x)만 가져와 정확성이 떨어짐.

    2) 그래서 monitoringdata대신 keyword에서 찾는 방향으로 정확성을 높였다.

  3. 그러나 속도가 아직도 빠르지 않고, 정확성은 조금 떨어진다.

    Elastic search와 형태소 분석기를 통해 검색을 진행해보자.

개발 환경 구축

  • JDK 1.8
  • Elasticsearch 7.9.1 + kibana 7.9.1
  • yarn package의 @elastic/elasticsearch 사용

엘라스틱 서치 기본 세팅

DB

mongoDB의 monitoringData 컬렉션을 엘라스틱 서치의 monitoringdata 인덱스로 옮겨 테스트 하였음.

  • monitoringData → 119641 docs, 1.5GB

클러스터

우선 single mode (test mode) 로 구축하였음

single mode란 엘라스틱서치 노드 하나만으로 구성된 클러스터를 말한다.

실제 서비스를 운영할 때는 최소 3개 이상의 물리적 노드로 클러스터를 구축하는 것이 좋다고 한다.
왜?
→ 만약 하나의 노드이며, 모든 기능을 수행하는 모드인 Single node mode로 클러스터가 구성되어있다면 해당 노드가 장애가 발생할 경우 안정적인 클러스터 운영 유지가 되지 않을 것이다. (가용성이 떨어진다.)

그래서 최소 3개 이상의 물리적 노드를 두어야 가용성이 높아지고 하나의 마스터 노드가 장애가 발생할 시 다른 후보 마스터 노드에게 역할을 위임받아 안정적인 클러스터 운영 유지를 할 수 있을 것이다.

형태소 분석기

형태소 분석기는 우선 엘라스틱서치에서 기본적으로 제공해주는 nori-analyzer를 사용하였다.
→ plugin으로 바로 다운로드가 가능하기 때문에 사용하기가 편리함.
→ "decompound_mode" : "mixed" 를 사용하면 정확성이 높아진다.
복합 명사 같은 것들을 쪼갬, ex) 삼성전자 → [삼성, 전자, 삼성전자]

AND
1차적으로 노리 형태소 분석기를 사용한 이유

→ 노리 형태소 분석기는 루씬에 있는 일본어 형태소 분석기 프로젝트인 Kuromoji를 재활용 한 것인데, Kuromoji는 일찍이 루씬 프로젝트에 merge되어 여러 해에 걸쳐 메모리 사용량과 속도면에서 많은 최적화가 이루어졌기 때문에 nori또한 그렇다.

→ 엘라스틱 서치 실무 가이드 책에서는 노리가 기존 형태소 분석기에 비해 30%이상 빠르고 메모리 사용량도 현저하게 줄었으며, 시스템 전반에 영향을 주지 않게 최적화되었다 라고 쓰여져있다.

→ 노리 형태소 분석기는 이러한 Kuromoji의 기능에 MeCab-ko-dic 사전을 추가했다고 보면 되는데, 노리는 기존의 형태소 분석기(은전한닢, open-korean-text, 아리랑)와는 달리 바이너리 버전으로 사전을 압축하여 같은 데이터를 담고 있지만 보다 작고(디스크공간을 덜 차지), 검색에 최적화함.

→ 참고자료를 통해 성능과 메모리 사용량을 확인할 수 있는데 노리와 아리랑 형태소 분석기가 가장 빠르고 메모리 사용량 결과가 가장 좋음


나머지(노드의 개수, 샤드와 레플리카 샤드의 개수, refresh_interval 연장, _all 필드 제거) 는 엘라스틱서치 최적화에 대한 이해를 완료하고 추후에 진행해볼 예정이다.

현재는 Default로 진행하였다.

현재 할 수 있는 성능 향상을 위한 것들..

Static Mapping

엘라스틱서치는 스키마리스라는 특성에 따라 데이터에 대한 매핑을 자동으로 생성하는 편리한 기능을 제공한다. 기본적으로 text타입과 keyword타입을 동시에 제공하는 멀티 필드 기능으로 구성된다. 만약 특정 필드를 사용하지 않는 경우에는 데이터의 공간 낭비를 초래한다.

→ 따라서 static한 매핑을 주었다.

PUT /monitoringdata
{
  "settings" : {
    "number_of_shards": 2,
    "number_of_replicas": 1,
    "index" : {
      "analysis" : {
        "tokenizer" : {
          "nori_user_dict_tokenizer" :{
            "type" : "nori_tokenizer",
            "decompound_mode" : "mixed"
          }
        },
        "analyzer" : {
          "nori_token_analyzer" : {
            "type" : "custom",
            "tokenizer" : "nori_user_dict_tokenizer"
          }
        }
      }
    }
  },
  "mappings": {
    "properties": {
      // field는 가명
      "field": {"type": "keyword"},
      "field": {"type": "integer"},
      "field": {"type": "date"},
      "field": {"type": "boolean"},
      "field": {"type": "date"},
      "field": {
        "properties" : {
          "field": {"type": "keyword"},
          "field": {"type": "date"},
          "field": {"type": "text", "analyzer" : "nori_token_analyzer"},
          "field": {"type": "integer"},
        }
      }
    }
  }
}

Filter Query

  • must : bool must 절에 지정된 모든 쿼리가 일치하는 document를 조회
  • filter : must와 같이 filter 절에 지정된 모든 쿼리가 일치하는 document를 조회하지만, Filter context에서 실행되기 때문에 score를 무시함
query: {
          bool: {
            must : {
                multi_match: {
                  query: req.body.searchInput,
                    fields: [
                      "detailData.content",
                      "detailData.contentPlainText",
                      "detailData.postTitle",
                    ],
                },
            },
            filter: [
              {
                term: {
                  brandId: req.user.brandId,
                },
              },
              {
                terms: {
                  channelKeyname: req.body.channels,
                },
              },
              {
                range: {
                  "detailData.uploadedAt": {
                    gte: new Date(req.body.startAt).getTime(),
                    lte: new Date(req.body.endAt).getTime(),
                  },
                },
              },
              {
                bool: {
                  should: [
                    {
                      bool: {
                        filter: [
                          {
                            terms: {
                              AnalysisResult: req.body.analysisResult,
                            },
                          },
                          {
                            bool: {
                              must_not: {
                                exists: {
                                  field: "trainedResult",
                                },
                              },
                            },
                          },
                        ],
                      },
                    },
                    {
                      bool: {
                        filter: {
                          terms: {
                            trainedResult: req.body.analysisResult,
                          },
                        },
                      },
                    },
                  ],
                },
              },
            ],
          },
        },

→ 크게 보면 must와 filter 두개로 나뉘어 지는데, must에서 match 쿼리를 사용하여 형태소 분석을 할 수 있는 검색을 진행하고 나머지는 filter 쿼리를 사용한다.

→ filter쿼리는 해당 문서가 쿼리절과 일치합니까? 라는 질문에 응답하는데 대답은 true or false이다. _score 값을 계산하지 않으므로 쿼리 실행을 최적화 할 수 있다.

node에서 @elastic/elasticsearch를 통한 _search 쿼리 사용방법

import { Client } from "@elastic/elasticsearch";

const esClient = new Client({
  node: "http://localhost:9200",
})

const result = await esClient.search( {...} );

Debug log를 통한 Performance 비교

  1. MongoDB - text index로 검색할 시
    대략 한달 데이터 - 17000개 중 "맥도날드" 키워드 검색 시
    약 2초 정도 소요됨을 알 수 있다.

  2. ElasticSearch로 검색 시
    위와 동일한 조건에서 elasticsearch로 "맥도날드" 키워드 검색 시

약 0.1초 정도 소요됨을 알 수 있다.

2초 정도의 시간이 단축되었다.

앞으로..

위에서 잠깐 얘기했던 엘라스틱 서치의 최적화 방향
-> 클러스터 설계, 노드의 개수, 샤드와 레플리카 샤드의 개수, refresh_interval 연장, _all 필드 제거 등등의 엘라스틱서치 최적화에 대한 조사를 진행할 예정이다.

끝!

0개의 댓글