저고데

[ElasticSearch] 4. Spring Page를 통해 검색 결과 페이지로 만들기 본문

ElasticSearch

[ElasticSearch] 4. Spring Page를 통해 검색 결과 페이지로 만들기

진철 2024. 1. 31. 20:57
728x90
반응형
지난 이야기

우선 지난 시간의 문제점에 대해서 잠깐 언급하고 시작하겠다.

1. "이름" 검색은 이상이 없지만 "나이" 검색은 오류가 발생한다.

2. 검색 결과가 모두 출력되어서 가독성이 낮다.

3. "temp12"로 검색했을 때는 괜찮지만 "temp"로 검색했을 때는 오류가 발생한다.

이제 본격적으로 문제 해결과정에 대해서 알아보자!

이름 검색 문제 해결하기
@Repository
public interface UserRepository extends ElasticsearchRepository<User, String> {
    List<User> findByName(String name);
    @Query("{\"wildcard\":{\"name.keyword\":\"*?0*\"}}")
    List<User> findByNameWildcard(String name);
	
    @Query("{\"wildcard\":{\"age.keyword\":\"*?0*\"}}")
    List<User> findByAge(int age);
}

엘라스틱 서치의 레포지토리 코드를 살펴보면 @Query 어노테이션을 통해서 검색어와 완전 일치하지 않아도 검색어가 포함이 되어있으면, 해당 결과를 반환한다.

Python에서 "apple"과 "app"이 서로 몇 개가 같은지를 판단할 때, 둘을 for문으로 쪼개서 a, p, p, l, e과 a, p, p으로 비교하는 것처럼

@Query 역시 단어를 나누어서 유사도를 판단한다.

하지만, 현재 나이 검색은 int형이기 때문에 단어 하나하나를 쪼갤 수가 없다고 판단하여 완전히 동일한 것만 반환하게 코드를 수정하였다.

@Repository
public interface UserRepository extends ElasticsearchRepository<User, String> {
    List<User> findByName(String name);
    @Query("{\"wildcard\":{\"name.keyword\":\"*?0*\"}}")
    List<User> findByNameWildcard(String name);
	
    List<User> findByAge(int age);
}

그 결과, 500에러가 발생하던 이전과 달리 정상적으로 잘 작동하는 것을 확인할 수가 있었다.

Page를 사용하여 검색 결과를 좀 더 깔끔하게 출력하기

실제로 게시판에서 검색을 하면, 하나가 아닌 여러 개의 결과가 나올 때도 있다.

게시판마다 다르겠지만, 특정 갯수만 화면에 출력되고 밑의 페이지를 통해서 다른 검색 결과를 볼 수 있다.

이는 스프링에서 제공하는 Page 기능을 통해 구현하였다.

@Repository
public interface UserRepository extends ElasticsearchRepository<User, String> {
    List<User> findByName(String name);
    @Query("{\"wildcard\":{\"name.keyword\":\"*?0*\"}}")
    Page<User> findByNameWildcard(String name, Pageable pageable);

    Page<User> findByAge(int age, Pageable pageable);
}

우선 Repository, Service, Controller의 List 부분을 모두 Page로 수정하고 Pageable 객체를 파라미터로 추가해준다.

Pageable은 앞서 언급했듯이 Spring에서 제공하는 Page 처리를 위한 인터페이스이다. 
이는 메소드 시그니처에 인자로 전달되어서 이름 그대로 페이지가 가능하게 해준다.

