Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

JOOHUUN

Monstache | mongodb - elasticsearch 연동하기 본문

카테고리 없음

Monstache | mongodb - elasticsearch 연동하기

JOOHUUN 2023. 8. 16. 14:45

Monstache

몽고디비 컬렉션들을 지속적으로 엘라스틱서치에 색인 시켜주는 도구 (Go 언어기반)

 

사용이유

사내 프로젝트에서 nosql을 사용하기로 하였고 몽고디비와 엘라스틱서치를 무엇을 쓸지 고민하고 있었다. 

Monstach를 사용하기로 한 결정적인 이유는 몽고디비의 검색엔진이다. 몽고디비는 DB 역할을 하기엔 충분 했지만 검색엔진으로 사용하기 적합하지 않아서 ES를 연동하여 검색엔진으로 사용하기로 결정했다. (아예 ES를 사용하는 방법도 있지만 ES는 스키마의 수정 및 변경에 대해 자유롭지 않아서 pass)

 

세팅과정

파일구조

 

1. replicaset_init.sh 파일 생성(script파일로 세팅을 자동화 진행)

스크립트 설명

docker compose가 실행중일 경우 컨테이너를 내리고 재시작 합니다.

컨테이너가 다 띄어진 후 mongo1 컨테이너에서 replicaset 설정을 진행하고 es01 컨테이너에서는 index mapping 후 index를 생성합니다. 

# replicaset_init.sh
DELAY=5

docker compose -f docker-compose-replicaset.yml down

docker compose -f docker-compose-replicaset.yml up -d

echo "****** Waiting for ${DELAY} seconds for containers to go up ******"
sleep $DELAY

docker exec mongo1 chmod +x /mongo/rs_init.sh
docker exec mongo1 /mongo/rs_init.sh

docker exec es01 chmod +x /usr/local/bin/index_init.sh
docker exec es01 /usr/local/bin/index_init.sh

 

2. docker-compose-replicaset.yml 생성

# docker-compose-replicaset.yml
version: '3.8'

services:
  es01:
    image: my-elastic:7.15.1
    container_name: es01
    environment:
      # Master 노드 es01은 localhost:9200 포트를 listens 하고있다.
      - node.name=es01
      # es cluster name
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es02
      - cluster.initial_master_nodes=es01,es02
      # elasticsearch 메모리 스왑 설정
      - bootstrap.memory_lock=true          # JVM heap memory swap 방지
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"    # Jvm heap memory 설정
      - xpack.security.enabled=false        # 8.x 버전 부터는 필수설정이라고한다.
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - es01:/usr/share/elasticsearch/data
      - ./es/index_init.sh:/usr/local/bin/index_init.sh
      - ./es/mapping.json:/usr/share/elasticsearch/config/mapping.json
    ports:
      - 9200:9200
      - 9300:9300
    networks:
      - mongo-network
  es02:
    image: my-elastic:7.15.1
    container_name: es02
    environment:
      - node.name=es02
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es01
      - cluster.initial_master_nodes=es01,es02
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - es02:/usr/share/elasticsearch/data
    ports:
      - 9301:9300
    networks:
      - mongo-network
    
  kibana:
    container_name: kibana
    image: docker.elastic.co/kibana/kibana:7.15.1
    environment:
      SERVER_NAME: kibana
      # Elasticsearch 기본 호스트는 http://elasticsearch:9200 이다. 현재 docker-compose 파일에 Elasticsearch 서비스 명은 es01로 설정되어있다.
      ELASTICSEARCH_HOSTS: http://es01:9200
    ports:
      - 5601:5601
    depends_on:
      - es01
      - es02
    networks:
      - mongo-network

  monstache:
    container_name: monstache
    restart: always
    image: rwynn/monstache
    command: -f ./monstache.config.toml &
    volumes:
      - ./config/monstache.config.toml:/monstache.config.toml
    depends_on:
      - es01
      - es02
      - mongo1
      - mongo2
      - mongo3
    links:
      - es01
      - es02
    ports:
      - "8080:8080"
    networks:
      - mongo-network

  mongo1:
    container_name: mongo1
    image: mongo:6.0
    volumes:
      - ./mongo/rs_init.sh:/mongo/rs_init.sh
      - ./mongo/init.js:/mongo/init.js
      - mongo1:/data/db
    networks:
      - mongo-network
    ports:
      - 27020:27017
    depends_on:
      - mongo2
      - mongo3
    links:
      - mongo2
      - mongo3
    restart: always
    entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "fn-replicaset" ]

  mongo2:
    container_name: mongo2
    image: mongo:6.0
    volumes:
      - mongo2:/data/db

    networks:
      - mongo-network
    ports:
      - 27018:27017
    restart: always
    entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "fn-replicaset" ]

  mongo3:
    container_name: mongo3
    image: mongo:6.0
    volumes:
      - mongo3:/data/db

    networks:
      - mongo-network
    ports:
      - 27019:27017
    restart: always
    entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "fn-replicaset" ]


