ElasticSearch를 프로젝트에 적용 해보자

장준혁·2024년 1월 28일

ElasticSearch

목록 보기
2/2
post-thumbnail

🔍 Elastic Search 를 프로젝트에 도입


Mere 프로젝트 진행중 웹 와이어프레임에 검색기능이 있음을 확인.

출시 목적으로 진행 하고 있는 프로젝트 이기에 사용자의 검색량이 증가 함에 따라 Mysql Sql Like 문을 사용할 시에 여러가지 문제점이 발생 할 것 이라 판단 하였다.

현재 진행중인 프로젝트 이므로 추가적으로 요구사항이 있을때 마다 계속 업데이트 하면서 수정 할 예정이다.

🤔 어떤 문제점이 발생할까?

  1. 정확도 문제 : 복잡한 텍스트 분석이나 유사도 계산 등의 기능을 제공하지 않는다.

  2. 단어의 일부를 검색하는 경우: 주로 전체 문자열 또는 문자열의 일부를 검색하는 데 사용,
    단어의 일부만을 검색하는 경우 원하지 않는 결과가 나올 수 있다.

  3. 확장성 문제: 단순히 문자열 일치만을 검사하기 때문에, 추후 검색 기능을 확장하고자 할 때 제약이 될 수 있다.

이 외에도 여러가지 태그를 검색시 관련 업종의 매장이 자동 검색 되게 유도하는 등 많은 기능이 논의 되고 있었다.

따라서 해당 문제점들을 해결하기 위해 Elastic , Logstash , Kibana 를 사용 하여 검색 기능을 구현 해보자.

🔧 환경 구성

해당 기능들을 구현하기 위해 Docker 를 사용하기로 결정.

Elastic , Logstash , Kibana 를 Docker 로 각각 컨테이너를 관리하며 테스트 해보니 여간 까다로운게 아니였다.

따라서 Docker Compose 를 사용, 각 컨테이너를 묶어서 통합으로 관리하며 테스트 하기로 결정.

  • 현재 디렉토리 및 파일 구성
  • Mere 프로젝트의 Stores , Tags 테이블 구조 일부


