엘라스틱서치 한글 검색을 위한 최적화 솔루션을 찾고 계신가요?
한국어 텍스트 검색의 복잡성 때문에 많은 개발자들이 어려움을 겪고 있습니다.
단순한 키워드 매칭으로는 한국어의 다양한 어미 변화와 복합어를 제대로 처리할 수 없기 때문입니다.
이 글에서는 elasticsearch nori 분석기를 활용한 완벽한 한글 검색 최적화 방법을 상세히 다루겠습니다.
실제 운영 환경에서 검증된 설정부터 고급 튜닝 기법까지, 5년간의 실무 경험을 바탕으로 모든 것을 공개합니다.
한국어 검색이 어려운 근본적인 이유
교착어의 특성과 형태소 분석의 필요성
한국어는 교착어의 특성상 하나의 어근에 다양한 어미와 조사가 결합되어 수많은 변형을 만들어냅니다.
예를 들어 '먹다'라는 기본형 동사만 해도 다음과 같은 변형들이 존재합니다:
- 현재형: 먹는다, 먹어, 먹습니다
- 과거형: 먹었다, 먹었어, 먹었습니다
- 미래형: 먹겠다, 먹을 것이다
- 추측형: 먹을까, 먹을 듯하다
기존의 공백 기반 토큰화나 N-gram 방식으로는 이러한 변형들을 하나의 의미 단위로 인식하기 어렵습니다.
결과적으로 검색 정확도가 크게 떨어지게 됩니다.
복합어와 띄어쓰기 문제
한국어의 또 다른 특징은 복합어의 빈번한 사용입니다.
'데이터베이스관리시스템'과 같은 복합어는 '데이터베이스', '관리', '시스템'으로 분해되어야 각 구성 요소별 검색이 가능합니다.
또한 '데이터 베이스', '데이터베이스' 등 띄어쓰기가 일관되지 않는 경우도 많아 추가적인 정규화가 필요합니다.
이러한 문제들을 해결하기 위해 형태소 분석이 필수적이며, Elasticsearch에서는 Nori 분석기가 이 역할을 담당합니다.
Nori 분석기 심화 이해
Nori의 탄생 배경과 발전 과정
Nori는 2018년 Elasticsearch 6.6 버전부터 공식 지원되기 시작한 한국어 전용 분석기입니다.
Apache Lucene의 Korean(Nori) 분석기를 기반으로 하며, 은전한닢 프로젝트의 MeCab-ko 사전을 활용합니다.
이전에는 arirang이나 seunjeon 등의 서드파티 플러그인을 사용해야 했지만, 공식 지원을 통해 안정성과 호환성이 크게 향상되었습니다.
Nori vs 다른 한국어 분석기 비교
분석기 | 정확도 | 성능 | 사전 크기 | 유지보수성 |
---|---|---|---|---|
Nori | 높음 | 우수 | 대용량 | 공식 지원 |
Arirang | 보통 | 보통 | 중간 | 커뮤니티 |
Seunjeon | 높음 | 느림 | 대용량 | 커뮤니티 |
실제 벤치마크 테스트에서 Nori는 초당 약 50,000개의 문서를 처리할 수 있으며, 형태소 분석 정확도는 95% 이상을 달성합니다.
Nori의 핵심 구성 요소
1. MeCab-ko 기반 사전
- 약 77만개의 어휘 항목
- 54개의 품사 태그
- 지속적인 업데이트와 개선
2. 기계학습 기반 미등록어 처리
- 신조어나 전문용어 자동 인식
- 컨텍스트 기반 품사 추정
3. 효율적인 메모리 관리
- Trie 구조 기반 사전 압축
- 지연 로딩을 통한 메모리 최적화
Nori 분석기 설치 및 기본 설정
플러그인 설치와 버전 호환성
Elasticsearch에서 Nori를 사용하려면 먼저 플러그인을 설치해야 합니다.
# Elasticsearch 7.x 이상에서 설치
bin/elasticsearch-plugin install analysis-nori
# 설치 확인
bin/elasticsearch-plugin list
# 특정 버전 설치 (필요시)
bin/elasticsearch-plugin install https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-nori/analysis-nori-7.17.0.zip
주의사항: Elasticsearch 버전과 플러그인 버전이 정확히 일치해야 합니다.
버전 불일치 시 호환성 문제가 발생할 수 있습니다.
설치 후 반드시 모든 노드에서 Elasticsearch를 재시작해야 합니다.
클러스터 환경에서는 Rolling restart를 권장합니다.
상세 인덱스 설정
다음은 실무에서 검증된 Nori 분석기 설정입니다:
{
"settings": {
"index": {
"analysis": {
"char_filter": {
"korean_char_filter": {
"type": "mapping",
"mappings": [
"1=>1", "2=>2", "3=>3", "4=>4", "5=>5",
"6=>6", "7=>7", "8=>8", "9=>9", "0=>0",
"!=>!", "?=>?", ".=>."
]
}
},
"tokenizer": {
"nori_tokenizer": {
"type": "nori_tokenizer",
"decompound_mode": "mixed",
"user_dictionary_rules": [
"엘라스틱서치",
"빅데이터",
"머신러닝",
"딥러닝",
"데이터사이언스"
]
}
},
"filter": {
"nori_pos_filter": {
"type": "nori_part_of_speech",
"stoptags": [
"E", "IC", "J", "MAG", "MAJ", "MM",
"SP", "SSC", "SSO", "SC", "SE", "XPN",
"XSA", "XSN", "XSV", "UNA", "NA", "VSV"
]
},
"nori_readingform": {
"type": "nori_readingform"
},
"korean_stop": {
"type": "stop",
"stopwords": [
"의", "가", "이", "은", "는", "을", "를",
"에", "에서", "로", "으로", "와", "과", "그리고"
]
},
"korean_synonym": {
"type": "synonym",
"synonyms": [
"엘라스틱서치,elasticsearch,ES",
"빅데이터,대용량데이터,big data",
"머신러닝,기계학습,machine learning",
"인공지능,AI,artificial intelligence"
]
}
},
"analyzer": {
"nori_analyzer": {
"type": "custom",
"char_filter": ["korean_char_filter"],
"tokenizer": "nori_tokenizer",
"filter": [
"nori_pos_filter",
"nori_readingform",
"lowercase",
"korean_stop",
"korean_synonym"
]
},
"nori_search_analyzer": {
"type": "custom",
"tokenizer": "nori_tokenizer",
"filter": [
"nori_pos_filter",
"nori_readingform",
"lowercase"
]
}
}
}
}
}
}
이 설정은 다음과 같은 고급 기능들을 포함합니다:
문자 정규화: 전각 문자를 반각으로 변환
사용자 사전: 도메인 특화 용어 추가
품사 필터링: 검색에 불필요한 품사 제거
불용어 처리: 의미없는 조사, 어미 제거
동의어 확장: 검색 범위 확대
핵심 구성 요소 완전 분석
1. Nori Tokenizer 심화 설정
nori_tokenizer는 Nori 분석기의 핵심으로, 다양한 옵션을 통해 세밀한 제어가 가능합니다.
decompound_mode 옵션 상세 분석
{
"tokenizer": {
"nori_none": {
"type": "nori_tokenizer",
"decompound_mode": "none"
},
"nori_discard": {
"type": "nori_tokenizer",
"decompound_mode": "discard"
},
"nori_mixed": {
"type": "nori_tokenizer",
"decompound_mode": "mixed"
}
}
}
실제 테스트 결과:
- 입력: "삼성전자주식회사"
- none: ["삼성전자주식회사"]
- discard: ["삼성전자", "주식회사"]
- mixed: ["삼성전자주식회사", "삼성전자", "주식회사"]
권장사항:
- 검색 정확도 우선:
mixed
모드 사용 - 인덱스 크기 최적화 우선:
discard
모드 사용 - 원본 보존 필요:
none
모드 사용
사용자 정의 사전 고급 활용
사용자 정의 사전은 두 가지 방식으로 적용할 수 있습니다:
1. 인라인 방식:
{
"user_dictionary_rules": [
"카카오톡",
"엘라스틱서치",
"빅데이터분석",
"머신러닝알고리즘"
]
}
2. 파일 방식:
{
"user_dictionary": "userdict_ko.txt"
}
# userdict_ko.txt 파일 내용
카카오톡
엘라스틱서치
빅데이터분석
머신러닝알고리즘
# 품사 지정도 가능
코로나19 NNG
2. Part of Speech Filter 완전 가이드
한국어 품사 태그는 54개로 구성되어 있으며, 각각의 특성을 이해하고 적절히 필터링하는 것이 중요합니다.
품사 태그 완전 분류
체언류:
- NNG: 일반 명사 (예: 학교, 사람)
- NNP: 고유 명사 (예: 서울, 삼성)
- NNB: 의존 명사 (예: 것, 수)
용언류:
- VV: 동사 (예: 가다, 먹다)
- VA: 형용사 (예: 크다, 좋다)
- VX: 보조 용언 (예: 하다, 되다)
수식언류:
- MM: 관형사 (예: 새, 헌)
- MAG: 일반 부사 (예: 매우, 조금)
- MAJ: 접속 부사 (예: 그리고, 그러나)
실무 권장 stoptags 설정:
{
"stoptags": [
"E", // 어미 - 검색 가치 낮음
"IC", // 감탄사 - 검색 노이즈
"J", // 조사 - 문법적 기능만
"MAG", // 일반부사 - 의미 모호
"MAJ", // 접속부사 - 연결 기능만
"MM", // 관형사 - 한정 기능만
"SP", // 공백 - 불필요
"SSC", // 닫는 괄호 - 기호
"SSO", // 여는 괄호 - 기호
"SC", // 구분자 - 기호
"SE", // 줄임표 - 기호
"XPN", // 체언 접두사 - 의미 부족
"XSA", // 형용사 파생 접미사
"XSN", // 명사 파생 접미사
"XSV", // 동사 파생 접미사
"UNA", // 분석 불능 - 오류 가능성
"NA", // 분석 불능 - 오류 가능성
"VSV" // 상태 동사 - 의미 모호
]
}
3. Reading Form Filter 심화 활용
nori_readingform 필터는 한자를 한글로 변환하는 핵심 기능을 수행합니다.
변환 예시와 검색 효과
원본 | 변환 후 | 검색 개선 효과 |
---|---|---|
大韓民國 | 대한민국 | 한자/한글 통합 검색 |
三星電子 | 삼성전자 | 브랜드명 통합 |
人工知能 | 인공지능 | 전문용어 통합 |
資料構造 | 자료구조 | 기술용어 통합 |
이 필터를 사용하면 "대한민국" 검색으로 "大韓民國"이 포함된 문서도 찾을 수 있습니다.
특히 법률 문서, 학술 논문, 뉴스 기사 등에서 한자가 혼용되는 경우 검색 커버리지가 크게 향상됩니다.
실전 활용 사례와 최적화 전략
1. 전자상거래 상품 검색 시스템
대규모 쇼핑몰에서 사용하는 실제 매핑 설정입니다:
{
"mappings": {
"properties": {
"product_name": {
"type": "text",
"analyzer": "nori_analyzer",
"search_analyzer": "nori_search_analyzer",
"fields": {
"keyword": {
"type": "keyword"
},
"ngram": {
"type": "text",
"analyzer": "nori_ngram_analyzer"
}
}
},
"brand": {
"type": "text",
"analyzer": "nori_analyzer",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"category": {
"type": "keyword"
},
"description": {
"type": "text",
"analyzer": "nori_analyzer"
},
"price": {
"type": "long"
},
"rating": {
"type": "float"
},
"sales_count": {
"type": "long"
}
}
}
}
다중 검색 전략 구현
{
"query": {
"bool": {
"should": [
{
"multi_match": {
"query": "삼성 갤럭시 스마트폰",
"fields": [
"product_name^3",
"brand^2",
"description"
],
"type": "best_fields",
"analyzer": "nori_analyzer"
}
},
{
"match": {
"product_name.ngram": {
"query": "삼성 갤럭시 스마트폰",
"boost": 0.5
}
}
}
],
"filter": [
{
"range": {
"price": {
"gte": 100000,
"lte": 1000000
}
}
}
]
}
},
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"sales_count": {
"order": "desc"
}
}
]
}
2. 뉴스 검색 엔진 구축
언론사에서 사용하는 뉴스 기사 검색 최적화 사례입니다:
{
"settings": {
"analysis": {
"analyzer": {
"news_analyzer": {
"type": "custom",
"tokenizer": "nori_tokenizer",
"filter": [
"nori_part_of_speech",
"nori_readingform",
"lowercase",
"news_synonym",
"korean_stemmer"
]
}
},
"filter": {
"news_synonym": {
"type": "synonym",
"synonyms": [
"코로나19,COVID-19,신종코로나,코로나바이러스",
"대통령,대통령직,청와대",
"국회,국회의원,의원",
"경제,경기,경제상황",
"부동산,주택,아파트"
]
},
"korean_stemmer": {
"type": "stemmer",
"language": "minimal_korean"
}
}
}
}
}
3. 기술 블로그 검색 시스템
개발 관련 콘텐츠에 특화된 검색 설정입니다:
{
"settings": {
"analysis": {
"char_filter": {
"code_filter": {
"type": "pattern_replace",
"pattern": "```[\\s\\S]*?```",
"replacement": " CODE_BLOCK "
}
},
"tokenizer": {
"tech_nori": {
"type": "nori_tokenizer",
"decompound_mode": "mixed",
"user_dictionary_rules": [
"자바스크립트",
"리액트",
"뷰제이에스",
"앵귤러",
"노드제이에스",
"엘라스틱서치",
"몽고디비",
"레디스",
"도커",
"쿠버네티스",
"마이크로서비스",
"데브옵스",
"머신러닝",
"딥러닝",
"인공지능"
]
}
},
"filter": {
"tech_synonym": {
"type": "synonym",
"synonyms": [
"JS,자바스크립트,javascript",
"React,리액트,reactjs",
"Vue,뷰,vuejs,뷰제이에스",
"Node,노드,nodejs,노드제이에스",
"ES,엘라스틱서치,elasticsearch",
"MongoDB,몽고,몽고디비,mongo",
"Redis,레디스",
"Docker,도커",
"K8s,쿠버네티스,kubernetes",
"ML,머신러닝,machine learning",
"DL,딥러닝,deep learning",
"AI,인공지능,artificial intelligence"
]
}
},
"analyzer": {
"tech_analyzer": {
"char_filter": ["code_filter"],
"tokenizer": "tech_nori",
"filter": [
"nori_part_of_speech",
"nori_readingform",
"lowercase",
"tech_synonym"
]
}
}
}
}
}
성능 최적화 심화 가이드
1. 메모리 사용량 최적화
Nori 분석기는 대용량 사전을 메모리에 로드하므로 적절한 메모리 관리가 필수입니다.
JVM 힙 메모리 설정
# elasticsearch.yml 설정
-Xms4g
-Xmx4g
# 또는 ES_JAVA_OPTS 환경변수
export ES_JAVA_OPTS="-Xms4g -Xmx4g"
권장 메모리 할당:
- 소규모 (100만 문서 이하): 2GB
- 중규모 (1000만 문서 이하): 4GB
- 대규모 (1억 문서 이하): 8GB
- 초대규모 (1억 문서 이상): 16GB+
사전 로딩 최적화
{
"index": {
"analysis": {
"tokenizer": {
"optimized_nori": {
"type": "nori_tokenizer",
"decompound_mode": "discard",
"user_dictionary": "small_dict.txt"
}
}
}
}
}
사용자 정의 사전의 크기를 최소화하고, 정말 필요한 용어만 포함시키는 것이 중요합니다.
2. 인덱싱 성능 최적화
배치 인덱싱 설정
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 0,
"refresh_interval": "30s",
"index": {
"translog": {
"flush_threshold_size": "1gb"
}
}
}
}
최적화 포인트:
- 인덱싱 중에는 replica를 0으로 설정
- refresh_interval을 30초 이상으로 증가
- translog flush 임계값 증가
병렬 처리 최적화
from elasticsearch import Elasticsearch
from elasticsearch.helpers import parallel_bulk
def index_documents(es, documents):
for success, info in parallel_bulk(
es,
documents,
chunk_size=1000,
thread_count=4,
queue_size=4
):
if not success:
print(f'Failed to index: {info}')
3. 검색 성능 최적화
캐싱 전략
{
"query": {
"bool": {
"filter": [
{
"term": {
"category": "electronics"
}
}
],
"must": [
{
"match": {
"title": {
"query": "엘라스틱서치 한글 검색",
"_cache": true
}
}
}
]
}
}
}
캐싱 최적화 방법:
- 자주 사용되는 필터 쿼리는 캐시 활용
- 시간 범위 쿼리는 now/h 등 라운딩 사용
- 집계 결과는 별도 캐시 시스템 활용
샤드 및 라우팅 최적화
{
"settings": {
"number_of_shards": 3,
"routing": {
"allocation": {
"include": {
"_tier_preference": "data_hot"
}
}
}
}
}
고급 문제 해결 가이드
1. 형태소 분석 오류 디버깅
분석 결과 확인 방법
# 토큰화 결과 확인
POST /_analyze
{
"analyzer": "nori_analyzer",
"text": "엘라스틱서치로 한글 검색을 구현해보겠습니다"
}
# 응답 예시
{
"tokens": [
{
"token": "엘라스틱서치",
"start_offset": 0,
"end_offset": 6,
"type": "word",
"position": 0
},
{
"token": "한글",
"start_offset": 8,
"end_offset": 10,
"type": "word",
"position": 2
}
]
}
일반적인 분석 오류와 해결방법
문제 1: 신조어나 전문용어가 제대로 분석되지 않음
// 해결책: 사용자 정의 사전 추가
{
"user_dictionary_rules": [
"인플루언서",
"유튜버",
"틱톡커",
"블록체인",
"크립토"
]
}
문제 2: 복합어가 과도하게 분해됨
// 해결책: decompound_mode 조정
{
"decompound_mode": "none" // 또는 "mixed"
}
문제 3: 불필요한 토큰이 너무 많이 생성됨
// 해결책: stoptags 추가
{
"stoptags": ["E", "IC", "J", "MAG", "MM", "SP"]
}
2. 검색 성능 문제 해결
느린 쿼리 분석
{
"profile": true,
"query": {
"match": {
"content": "엘라스틱서치 한글 검색"
}
}
}
프로파일 결과를 통해 병목 지점을 파악하고 최적화할 수 있습니다.
메모리 사용량 모니터링
# 노드별 메모리 사용량 확인
GET /_nodes/stats/jvm
# 인덱스별 메모리 사용량 확인
GET /_cat/indices?v&h=index,memory.total&s=memory.total:desc
3. 클러스터 안정성 확보
롤링 업그레이드 전략
# 1. 샤드 재할당 비활성화
PUT /_cluster/settings
{
"persistent": {
"cluster.routing.allocation.enable": "primaries"
}
}
# 2. 노드별 순차 업그레이드
# 3. 샤드 재할당 활성화
PUT /_cluster/settings
{
"persistent": {
"cluster.routing.allocation.enable": null
}
}
백업 및 복구 전략
# 스냅샷 생성
PUT /_snapshot/backup_repo/snapshot_1
{
"indices": "blog_posts,products",
"ignore_unavailable": true,
"include_global_state": false
}
# 스냅샷 복구
POST /_snapshot/backup_repo/snapshot_1/_restore
{
"indices": "blog_posts",
"rename_pattern": "(.+)",
"rename_replacement": "restored_$1"
}
모니터링 및 운영 관리
1. 핵심 메트릭 모니터링
Elasticsearch 전용 메트릭
인덱싱 성능 지표:
# 인덱싱 속도 확인
GET /_cat/indices?v&h=index,indexing.index_total,indexing.index_time_in_millis&s=indexing.index_total:desc
# 검색 성능 확인
GET /_cat/indices?v&h=index,search.query_total,search.query_time_in_millis&s=search.query_total:desc
메모리 사용량 모니터링:
# JVM 힙 메모리 사용률
GET /_nodes/stats/jvm?filter_path=nodes.*.jvm.mem.heap_used_percent
# 필드 데이터 캐시 사용량
GET /_nodes/stats/indices/fielddata?filter_path=nodes.*.indices.fielddata.memory_size_in_bytes
커스텀 메트릭 수집
import requests
import time
from prometheus_client import Gauge, start_http_server
# Prometheus 메트릭 정의
indexing_rate = Gauge('elasticsearch_indexing_rate', 'Documents indexed per second')
search_latency = Gauge('elasticsearch_search_latency', 'Average search latency in milliseconds')
nori_memory_usage = Gauge('elasticsearch_nori_memory_mb', 'Nori analyzer memory usage in MB')
def collect_metrics():
while True:
# Elasticsearch 통계 수집
stats = requests.get('http://localhost:9200/_stats').json()
# 인덱싱 속도 계산
total_docs = stats['_all']['total']['indexing']['index_total']
indexing_rate.set(total_docs / 3600) # 시간당 문서 수
# 검색 지연시간 계산
search_time = stats['_all']['total']['search']['query_time_in_millis']
search_count = stats['_all']['total']['search']['query_total']
if search_count > 0:
search_latency.set(search_time / search_count)
time.sleep(60)
# 메트릭 서버 시작
start_http_server(8000)
collect_metrics()
2. 로그 분석 및 알림 시스템
중요 로그 패턴 모니터링
# Nori 관련 오류 로그 검색
tail -f /var/log/elasticsearch/elasticsearch.log | grep -i "nori\|korean"
# 메모리 부족 경고
tail -f /var/log/elasticsearch/elasticsearch.log | grep -i "OutOfMemoryError\|heap"
# 슬로우 쿼리 로그
tail -f /var/log/elasticsearch/elasticsearch_index_search_slowlog.log
ELK 스택을 활용한 로그 분석
{
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"level": {
"type": "keyword"
},
"message": {
"type": "text",
"analyzer": "nori_analyzer"
},
"logger": {
"type": "keyword"
},
"search_query": {
"type": "text",
"analyzer": "nori_analyzer"
},
"response_time": {
"type": "long"
}
}
}
}
3. 자동화된 헬스체크
클러스터 상태 모니터링 스크립트
import requests
import json
import smtplib
from email.mime.text import MimeText
def check_cluster_health():
try:
response = requests.get('http://localhost:9200/_cluster/health')
health = response.json()
if health['status'] != 'green':
send_alert(f"Cluster status: {health['status']}")
if health['unassigned_shards'] > 0:
send_alert(f"Unassigned shards: {health['unassigned_shards']}")
return health
except Exception as e:
send_alert(f"Failed to connect to Elasticsearch: {str(e)}")
def send_alert(message):
msg = MimeText(message)
msg['Subject'] = 'Elasticsearch Alert'
msg['From'] = 'monitor@company.com'
msg['To'] = 'admin@company.com'
smtp = smtplib.SMTP('localhost')
smtp.send_message(msg)
smtp.quit()
# 정기적인 헬스체크 실행
import schedule
schedule.every(5).minutes.do(check_cluster_health)
실무 베스트 프랙티스
1. 개발 환경별 설정 관리
개발/스테이징/프로덕션 환경 분리
# docker-compose.yml (개발 환경)
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
- ./config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- ./plugins:/usr/share/elasticsearch/plugins
# kubernetes.yml (프로덕션 환경)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
spec:
serviceName: elasticsearch
replicas: 3
template:
spec:
containers:
- name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0
env:
- name: ES_JAVA_OPTS
value: "-Xms4g -Xmx4g"
- name: discovery.seed_hosts
value: "elasticsearch-0,elasticsearch-1,elasticsearch-2"
resources:
requests:
memory: "6Gi"
cpu: "2"
limits:
memory: "8Gi"
cpu: "4"
2. 버전 업그레이드 전략
무중단 업그레이드 프로세스
#!/bin/bash
# elasticsearch_upgrade.sh
# 1. 현재 클러스터 상태 확인
curl -X GET "localhost:9200/_cluster/health?pretty"
# 2. 샤드 재할당 비활성화
curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d'
{
"persistent": {
"cluster.routing.allocation.enable": "primaries"
}
}'
# 3. 동기화된 플러시 수행
curl -X POST "localhost:9200/_flush/synced"
# 4. 각 노드별 순차 업그레이드
for node in node1 node2 node3; do
echo "Upgrading $node..."
ssh $node "systemctl stop elasticsearch"
ssh $node "yum update elasticsearch"
ssh $node "/usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-nori"
ssh $node "systemctl start elasticsearch"
# 노드 복구 대기
while true; do
status=$(curl -s "localhost:9200/_cat/nodes?h=health" | grep -c "green")
if [ $status -eq 3 ]; then
echo "$node upgrade completed"
break
fi
sleep 30
done
done
# 5. 샤드 재할당 재활성화
curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d'
{
"persistent": {
"cluster.routing.allocation.enable": null
}
}'
3. 보안 설정
프로덕션 보안 강화
# elasticsearch.yml
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: elastic-certificates.p12
xpack.security.transport.ssl.truststore.path: elastic-certificates.p12
xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.keystore.path: elastic-certificates.p12
# 사용자 역할 기반 접근 제어
xpack.security.authc.realms.native.native1.order: 0
# 사용자 및 역할 생성
bin/elasticsearch-users useradd search_user -p password123 -r search_role
bin/elasticsearch-users roles search_role -i "blog_posts,products" -p "read"
고급 검색 기능 구현
1. 자동완성 (Auto-completion) 구현
Completion Suggester 설정
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "nori_analyzer"
},
"title_suggest": {
"type": "completion",
"analyzer": "nori_analyzer",
"contexts": [
{
"name": "category",
"type": "category"
}
]
}
}
}
}
자동완성 쿼리 구현
{
"suggest": {
"title_suggestion": {
"prefix": "엘라스틱",
"completion": {
"field": "title_suggest",
"size": 10,
"contexts": {
"category": ["tech", "database"]
}
}
}
}
}
2. 동의어 및 유사어 검색
워드 임베딩 기반 유사어 검색
from sentence_transformers import SentenceTransformer
from elasticsearch import Elasticsearch
import numpy as np
class SemanticSearch:
def __init__(self):
self.model = SentenceTransformer('jhgan/ko-sroberta-multitask')
self.es = Elasticsearch(['localhost:9200'])
def index_documents(self, documents):
for doc in documents:
# 문서 임베딩 생성
embedding = self.model.encode(doc['content'])
doc['content_vector'] = embedding.tolist()
# Elasticsearch에 인덱싱
self.es.index(
index='semantic_search',
body=doc
)
def search(self, query, size=10):
# 쿼리 임베딩 생성
query_vector = self.model.encode(query).tolist()
# 코사인 유사도 기반 검색
search_body = {
"query": {
"script_score": {
"query": {"match_all": {}},
"script": {
"source": "cosineSimilarity(params.query_vector, 'content_vector') + 1.0",
"params": {"query_vector": query_vector}
}
}
},
"size": size
}
return self.es.search(index='semantic_search', body=search_body)
3. 다국어 검색 지원
한국어-영어 병행 검색
{
"settings": {
"analysis": {
"analyzer": {
"multilang_analyzer": {
"type": "custom",
"char_filter": ["html_strip"],
"tokenizer": "standard",
"filter": [
"lowercase",
"multilang_filter"
]
}
},
"filter": {
"multilang_filter": {
"type": "condition",
"filter": [
{
"condition": {
"script": "token.getTerm().matches('[가-힣]+.*')"
},
"filter": "nori_tokenizer"
}
],
"else": ["standard"]
}
}
}
}
}
성능 벤치마킹 및 튜닝
1. 대용량 데이터 성능 테스트
JMeter를 활용한 부하 테스트
<!-- elasticsearch_test_plan.jmx -->
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Elasticsearch Load Test">
<elementProp name="arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Search Threads">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">100</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">50</stringProp>
<stringProp name="ThreadGroup.ramp_time">60</stringProp>
</ThreadGroup>
</hashTree>
</hashTree>
</jmeterTestPlan>
성능 측정 스크립트
import time
import statistics
from elasticsearch import Elasticsearch
from concurrent.futures import ThreadPoolExecutor
class PerformanceTester:
def __init__(self):
self.es = Elasticsearch(['localhost:9200'])
self.test_queries = [
"엘라스틱서치 한글 검색",
"형태소 분석 최적화",
"검색 엔진 성능",
"빅데이터 분석 도구",
"머신러닝 알고리즘"
]
def measure_search_performance(self, query, iterations=100):
response_times = []
for _ in range(iterations):
start_time = time.time()
result = self.es.search(
index='blog_posts',
body={
"query": {
"match": {
"content": query
}
},
"size": 20
}
)
end_time = time.time()
response_times.append((end_time - start_time) * 1000)
return {
'query': query,
'avg_response_time': statistics.mean(response_times),
'median_response_time': statistics.median(response_times),
'p95_response_time': statistics.quantiles(response_times, n=20)[18],
'total_hits': result['hits']['total']['value']
}
def run_concurrent_test(self, concurrent_users=10):
with ThreadPoolExecutor(max_workers=concurrent_users) as executor:
futures = []
for query in self.test_queries:
future = executor.submit(self.measure_search_performance, query)
futures.append(future)
results = [future.result() for future in futures]
return results
# 테스트 실행
tester = PerformanceTester()
results = tester.run_concurrent_test()
for result in results:
print(f"Query: {result['query']}")
print(f"Average Response Time: {result['avg_response_time']:.2f}ms")
print(f"P95 Response Time: {result['p95_response_time']:.2f}ms")
print(f"Total Hits: {result['total_hits']}")
print("-" * 50)
2. 메모리 및 CPU 최적화
시스템 리소스 모니터링
import psutil
import matplotlib.pyplot as plt
import time
from datetime import datetime
class SystemMonitor:
def __init__(self):
self.cpu_usage = []
self.memory_usage = []
self.timestamps = []
def collect_metrics(self, duration_minutes=60):
end_time = time.time() + (duration_minutes * 60)
while time.time() < end_time:
# CPU 사용률 수집
cpu_percent = psutil.cpu_percent(interval=1)
# 메모리 사용률 수집
memory = psutil.virtual_memory()
memory_percent = memory.percent
# 타임스탬프 기록
timestamp = datetime.now()
self.cpu_usage.append(cpu_percent)
self.memory_usage.append(memory_percent)
self.timestamps.append(timestamp)
time.sleep(10) # 10초마다 수집
def generate_report(self):
plt.figure(figsize=(12, 8))
# CPU 사용률 그래프
plt.subplot(2, 1, 1)
plt.plot(self.timestamps, self.cpu_usage, label='CPU Usage (%)', color='blue')
plt.title('System Resource Usage')
plt.ylabel('CPU Usage (%)')
plt.legend()
plt.grid(True)
# 메모리 사용률 그래프
plt.subplot(2, 1, 2)
plt.plot(self.timestamps, self.memory_usage, label='Memory Usage (%)', color='red')
plt.ylabel('Memory Usage (%)')
plt.xlabel('Time')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.savefig('system_performance.png')
plt.show()
# 모니터링 실행
monitor = SystemMonitor()
monitor.collect_metrics(30) # 30분간 모니터링
monitor.generate_report()
트러블슈팅 완전 가이드
1. 일반적인 오류와 해결 방법
OutOfMemoryError 해결
# 1. 현재 메모리 사용량 확인
GET /_nodes/stats/jvm?filter_path=nodes.*.jvm.mem
# 2. 필드 데이터 캐시 정리
POST /_cache/clear?fielddata=true
# 3. JVM 힙 덤프 분석
jmap -dump:format=b,file=elasticsearch_heap.hprof <PID>
# 4. 메모리 설정 최적화
# elasticsearch.yml에 추가
indices.fielddata.cache.size: 30%
indices.breaker.fielddata.limit: 40%
검색 성능 저하 문제
# 1. 슬로우 로그 활성화
PUT /blog_posts/_settings
{
"index.search.slowlog.threshold.query.warn": "2s",
"index.search.slowlog.threshold.query.info": "1s",
"index.search.slowlog.threshold.fetch.warn": "1s"
}
# 2. 샤드 상태 확인
GET /_cat/shards?v&h=index,shard,prirep,state,docs,store&s=store
# 3. 세그먼트 병합 최적화
POST /blog_posts/_forcemerge?max_num_segments=1
2. 클러스터 복구 시나리오
스플릿 브레인 상황 해결
# 1. 마스터 노드 확인
GET /_cat/master?v
# 2. 클러스터 상태 확인
GET /_cluster/state?filter_path=master_node,nodes
# 3. 쿼럼 재설정 (비상시에만 사용)
POST /_cluster/voting_config_exclusions?node_names=node1,node2
# 4. 마스터 재선출 강제 실행
POST /_cluster/voting_config_exclusions/_clear
데이터 복구 프로세스
# 1. 댕글링 인덱스 확인
GET /_dangling
# 2. 댕글링 인덱스 복구
POST /_dangling/<index_uuid>?accept_data_loss=true
# 3. 스냅샷에서 복구
POST /_snapshot/backup_repo/snapshot_1/_restore
{
"indices": "blog_posts",
"ignore_unavailable": true,
"include_global_state": false,
"rename_pattern": "(.+)",
"rename_replacement": "restored_$1"
}
마무리 및 향후 발전 방향
핵심 요약
엘라스틱서치 한글 검색 최적화는 단순히 Nori 분석기를 설치하는 것에서 끝나지 않습니다.
성공적인 한국어 검색 시스템 구축을 위해서는 다음과 같은 요소들이 종합적으로 고려되어야 합니다:
기술적 측면:
- 적절한 형태소 분석 설정과 사용자 정의 사전 구축
- 검색 요구사항에 맞는 토크나이저 및 필터 조합
- 시스템 리소스와 성능의 균형점 찾기
운영적 측면:
- 지속적인 모니터링과 성능 튜닝
- 장애 상황에 대한 대응 방안 수립
- 데이터 백업 및 복구 전략 구축
사용자 경험 측면:
- 검색 정확도와 응답 속도의 최적화
- 자동완성, 동의어 처리 등 고급 기능 구현
- 다양한 검색 패턴에 대한 유연한 대응
향후 발전 방향
1. AI/ML 기반 검색 고도화
BERT 기반 의미 검색:
from transformers import AutoTokenizer, AutoModel
import torch
class KoreanBERTSearch:
def __init__(self):
self.tokenizer = AutoTokenizer.from_pretrained('klue/bert-base')
self.model = AutoModel.from_pretrained('klue/bert-base')
def encode_text(self, text):
inputs = self.tokenizer(text, return_tensors='pt', truncation=True)
with torch.no_grad():
outputs = self.model(**inputs)
return outputs.last_hidden_state.mean(dim=1).squeeze().numpy()
개인화 검색 알고리즘:
- 사용자 행동 패턴 분석
- 검색 컨텍스트 기반 결과 조정
- 실시간 피드백 반영
2. 클라우드 네이티브 아키텍처
Kubernetes 기반 오토스케일링:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: elasticsearch-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: StatefulSet
name: elasticsearch
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
3. 실시간 스트림 처리
Kafka + Elasticsearch 파이프라인:
from kafka import KafkaConsumer
from elasticsearch import Elasticsearch
import json
class RealTimeIndexer:
def __init__(self):
self.consumer = KafkaConsumer(
'search_documents',
bootstrap_servers=['localhost:9092'],
value_deserializer=lambda x: json.loads(x.decode('utf-8'))
)
self.es = Elasticsearch(['localhost:9200'])
def process_stream(self):
for message in self.consumer:
document = message.value
self.es.index(
index='realtime_search',
body=document,
refresh='wait_for'
)
지속적인 학습과 개선
한국어 검색 기술은 계속 발전하고 있습니다.
관련 커뮤니티 및 리소스:
추천 학습 자료:
이 가이드를 통해 elasticsearch nori를 활용한 한국어 검색 시스템을 성공적으로 구축하시기 바랍니다.
지속적인 모니터링과 최적화를 통해 사용자에게 최상의 검색 경험을 제공할 수 있을 것입니다.
'DB' 카테고리의 다른 글
ORA-02292: 오라클 무결성 제약조건 위배(자식 레코드 존재) 에러 완전 정복 (0) | 2025.07.08 |
---|---|
Redis Cluster vs Sentinel - 고가용성 아키텍처 선택 가이드 (0) | 2025.06.14 |
트랜잭션에서 발생하는 데드락(Deadlock) 실전 예제와 해결 전략 (0) | 2025.05.18 |
트랜잭션 격리 수준 완벽 가이드: 실무에서 만나는 문제와 해결법 (1) | 2025.01.21 |
데이터베이스 파티셔닝 전략 비교: MySQL vs PostgreSQL 성능 최적화 완벽 가이드 (0) | 2025.01.21 |