@GetMapping("/search")
public String search(
        @RequestParam String query,
        @RequestParam String searchType,
        @RequestParam(defaultValue = "0") int page,
        Model model) {

    int pageSize = 10;
    Pageable pageable = PageRequest.of(page, pageSize);

    if (searchType.equals("name") && !query.isEmpty()) {
        Page<User> userPage = userService.findByNameWildcard(query, pageable);
        List<User> users = userPage.getContent();

        model.addAttribute("users", users);
        model.addAttribute("currentPage", page);
        model.addAttribute("totalPages", userPage.getTotalPages());
    } else if (searchType.equals("age") && !query.isEmpty()) {
        Page<User> userPage = userService.findByAge(Integer.parseInt(query), pageable);
        List<User> users = userPage.getContent();

        model.addAttribute("users", users);
        model.addAttribute("currentPage", page);
        model.addAttribute("totalPages", userPage.getTotalPages());
    }

    return "home";
}

가장 핵심이라고 할 수 있는 Controller 코드이다.

pageable 변수는 PageRequest로 선언하였고 한 페이지에 보이는 데이터 갯수 등과 같이 원하는 설정을 추가하였다.

<div th:if="${totalPages > 1}">
    <div>
        <span th:each="pageNumber : ${#numbers.sequence(0, totalPages - 1)}">
            <a th:href="@{/search(query=${query}, searchType=${searchType}, page=${pageNumber})}"
               th:text="${pageNumber + 1}"
               th:class="${pageNumber == currentPage ? 'current-page' : ''}"></a>
        </span>
    </div>
</div>

마지막으로 home.html에 page를 전달하여 each 문법을 사용해 페이지마다 10개의 데이터가 보이도록 코드를 작성하였다.

Page 또한, 반복문처럼 1이 아닌 0부터 시작이기 때문에 이를 주의해야한다.

그 결과 !

이전과 달리, 모든 결과값을 한 페이지에 우겨(?)넣지 않아서 꽤나 깔끔해졌다.

하지만 또 다른 문제점이 발생했다.

속상했다.

하단의 2페이지를 누르는 순간 .. (사진에는 page=1이 되어있지만, 사실 +1를 빼먹어서 그런겁니다 하하 ..)

검색 결과가 보이지 않는 불상사가 발생하였다 ㅠ

하지만 ! url을 보면 "http://127.0.0.1:8080/search?query=&searchType=&page=1"이런 식으로 되어있다.

즉, page 숫자만 제대로 입력되고 query와 searchType이 없는 것을 확인할 수 있다.

@GetMapping("/search")
public String search(
	// 생략 //
        model.addAttribute("query", query);
        model.addAttribute("searchType", searchType);
    } else if (searchType.equals("age") && !query.isEmpty()) {
	// 생략 //
        model.addAttribute("query", query);
        model.addAttribute("searchType", searchType);
    }

    return "home";
}

간단하게 Model 객체에 query, searchType을 전달함으로써, 문제를 해결할 수 있었다.

"temp"로 검색했을 때, 오류 발생의 원인은 ?

정말 다행히게도 page 기능을 구현하고 나서, temp를 검색했을 때는 이상 없이 잘 작동하였다.

thymeleaf에서는 1만 개 이상의 데이터를 한 화면에 출력할 때 한계가 있거나,

반대로 엘라스틱 서치에서 1만 개 이상의 데이터를 반환할 때 데이터 용량의 한계가 있지 않나 생각이 든다.

(하지만, 쿠팡이나 카카오 같은 수 억 개에 달하는 데이터 역시 엘라스틱 서치로 사용한다고 알고 있는데, 아마 전자가 원인이지 않나 조심스레 추측해본다.)

 

아무튼!

이번 시간에는 page를 통해서 검색 결과를 조금 더 가독성 있게 출력해보았다.

엘라스틱 서치와 같은 검색 엔진에 데이터를 넣을 때는 비동기 처리를 이용하는 것으로 알고 있다. (유튜브에서 봤음.)

다음 시간에는 쇼핑몰과 같은 웹 서비스에서 물품을 등록하고 그 물품 데이터가 어떻게 비동기 처리로 엘라스틱 서치 DB에 저장되는지 한 번 아키텍쳐를 생각해보도록 하겠다.(비동기 처리 공부한다는 소리임.)

728x90
반응형