volumes:
  es01:
    driver: local
  es02:
    driver: local
  kibana:
    driver: local
  mongo1:
    driver: local
  mongo2:
    driver: local
  mongo3:
    driver: local

networks:
  mongo-network:
    driver: bridge

3. jaso 플러그인 설치된 Docker Image를 사용하기위해 Dockerfile 파일 생성

엘라스틱서치 컨테이너에서 사용된 이미지는 jaso 분석기를 사용하기위해 따로 Dockerfile을 만들어서 빌드하겠습니다. 

# Dockerfile-es
FROM docker.elastic.co/elasticsearch/elasticsearch:7.15.1

RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install --batch https://github.com/skyer9/elasticsearch-jaso-analyzer/releases/download/v7.15.1/jaso-analyzer-plugin-7.15.1-plugin.zip
docker build -f Dockerfile-es -t my-elastic:7.15.1 .

4. 몽고디비 replicaset 설정 (위의 폴더 구조에서 mongo 폴더 안에 있는 내용입니다.)

아래 스크립트는 docker container가 띄어진 후에 실행 되는 내용입니다.

monstache를 사용하기 위해서는 몽고 리플리카셋이 구성 되어 있어야 합니다. Primary 노드1개와 Secondary 노드 2개로 구성하도록 설정 했습니다.

rs.initiate() 명령어로 설정이 완료되면 잠시 멈췄다가 init.js 파일이 실행 되는데 여기서는 몽고디비 유저를 생성하는 구간인데 직접 mongosh 로 접속해서 createUser 명령어로 생성해도 됩니다. 처음 부터 끝까지 자동화 하기 위해 작성해  봤습니다.

# mongo/rs_init.sh
DELAY=10

mongosh <<EOF
var config = {
    "_id": "my-test-replicaset",
    "version": 1,
    "members": [
        {
            "_id": 1,
            "host": "mongo1:27017",
            "priority": 2
        },
        {
            "_id": 2,
            "host": "mongo2:27018",
            "priority": 1
        },
        {
            "_id": 3,
            "host": "mongo3:27019",
            "priority": 1
        }
    ]
};
rs.initiate(config, { force: true });
EOF

echo "****** Waiting for ${DELAY} seconds for replicaset configuration to be applied ******"

sleep $DELAY

mongosh < /mongo/init.js
# mongo/init.js

db_name='test'
use(db_name)
db.createUser({user: 'test_user', pwd: 'test_pwd', roles: [ { role: 'readWrite', db: 'fn' } ]});

 

5. 엘라스틱서치 인덱스 설정 (위의 폴더 구조에서 es 폴더 안에 있는 내용입니다.)

몽고디비에 데이터가 들어가면 자동으로 인덱스를 생성하면서 default값으로 인덱스가 설정되지만 jaso 검색엔진을 사용하기 위해서는 별도의 인덱스 설정이 필요합니다. 저의 경우는 초성검색과 검색어추천을 사용하기 위해서 별도의 인덱스 설정 작업이 필요 했습니다. (데이터를 넣기 전에 먼저 필수로 설정해 줍니다.)

