Meilisearch 설치 방법은 지난 포스팅에서 다루었고, 오늘은 실제 spring에서 사용하는 예제를 기록하려고한다.
구글에 한국 블로그 중에 meilisearch를 실제로 사용한 예시 코드가 잘 없다..
의존성 추가 (Maven)
pom.xml에 아래와 같이 meilisearch sdk를 추가한다.
나의 경우 다른 버전의 okhttp를 사용해야하는데 meilisearch sdk 내부적으로 okhttp를 사용하기 때문에 exclusion으로 제외하고, 따로 주입하였다.
문제가 없는 사람들은 exclusions는 무시하고 진행하면 될 듯하다.
<!--MeiliSearch -->
<dependency>
<groupId>com.meilisearch.sdk</groupId>
<artifactId>meilisearch-java</artifactId>
<version>0.14.1</version>
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.0</version>
</dependency>
Meilisearch 서버와 통신하는 Client 객체
아래 예시에서는 생성자가 아닌 @Postconstruct를 사용해서 Client 객체와 Index 객체를 초기화하고 있다.
그 이유는 egovframework의 클래스를 사용해서 properties 파일을 읽어 IP, Port, master key 정보를 가져오는데, 의존성 주입 순서가 맞지 않아 @Postconstruct를 사용하였다. 따라서 별 문제가 없다면 그냥 생성자에서 초기화하면 된다.
@Service
public class MeilisearchService {
private Client client;
/*
Meilisearch는 데이터를 JSON 문서로 저장한다.
Meilisearch에서 Index는 각 문서를 모아두는 폴더와 유사하다.
즉, 검색 대상 범위에 해당하며 Index 객체를 통해서 해당 Index의 데이터를 검색한다.
*/
private Index index;
@PostConstruct
public void init() {
this.client = initializeClient();
this.index = this.client.index("integration");
Settings settings = newSearchableAttributes("title", "contents"); //검색 대상 필드 지정
this.index.updateSettings(settings);
this.index.updateSortableAttributesSettings(new String[] {"regDt"}); //정렬 대상 필드 지정
this.index.updateFilterableAttributesSettings(new String[] {"category"}); //필터링 대상 필드 지정
syncSearchInedx(); //Meilisearch 객체 생성 시 = 서버 실행 시 => DB데이터와 검색엔진의 인덱스 데이터를 동기화
}
/** 클라이언트 초기화 */
private Client initializeClient() {
String ip = "Meilisearch Server IP";
String port = "Meilisearch Server Port";
String url = String.format("http://%s:%s", ip, port);
String masterkey = "Meilisearch Server Master Key";
Config config = new Config(url, masterkey);
return new Client(config);
}
/** 검색 세팅 정보 생성 */
private Settings newSearchableAttributes(String... attributes) {
Settings settings = new Settings();
return settings.setSearchableAttributes(attributes);
}
}
검색 엔진 DB 동기화
검색 엔진 서버는 DB와 별도로 동작하기 때문에 DB에 데이터가 변경되면 검색 엔진의 검색 결과와 차이가 생길 수 있다.
따라서 동기화를 해주어야하는데, 동기화 주기와 방식은 상황에 따라 다르다.
나의 경우 통합 검색에서만 기능을 사용하기 때문에 반드시 실시간으로 적용이 될 필요는 없었다.
따라서 일정주기로 동기화 스케줄링을 돌려 데이터를 맞춰주었다.
Meilisearch에 데이터를 저장하는 방법은 간단하다.
저장할 데이터를 JSON 문자열로 변환한 후 sdk에서 제공하는 addDocuments() 함수를 사용해서 데이터를 저장할 수 있고, 함수 실행 후 응답으로 TaskInfo라는 작업 정보를 반환하기 때문에 성공여부와 taskId와 같은 정보를 받아 활용할 수 있다.
String jsonData = objectMapper.writeValueAsString(indexDataSet);
TaskInfo response = this.index.addDocuments(jsonData);
검색 기능 구현
검색 방법 또한 간단하다. 인터넷에 정보가 많지 않지만 문서 정리는 매우 잘 되어있기 때문에 문서를 쭉 읽어보는 것을 추천한다.
SearchRequest 객체를 이용해서 검색 조건을 맞춰준 다음 index 객체를 통해서 search() 함수를 호출하면 끝이다.
응답객체는 Searchable 인터페이스를 구현한 여러 객체가 제공되기 때문에 다른 형태의 응답을 원한다면 문서를 찾아보면 된다.
아래는 카테고리 별 검색을 구현한 예시이다.
페이징, 정렬, 필터링, 하이라이트 표시 등 많은 기능이 존재한다.
가장 중요한 점은 rankingScoreThreshold 필드인데 한국어 검색의 경우 정확도가 매우 낮다. 즉, "표준"이라는 키워드를 검색하면 '표'가 들어간 단어가 대부분 노출된다.
이를 방지하기 위해 RankingScore라는 매칭 점수를 나타내는 필드를 활용하여 매칭 점수가 높은 값만 가져오도록 조건을 줄 수 있다.
먼저 showRankingScore를 통해 정확히 일치한 경우의 점수를 확인해보니 평균 0.93으로 나왔다.
나의 경우 정확히 일치하는 값만 표출하기 위해 0.9로 조건을 주었는데, 이 또한 직접 확인해보고, 허용 범위 기준을 정한 뒤 조건을 넣어주면 된다.
한글은 1.0을 넣었을 땐 검색이 안되는 것 같다.
/** 카테고리 검색 API 호출 */
private MeilisearchDTO.Response search(String Keyword, String category, MeilisearchDTO.Request searchParam) {
SearchRequest searchRequest = SearchRequest.builder()
.q(Keyword)
.rankingScoreThreshold(0.9) //검색 정확도 0 ~ 1.0 => 1로 갈수록 완벽한 정확도 요구
// .showRankingScore(true) //검색 정확도 표출
.filter(new String[] {category})
.hitsPerPage(searchParam.getPageSize())
.page(searchParam.getPageNum())
.sort(searchParam.getSorts())
.attributesToHighlight(new String[] {"title", "contents"}) //전체 지정은 ["*"] 사용하면됨
.highlightPreTag("") //검색어와 매칭된 텍스트를 원하는 태그로 감쌀 수 있다.
.highlightPostTag("") //별도의 지정이 없는 경우 <em> 태그로 감싸진다.
.build();
SearchResultPaginated result = (SearchResultPaginated) this.index.search(searchRequest);
return MeilisearchDTO.Response.of(result);
}
하이라이트 표현을 하는 경우 아래와 같이 _formatted라는 필드가 추가되어 태그가 추가된 텍스트를 받을 수 있다.
{
"id": 50393,
"title": "Kung Fu Panda Holiday",
"poster": "https://image.tmdb.org/t/p/w1280/gp18R42TbSUlw9VnXFqyecm52lq.jpg",
"overview": "The Winter Feast is Po's favorite holiday. Every year he and his father hang decorations, cook together, and serve noodle soup to the villagers. But this year Shifu informs Po that as Dragon Warrior, it is his duty to host the formal Winter Feast at the Jade Palace. Po is caught between his obligations as the Dragon Warrior and his family traditions: between Shifu and Mr. Ping.",
"release_date": 1290729600,
"_formatted": {
"id": 50393,
"title": "Kung Fu Panda Holiday",
"poster": "https://image.tmdb.org/t/p/w1280/gp18R42TbSUlw9VnXFqyecm52lq.jpg",
"overview": "The <em>Winter Feast</em> is Po's favorite holiday. Every year he and his father hang decorations, cook together, and serve noodle soup to the villagers. But this year Shifu informs Po that as Dragon Warrior, it is his duty to host the formal <em>Winter Feast</em> at the Jade Palace. Po is caught between his obligations as the Dragon Warrior and his family traditions: between Shifu and Mr. Ping.",
"release_date": 1290729600
}
}
'개발 > 기능 개발' 카테고리의 다른 글
[Spring] Filter & Interceptor & AOP (0) | 2023.05.29 |
---|---|
[Spring] IoC & DI (0) | 2023.05.24 |
[인증] JWT(JSON Web Token) (0) | 2023.04.19 |
[Spring] Rest Docs와 Swagger UI (0) | 2023.04.11 |
[Spring] Microservice 관리를 위한 Eureka Server 와 Spring Cloud Gateway (0) | 2023.02.26 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!