프로젝트 루트 디렉토리에 docker-compose.yml을 생성한다.
1. Docker Compose 파일 작성
docker-compose.yml
version: '3.7'
services:
es:
image: docker.elastic.co/elasticsearch/elasticsearch:8.7.1
container_name: es
environment:
- node.name=es-node
- cluster.name=search-cluster
- discovery.type=single-node
- bootstrap.memory_lock=true
- ES_JAVA_OPTS=-Xms1g -Xmx1g
- xpack.security.enabled=false
- xpack.security.http.ssl.enabled=false
- xpack.security.transport.ssl.enabled=false
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- es-data:/usr/share/elasticsearch/data
ports:
- 9200:9200 # https
- 9300:9300 #tcp
networks:
- es-bridge
kibana:
image: docker.elastic.co/kibana/kibana:8.7.1
container_name: kibana
environment:
SERVER_NAME: kibana
ELASTICSEARCH_HOSTS: http://es:9200
ports:
- 5601:5601
depends_on:
- es
networks:
- es-bridge
volumes:
es-data:
driver: local
networks:
es-bridge:
driver: bridge
version: Docker Compose 파일 버전 지정
services: Docker Compose에서 정의하는 개별 서비스
volumes: 컨테이너와 호스트 간 데이터 저장/공유를 위한 볼륨 정의
networks: 서비스들이 통신할 네트워크 정의
- ElasticSearch 서비스 (es)
discovery.type=single-node: Elasticsearch를 단일 노드 모드로 실행
bootstrap.memory_lock=true: 메모리 스왑 방지
ES_JAVA_OPTS=Xms1g -Xmx1g: JVM 최소 힙 메모리 크기와 최대 힙 메모리 크기를 1GB로 설정
로컬 개발 환경에서 실행하기 위해 다음과 같이 Xpack 보안 기능과 SSL 설정을 비활성화 했다.
xpack.security.enabled=false
xpack.security.http.ssl.enabled=false
xpack.security.transport.ssl.enabled=false
ulimits:memlock: 메모리 락 한도 설정 (-1은 무제한)
- Kinaba 서비스
ELASTICSEARCH_HOSTS: 연결한 Elasticsearch 호스트 지정
- ElasticSearch 서비스를 es라고 정의해두었으므로 http://es:9200로 연결
- 만약 서비스 이름이 elasticsearch라면 http://elasticsearch:9200로 연결
depends on: Kibana 서비스가 es 서비스에 종속됨
- es 서비스 시작된 후에 Kibana 시작
- 네트워크 정의
networks:es-bridge: 두 서비스 간의 통신을 위한 브리지 네트워크
터미널에서 docker-compose up 명령을 통해 실행하고, 정상적으로 실행되는지 확인해보자.
http://localhost:9200
http://localhost:5601
2. Nori 설치
Nori는 한글 형태소 분석기로 카카오에서 개발하여 Elasticsearch에서 공식 지원한다.
cmd를 열어 다음 명령어를 입력해주어 설치하자.
# es container 확인
$ docker ps
# es컨테이너 접속
$ docker exec -it {containerId} /bin/bash
$ cd /usr/share/elasticsearch/bin/
$ ./elasticsearch-plugin install analysis-nori
docker compose down 후, 재실행한다.
3. ElasticSearch 설정 및 서비스 구현
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
application.yml
spring:
elasticsearch:
host: localhost:9200
ElasticSearchConfig
@Configuration
@EnableElasticsearchRepositories
public class ElasticSearchConfig extends ElasticsearchConfiguration {
@Value("${spring.elasticsearch.host}")
private String host;
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder()
.connectedTo(host)
.build();
}
}
@EnalbeElasticsearchRepositories: elastic 레포지토리 지원 활성화
Post
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post extends TimeStamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String content;
@Builder
public Post(String username, String title, String content) {
this.username = username;
this.title = title;
this.content = content;
}
}
PostDocument
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Document(indexName = "posts", writeTypeHint = WriteTypeHint.FALSE)
@Setting(settingPath = "elastic/post-setting.json")
@Mapping(mappingPath = "elastic/post-mapping.json")
public class PostDocument {
@Id
private Long id;
private String username;
private String title;
private String content;
@Builder
public PostDocument(Long id, String username, String title, String content) {
this.id = id;
this.username = username;
this.title = title;
this.content = content;
}
public static PostDocument from(Post post) {
return PostDocument.builder()
.id(post.getId())
.username(post.getUsername())
.title(post.getTitle())
.content(post.getContent())
.build();
}
}
@Document
Elasticsearch 인덱스를 지정하고, 해당 클래스가 Elasticsearch 도큐먼트로 매핑되도록 정의
- indexName: Elasticsearch 인덱스 이름 지정 (필수)
- writeTypeHint: 도큐먼트 타입 힌트작성 여부 (기본값: true)
- 기본적으로 Spring Data Elasticsearch는 도큐먼트에 '_class' 필드 생성한다.
- createIndex: 인덱스 자동 생성 여부 (기본값: true)
- true로 설정된 경우, Spring Data Elasticsearch가 애플리케이션 시작시 @Document에 의해 생성된 인덱스가 있는지 확인하고 없으면 생성한다.
- shards, replicas, refreshInterval, versionType
@Mapping
Elasticsearch 인덱스의 매핑을 정의하는 JSON 파일의 경로 지정
(런타임 필드로 인덱스 매핑을 정의하면, 클래스 경로에 해당 JSON 파일의 경로를 설정해야한다.)
post-mapping.json 파일은 resources/elastic 폴더 하위에 있다.
@Setting
Elasticsearch 인덱스의 설정을 정의하는 JSON 파일 경로 지정
post-setting.json 파일은 resources/elastic 폴더 하위에 있다.
post-setting.json
인덱스의 분석기, 토크나이저와 같은 설정을 포함할 수 있다.
{
"analysis": {
"analyzer": {
"korean": {
"type": "nori"
}
}
}
}
post-mapping.json
필드의 데이터 타입(text, keyword 등), 분석기 설정, 필드 속성 등이 정의된다.
{
"properties": {
"id": {
"type": "long"
},
"username": {
"type": "text",
"analyzer": "korean"
},
"title": {
"type": "text",
"analyzer": "korean"
},
"content": {
"type": "text",
"analyzer": "korean"
}
}
}
PostSearchController
@RestController
@RequestMapping("api/v1/posts")
@RequiredArgsConstructor
public class PostSearchController {
private final PostSearchService postSearchService;
@GetMapping("/search")
ResponseEntity<List<PostDocument>> search(@RequestParam String keyword) {
return ResponseEntity.ok(postSearchService.searchByKeyword(keyword));
}
}
PostService
@Service
@RequiredArgsConstructor
public class PostService {
private final PostElasticRepository postElasticRepository;
public Post add(PostRequestDto request) {
Post post = Post.builder()
.username(request.getUsername())
.title(request.getTitle())
.content(request.getContent())
.build();
postRepository.save(post);
postElasticRepository.save(PostDocument.from(post));
return post;
}
}
수정 기능은 postElasticRepository.save를 통해 PostDocument를 다시 저장해주면 해당 id이 document가 수정된다.
삭제 기능은 postElasticRepository.deleteById 또는 delete를 사용해 삭제할 수 있다.
PostSearchService
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostSearchService {
private final PostElasticRepository postElasticRepository;
public List<PostDocument> searchByKeyword(String keyword) {
return postElasticRepository.findByTitle(keyword);
}
}
PostElasticRepository
@Repository
public interface PostElasticRepository extends ElasticsearchRepository<PostDocument, Long> {
List<PostDocument> findByTitle(String title);
}
ElasticsearchRepository는 Spring Data Elasticsearch에서 지원한다.
기본적인 CRUD가 지원되고, 메서드 이름을 기반으로 쿼리를 자동 생성할 수 있다. (Query mehtods)
findByTitle은 파라미터가 '제목'일 때 다음과 같은 elasticsearch query가 생성된다.
{
"query": {
"match": {
"title": "제목"
}
}
}
4. API 테스트
Postman과 Kibana를 사용하여 Elasticsearch에 저장된 데이터를 확인하고, 데이터를 검색해보자.
우선애플리케이션을 실행하고, 데이터 확인을 위해 Analytics(Kibana이다) > Discover로 이동해 DataView를 만들어준다.
만약 date 필드가 있으면 Timestamp field를 설정해주자.
Postman을 사용해 요청을 보내보자.
DataView에서 Refresh 버튼을 클릭하면, 저장된 데이터를 확인할 수 있다.
PostMan으로 요청을 보내서 Elasticsearch 검색 기능 동작을 확인해보자.
'Spring' 카테고리의 다른 글
[Spring] Elastic Cloud + Spring Boot 3.x 연동하기 (0) | 2024.08.23 |
---|---|
[Spring] Spring Boot 프로젝트 이름 변경하기 (0) | 2024.08.08 |
[WebFlux] Reactive Streams (0) | 2023.08.09 |
[Spring] 의존성 주입(DI: Dependency Injection) (0) | 2023.07.22 |
[Spring] Junit5 테스트 No tests found for given includes 오류 해결 (0) | 2023.06.21 |