Elasticsearch 활용한 검색서비스 만들기 3편(검색어 자동완성 기능 + 검색 고도화하기)

2025. 3. 14. 23:04·Backend/프로젝트
목차
  1. 1. 검색어 자동완성(추천 검색어) 기능 구현하기
  2. 1-1 ElasticsearchClient Gradle 의존성 추가
  3. 1-2 Config 파일에 ElasticsearchClient 빈 등록
  4. 1-3 Document클래스에 @JsonIgnoreProperties(ignoreUnknown = true) 추가
  5. 1-4 자동완성(추천 검색어)를 제공해주는 서비스 로직 구현
  6. 1-5 동작 테스트
  7. 2. 기존 방식의 한계
  8. 3. N-gram 분석기 적용하기
  9. 3-1 N-gram 분석기란?
  10. 3-2 N-gram분석기 설정
  11. 3-3 개선 결과 확인하기
  12. 4. 검색 고도화
  13. 4-1 BoolQuery란?
  14. 4-2 검색 메서드 구현
  15. 4-3 검색 쿼리를 생성하는 메서드 구현
  16. 4-4 필터 검색 컨트롤러 구현
  17. 4-5 결과 확인
반응형

2편에 이어 3편에서는 본격적으로 자동완성 기능과, 검색 기능을 고도화하는 작업을 진행해보겠습니다!

 

1. 검색어 자동완성(추천 검색어) 기능 구현하기

 

1-1 ElasticsearchClient Gradle 의존성 추가

ElasticsearchClient는 엘라스틱서치와 통신하기 위한 Java 클라이언트로 직관적인 빌더 패턴과 최신 기능을 제공합니다. 우선 build.gradle에 아래 의존성을 추가해줍니다.

implementation 'co.elastic.clients:elasticsearch-java:8.11.2'

 

 

1-2 Config 파일에 ElasticsearchClient 빈 등록

ElasticsearchClient를 사용하기 위해 @Bean으로 등록해야합니다.

ElasticsearchConfig 클래스에 아래 설정을 추가해줍니다.

    @Bean
    public ElasticsearchClient elasticsearchClient() {
        RestClient restClient = RestClient.builder(host).build();
        return new ElasticsearchClient(new RestClientTransport(restClient, new JacksonJsonpMapper()));
    }

 

 

1-3 Document클래스에 @JsonIgnoreProperties(ignoreUnknown = true) 추가

Elasticsearch의 응답에서 "_class" 필드는 일반적으로 객체의 타입을 나타내는 메타 데이터로 사용되며,  @JsonIgnoreProperties 또는 @JsonProperty 등의 어노테이션을 사용하여 이를 무시하거나 처리할 수 있습니다.

 

Document 클래스에 @JsonIgnoreProperties(ignoreUnknown = true)를 추가해줍니다.

@JsonIgnoreProperties(ignoreUnknown = true)

 

 

1-4 자동완성(추천 검색어)를 제공해주는 서비스 로직 구현

사용자가 입력한 검색어와 일치하는 데이터를 찾기 위해 matchPhrase() 쿼리를 사용합니다. title 필드에서 입력된 키워드와 유사한 값을 검색하여 최대 5개의 검색 결과를 반환합니다.

    public List<SearchAutoCompleteResponse> findAutoCompleteSuggestionByKeyword(String keyword) {
        try {
            SearchRequest searchRequest = new SearchRequest.Builder()
                    .index("tickets")
                    .query(q -> q
                            .matchPhrase(m -> m
                                    .field("title")
                                    .query(keyword)
                            )
                    )
                    .size(5)
                    .build();

            SearchResponse<SearchAutoCompleteResponse> searchResponse = elasticsearchClient.search(searchRequest, SearchAutoCompleteResponse.class);

            return searchResponse.hits().hits().stream()
                    .map(Hit::source)
                    .toList();
        } catch (IOException e) {
            log.error("Elasticsearch 자동완성 중 오류 발생: {}", e.getMessage(), e);
            return Collections.emptyList();
        }
    }
