엘라스틱서치로 검색기능 개발하기



High Level Rest Client 란?

ElasticSearch를 사용하는 자바 애플리케이션을 만들기 위한 client api이다.


(1) 의존성

implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'

Spring Data Elasticsearch를 사용하여 Spring 애플리케이션에서 Elasticsearch 인덱싱, 검색 및 쿼리를 사용할 수 있다.

또한, Spring Data Elasticsearch 에서 제공 하는 ElasticsearchRepository 인터페이스를 확장하는 저장소 인터페이스를 정의하면 해당 문서 클래스에 대한 CRUD 작업이 기본적으로 사용 가능하다.



(2) 환경 설정

@EnableElasticsearchRepositories("...")
@Configuration
public class ElasticSearchConfig {

	@Value("${...}")
	String elasticHost;

	@Bean
	public RestHighLevelClient restHighLevelClient() {
		return new RestHighLevelClient(RestClient.builder(HttpHost.create(elasticHost)));
	}

}


spring:
  elasticsearch:
    rest:
      uris: http://localhost:9200


ElasticSearch는 여러 노드를 등록할 수 있으며 HttpHost를 추가해주면 된다.

HttpHost의 파라미터로는 hostname, port 그리고 http 또는 https 사용의 스키마가 들어간다.



(3) 인덱스 생성

@ToString
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Document(indexName = "post")
public class PostIndex {

	@Id
	private String id;

	@Field(type = FieldType.Text)
	private String content;

	@Field(type = FieldType.Date)
	private ZonedDateTime createdAt;

	@Field(type = FieldType.Date)
	private ZonedDateTime updatedAt;

}


@Repository
public interface PostIndexRepository extends ElasticsearchRepository<PostIndex, Long> {
	List<PostIndex> findByContent(String content);
}

ElasticsearchRepository를 상속받았다.

JpaRepository와 사용이 유사하다.


@Slf4j
@RequiredArgsConstructor
@Service
public class PostService {

	private final PostRepository postRepository;
	private final ImageService imageService;
	private final PostIndexRepository postIndexRepository;

	@Transactional
	public void create(User user, CreatePostRequest request) {
		Post post = Post.builder()
			.user(user)
			.content(request.getContent())
			.build();

		Post savedPost = postRepository.save(post);

		// imageService.create(post, request.getImages());

		PostIndex postIndex = PostIndex.builder()
			.id(String.valueOf(savedPost.getId()))
			.content(request.getContent())
			.createdAt(savedPost.getCreatedAt().atZone(ZoneId.of("Asia/Seoul")))
			.updatedAt(savedPost.getUpdatedAt().atZone(ZoneId.of("Asia/Seoul")))
			.build();

		postIndexRepository.save(postIndex);
	}


	. . .


}

검색을 하기 위해 Post 객체를 저장할 때

PostIndex도 함께 저장해줘야 한다.

DB에 데이터를 저장하듯 IndexRepository를 이용하여 저장하면 엘라스틱서치에 데이터가 저장된다.


@Test
void getAllIndex() {
	List<MultipartFile> images = new ArrayList<>();

	List<CreatePostRequest> requests = List.of(
		new CreatePostRequest("우주의 이야기", images),
		new CreatePostRequest("우유의 이야기", images),
		new CreatePostRequest("우기의 사랑", images),
		new CreatePostRequest("우리 은행", images),
		new CreatePostRequest("우리", images),
		new CreatePostRequest("우리나라", images),
		new CreatePostRequest("우주", images),
		new CreatePostRequest("우주의 나라", images)
	);

	for (CreatePostRequest p : requests) {
		postService.create(null, p);
	}

	List<PostIndex> index = postService.getAllIndex("우주의");

	for (PostIndex i : index) {
		System.out.println(i.toString());
	}
}



결과

인덱스생성

노란색 부분을 보면 @Document 어노테이션으로 설정한 Indexname으로 post가 생성되어 저장된 것을 확인할 수 있다.



ElasticSearch

엘라스틱 서치에 대해 처음 학습하면서 역색인 방식으로 텍스트를 저장하기 때문에

엘라스틱 서치의 검색이 빠르다는 것으로 게시글을 마쳤는데

엘라스틱 서치에 대해 조금 더 알아보자.


(1) Elasticsearch vs. Relational DB

vs

Elasticsearch는 키워드가 어떤 Document에 있다고 저장하는 방식이다.

해쉬 테이블과 같이 big-o 표기법에 따르면 검색 시 O(1)의 효과를 낸다.

Relational DB는 텍스트 검색 시 문서 개수만큼 연산이 수행되기 때문에 O(N)이다.


Elasticsearch에 다음과 같이 저장된다.

el



(2) Mapping

매핑은 스키마라고 보면 된다.

엘라스틱 서치 매핑을 하지 않으면 모두 문자열로 저장되기 때문에 데이터를 시각화할 때 불편한 상황이 발생할 수 있다.

때문에 실무에서 매핑없이 데이터를 넣는 것은 위험하다.


Spring Data Elasticsearch를 사용했기 때문에 인덱스 매니저의 Mapping에 들어가보면

다음과 같이 각 필드가 잘 매핑되어 있는 것을 확인할 수 있다.

매핑



참고

Minsuk Heo 허민석의 유튜브 강의

https://youtube.com/playlist?list=PLVNY1HnUlO24LCsgOxR_eK2Yi4sOgH9Pg