본문 바로가기

Spring boot 를 이용하여 Elasticsearch 에 접근 하기

이번 포스팅에서는 스프링부트를 활용해서 엘라스틱서치에 데이터를 넣는 방법까지 해보도록 하겠습니다.  또한 기본적인 엘라스틱서치에 대한 내용을 소개해드리려고 합니다. 기본적으로 엘라스틱서치는 루씬 기반 검색엔진으로 오픈소스 입니다. ELK (Elasticsearch Logstash Kibana) 스택으로 아주 유명합니다. 제품간 연동이 메뉴얼을 조금만 읽으면 손쉽게 연동이 가능합니다.

먼저 엘라스틱서치는 분산형 Restful 검색 및 분석이 가능하고 정형, 비정형, 위치정보, 메트릭 등 원하는 방법으로 다양한 유형의 검색을 수행할 수 있습니다. 또한 작은 규모로 적용해도 이후 점차 쉽게 확대할 수 있으며, API 등을 이용해 구조를 단순화하고 설치하기 쉽다고 합니다.

대표적으로 깃허브, 이베이, 가디언 같은 기업이 엘라스틱서치 기술로 내부 검색 기능을 구축했다고 합니다

이제는 해당 검색엔진을 Spring boot에서 사용하는 방법에 대해서 알아보도록 하겠습니다.

우선 필요로하는 라이브러리를 다운로드/임포트 합니다.

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-elasticsearch

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

저는 gradle을 이용하였습니다.

이후 적절한 패키지아래 설정클래스를 만들어줍니다. 클러스터 환경이라면 Master 노드를 모두 입력해 줍니다. 혹은 2개 이상의 Elasticsearch 서버에서 데이터를 불러올 경우에는 새로운 빈을 등록하여 @Qualifier 어노테이션을 통해서 각각의 빈의 설정된 클러스터 서버에 접근이 가능합니다.

@Configuration
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {

    @Value("${ELASTICSEARCH.HOST}")
    private String host;  // 127.0.0.1:9200,127.0.0.2:9200

    @Override
    public ElasticsearchOperations elasticsearchOperations(ElasticsearchConverter elasticsearchConverter, RestHighLevelClient elasticsearchClient) {
        return new ElasticsearchRestTemplate(elasticsearchClient());
    }

    @Override
    public RestHighLevelClient elasticsearchClient() {
        ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo(host.split(","))
                .build();
        return RestClients.create(clientConfiguration).rest();
    }
}

추가한 라이브러리는 ElasticsearchOperations, RestHighLevelClient 을 이용하여 데이터를 검색, 수정, 삭제등을 수행할 수 있게 도와주고 있습니다.

아래처럼 도메인을 생성하여 데이터를 저장할 수 있고 연결된 엘라스틱서치의 index명을 통해서 데이터를 검색, 수정, 삭제 등... 작업을 할 수 있습니다. 참고로 엘라스틱서치는 기본으로 키워드 기반의 랭킹 알고리즘 BM25을 이용한 검색을 수행합니다. 알고리즘에 대해서 더 많은 내용을 알고싶다면 비슷한 검색/추천 알고리즘인 TF-IDF 라는 키워드를 알고 있으면 비교하는 문서가 많이 검색됩니다.

bm25

다시 Java를 이용하여 도메인 클래스를 생성합니다.

@Getter @Setter @ToString
@Document(indexName = "#{elasticsearchIndex}")
public class AnalysisTitle {

    @Id
    private String id;

    @Field(type = FieldType.Keyword)
    private String type;

    @Field(type = FieldType.Keyword)
    private String searchKeyword;
    
    @Field(type = FieldType.Keyword)
    private String title;

    @Field(type = FieldType.Long)
    private int count;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd")
    @Field(type = FieldType.Date, format = DateFormat.basic_date)
    private Date reg_dt;
}