@Getter
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class SearchAutoCompleteResponse {

    @JsonProperty("title")
    private String title;
}

 

  • index("tickets") : Elasticsearch의 tickets 인덱스를 대상으로 검색 수행
  • matchPhrase() : 입력된 keyword와 유사한 title을 검색
  • size(5) : 최대 5개의 검색 결과 반환
    @GetMapping("/suggest")
    public ResponseEntity<List<SearchAutoCompleteResponse>> autoComplete(@RequestParam("keyword") String keyword) {
        return ResponseEntity.ok(ticketSearchService.findAutoCompleteSuggestionByKeyword(keyword));
    }

 

 

 

1-5 동작 테스트

이제 /suggest?keyword=모차르트와 같이 요청을 보내면, title에 "모차르트"가 포함된 검색어 목록이 반환되는 것을 확인할 수 있습니다.

 

 

2. 기존 방식의 한계

지금까지 사용자가 입력한 단어를 통해 자동완성 검색어 기능을 개발했습니다.

하지만 기존 방식에는 크나큰 한계가 있으니.... 아래 예시를 통해 확인해보겠습니다.

차르트, 혹은 모차라는 단어로 검색할 경우..빈 값을 반환하는 것을 확인할 수 있습니다. 그 이유는 무엇일까요?

Nori 분석기를 통해 확인해보겠습니다.

기존 Nori 분석기는 한글 형태소 단위로 분할하기 때문에 모차르트라는 단어는 그대로 "모차르트" 라는 하나의 단어로 인덱싱하게 됩니다.

그렇기 때문에 모차르트의 일부인 모차, 차르트로 검색해도 매칭되는 타이틀이 없어 빈 값을 반환하게 됩니다.

이 문제를 해결하기 위해 N-gram분석기를 적용해보겠습니다.

 

 

 

3. N-gram 분석기 적용하기

3-1 N-gram 분석기란?

N-gram 분석기는 문자열을 연속된 N개의 문자 단위로 분할하는 분석기입니다. 예를들어 "모차르트"라는 단어가 존재한다면?

"모차", "차르", "르트" 같이 여러 개의 토큰을 생성하여 검색 가능성을 높입니다. 단 인덱스의 크기가 증가한다는 단점이 있습니다.

 

3-2 N-gram분석기 설정

Elasticsearch의 인덱스 매핑을 수정하여 N-gram 분석기를 적용해줍니다.

{
  "analysis": {
    "tokenizer": {
      "custom-nori-tokenizer": {
        "type": "nori_tokenizer",
        "decompound_mode": "mixed"
      },
      "custom-ngram-tokenizer": {
        "type": "ngram",
        "min_gram": 2,
        "max_gram": 3,
        "token_chars": ["letter", "digit"]
      }
    },
    "analyzer": {
      "custom-nori-analyzer": {
        "type": "custom",
        "tokenizer": "custom-nori-tokenizer",
        "filter": [
          "lowercase",
          "trim",
          "nori_part_of_speech"
        ]
      },
      "custom-ngram-analyzer": {
        "type": "custom",
        "tokenizer": "custom-ngram-tokenizer",
        "filter": [
          "lowercase",
          "trim"
        ]
      }
    }
  }
}
{
  "properties": {
    "id": {
      "type": "long"
    },
    "title": {
      "type": "text",
      "analyzer": "custom-nori-analyzer",
      "fields": {
        "ngram": {
          "type": "text",
          "analyzer": "custom-ngram-analyzer"
        }
      }
    },
    "content": {
      "type": "text",
      "analyzer": "custom-nori-analyzer"
    },
    "runningTime": {
      "type": "text"
    },
    "startDate": {
      "type": "date",
      "format": "yyyy-MM-dd'T'HH:mm:ss.SSS||epoch_millis"
    },
    "endDate": {
      "type": "date",
      "format": "yyyy-MM-dd'T'HH:mm:ss.SSS||epoch_millis"
    },
    "place": {
      "type": "text",
      "analyzer": "custom-nori-analyzer"
    },
    "category": {
      "type": "keyword"
    }
  }
}