환경 설정에 큰 영향을 주는 파일들만 간략하게 설명 하겠다.

  • docker-compose.yml : Docker Compose의 설정 파일로서, 여러 개의 Docker 컨테이너들이 어떻게 작동하고 상호작용할지를 정의하며 이 파일을 사용하면 여러 개의 컨테이너를 한 번에 관리하고, 각 컨테이너간의 의존성을 쉽게 관리할 수 있다.

    Logstash 의 pipeline 디렉토리에 conf 등 많은 설정 파일이 포함되어있기 때문에 해당 부분 mount 가 올바르지 않다면 설정 에러가 계속 발생한다.

    설정 파일을 하나하나 mount 가능하지만 디렉토리 전체를 mount 해서 하위의 설정 파일이 자동으로 mount 되도록 했다.
  version: '3.2'

  services:

    elasticsearch:
      container_name: elasticsearch
      hostname: elasticsearch
      build:
        context: elasticsearch/
        args:
          ELK_VERSION: $ELK_VERSION
      volumes: # Docker Container 과 데이터를 공유 하기 위해 Mount 작업
        - type: bind
          source: ./elasticsearch/config/elasticsearch.yml
          target: /usr/share/elasticsearch/config/elasticsearch.yml
          read_only: true
        - type: volume
          source: elasticsearch
          target: /usr/share/elasticsearch/data
      ports:
        - "9200:9200"
        - "9300:9300"
      environment:
        ES_JAVA_OPTS: "-Xmx256m -Xms256m"
        discovery.type: single-node
      networks: # search 네트워크 공유
        - search

    logstash:
      container_name: logstash
      hostname: logstash
      build:
        context: logstash/
        args:
          ELK_VERSION: $ELK_VERSION
      volumes: # Docker Container 과 데이터를 공유 하기 위해 Mount 작업
        - type: bind
          source: ./logstash/config/logstash.yml
          target: /usr/share/logstash/config/logstash.yml
          read_only: false
        - type: bind
          source: ./logstash/pipeline
          target: /usr/share/logstash/pipeline
          read_only: false
        - type: bind
          source: ./logstash/config/pipelines.yml
          target: /usr/share/logstash/config/pipelines.yml
          read_only: false
      ports:
        - "5000:5000/tcp"
        - "5000:5000/udp"
        - "9600:9600"
      environment:
        LS_JAVA_OPTS: "-Xmx256m -Xms256m"
      networks: # search 네트워크 공유
        - search

    kibana:
      container_name: kibana
      hostname: kibana
      build:
        context: kibana/
        args:
          ELK_VERSION: $ELK_VERSION
      volumes:
        - type: bind
          source: ./kibana/config/kibana.yml
          target: /usr/share/kibana/config/kibana.yml
          read_only: false
      ports:
        - "5601:5601"
      depends_on:
        - elasticsearch
      networks: # search 네트워크 공유
        - search

      volumes:
        elasticsearch:


      networks: # Docker-Compose 파일에 정의된 Container 이 동일한 네트워크를 공유하도록 정의
        search:
          driver: bridge
  • store_index.conf : Logstash 와 Mysql 의 연동을 정의 하며 Mysql 의 Stores(매장) 테이블 정보를 입력 값 으로 입력받아 Elastic 에 출력 값 을 전달한다.
  input {
        tcp {
                port => 5001
        }
        
        jdbc {
                jdbc_driver_library => "/usr/share/logstash/pipeline/mysql-connector-java-8.0.15.jar"
                jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
                jdbc_connection_string => "jdbc:mysql://host.docker.internal:3306/(본인의 Mysql Database Name)?useSSL=false&serverTimezone=UTC"
                jdbc_user => "Mysql User 정보 입력"
                jdbc_password => "Mysql Password 정보 입력"	  
                schedule => "* * * * *"
                statement => "SELECT storeId , storeName , grade FROM stores WHERE updatedAt > :sql_last_value"
                tracking_column => "updatedAt"
                use_column_value => true
                last_run_metadata_path => "/usr/share/logstash/pipeline/last_store_update_time.yml"
                lowercase_column_names => false
        }
}

  output {
          stdout {
                  codec => rubydebug
          }
          elasticsearch {
                  hosts => "elasticsearch:9200"
                  index => "stores"
                  document_id => "%{storeId}"
                  doc_as_upsert => true  # 문서가 이미 존재하는 경우 업데이트하거나 새 문서로 작성할지 여부 , 업데이트하지 않음
                  template => "/usr/share/logstash/pipeline/template/store_template.json"  # index patttern type 지정
          }
  }
  • tag_index.conf : Logstash 와 Mysql 의 연동을 정의 하며 Mysql 의 tags(해쉬 태그) 테이블 정보를 입력 값 으로 입력받아 Elastic 에 출력 값 을 전달한다.
  input {
        tcp {
                port => 5001
        }
        
        jdbc {
                jdbc_driver_library => "/usr/share/logstash/pipeline/mysql-connector-java-8.0.15.jar"
                jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
                jdbc_connection_string => "jdbc:mysql://host.docker.internal:3306/(본인의 Mysql Database Name)?useSSL=false&serverTimezone=UTC"
                jdbc_user => "Mysql User 정보 입력"
                jdbc_password => "Mysql Password 정보 입력"	  
                schedule => "* * * * *"
                statement => "SELECT * FROM tags WHERE updatedAt > :sql_last_value"
                tracking_column => "updatedAt"
                use_column_value => true
                last_run_metadata_path => "/usr/share/logstash/pipeline/last_tag_update_time.yml"
                lowercase_column_names => false
        }
}

  output {
          stdout {
                  codec => rubydebug
          }
          elasticsearch {
                hosts => "elasticsearch:9200"
                index => "tags"
                document_id => "%{tagId}"
                doc_as_upsert => true  # 문서가 이미 존재하는 경우 업데이트하거나 새 문서로 작성할지 여부 , 업데이트하지 않음
                template => "/usr/share/logstash/pipeline/template/tag_template.json"  # index patttern type 지정
        }
  }
  • store_template.json : stores 인덱스에서 문서들이 "storeId"(long), "storeName"(text), "grade"(integer)라는 이름의 필드를 가지도록 설정
    	{
          "index_patterns": [
              "stores"
          ],
          "mappings": {
              "properties": {
                  "storeId": {
                      "type": "long"
                  },
                  "storeName": {
                      "type": "text"
                  },
                  "grade": {
                      "type": "integer"
                  }
              }
          }
      }
  • store_template.json : tags 인덱스에서 문서들이 "tagId"(long), "tagName"(text)라는 이름의 필드를 가지도록 설정
    	{
          "index_patterns": [
          	"tags"
          ],
          "mappings": {
              "properties": {
                  "tagId": {
                      "type": "long"
                  },
                  "tagName": {
                      "type": "text"
                  }
              }
          }
      }
  • pipelines.yml : Logstash에서 파이프라인 설정을 관리하는 데 사용되며 conf 확장자 로 정의 해놓은 pipeline 파일 경로를 매칭 하는 역할을 한다.
      - pipeline.id: tag_index
        path.config: "/usr/share/logstash/pipeline/tag_index.conf"
      - pipeline.id: store_index
        path.config: "/usr/share/logstash/pipeline/store_index.conf"

🏃🏻‍♂️ 실행

  1. docker-compose 파일이 있는 경로로 이동해서 실행 해보자


  2. docker-compose 가 실행되었다면 localhost:5601 Kibana 로 접속해서 색인 설정을 요청 해보자.
  • Kibana 를 통해 tags 인덱스에 Put 요청
    (index 구성 및 매핑 설정 을 위한 template.json 파일이 있다면 필요없음)



  • Kibana 를 통해 tags 인덱스에 Put 요청
    (index 구성 및 매핑 설정 을 위한 template.json 파일이 있다면 필요없음)