ElasticsearchOperations 를 통해서 아래와 같은 예시를 통해서 데이터를 색인/검색 할 수 있습니다.

    public <T> void bulk(String indexName, List<T> documents, Class<T> tClass) {
        elasticsearchIndex.setIndexName(indexName);
        List<IndexQuery> queries = new ArrayList<>();
        for (T document : documents) {
            IndexQuery query = new IndexQueryBuilder()
                    .withObject(document)
                    .build();
            queries.add(query);
        }
        IndexOperations indexOps = elasticsearchTemplate.indexOps(tClass);
        //log.info(indexOps.getIndexCoordinates().getIndexName());
        if (!indexOps.exists()) {
            indexOps.create();
            indexOps.putMapping(indexOps.createMapping());
        }
        elasticsearchTemplate.bulkIndex(queries, IndexCoordinates.of(indexName));
    }

또한 RestHighLevelClient 를 이용하여 아래의 예시를 이용하여 데이터 검색/색인/삭제(가)이 가능합니다.

@Slf4j
@Repository
@RequiredArgsConstructor
public class ElasticsearchRepository {

    private final ElasticsearchOperations elasticsearchTemplate;
    private final RestHighLevelClient client;

	// bulk 색인
    public <T> void bulk(String indexName, List<T> documents, Class<T> tClass) {
        List<IndexQuery> queries = new ArrayList<>();
        for (T document : documents) {
            IndexQuery query = new IndexQueryBuilder()
                    .withObject(document)
                    .build();
            queries.add(query);
        }
        IndexOperations indexOps = elasticsearchTemplate.indexOps(tClass);
        if (!indexOps.exists()) {
            indexOps.create();
            indexOps.putMapping(indexOps.createMapping());
        }
        elasticsearchTemplate.bulkIndex(queries, IndexCoordinates.of(indexName));
    }
	
    // 검색
    public SearchResponse search(String indexName, SearchSourceBuilder searchSourceBuilder) throws IOException {
        SearchRequest searchRequest = getSearchRequest(indexName, searchSourceBuilder);
        return client.search(searchRequest, RequestOptions.DEFAULT);
    }
	
    // 스크롤 검색
    public SearchResponse searchWithScroll(String indexName, SearchSourceBuilder searchSourceBuilder) throws IOException {
        SearchRequest searchRequest = getSearchRequest(indexName, searchSourceBuilder);
        searchRequest.scroll(new Scroll(TimeValue.timeValueMinutes(120)));
        return client.search(searchRequest, RequestOptions.DEFAULT);
    }
	
    // 스크롤 클리어
    public ClearScrollResponse clearScroll(String scrollId) throws IOException {
        ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
        clearScrollRequest.addScrollId(scrollId);
        return client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
    }
	
    // 스크롤ID를 이용한 검색
    public SearchResponse scrollSearch(String scrollId) throws IOException {
        SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
        scrollRequest.scroll(new Scroll(TimeValue.timeValueMinutes(1L)));
        return client.scroll(scrollRequest, RequestOptions.DEFAULT);
    }

	// 색인
    public IndexResponse index(String indexName, String id, Map<String, Object> source) throws IOException {
        IndexRequest request = new IndexRequest(indexName)
                .id(id)
                .source(source);
        return client.index(request, RequestOptions.DEFAULT);
    }
	
    // 검색
    public GetResponse getById(String indexName, String id) throws IOException {
        GetRequest request = new GetRequest(indexName, id);
        return client.get(request, RequestOptions.DEFAULT);
    }

	// 업데이트
    public UpdateResponse update(String indexName, String id, Map<String, Object> source) throws IOException {
        UpdateRequest request = new UpdateRequest(indexName, id).doc(source);
        return client.update(request, RequestOptions.DEFAULT);
    }
	
    // 삭제
    public DeleteResponse deleteById(String indexName, String id) throws IOException {
        DeleteRequest request = new DeleteRequest(indexName, id);
        return client.delete(request, RequestOptions.DEFAULT);
    }
    
    private SearchRequest getSearchRequest(String indexName, SearchSourceBuilder searchSourceBuilder) {
        SearchRequest searchRequest = new SearchRequest("posts");
        searchRequest.indices(indexName).source(searchSourceBuilder);
        return searchRequest;
    }
}

이상으로 스프링부트를 이용한 엘라스틱서치 연동 및 검색/색인/삭제 방법에 대해서 알아보았습니다.

엉망진창

개인 블로그 입니다. 코딩, 맛집, 정부정책, 서비스, ~방법 등 다양한 정보를 소개합니다