이제 검색할 때 title.ngram 필드를 대상으로 검색하면 부분 단어 일치의 경우도 자동완성 검색어를 확인할 수 있습니다.

 

3-3 개선 결과 확인하기

N-gram 적용 후 "모차", "차르트"로 검색해도 "모차르트"가 포함된 검색어가 반환되는 것을 확인할 수 있습니다.

 

 

4. 검색 고도화

기존에는 해당 티켓의 타이틀을 통해서만 검색 결과를 제공했습니다. 이번에는 검색할 경우 타이틀, 카테고리, 시작일자 등의 조건을 적용할 수 있도록 Elasticsearch의 BoolQuery를 활용하여 필터링 기능을 추가해보겠습니다.

 

4-1 BoolQuery란?

Elasticsearch의 BoolQuery는 여러 개의 검색 조건을 조합하여 보다 복합적인 검색을 수행할 수 있도록 도와줍니다.

  • must: 모든 조건을 충족해야 검색 결과에 포함됨 (AND 조건)
  • should: 하나 이상의 조건을 충족하면 검색 결과에 포함됨 (OR 조건)
  • filter: 필터링 조건을 적용하되 점수 계산에 영향을 미치지 않음

이러한 기능을 활용하면 검색어뿐만 아니라 카테고리, 날짜 필터링 등의 다양한 조건을 조합할 수 있습니다.

 

 

4-2 검색 메서드 구현

검색어, 카테고리, 공연 시작일, 공연 종료일을 기준으로 필터링하여 검색하는 메서드를 구현해줍니다.

public Page<TicketDocument> searchTicketsByFilter(String title, String category, LocalDateTime startDate, LocalDateTime endDate, int page, int size) {
        Pageable pageable = PageRequest.of(page, size);

        try {
            Query query = buildSearchQuery(title, category, startDate, endDate);
            SearchRequest searchRequest = SearchRequest.of(s -> s
                    .index("tickets")
                    .query(query)
                    .from(page * size)
                    .size(size)
            );

            SearchResponse<TicketDocument> searchResponse = elasticsearchClient.search(searchRequest, TicketDocument.class);

            List<TicketDocument> tickets = searchResponse.hits().hits().stream()
                    .map(Hit::source)
                    .toList();

            return new PageImpl<>(tickets, pageable, searchResponse.hits().total().value());

        } catch (IOException e) {
            log.error("Elasticsearch 검색 중 오류 발생: {}", e.getMessage(), e);
            return Page.empty(pageable);
        }
    }

 

 

4-3 검색 쿼리를 생성하는 메서드 구현

    private Query buildSearchQuery(String title, String category, LocalDateTime startDate, LocalDateTime endDate) {
        BoolQuery.Builder boolQuery = new BoolQuery.Builder();

        if (title != null) boolQuery.must(MatchQuery.of(m -> m.field("title.ngram").query(title))._toQuery());
        if (category != null) boolQuery.must(TermQuery.of(t -> t.field("category").value(category))._toQuery());

        if (startDate != null && endDate != null) {
            boolQuery.must(QueryBuilders.range().field("startDate")
                    .gte(JsonData.of(ELASTICSEARCH_DATE_FORMATTER.format(startDate)))
                    .lte(JsonData.of(ELASTICSEARCH_DATE_FORMATTER.format(endDate)))
                    .build()._toQuery());
        }
        return boolQuery.build()._toQuery();
    }

 

 

4-4 필터 검색 컨트롤러 구현

    @GetMapping()
    public Page<TicketDocument> searchTicketsByFilter(
            @RequestParam(required = false) String title,
            @RequestParam(required = false) String category,
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        return ticketSearchService.searchTicketsByFilter(title, category, startDate, endDate, page, size);
    }

 

 

4-5 결과 확인

tickets/search?category=뮤지컬

 

 

tickets/search?title=모차

 

 

tickets/search?title=모차&category=연극

 

