JOOHUUN
Monstache | mongodb - elasticsearch 연동하기 본문
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"