jaso 플러그인에서 특수문자가 포함된 내용은 검색이 되지 않기 때문에 특수문자들은 제거 처리 해줬습니다.

ex)  '오늘도 [하루가] 지나간다.'  --->   '하루가' 검색시 결과에 안나옵니다. 

# es/index_init.sh

# Elasticsearch 호스트 및 포트 설정
host="localhost"
port="9200"

# 인덱스 이름
index_name="my_test_index"

cd /usr/share/elasticsearch/config/;

# 인덱스 매핑 설정 파일 경로
mapping_file="mapping.json"

# 인덱스 생성 요청
curl -XPUT -H "Content-Type: application/json" "http://${host}:${port}/${index_name}" -d "@${mapping_file}"

# 응답 확인
response_code=$?
if [ $response_code -eq 0 ]; then
  echo "인덱스 '${index_name}'가 성공적으로 생성되었습니다."
else
  echo "인덱스 생성에 실패했습니다."
fi
# es/mapping.json

{
  "settings": {
    "index": {
      "number_of_shards": 8,
      "number_of_replicas": 1,
      "analysis": {
        "filter": {
          "suggest_filter": {
            "type": "edge_ngram",
            "min_gram": 1,
            "max_gram": 50
          }
        },
        "tokenizer": {
          "jaso_search_tokenizer": {
            "type": "jaso_tokenizer",
            "mistype": true,
            "chosung": true
          },
          "jaso_index_tokenizer": {
            "type": "jaso_tokenizer",
            "mistype": true,
            "chosung": true
          }
        },
        "analyzer": {
          "suggest_search_analyzer": {
            "type": "custom",
            "tokenizer": "jaso_search_tokenizer",
            "char_filter": [
              "special_char_filter"
            ],
            "filter": [
              "lowercase"
            ]
          },
          "suggest_index_analyzer": {
            "type": "custom",
            "tokenizer": "jaso_index_tokenizer",
            "char_filter": [
              "special_char_filter"
            ],
            "filter": [
              "suggest_filter",
              "lowercase"
            ]
          }
        },
        "char_filter": {
          "special_char_filter": {
            "type": "mapping",
            "mappings": [
              "( => ",
              ") => ",
              "[ => ",
              "] => ",
              "\" => ",
              "' => "
            ]
          }
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "created_at": {
        "type": "date"
      },
      "news_source_slug": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "컬럼1": {
        "type": "text",
        "analyzer": "suggest_index_analyzer",
        "search_analyzer": "suggest_search_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "컬럼2": {
        "type": "text",
        "analyzer": "suggest_index_analyzer",
        "search_analyzer": "suggest_search_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "컬럼3": {
        "type": "text",
        "analyzer": "suggest_index_analyzer",
        "search_analyzer": "suggest_search_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      }
  }
}
}

6. 마지막으로 monstache config 파일 작성입니다.

mongodb와 elasticsearch 를 연동 해주는 부분입니다. replicaset-init 실행시키면 지금 까지 작성했던 파일들이 순서에 맞춰서 설정이 진행됩니다.

# config/monstache.config.toml

# "mongodb://mongo1:27017,mongo2:27018,mongo3:27019/<몽고디비명>?replicaSet=<리플리카셋명>"
mongo-url = "mongodb://mongo1:27017,mongo2:27018,mongo3:27019/test?replicaSet=my-test-replicaset"
elasticsearch-urls = ["http://es01:9200", "http://es02:9200"]
elasticsearch-max-conns = 4
elasticsearch-max-seconds = 5
elasticsearch-max-bytes = 8000000
dropped-collections = false
dropped-databases = false

namespace-regex = "test" # 몽고디비명
direct-read-namespaces = ["test.^"] # 직접읽기를 수행할 몽고디비 지정
change-stream-namespaces = [ '' ] 

# 몽고디비의 네임스페이스와 엘라스틱서치의 index를 일치시킵니다
[[mapping]]
namespace = "test.my_test_index"
index = "my_test_index"

 

 

Comments