필터 조건에 따른 티켓 정보 데이터를 정상적으로 가져오는 것을 확인할 수 있습니다.

반응형

'Backend > 프로젝트' 카테고리의 다른 글

K6 + InfluxDB + Grafana를 활용한 부하테스트 진행하기(docker)  (2) 2025.04.20
Elasticsearch 활용한 검색서비스 만들기 4편 (Elasticsearch Testcontainers 활용한 테스트)  (0) 2025.03.20
Elasticsearch 활용한 검색서비스 만들기 2편(feat. Spring Boot)  (0) 2025.03.12
Elasticsearch 활용한 검색 서비스 만들기 1편 (Docker로 Elasticsearch + Kibana 구축)  (0) 2025.03.10
무중단 배포 적용하기  (1) 2025.01.01
  1. 1. 검색어 자동완성(추천 검색어) 기능 구현하기
  2. 1-1 ElasticsearchClient Gradle 의존성 추가
  3. 1-2 Config 파일에 ElasticsearchClient 빈 등록
  4. 1-3 Document클래스에 @JsonIgnoreProperties(ignoreUnknown = true) 추가
  5. 1-4 자동완성(추천 검색어)를 제공해주는 서비스 로직 구현
  6. 1-5 동작 테스트
  7. 2. 기존 방식의 한계
  8. 3. N-gram 분석기 적용하기
  9. 3-1 N-gram 분석기란?
  10. 3-2 N-gram분석기 설정
  11. 3-3 개선 결과 확인하기
  12. 4. 검색 고도화
  13. 4-1 BoolQuery란?
  14. 4-2 검색 메서드 구현
  15. 4-3 검색 쿼리를 생성하는 메서드 구현
  16. 4-4 필터 검색 컨트롤러 구현
  17. 4-5 결과 확인
'Backend/프로젝트' 카테고리의 다른 글
  • K6 + InfluxDB + Grafana를 활용한 부하테스트 진행하기(docker)
  • Elasticsearch 활용한 검색서비스 만들기 4편 (Elasticsearch Testcontainers 활용한 테스트)
  • Elasticsearch 활용한 검색서비스 만들기 2편(feat. Spring Boot)
  • Elasticsearch 활용한 검색 서비스 만들기 1편 (Docker로 Elasticsearch + Kibana 구축)
여포개발자
여포개발자
여포개발자
어제보다 오늘 더
여포개발자
전체
오늘
어제
  • 분류 전체보기 (140)
    • Backend (41)
      • 프로젝트 (18)
      • MSA 전환 (10)
      • spring (6)
      • JPA (7)
    • JAVA (11)
    • Kotlin 정리 (11)
    • 알고리즘 (59)
      • 프로그래머스 LV0 (5)
      • 프로그래머스 LV1 (12)
      • 프로그래머스 LV2 (17)
      • 프로그래머스 LV3 (8)
      • 백준 (14)
      • 소프티어 (3)
    • 네트워크 (3)
    • Docker (3)
    • SQL (5)
    • Kafka (5)
    • 일상 (1)
    • .NET (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • #프로그래머스 #자바
  • Kotiln
  • 프로그래머스LV1
  • JAVA #프로그래머스 #LV0
  • docker
  • 프로그래머스
  • 네트워크
  • #JAVA #프로그래머스 #LV1 #모두화이팅
  • 자바 #백준
  • java
  • 백준
  • docker #MySQL
  • Kotlin
  • #JPA #JAVA
  • #프로그래머스
  • #JAVA #프로그래머스
  • JPA
  • 오블완
  • TroubleShooting #JPA
  • MSA
  • 모니터링
  • 프로젝트
  • HTTP
  • #JAVA #프로그래머스 #LV1
  • Spring
  • 티스토리챌린지

최근 댓글

최근 글

반응형
hELLO· Designed By정상우.v4.5.2
여포개발자
Elasticsearch 활용한 검색서비스 만들기 3편(검색어 자동완성 기능 + 검색 고도화하기)
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.