이전 포스팅은 여기로
2024.03.13 - [개발/ELK] - MacOS 에서 ElasticSearch 설치하기 ( homebrew 사용 X )
개발환경
IDE : intelliJ
FrameWork : springboot 3.2.3
Launguage java 17
DB : h2
ElasticSearch : 8.7.1
BuildTool: Gradle
Plugin: spring-boot-starter-data-elasticsearch
처음 ElasticSearch 설치한 뒤로 시간이 별로 안 지난 줄 알았는데 벌써 열흘가까이 흘렀다. 와하하! 시간이 왜 이렇게 빠른 걸까...
각설하고, 어서 빨리 본론으로 넘어가 보자.
SpringBoot + Spring Data ElasticSearch Configuration 설정하기 ( spring data es VS java-api-client? )
먼저 springboot에서 es를 사용하려면 아래와 같은 라이브러리가 필요하다. 버전을 기재하지 않으면 springboot에 맞는 버전이 자동으로 다운되는데 연동할 es 서버와 버전이 일치하지 않으면 문제가 많으니 꼭 버전을 확인하자.
나는 es를 처음에 7.17로 했다가 버전 문제로 Configuration 이 정상적으로 import 되지 않아 es를 8.7로 올렸다.
- spring-data-elasticsearch 버전 확인 사이트
https://docs.spring.io/spring-data/elasticsearch/reference/elasticsearch/versions.html
spring-boot-starter-data-elasticsearch
처음에는 인터넷에 springboot + es 예제를 찾아서 따라 해보려고 했는데 Configuration 설정하는 부분에서 RestHighLevelClient 가 나는 무슨 짓을 해도 import 되지 않았다.
spring-data-elasticsearch 버전 문제인가 싶어서 아래 기재한 공식 홈페이지도 들어가 봤는데 여기서는 spring data에서 제공하는 라이브러리가 아니라 es에서 제공하는 co.elastic.clients:elasticsearch-java를 설치하라고 나왔다.
https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/installation.html
제공하는 기능은 별 차이 없어 보이는데 인터넷에 돌아다니는 설정방법은 제각각이라, 너무 헷갈렸다.
그리고 ES가 8 버전이 되면서 지원하는 클라이언트 명이 바뀌었다. 이거 때문에 인터넷에 있는 수많은 설정들은 내게 혼란만 더 줬다. 기존에는 Java Rest Client, Java Transport Client였는데 지금은 그냥 Java API Client라고 표현한다. 자세한 내용은 아래 링크를 들어가면 알 수 있다.
https://www.elastic.co/guide/en/elasticsearch/client/index.html
아무튼 사용을 해야 하니 spring-boot-starter-data-elasticsearch 랑 co.elastic.clients:elasticsearch-java 가 무슨 차이인지 찾아봤다.
결과적으로는 둘 다 Java를 통해서 ES와 통신할 수 있게 하는 역할을 한다. 둘 중에 원하는 걸 선택해서 사용하면 된다. spring data elasticsearch 같은 경우에는 spring data JPA와 유사하게 ElasticSearchRepository를 제공해서 보다 편하게 데이터 CRUD를 사용할 수 있다.
- spring-data-elasticsaerch와 java-api-client
Spring Data Elasticsearch and the Java High-Level REST Client are both ways to interact with Elasticsearch from a Java application, but they serve different purposes and have different features:
Spring Data Elasticsearch:
Spring Data Elasticsearch is part of the Spring Data project, which provides a higher-level abstraction over data persistence technologies, including Elasticsearch.
It offers features such as repository support, allowing you to define Elasticsearch repositories with CRUD (Create, Read, Update, Delete) operations using Spring Data's repository abstraction.
Spring Data Elasticsearch provides automatic mapping between Java objects and Elasticsearch documents, simplifying the process of converting data between your Java application and Elasticsearch.
It integrates seamlessly with other Spring components, such as Spring Boot, Spring MVC, and Spring Security, making it a good choice for Spring-based applications.
Java High-Level REST Client:
The Java High-Level REST Client is a standalone Java client provided by Elasticsearch for interacting with an Elasticsearch cluster using RESTful HTTP requests.
It offers a lower-level abstraction compared to Spring Data Elasticsearch, providing direct access to Elasticsearch's REST API without additional abstractions.
With the Java High-Level REST Client, you have more control over the HTTP requests and responses sent to and received from Elasticsearch.
While it doesn't offer the repository abstraction or automatic mapping features of Spring Data Elasticsearch, it provides flexibility and direct access to Elasticsearch's features.
In summary, Spring Data Elasticsearch is a higher-level abstraction specifically designed to work with Elasticsearch within the Spring ecosystem, providing features such as repository support and automatic mapping. On the other hand, the Java High-Level REST Client is a standalone client provided by Elasticsearch for direct interaction with an Elasticsearch cluster using RESTful HTTP requests, offering more flexibility and control over the communication with Elasticsearch. The choice between them depends on factors such as the level of abstraction desired, integration with other Spring components, and the specific requirements of your application.
내가 작업을 시작한 목적은 추후 실무에서 es를 통한 검색엔진 개선을 할 경우 도입 시 좀 더 편하게 접근할 수 있도록 하는 공부 목적과 실 제로 like 조회 시 검색속도가 얼마나 개선될 것인지에 대한 궁금증으로 시작했기 때문에 보다 사용이 간편한 spring-data-elasticsearch를 사용하기로 결정했다. ( spring-data-elasticsearch를 사용해도 java-api-client에서 제공하는 기능을 사용할 수 있다)
- spring-data-elasticsearch 초기 설정 참고 링크
https://docs.spring.io/spring-data/elasticsearch/reference/elasticsearch/clients.html
나는 데이터 처리량이 크지 않기 때문에 비동기식이(Reactive Rest Client) 아니라 동기식(Imperative Rest Client)으로 연결을 구성했다. 결과적으로 ES 서버와 연결을 위해서 작성한 @Configuration 코드는 다음과 같다.
package com.es.demo.es_sample.config;
import com.es.demo.es_sample.repository.es.EsRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
@Configuration
public class ESConfig extends ElasticsearchConfiguration {
@Value("${spring.elasticsearch.host}")
String host;
@Value("${spring.elasticsearch.port}")
String port;
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder()
.connectedTo(host + ":" +port)
.withConnectTimeout(300000)
.build();
}
}
요약하면 다음과 같다.
- spring-data-elasticsearch 나 elastic java api client 나 둘 다 es와 java application 간 통신을 도와준다. 본인이 서비스하는 애플리케이션 성격에 맞게 선택해서 사용하도록 하자.
ElasticsearchOperations vs ElasticRepository ( feat. ElasticsearchTemplate )
ES 클러스터에 검색 쿼리를 직접 날려서 조회도 가능하지만 ( ex. {"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}} ) 웬만하면 ElasticsaerchOperation과 ElasticRepository를 사용해서 ES 내의 데이터 조작을 간편하게 할 수 있다.
처음에는 ElasticsearchOperations와 ElasticRepository 가 큰 차이가 있나 싶었는데 뭘 쓰냐 차이지 둘 다 기본적인 데이터 CRUD는 편리하게 가능하다.
둘 다 사용해 봤는데 ElasticserachOperations 가 index 조작까지 확장된 작업이 가능했고, repository는 spring-data-jpa처럼 데이터 crud 만 가능했다.
아, 그리고 검색하다 보면 ElasticsearchTemplate 관련해서도 많이 나오는데 Elasticearch version 4.0 이전에 많이 사용한 ElasticserachOperations의 구현체중 하나라고 한다. 최신 버전에서는 ElasticserachOperations로 사용하면 될 것 같다.
- ElasticRepository 참고 링크 - 실제로 쿼리로 어떻게 변화되어 날아가는가에 대해 확인할 수 있다.
ElasticsearchOperations 나 ElsticsearchRepositroy 가 아니라 java-api-client에서 제공하는 다른 방법을 통해서도 index 생성이나 데이터 crud 가 충분히 가능하다. 문서를 읽어보면 알겠지만 java-api-client를 사용하면 보다 상세하게 설정할 수 있다.
- java-api-client 참고 링크
https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/lists-and-maps.html
Document를 사용해서 index에 들어갈 항목 생성하기
JPA에서 @Entity를 사용했던 것처럼 spring-data-elasticsearch에서는 @Document 사용해서 index 안에 적재될 데이터를 구성할 수 있다.
@Document(indexName = "") 형식으로 사용할 수 있는데, 지정한 indexName에 맞춰 ES클러스터에 데이터가 들어간다.
만약 인덱스가 없다면, 자동으로 새로 생성된다.
package com.es.demo.es_sample.domain.document;
import com.es.demo.es_sample.domain.dto.CharacterDto;
import com.es.demo.es_sample.domain.entity.Character;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Builder
@Document(indexName = "characters")
public class CharacterDocument {
@Id
private Long id;
@Field(name = "world_name", type = FieldType.Text)
private String worldName;
@Field(name = "character_name", type = FieldType.Text)
private String characterName;
@Field(name = "character_level", type = FieldType.Integer)
private Integer characterLevel;
//============================================================== //
public static CharacterDocument dtoToDocument(CharacterDto characterDto) {
return builder()
.id(characterDto.getId())
.worldName(characterDto.getWorld_name())
.characterName(characterDto.getCharacter_name())
.characterLevel(characterDto.getCharacter_level()).build();
}
public static CharacterDocument entityToDocument(Character character) {
return builder()
.id(character.getId())
.worldName(character.getWorldName())
.characterName(character.getCharacterName())
.characterLevel(character.getCharacterLevel()).build();
}
}
여기서 내가 겪었던 문제는 @Field인데, 처음에 @Field를 사용하지 않고
private String characte_name;
형식으로 사용했더니 ElasticSearchRepository 인터페이스에서 findByCharacter_Name 또는 findByCharacterName으로 새로운 메서드를 구현했을 때 오류가 발생했다. 자동으로 _ 가 카멜표기로 전환될 줄 알았는데 안되길래 번거롭지만 위 코드처럼 @Field를 넣어 실제 ES클러스터 내의 값과 분리해서 작성했다.
http://localhost:9200/characters/_search/ 로 조회 해보면 다음과 같이 데이터가 들어간 것을 알 수 있다.
- http://localhost:9200/characters/_search
{
"took": 7,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 10000,
"relation": "gte"
},
"max_score": 1.0,
"hits": [
{
"_index": "characters",
"_id": "1",
"_score": 1.0,
"_source": {
"_class": "com.es.demo.es_sample.domain.document.CharacterDocument",
"id": 1,
"world_name": "루나",
"character_name": "오지환",
"character_level": 296
}
},
{
"_index": "characters",
"_id": "2",
"_score": 1.0,
"_source": {
"_class": "com.es.demo.es_sample.domain.document.CharacterDocument",
"id": 2,
"world_name": "스카니아",
"character_name": "단솜",
"character_level": 296
}
},
{
"_index": "characters",
"_id": "3",
"_score": 1.0,
"_source": {
"_class": "com.es.demo.es_sample.domain.document.CharacterDocument",
"id": 3,
"world_name": "루나",
"character_name": "승준",
"character_level": 295
}
},
{
"_index": "characters",
"_id": "4",
"_score": 1.0,
"_source": {
"_class": "com.es.demo.es_sample.domain.document.CharacterDocument",
"id": 4,
"world_name": "크로아",
"character_name": "솝상",
"character_level": 295
}
},
{
"_index": "characters",
"_id": "5",
"_score": 1.0,
"_source": {
"_class": "com.es.demo.es_sample.domain.document.CharacterDocument",
"id": 5,
"world_name": "이노시스",
"character_name": "테룽이",
"character_level": 295
}
},
{
"_index": "characters",
"_id": "6",
"_score": 1.0,
"_source": {
"_class": "com.es.demo.es_sample.domain.document.CharacterDocument",
"id": 6,
"world_name": "엘리시움",
"character_name": "버터",
"character_level": 295
}
},
{
"_index": "characters",
"_id": "7",
"_score": 1.0,
"_source": {
"_class": "com.es.demo.es_sample.domain.document.CharacterDocument",
"id": 7,
"world_name": "스카니아",
"character_name": "비올레타개빡",
"character_level": 295
}
},
{
"_index": "characters",
"_id": "8",
"_score": 1.0,
"_source": {
"_class": "com.es.demo.es_sample.domain.document.CharacterDocument",
"id": 8,
"world_name": "제니스",
"character_name": "보마노랑이",
"character_level": 295
}
},
{
"_index": "characters",
"_id": "9",
"_score": 1.0,
"_source": {
"_class": "com.es.demo.es_sample.domain.document.CharacterDocument",
"id": 9,
"world_name": "베라",
"character_name": "압도",
"character_level": 294
}
},
{
"_index": "characters",
"_id": "10",
"_score": 1.0,
"_source": {
"_class": "com.es.demo.es_sample.domain.document.CharacterDocument",
"id": 10,
"world_name": "루나",
"character_name": "쀼챠",
"character_level": 294
}
}
]
}
}
이때 고민했던 부분이 데이터를 적재하는 테스트 코드를 작성할 때 ES클러스터에 데이터가 계속 쌓이는 현상을 어떻게 처리할 것인가였다.
일반적으로 rdb에 연동해서 테스트코드를 작성하면 트랜잭션 설정을 통해 자동 롤백이 되어 편했는데, ES클러스터는 그게 안됐다.
인메모리모드가 제공되면 좋은데, 결국 방법을 찾지 못해서 Test 코드 실행 전에 때에 따라 index를 초기화하는 메서드를 작성해서 필요에 따라 호출하는 방향으로 구현했다
- ElasticsearchClient를 통한 삭제 코드
@BeforeEach
void setUp() {
// Create the low-level client
restClient = RestClient.builder(
new HttpHost("localhost", 9200)).build();
// Create the transport with a Jackson mapper
transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
// And create the API client
client = new ElasticsearchClient(transport);
}
BooleanResponse deleteIndex(String indexName) throws IOException {
if(client.indices().exists(o -> o.index("characters")).value()) {
client.indices()
.delete(g -> g.index(indexName));
}
return client.indices().exists(o -> o.index("characters"));
}
- ElasticsearchOperations를 통한 삭제 코드
if(elasticsearchOperations.indexOps(IndexCoordinates.of(indexName)).exists()) {
elasticsearchOperations.indexOps(IndexCoordinates.of(indexName)).delete();
}
ElasticsearchRepository를 통해 데이터 검색하기
ElasticsearchRepository를 사용하는 방법은 spring-data-jpa와 동일하다. charactername을 통한 검색을 구현하기 위해 아래와 같이 작성했다.
- Repository
public interface EsRepository extends ElasticsearchRepository<CharacterDocument, Long> {
Optional<CharacterDocument> findByCharacterName(String name);
List<CharacterDocument> findByCharacterNameLike(String name);
}
- Controller
@GetMapping("/search/v4/es")
public ResponseEntity<ResponseDto> search_v4_es(@RequestParam("name") String name) {
List<CharacterDto> result = elasticSearchService.findByNameLike(name);
return ResponseEntity.status(HttpStatus.OK)
.body(ResponseDto.builder().message("정상호출")
.characterDtoList(result).build());
}
아 그리고 JpaRepository 랑 사용하면 충돌 오류가 난다.
ElasticsearchRepository와 패키지를 완전히 분리하거나 아래와 같이 설정해 주자 ( 나는 분리해서 써서 사용하지 않아도 문제가 없었다 ).
@EnableJpaRepositories(
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE, classes = ElasticSearchRepository.class))
@EnableElasticsearchRepositories(
includeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE, classes = ElasticSearchRepository.class))
@SpringBootApplication
RDBMS like vs ES 검색속도 차이
SpringBoot + ES 연동은 끝이 났고, 원래 궁금했던 RDBMS like와 ES 간의 검색속도 차이를 비교해 보기 위해 30만 건의 더미 데이터를 삽입하여 수행속도를 비교해봤다.
검색내용 | rdb | es |
김 | 668ms | 268ms |
박 | 422ms | 190ms |
이 | 439ms | 206ms |
단순한 이름에 대한 검색인데도 불구하고 유의미하게 차이가 나는 걸 확인할 수 있었다. 사실 만 건까지는 큰 차이가 안 보여서 속상했는데 눈에 보이는 결과가 나오니 속 시원했다.
마치며
글로 정리하니 짧게 마무리되었지만 ( 그래서 약간 현타도 오지만...) 맨땅에 헤딩하듯이 삽질하면서 구축했다. 누군가에게 이 글이 도움이 되었으면 좋겠다.
추가로, 그렇다면 ES클러스터를 데이터 저장소로 쓰게 되면 RDB는 의미가 없는가? 에 대한 의문이 들었는데 일반적으로 둘 다 쓴다고 하더라. RDB 가 저장 목적이라면 조회는 ES로 하는..?
그렇다면 RDB에 있는 데이터를 ES로 어떻게 동기화시킬 것인가를 찾아보다가 logstash를 사용해야 한다는 정보를 알게 되었다. 시작한 거 RDB -> ES 연동까지 해보고 싶어서 다음글은 logstash를 통한 연동 방법이 되지 않을까 싶다.
'Dev > ELK' 카테고리의 다른 글
ElaticSearch 란 무엇일까? 동작 방식을 이해해보자 (4) | 2024.03.14 |
---|---|
MacOS 에서 ElasticSearch 설치하기 ( homebrew 사용 X ) (0) | 2024.03.13 |