| 데이터 동기화
| Logstash
기존 엔티티에 @Document 어노테이션 등을 추가해주었고,
es 인덱스에서는 사용하지만 MySQL 테이블에서는 사용하지 않는 필드에 @Transient 어노테이션을 사용해 정의했다.
[네이버 클라우드 캠프] - elk 스택을 활용한 데이터베이스 연동
| 인덱스 매핑
기존 logstash 매핑만으로 연관된 데이터를 확인할 수 없기 때문에 Kibana에서 미리 인덱스를 매핑해주었다.
PUT /board
{
"settings": {
"analysis": {
"tokenizer": {
"edge_ngram_tokenizer": {
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 5,
"token_chars": [
"letter",
"digit"
]
}
},
"analyzer": {
"ngram_analyzer": {
"type": "custom",
"tokenizer": "edge_ngram_tokenizer",
"filter": [
"lowercase",
"asciifolding"
]
},
"synonym_ngram_analyzer": {
"type": "custom",
"tokenizer": "edge_ngram_tokenizer",
"filter": [
"lowercase",
"synonym_filter"
]
}
},
"filter": {
"synonym_filter": {
"type": "synonym",
"synonyms": [
"요리, 양식, 중식, 한식, 분식, 디저트",
"식사, 음식, 존맛, 맛집탐방, 맛집추천",
"비건, 채식주의자, 친환경, 환경",
"레스토랑, 식당, 음식점",
"청소, 정돈, 청소기",
"살균, 방역, 세균, 오염, 더러움, 지저분",
"걸레질, 청결, 정돈",
"재테크, 저금, 절약, 저축",
"주식, 해외주식, 코스피, 코스닥, 미장, 투자, 부자",
"부동산, 아파트, 주거지, 주거",
"정책, 청년, 지원금, 정부",
"변호사, 법률, 법원, 법무",
"혜택, 보조금, 정부, 행정, 규정, 제도",
"국가, 방침, 규제, 지침",
"예쁜, 아름다운, 성형, 예쁘다",
"교정, 치과, 미용, 이빨, 치아",
"미녹시딜, 탈모, 머리카락",
"병원, 감기, 몸살, 의료, 진료, 약국, 질환, 처방",
"약국, 약사, 처방전, 부작용, 복용량",
"안과, 내과, 외과, 진단, 의사, 간호사",
"의류, 옷장, 패션, 옷입기, 명품, 복장",
"상의, 하의, 바지, 티셔츠, 외투, 신발",
"쇼핑, 아울렛, 백화점, 마트, 할인점",
"커피, 원두, 아아, 모카, 카페, 라떼, 에이드, 스무디, 음료수, 카페인, coffee, cafe",
"강의, 인문학, 공부, 학습, 개발, 교육, 학원, 학교, 대학교, 수업, 과목, 철학, 교육, 수행, 강좌, 교사, 강사",
"여행, 등산, 운동, 달리기, 뛰기, 액티비티, 숙소",
"재난, 침수, 태풍, 호우, 강풍, 낙뢰, 풍랑, 폭염, 황사, 지진, 해일, 화산, 가뭄, 홍수, 조수, 산사태, 녹조, 추락, 적조"
]
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ngram_analyzer",
"search_analyzer": "standard"
},
"content": {
"type": "text",
"analyzer": "ngram_analyzer",
"search_analyzer": "standard"
},
"nickname": {
"type": "text",
"analyzer": "ngram_analyzer",
"search_analyzer": "standard"
},
"synonym_title": {
"type": "text",
"analyzer": "synonym_ngram_analyzer"
},
"synonym_content": {
"type": "text",
"analyzer": "synonym_ngram_analyzer"
},
"reg_date": {
"type": "date"
},
"update_time": {
"type": "date"
},
"views": {
"type": "integer"
},
"likes_count": {
"type": "integer"
},
"category_name": {
"type": "text"
},
"reply_count": {
"type": "integer"
},
"is_deleted": {
"type": "boolean"
}
}
}
}
| 연관 검색
| tokenizer
토크나이저(Tokenizer)
토크나이저는 텍스트를 개별 토큰(일반적으로 단어)으로 분리하는 역할을 한다. 예를 들어, 공백을 기준으로 단어를 나누거나, 특정 길이로 텍스트를 자르는 등의 작업을 수행한다.
우리의 검색 시스템에서는 다음과 같이 토크나이저와 분석기를 정의했다:
"tokenizer": {
"edge_ngram_tokenizer": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 5,
"token_chars": [
"letter",
"digit"
]
}
},
"analyzer": {
"ngram_analyzer": {
"type": "custom",
"tokenizer": "edge_ngram_tokenizer",
"filter": [
"lowercase",
"asciifolding"
]
},
"synonym_ngram_analyzer": {
"type": "custom",
"tokenizer": "edge_ngram_tokenizer",
"filter": [
"lowercase",
"synonym_filter"
]
}
},
검색의 정확성과 유연성을 높이기 위해 edge_ngram 토크나이저를 사용했고, 다양한 길이의 검색어를 처리하기 위해 1~5자로 명시해주었다. 그리고 각 필드에 사용할 분석기도 정의했다.
| edge_ngram 토크나이저
- 부분 일치 검색 지원: 사용자가 검색어의 일부만 입력해도 관련 결과를 찾을 수 있다.
- 자동완성 기능 구현 용이: 사용자가 타이핑하는 동안 실시간으로 검색 제안을 제공할 수 있다.
- 한글 검색 최적화: 한글의 특성상 초성 검색이나 부분 검색에 유용하다.
| 분석기
분석기(Analyzer):
분석기는 토크나이저와 필터(선택적)를 조합하여 텍스트를 처리하는 더 큰 단위이다. 텍스트를 받아 검색에 사용될 토큰들로 변환하는 전체 과정을 담당한다. 일반적으로 문자 필터(선택적), 토크나이저, 토큰 필터로 구성된다.
"filter": {
"synonym_filter": {
"type": "synonym",
"synonyms": [
"요리, 양식, 중식, 한식, 분식, 디저트",
"식사, 음식, 존맛, 맛집탐방, 맛집추천",
"비건, 채식주의자, 친환경, 환경",
"레스토랑, 식당, 음식점",
"청소, 정돈, 청소기",
"살균, 방역, 세균, 오염, 더러움, 지저분",
"걸레질, 청결, 정돈",
"재테크, 저금, 절약, 저축",
"주식, 해외주식, 코스피, 코스닥, 미장, 투자, 부자",
"부동산, 아파트, 주거지, 주거",
"정책, 청년, 지원금, 정부",
"변호사, 법률, 법원, 법무",
"혜택, 보조금, 정부, 행정, 규정, 제도",
"국가, 방침, 규제, 지침",
"예쁜, 아름다운, 성형, 예쁘다",
"교정, 치과, 미용, 이빨, 치아",
"병원, 감기, 몸살, 의료, 진료, 약국, 질환, 처방",
"약국, 약사, 처방전, 부작용, 복용량",
"안과, 내과, 외과, 진단, 의사, 간호사",
"의류, 옷장, 패션, 옷입기, 명품, 복장",
"상의, 하의, 바지, 티셔츠, 외투, 신발",
"쇼핑, 아울렛, 백화점, 마트, 할인점",
"커피, 원두, 아아, 모카, 카페, 라떼, 에이드, 스무디, 음료수, 카페인, coffee, cafe",
"강의, 인문학, 공부, 학습, 개발, 교육, 학원, 학교, 대학교, 수업, 과목, 철학, 교육, 수행, 강좌, 교사, 강사",
"여행, 등산, 운동, 달리기, 뛰기, 액티비티, 숙소",
"재난, 침수, 태풍, 호우, 강풍, 낙뢰, 풍랑, 폭염, 황사, 지진, 해일, 화산, 가뭄, 홍수, 조수, 산사태, 녹조, 추락, 적조"
]
}
| ngram_analyzer:
- edge_ngram_tokenizer를 사용하여 토큰화한다.
- lowercase 필터로 모든 문자를 소문자로 변환하여 대소문자 구분 없이 검색할 수 있게 한다.
- asciifolding 필터로 악센트가 있는 문자를 ASCII 문자로 변환하여 다국어 검색을 지원한다.
| synonym_ngram_analyzer:
- edge_ngram_tokenizer를 사용하여 토큰화한다.
- lowercase 필터로 모든 문자를 소문자로 변환하여 대소문자 구분 없이 검색할 수 있게 한다.
- synonym_filter를 적용하여 동의어/유의어 검색을 지원한다.
이렇게 설정하면 커피 ≓ 원두 ≓ cafe ≓ ... 처럼 각 행이 같은 의미로 취급된다.
| 사용
| 연관 검색
ElasticsearchBoardServiceImpl에서는 연관 검색을 위해 synonym_ngram_analyzer를 사용했고, edge_ngram 토크나이저와 동의어 필터를 조합하여 구현했다.
@Override
public PageResponseDTO<SearchDTO> searchWithSynonyms(PageRequestDTO pageRequestDTO, String keyword) {
Pageable pageable = commonUtils.createPageable(pageRequestDTO);
Query searchQuery = buildSynonymSearchQuery(keyword, pageable);
SearchHits<Board> searchHits = executeSynonymSearch(searchQuery);
List<SearchDTO> synonymDtoList = mapSearchHitsToDTO(searchHits);
return commonUtils.createPageResponseDTO(pageRequestDTO, synonymDtoList, searchHits.getTotalHits());
}
private Query buildSynonymSearchQuery(String keyword, Pageable pageable) {
return new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(keyword, "synonym_title", "synonym_content")
.type(MultiMatchQueryBuilder.Type.BEST_FIELDS)
.analyzer("synonym_ngram_analyzer"))
.withPageable(pageable)
.build();
}
"synonym_title"과 "synonym_content" 필드에 대해 멀티매치 쿼리를 사용했다.
이 필드들은 synonym_ngram_analyzer를 사용하여 분석되므로, 동의어/유의어 검색이 가능해진다.
이 방식으로, 사용자가 입력한 키워드와 유사한 의미를 가진 단어들로 작성된 게시글이 연관 검색 결과에 포함된다.
| 연관 게시글 추천
현재 게시글의 제목과 내용을 기반으로 유사한 게시글을 찾아 제안한다.
@Override
public List<SuggestedBoardDTO> getSuggestedBoards(Long currentBoardId, String title, String content) {
Query searchQuery = buildSuggestedBoardSearchQuery(title, content);
SearchHits<Board> searchHits = executeSuggestedBoardSearch(searchQuery);
return filterAndMapSuggestedBoards(searchHits, currentBoardId, 4);
}
private Query buildSuggestedBoardSearchQuery(String title, String content) {
return new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.boolQuery()
.should(QueryBuilders.multiMatchQuery(title, "synonym_title")
.analyzer("synonym_ngram_analyzer")
.boost(2.0f)) // 제목에 가중치 2
.should(QueryBuilders.multiMatchQuery(content, "synonym_content")
.analyzer("synonym_ngram_analyzer")
.boost(1.0f))) // 내용에 가중치 1
.withPageable(PageRequest.of(0, 10)) // 일단 10개를 가져오고 나중에 필터링
.build();
}
제목과 내용 모두를 고려했고, 제목 매칭에 더 높은 가중치(2.0)를 부여해 제목 유사성을 더 중요하게 취급했다. 그리고 현재 게시글을 결과에서 제외하고, 최종적으로 4개의 연관 게시글만 랜덤으로 반환했다.
교육과정을 수료하고 바우처 기간이 지나버렸기 때문에 프로젝트를 실행시키려면 다시 elasticsearch 클러스터를 만들어야 한다.
기존에 사용하던 ncp 클러스터 차이를 느껴보고, 로컬 환경에 ELK 스택을 구축하기 위해 docker-compose를 사용했다.
2024.08.10 - [네이버 클라우드 캠프] - NCP Search Engine Service to Local ELK
'네이버 클라우드 캠프' 카테고리의 다른 글
NCP Search Engine Service to Local ELK (0) | 2024.08.10 |
---|---|
elk 스택을 활용한 데이터베이스 연동 (0) | 2024.08.09 |
JPA에서 MyBatis로의 전환: 구조적 차이와 느낀 점 (0) | 2024.08.09 |
Projection을 사용한 명명 메서드: 성능 비교와 느낀 점 (0) | 2024.07.29 |
Spring data JPA vs ElasticSearch (0) | 2024.07.24 |