
Spring Boot
: 사용자 검색 API, 관리자 데이터 추가 APIElasticsearch
: 역색인 바탕 데이터 검색 엔진 & NoSQLMySQL
: 트랜잭션 보장용 데이터 영속 보관소 & 데이터 관리Spring Batch
: 대규모 MySQL 데이터 ES 이관 배치 처리JavaScript
: 검색 데이터 페이징 처리 결과 반환Python
: xls 원본 데이터 csv 컨버팅
- 단어(entry) 우선, 설명(definition) 검색 데이터 일치 추천 연산 정렬
- 검색어 일치 하이라이팅, 페이지네이션 반환
- MySQL 사전 저장된 75000개 가량 데이터 ES 배치 이관, 이벤트 기반 실시간 동기화
- Kafka 같은 메세지 큐 도입으로 대용량 이벤트 동기화 처리
- 초성 검색, 자동완성 등 ES 기능 추가 공부 필요
// index 초기화, PUT /koreans
{
"settings": {
"index": { "max_ngram_diff": 30 },
"analysis": {
"analyzer": {
"my_custom_analyzer": {
"type": "custom", "char_filter": [],
"tokenizer": "my_nori_tokenizer",
"filter": ["lowercase_filter", "synonym_filter"]
},
"autocomplete_analyzer": {
"type": "custom", "tokenizer": "autocomplete_tokenizer", "filter": ["lowercase"]
},
"ngram_analyzer": {
"type": "custom", "tokenizer": "ngram_tokenizer", "filter": ["lowercase"]
},
"chosung_analyzer": { // 나중에 추가하기
"type": "custom", "tokenizer": "standard", "filter": ["lowercase"]
}
},
"tokenizer": {
"my_nori_tokenizer": {
"type": "nori_tokenizer", "decompound_mode": "mixed", "discard_punctuation": "true",
"user_dictionary": "dict/userdict_ko.txt", "lenient": true
},
"autocomplete_tokenizer": {
"type": "edge_ngram", "min_gram": 2, "max_gram": 30, "token_chars": ["letter", "digit"]
},
"ngram_tokenizer": {
"type": "ngram", "min_gram": 2, "max_gram": 30, "token_chars": ["letter", "digit"]
}
},
"filter": {
"lowercase_filter": { "type": "lowercase" },
"synonym_filter": {
"type": "synonym", "synonyms_path": "dict/synonym-set.txt", "lenient": true
}
},
"normalizer": {
"chosung_normalizer": { "type": "custom", "filter": ["lowercase"] }
}
}
},
"mappings": {
"properties": {
"entry": {
"type": "text", "analyzer": "my_custom_analyzer",
"fields": {
"autocomplete": { "type": "text", "analyzer": "autocomplete_analyzer" },
"ngram": { "type": "text", "analyzer": "ngram_analyzer" },
"chosung": { "type": "keyword", "normalizer": "chosung_normalizer" } // 나중에 추가하기
}
},
"type": { "type": "keyword" },
"pos": { "type": "keyword" },
"definition": {
"type": "text", "analyzer": "my_custom_analyzer",
"fields": {
"ngram": { "type": "text", "analyzer": "ngram_analyzer" }
}
},
"entry_chosung": {
"type": "keyword", "normalizer": "chosung_normalizer",
"fields": {
"autocomplete": { "type": "text", "analyzer": "autocomplete_analyzer" },
"ngram": { "type": "text", "analyzer": "ngram_analyzer" }
}
}
}
}
}
// search 예시, GET /koreans/_search
{
"size": 10,
"query": {
"dis_max": {
"queries": [
{ "match": { "entry": { "query": "검색", "boost": 2 }}},
{ "match": { "entry.ngram": { "query": "검색", "boost": 1 }}},
{ "match": { "entry.autocomplete": { "query": "검색", "boost": 0.2 }}}
]
}
},
"highlight": {
"fields": {
"entry": {
"pre_tags": ["<strong>"],
"post_tags": ["</strong>"]
},
"definition": {
"pre_tags": ["<strong>"],
"post_tags": ["</strong>"]
}
}
}
}
- 향후 초성 기반 검색, 영문 포함 검색 확장하여 재인덱싱 예정
- WAS의
ElasticsearchClient
통해 추천 점수 바탕 정렬
@Slf4j
@Component
@RequiredArgsConstructor
public class TransactionalKoreanEventListener implements KoreanEventListener {
private final ElasticsearchClient elasticsearchClient;
@Override
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void listenCreateEvent(CreateEvent event) throws IOException {
log.info("생성 이벤트 발생, {}", event.getId());
elasticsearchClient.index(i -> i.index("koreans")
.id(String.valueOf(event.getId()))
.document(event.getDto()));
}
@Override
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void listenUpdateEvent(UpdateEvent event) throws IOException {
log.info("전체 업데이트 이벤트 발생, {}", event.getId());
elasticsearchClient.update(u -> u.index("koreans")
.id(String.valueOf(event.getId()))
.doc(event.getDto())
.docAsUpsert(true)
, KoreanUpdateDTO.class);
}
// ...
ApplicationEventPulisher
의존성 주입- 커스텀 이벤트 객체 리스너 등록