과거 Spring에서의 BoolQuery사용
기존에는 AbstractElasticsearchConfiguration을 통해 Elasticsearch 설정을 처리해주고 elasticsearch.core.query 안의 관련 항목들을 통해 복잡한 쿼리문을 진행했던 것과 달리, Spring 3.* 및 Elasticsearch 8.* 버전에서는 많은 변경 사항이 있습니다. 이러한 변경 사항은 Elasticsearch와 Spring Data Elasticsearch 라이브러리의 최신 업데이트로 인해 발생했습니다. 이 글에서는 이러한 변경 사항에 대해 자세히 알아보고, 새로운 버전에서 Elasticsearch를 설정하는 방법과 주요 기능을 살펴보겠습니다."
복잡하지 않은 쿼리문
동적이지 않은 쿼리가 필요할때는 ElasticSearchRepository를 상속받은 Repository를 통해 메서드명과 Query 어노테이션을 해결할 수 있습니다. 아래는 그 예시입니다.
public interface questionsRepository extends ElasticsearchRepository<Question, String> {
@Query("{\"bool\": {\"must\": [{\"match\": {\"field1\": \"?0\"}},{\"match\": {\"field2\": \"?1\"}}]}}")
List<YourEntity> findByField1AndField2(String field1, String field2);
//메서드 명만을 기입
List<YourEntity> findByField1AndField2(String field1, String field2);
}
간단한 쿼리문은 예시가 많고 관련 공식문서, 블로그가 많아서 넘어가겠습니다
복잡하고 동적인 쿼리문 in Elasticsearch 8.*
이번에는 다음과 같은 쿼리문을 만들어야된다고 생각해보겠습니다. 문제들 데이터가 있는데 이제 조건이 다음과 같습니다.
- 동일한 examId
- 검색어 포함(contain)
- tag중에 year - '20년', category - '표지'
추가할 조건은 이제 tag가 동적으로 주어진다는 것입니다. 이런 Query문을 해결할려면 저희가 사용해야할 내용은 바로 QueryDSL과 BoolQuery입니다.
QueryDsl
QueryDSL은 Java로 작성된 Type-safe한 쿼리를 생성하기 위한 프레임워크입니다. 이를 사용하면 SQL이나 Elasticsearch와 같은 데이터베이스에 대한 쿼리를 문자열이 아닌 Java 코드로 작성할 수 있습니다.
BoolQuery
BoolQuery는 Elasticsearch의 쿼리 작성을 위한 DSL(Domain Specific Language) 중 하나입니다. 불린 연산자(AND, OR, NOT)를 사용하여 여러 조건을 결합할 수 있습니다. must, must_not, should, filter 등의 조건을 사용하여 쿼리를 구성할 수 있습니다.
아래는 저희가 객체지향 쿼리로 만들어야할 실제 BoolQuery의 내용입니다.
{
"query": {
"bool": {
"must": [
{
"term": {
"examId": 1
}
},
{
"term": {
"tagsMap.category": "상황"
}
},
{
"term": {
"tagsMap.category": "화물"
}
},
{
"term": {
"tagsMap.year": "20년"
}
},
{
"match_phrase_prefix": {
"question": "운전"
}
}
]
}
}
}
만일 문제 데이터 분류에 있어 tag가 하나 더 추가된다면 다음과 같은 줄이 추가될 것 입니다.
{
"term": {
"tagsMap.category": "표지"
}
}
이 내용을 BoolQuery, NativeQuery, Elasticsearch Template을 이용하여 해결하겠습니다
BoolQuery, ElasticsearchTemplate을 활용한 ComplexQuery 처리
달라진점
- 이전: 새로운 Config에서 Java rest client를 이용하여 bean에 elasticsearch Template등록이 필요했습니다. 추가로 아래 내용 import해줘야했습니다. import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
ElasticsearchTemplate (Spring Data Elasticsearch 3.2.3.RELEASE API)
ElasticsearchTemplate (Spring Data Elasticsearch 3.2.3.RELEASE API)
Executes the given SearchQuery against elasticsearch and return result as CloseableIterator using custom mapper. Returns a CloseableIterator that wraps an Elasticsearch scroll context that needs to be closed in case of error.
docs.spring.io
Template: Spring Data Elasticsearch에서 Elasticsearch와 상호 작용하는 주요 클래스 중 하나입니다. 이 클래스는 Elasticsearch 클라이언트를 래핑하고 Elasticsearch와의 통신을 담당합니다
ElasticsearchRestTemplate 메서드
ElasticsearchRestTemplate을 사용한 메서드들은 Elasticsearch의 다양한 쿼리와 동작을 수행한다.
- search()
- 일반적인 검색 쿼리를 실행하는데 사용된다.복잡한 검색 조건과 함께 사용하여 다양한 결과를 반환받을 수 있다.
- 결과를 SearchHits 객체로 받아 각각의 검색 결과를 확인할 수 있다.
- multiGet()
- 여러 ID에 해당하는 문서를 한 번의 요청으로 조회할 때 사용된다.
- 대량의 데이터를 빠르게 가져와야 할 때 효과적이다.
- 반환된 객체는 주어진 ID 리스트 순서대로 문서를 포함한다.
- multiSearch()
- 여러 검색 쿼리를 동시에 실행할 때 사용된다.
- 각각의 쿼리는 독립적으로 수행되며 각 쿼리의 결과는 별도로 반환된다.
- 대량의 복잡한 쿼리를 병렬로 처리할 때 유용하다.
- get()
- 단일 문서의 ID를 사용하여 해당 문서를 조회할 때 사용된다.
- 반환되는 객체는 조회된 단일 문서의 정보를 포함한다.
- save()
- 주어진 엔티티를 Elasticsearch에 저장하거나 업데이트할 때 사용된다.
- 색인 생성 및 갱신을 위해 사용된다.
해결과정
- 예전 BoolQuery처리방식과 NativeSearchQuery 사용법을 학습 후 현재 버전에 넘어오면서 변경된 점을 공식문서(Spring.io, spring관련 최신 뉴스 게시 사이트, baeldung, springboot 3.2*+elasticsearch comple Query 자료 검색, queryDsl 공식문서) 를 토대로 학습했습니다.
- kibana에서 사용했던 쿼리문을 실제 java에서 사용하기위해 다음과 같은 과정을 거치면 됩니다
- BoolQueryBuilder 클래스 생성 및 빈 등록
- 원하는 BoolQuery문 생성()
- Service로직에 RequiredArgsConstructor와 private final을 이용한 의존성 주입, 참고로 boolQueryBuilder와 ElasticsearchTemplate 의존성 주입 해줘야한다
- NativeQuery: Elasticsearch Java 고수준 클라이언트에서 제공하는 클래스로 Elasticsearch의 네이티브 쿼리를 실행하기 위한 것이다 네이티브 쿼리는 Elasticsearch의 Query DSL이나 JSON 형식으로 작성된 쿼리를 실행할 때 사용되기에 코드 작성 필요하다
- SearchHits: Elasticsearch에서 검색된 결과를 나타내는 객체입니다. 이 객체는 검색 결과의 메타데이터와 검색된 문서의 목록을 포함합니다. 각 문서는 해당 인덱스의 문서 유형에 따라 매핑된 Java 객체로 변환됩니다.
- elasticsearchTemplate: 이것은 Spring Data Elasticsearch에서 제공하는 Elasticsearch 작업을 수행하기 위한 핵심 클래스입니다. 이 클래스를 사용하면 Elasticsearch와 통신하여 인덱스 생성, 쿼리 실행, 문서 삽입, 업데이트 등의 작업을 수행할 수 있습니다. elasticsearchTemplate 객체는 Elasticsearch 클라이언트를 래핑하고 있으며, Spring 애플리케이션과 Elasticsearch 간의 통신을 쉽게 처리할 수 있도록 도와줍니다.
BoolQuery작성 예시
전체 코드
BoolQueryBuilder
package capstone.examlab.questions.repository;
import capstone.examlab.questions.dto.search.QuestionsSearch;
import co.elastic.clients.elasticsearch._types.query_dsl.*;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Component
public class BoolQueryBuilder {
public Query searchQuestionsQuery(Long examId, QuestionsSearch questionsSearch) {
// "must" 조건을 추가할 리스트 생성
List<Query> mustQueries = new ArrayList<>();
// "must" 조건(examId, tagsMap)에 해당하는 Term 쿼리들 추가
mustQueries.add(new TermQuery.Builder().field("examId").value(examId).build()._toQuery());
Map<String, List<String>> tagsMap = questionsSearch.getTagsMap();
if (tagsMap != null) {
for (Map.Entry<String, List<String>> entry : tagsMap.entrySet()) {
String key = "tagsMap."+entry.getKey();
List<String> values = entry.getValue();
// 각 tag 'key-value'에 대해 쿼리 생성
for (String value : values) {
mustQueries.add(new TermQuery.Builder().field(key).value(value).build()._toQuery());
}
}
}
List<String> includes = questionsSearch.getIncludes();
//검색어 포함 여부를 위한 MatchPhrasePrefix 쿼리 추가
if(includes!=null){
for (String include : includes) {
mustQueries.add(new MatchPhrasePrefixQuery.Builder()
.field("question")
.query(include)
.build()._toQuery());
}
}
BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder();
// 생성했던 "must" 조건 설정
boolQueryBuilder.must(mustQueries);
return boolQueryBuilder.build()._toQuery();
}
}
QuestionController.java
@RequiredArgsConstructor
@RestController
@RequestMapping("exams/")
public class QuestionsController {
private final QuestionsService questionsService;
//Read API
@GetMapping("{examId}/questions")
public QuestionsList selectQuestions(@PathVariable @ValidExamId Long examId, @RequestBody QuestionsSearch questionsSearch) {
log.info("questionOptionDto = {}", questionsSearch);
return questionsService.searchFromQuestions(examId, questionsSearch);
}
}
QuestionsServiceImpl.java
@Service
@RequiredArgsConstructor
public class QuestionsServiceImpl implements QuestionsService {
private final QuestionsRepository questionsRepository;
private final ImageService imageService;
private final BoolQueryBuilder boolQueryBuilder;
private final ElasticsearchTemplate elasticsearchTemplate;
@Override
public QuestionsList searchFromQuestions(Long examId, QuestionsSearch questionsSearch) {
Query query = boolQueryBuilder.searchQuestionsQuery(examId, questionsSearch);
//쿼리문 코드 적용 및 elasticSearch 통신 관련
NativeQuery searchQuery = new NativeQuery(query);
searchQuery.setPageable(PageRequest.of(0, questionsSearch.getCount()));
SearchHits<QuestionEntity> searchHits = elasticsearchTemplate.search(searchQuery, QuestionEntity.class);
QuestionsList questionsList = new QuestionsList();
int count = 0;
for (SearchHit<QuestionEntity> hit : searchHits) {
if (count >= questionsSearch.getCount()) {
break;
}
QuestionEntity entity = hit.getContent();
Question question = Question.builder()
.id(entity.getId())
.type(entity.getType())
.question(entity.getQuestion())
.questionImagesIn(new ArrayList<>(entity.getQuestionImagesIn()))
.questionImagesOut(new ArrayList<>(entity.getQuestionImagesOut()))
.options(new ArrayList<>(entity.getOptions()))
.answers(new ArrayList<>(entity.getAnswers()))
.commentary(entity.getCommentary())
.commentaryImagesIn(new ArrayList<>(entity.getCommentaryImagesIn()))
.commentaryImagesOut(new ArrayList<>(entity.getCommentaryImagesOut()))
.tagsMap(new HashMap<>(entity.getTagsMap()))
.build();
questionsList.add(question);
count++;
}
return questionsList;
}
}
Test결과
정리
Spring Data Elasticsearch의 최신 버전에서는 Spring Boot의 자동 구성 기능을 사용하여 Elasticsearch와의 상호 작용을 간편하게 할 수 있습니다. 추가로 관련 라이브러리의 상호작용도 변경 내용이 많습니다(이 내용을 몰랐다면 삽질하는 경험을 하게됨, 아주 심하게). 이전에는 직접 NativeQuery를 생성하고 설정해야했지만, 현재는 Spring Data Elasticsearch가 자동으로 이를 처리해줍니다. 따라서 대부분의 경우에는 직접적인 NativeQuery의 생성과 설정이 필요하지 않습니다.
또한, Elasticsearch의 공식 Java 클라이언트 라이브러리에서 BoolQuery가 core.query 패캐지에서 deprecated되었고, 새로운 패키지인 co.elastic.clients.elasticsearch._types.query_dsl.Boolquery 로 이동되었습니다. 이에 따라 새로운 방식으로 BoolQuery를 사용해야합니다.
이전에는 BoolQuery를 사용할 때 BoolQueryBuilder 클래스를 직접 생성하고 필요한 조건을 설정한 다음, Spring의 Component 어노테이션을 사용하여 빈으로 등록했습니다. 그러나 최신 버전에서는 Spring Data Elasticsearch와 함께 사용되는 새로운 패키지의 클래스를 사용하여 BoolQuery를 구성하고 필요한 메서드를 생성한 후 Component 어노테이션을 통해 빈으로 등록할 수 있습니다.
이것은 Spring Data Elasticsearch 및 Elasticsearch Java 클라이언트 라이브러리의 버전 간의 변화이며, 업데이트된 버전에서는 이러한 변화가 있을 수 있습니다. 새로운 버전의 문서를 참조하여 가장 최신 방법을 학습하고 적용하는 것이 좋습니다.
출처
Elasticsearch Queries with Spring Data | Baeldung
관련 spring깃헙 내용: https://github.com/spring-projects/spring-data-elasticsearch
BooleanlQuery관련: https://opster.com/guides/elasticsearch/search-apis/elasticsearch-bool-queries/
'데이터베이스 > ElasticSearch' 카테고리의 다른 글
Elasticsearch DB 구축 과정과 최적화 전략 (0) | 2024.04.11 |
---|---|
Start Elasticsearch with docker (0) | 2024.03.03 |