type : "ngram" : 토크나이저의 유형이 N-gram임을 나타낸다.
min_gram : 1 : 생성되는 N-gram의 최소 크기를 지정하며 이 설정은 각 토큰이 최소한 한 개의 아이템을 포함해야 함을 의미한다.
max_gram : 10 : 생성되는 N-gram의 최대 크기를 지정하며 이 설정은 각 토큰이 최대 10개의 아이템을 포함할 수 있음을 의미한다.

색인 설정이 완료 되었다면 Kibana GET 요청을 통해 검색을 진행 할 수 있다.

  • tags 인덱스 get 요청 , tags 인덱스 document 의 tagName 에 "매" 가 하나라도 포함되어있다면 반환




    정상적으로 데이터 값이 반환 됨을 확인 할 수 있다.


😂 포트 포워딩

이번 작성 마무리 하기전에 굉장히 골머리를 앓았던 문제점 에 대해서 간략히 설명 해보겠다.

Command Prompt 로 Docker 명령어를 실행했을때는
docker run -it -d -p 9200:9200 -p 9300:9300 -e ...
으로 실행했었고 동작도 잘 되었기에 명령어의 세부 옵션을 하나하나 뜯어보지는 않았다.

Docker-Compose 를 작동 후 ELK 모두 정상적으로 작동하는 것을 확인했었고
Local -> Container 로 통신이 가능했기에 Container -> Local 로도 접근이 가능할 줄 알았다.

Local 에서 작동하고 있는 Mysql 에 접근하려고 시도 했었지만 Container 에서 User 의 Localhost 로 접근하지 못하는 문제가 발생했었다.

왜 이런 문제가 발생 하는 가 에 대해서 많은 시간을 할애 했고 내린 결론은 "포트 포워딩" 문제 였다.

Docker 명령어를 Command 에 입력시에 -p 옵션을 주는데 이 옵션이 User 의 Local 과 컨테이너의 Local Port 를 연결 해주는 역할을 하는 것 이였다.

그렇다면 왜 "포트 포워딩" 을 했음에도 불구하고 Container 에서 User Local 로의 접근이 불가능 할까?

-p 옵션을 주는 순간 User Local 에서 Container 의 포트 포워딩이 이루어지지만 이는 "단방향" 이므로
Container 에서 User Local 로는 접근이 불가능 했다.

초기 Logstash conf 파일의 Jdbc Connection 설정을
jdbc_connection_string => "jdbc:mysql://localhost:3306 ... "   으로 했었고

이는 Container 의 Localhost:3306 즉 Container 의 내부의 3306 포트를 뜻했으며 User 의 Localhost:3306 과는 다르게 작동했다.

따라서 jdbc_connection_string => jdbc:mysql://host.docker.internal:3306 ... "   으로 설정 해줘야 의도한 대로 작동 한다.

📗 정리

처음에는 검색 기능이 단순하다고 생각했다.
그저 사용자가 원하는 정보를 키워드로 입력하면, 관련 정보를 찾아내는 것이 전부일 줄 알았으나 실제로는 그렇지 않았다.

기존에 진행했던 토이 프로젝트들은 기능이 단순하고, 고려해야 할 사항도 크게 많지 않아서 무리 없이 진행할 수 있었다.
이전의 경험을 바탕으로, 새로운 프로젝트를 진행하는데 어려움이 없을 것이라는 자신감을 가지고 있었다. 그러나, 이번에는 실제 사용을 고려하는 프로젝트에 참여하게 되었고, 이에 따라 기존의 작업 방식을 그대로 사용하면 어떤 문제가 발생할지 알 수 없었다.

특히 Docker에 관한 지식이 거의 없었기 때문에, 이를 활용한 초기 설정에 많은 어려움을 겪었다.
환경 설정을 할수록 기본 개념을 이해하고 새로운 기술을 학습하는 것의 중요성을 더욱 느끼게 되었다.

특히, 사용자의 로컬에서 Docker 컨테이너로의 접근은 가능하지만, 반대로 컨테이너에서 사용자의 로컬로의 접근은 불가능한 "양방향 포트 포워딩" 의 개념에 대해 이해하는 데에 상당한 시간이 소요되었다.

원래는 ELK를 학습하기 위해 많은 시간을 구글링하여 보낼거라 예상 했으나, 실제로는 Docker를 학습하기 위해 구글링한 시간이 훨씬 많았다.
ELK에 대한 정보를 찾는 것보다 Docker에 대한 이해를 더욱 확실히 하는 것이 중요하다는 것을 깨달았다.

아직 갈 길이 멀지만, 초기 진행을 할 수 있을 정도로 감을 잡았다고 생각한다.
검색 기능에 몇몇 필수적으로 해결해야 하는 요구 사항이 있어서, 추가적으로 매장 등급과 매핑 전략에 따른 유사도 점수 조정 기능을 보완하려고 한다.

이렇게 보완한 후에 다음 글을 작성할 계획이다.

당시 가지고 있던 기술 개념으로 작성된 글 이므로 정확하지 않을 수 있음

문제가 발견 된다면 추후 수정 예정

profile
wkd86591247@gmail.com

0개의 댓글