Rest High Level Client
Elastic Search 클라이언트를 사용해 아래 작업을 수행할 수 있다.
Elastic Search 가 버전 업그레이드 됨에 따라 2가지 방식을 제공한다.
HTTP 클라이언트: REST API 를 사용한 클라이언트
Transport 클라이언트: 네티 TCP모듈을 이용한 클라이언트
Transport 클라이언트 가 네티 소켓을 이용해 직접 통신하기에 속도 측명에서 약간의 우위를 가지고 있지만
Elastic Search 7.0 부턴 속도차이가 많이 줄어들었고 HTTP 클라이언트 사용을 권장하고 있다.
Transport 클라이언트 는 Deprecated 될 예정
아래 의존성 주입을 통해 사용할 수 있다.
1 2 3
| dependencies { compile 'org.elasticsearch.client:elasticsearch-rest-high-level-client:6.4.3' }
|
1 2 3 4 5 6 7 8 9 10 11
| @Value("${elasticsearch.host}") private String esHost; @Value("${elasticsearch.port}") private Integer esPort;
@Bean public RestHighLevelClient restHighLevelClient() { return new RestHighLevelClient( RestClient.builder(new HttpHost(esHost, esPort, "http")) ); }
|
XContentBuilder
RestHighLevelClient 은 내부적으로 org.apache.http 를 통해 Elastic Search 와 REST API 를 사용해 통신한다.
XContentBuilder 는 이때 json 구조체를 만들 때 사용하는 클래스로 RestHighLevelClient 와 밀접하게 사용된다.
아래와 같이 XContentBuilder 를 설정해서 movie_rest 인덱스에 문서저장 요청을 할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| RestHighLevelClient client = ...;
XContentBuilder builder = XContentFactory.jsonBuilder() .startObject() .field("movieCd", requestDto.getMovieCd()) .field("movieNm", requestDto.getMovieNm()) .field("movieNmEn", requestDto.getMovieNmEn()) .field("prdtYear", requestDto.getPrdtYear()) .endObject();
IndexRequest request = new IndexRequest("movie_rest", "_doc").source(builder); IndexResponse response = client.index(request, RequestOptions.DEFAULT);
|
RestHighLevelClient 에서 제공하는 문서관련 함수는 아래와 같다.
get: 문서 조회
index: 문서 추가
update: 문서 수정, 문서 Upsert
delete: 문서 삭제
QueryBuilders
분석기를 통해 검색하는 경우 QueryBuilders 를 사용해 복잡한 쿼리를 DSL 형식으로 작성 가능하다.
1 2 3 4 5 6 7 8 9 10 11 12 13
| QueryBuilder query = QueryBuilders.matchQuery("movieNm", requestDto.getMovieNm()); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder() .query(query) .from(0) .size(5) .sort(new FieldSortBuilder("movieCd").order(SortOrder.DESC)); SearchRequest searchRequest = new SearchRequest(INDEX_NAME); searchRequest.types(TYPE_NAME); searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT); return searchResponse;
|
QueryBuilders 에서 matchQuery 외에도 다양한 함수를 통해 검색할 수 있다.
boolQuery
matchQuery
matchAllQuery
termQuery
nestedQuery
데모코드
https://github.com/Kouzie/spring-boot-demo/tree/main/es-demo/es-client
Spring Data Elastic Search
Spring Data 팀에서 Elastic Search 를 위한 라이브러리를 만들어 두었다.
단순한 문서의 CRUD, 검색쿼리만 사용한다면 Spring Data Elastic Search 사용이 더 편하다.
https://www.baeldung.com/spring-data-elasticsearch-tutorial
https://www.baeldung.com/spring-data-elasticsearch-queries
https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/
버전은 아래와 같은데 Spring Data 버전과 Spring Boot 버전의 의존성이 엮여있기 때문에
Elastic Search 버전에 맞춰 Spring Boot 버전도 맞춰주어야 한다.
| Spring Data Train |
Spring Data Elasticsearch |
Elasticsearch |
Spring Boot |
| 2023.0 |
5.1.x |
8.7.0 |
3.1.x |
| 2022.0 |
5.0.x |
8.5.3 |
3.0.x |
| 2021.2 |
4.4.x |
7.17.3 |
2.7.x |
| 2021.1 |
4.3.x |
7.15.2 |
2.6.x |
| 2021.0 |
4.2.x |
7.12.0 |
2.5.x |
| 2020.0 |
4.1.x |
7.9.3 |
2.4.x |
| Neumann |
4.0.x |
7.6.2 |
2.3.x |
| Moore |
3.2.x |
6.8.12 |
2.2.x |
| Lovelace |
3.1.x |
6.2.2 |
2.1.x |
| Kay |
3.0.x |
5.5.0 |
2.0.x |
| Ingalls |
2.1.x |
2.4.0 |
1.5.x |
spring-data-elasticsearch 또한 내부적으로 elasticsearch-rest-high-level-client 를 사용하기 때문에
아래와 같이 java config 설정을 해주어야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Configuration @EnableElasticsearchRepositories public class ElasticSearchConfig extends AbstractElasticsearchConfiguration {
@Override public RestHighLevelClient elasticsearchClient() { ClientConfiguration clientConfiguration = ClientConfiguration.builder() .connectedTo("192.168.10.234:9200") .build(); return RestClients.create(clientConfiguration) .rest(); } }
|
Mapping
@Document, @Field, @Id 3개 어노테이션으로 간단하게 인덱스와 매핑할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Getter @Document(indexName = "member") public class MemberDocument {
@Id private String id;
@Field(type = FieldType.Text, analyzer = "standard") private String name;
protected MemberDocument() { }
public MemberDocument(AddMemberRequestDto requestDto) { this.name = requestDto.getName(); } }
|
복잡한 매핑 처리를 해야한다면 아래와 같이 @Mapping 어노테이션으로 처리하는 것을 권장한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @Getter @Setter @Document(indexName = "movie_search") @Mapping(mappingPath = "movie_search_mapping.json") public class MovieSearch { @Id String id; String movieCd; String movieNm; String movieNmEn; String prdtYear; String openDt; String typeNm; String prdtStatNm; String nationAlt; List<String> genreAlt; String repNationNm; String repGenreNm; List<Director> directors; List<Company> companys;
@Getter @Setter public static class Director { String peopleNm; }
@Getter @Setter public static class Company { String companyCd; String companyNm; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| { "_doc": { "properties": { "movieCd": { "type": "keyword" }, "movieNm": { "type": "text", "analyzer": "standard" }, "movieNmEn": { "type": "text", "analyzer": "standard" }, "prdtYear": { "type": "integer" }, "openDt": { "type": "integer" }, "typeNm": { "type": "keyword" }, "prdtStatNm": { "type": "keyword" }, "nationAlt": { "type": "keyword" }, "genreAlt": { "type": "keyword" }, "repNationNm": { "type": "keyword" }, "repGenreNm": { "type": "keyword" }, "companies": { "properties": { "companyCd": { "type": "keyword" }, "companyNm": { "type": "keyword" } } }, "directors": { "properties": { "peopleNm": { "type": "keyword" } } } } } }
|
query-creation
Spring Data 프로젝트인만큼 Repository 구조를 사용한다.
1 2
| public interface MovieSearchRepository extends ElasticsearchRepository<MovieSearch, String> { ... }
|
간단한 쿼리의 경우 query-creation 문법을 사용해 쿼리작성하는 것을 추천한다.
아래처럼 movieNm 필드를 기반으로 검색하는 쿼리를 query-creation 으로 생성
1 2 3
| public interface MovieSearchRepository extends ElasticsearchRepository<MovieSearch, String> { List<MovieSearch> findByMovieNm(String movieNm, Pageable pageable); }
|
아래와 같이 페이징 객체를 설정해서 호출할 경우 실제 POST 요청이 Elastic Search 로 전송된다.
1 2 3
| String movieNm = "우리"; Pageable pageable = PageRequest.of(0, 10); repository.findByMovieNm(movieNm, pageable);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| POST /movie_search/_search?typed_keys=true&max_concurrent_shard_requests=5&search_type=query_then_fetch&batched_reduce_size=512 { "from": 10, "size": 11, "query": { "bool": { "must": [ { "query_string": { "query": "우리", "fields": [ "movieNm^1.0" ], "type": "best_fields", "default_operator": "and", "max_determinized_states": 10000, "enable_position_increments": true, "fuzziness": "AUTO", "fuzzy_prefix_length": 0, "fuzzy_max_expansions": 50, "phrase_slop": 0, "escape": false, "auto_generate_synonyms_phrase_query": true, "fuzzy_transpositions": true, "boost": 1 } } ], "adjust_pure_negative": true, "boost": 1 } }, "version": true, "explain": false }
|
ElasticsearchOperations
Srping Data ElasticSearch 에서 복잡한 쿼리를 처리해야 할 경우 NativeSearchQueryBuilder 를 사용해 DSL 형태의 Query 클래스 작성이 가능하다.
빈으로 등록된 ElasticsearchOperations 를 통해 호출한다.
1 2 3 4 5 6 7 8 9 10
| private final ElasticsearchOperations operations;
... Query searchQuery = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.matchQuery("movieNm", "우리")) .build(); SearchHits<MovieSearch> searchHits = operations.search(query, MovieSearch.class); List<MovieSearch> result = searchHits.stream() .map(SearchHit::getContent) .collect(Collectors.toList());
|
데모코드
https://github.com/Kouzie/spring-boot-demo/tree/main/es-demo/es-spring