Hibernate 검색을 사용하여 페이지가 매겨진 전체 텍스트 검색으로 Spring Boot REST API 구축

previous article에서 Hibernate Search를 사용하여 Spring Boot Rest API에 전체 텍스트 검색을 추가하는 방법을 배웠습니다.

이 기사에서는 이를 바탕으로 기존 REST API에 페이지가 매겨진 검색을 추가하는 방법을 알아봅니다.

프로젝트 설정



이전 블로그 게시물을 확인하여 Spring Initializer를 사용하여 프로젝트를 설정하는 방법에 대한 자세한 연습을 얻을 수 있습니다.

Github에 대한 마지막 기사의 최종 결과를 얻을 수도 있습니다.

데이터 모델 확장



가장 먼저 해결해야 할 일은 페이지 매김을 추가하는 데 필요한 새 데이터를 수신하는 방법을 찾는 것입니다.

이를 위해 SearchRequestDTO를 확장할 수 있습니다.

package com.mozen.springbootpaginatedsearch.model;

import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.validation.constraints.Min;

@Data
@EqualsAndHashCode(callSuper = true)
public class PageableSearchRequestDTO extends SearchRequestDTO{

    @Min(0)
    private int pageOffset;
}


새로운 단일 필드인 pageOffset만 정의하면 됩니다. 이 필드는 쿼리하려는 페이지의 인덱스를 제어하는 ​​데 사용됩니다.

또한 새 PageDTO를 정의합니다. 이 데이터 구조는 페이지가 매겨진 검색 결과를 저장하는 데 사용됩니다.

package com.mozen.springbootpaginatedsearch.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageDTO<T> {

    private List<T> content;
    private long total;
}


데이터 계층 확장



SearchRepository 인터페이스에서 새로운 searchPageBy 함수를 선언합니다.

package com.mozen.springbootpaginatedsearch.repository;

import com.mozen.springbootpaginatedsearch.model.PageDTO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;

import java.io.Serializable;
import java.util.List;

@NoRepositoryBean
public interface SearchRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {

    List<T> searchBy(String text, int limit, String... fields);

    PageDTO<T> searchPageBy(String text, int limit, int offset, String... fields);
}


시그니처는 기존 searchBy 함수와 매우 유사합니다. 쿼리할 페이지를 나타내는 새 오프셋 매개변수를 추가하기만 하면 됩니다.

이 변경 사항을 SearchRepositoryImpl 클래스에 복제합니다.

package com.mozen.springbootpaginatedsearch.repository;

import com.mozen.springbootpaginatedsearch.model.PageDTO;
import org.hibernate.search.engine.search.query.SearchResult;
import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.session.SearchSession;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.io.Serializable;
import java.util.List;

@Transactional
public class SearchRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID>
        implements SearchRepository<T, ID> {

    private final EntityManager entityManager;

    public SearchRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
        super(domainClass, entityManager);
        this.entityManager = entityManager;
    }

    public SearchRepositoryImpl(
            JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityManager = entityManager;
    }

    @Override
    public List<T> searchBy(String text, int limit, String... fields) {

        SearchResult<T> result = getSearchResult(text, limit, 0, fields);

        return result.hits();
    }

    @Override
    public PageDTO<T> searchPageBy(String text, int limit, int offset, String... fields) {
        SearchResult<T> result = getSearchResult(text, limit, offset, fields);

        return new PageDTO<T>(result.hits(), result.total().hitCount());
    }

    private SearchResult<T> getSearchResult(String text, int limit, int offset, String[] fields) {
        SearchSession searchSession = Search.session(entityManager);

        SearchResult<T> result =
                searchSession
                        .search(getDomainClass())
                        .where(f -> f.match().fields(fields).matching(text).fuzzy(2))
                        .fetch(offset, limit);
        return result;
    }
}


새로운 "오프셋"인수를 추가하여 기존 getSearchResult 메서드를 재사용할 수 있습니다. 그런 다음 우리는 Hibernate Search fetch() 메서드에서 이 인수를 사용합니다. 이 메서드는 페이지 매김 목적을 위해 오프셋 매개변수를 허용하는 서명을 이미 제공합니다.

PageDTO는 검색 쿼리의 결과를 사용하여 작성됩니다.

비즈니스 레이어 확장



중복을 피하기 위해 검색할 필드를 처리하는 부분을 추출하여 기존 논리를 기반으로 구축한 다음 searchPlant() 메서드를 사용하는지 searchPlantPage() 메서드를 사용하는지에 따라 페이지 매김을 사용하거나 사용하지 않고 저장소 기능을 호출할 수 있습니다.

package com.mozen.springbootpaginatedsearch.service;

import com.mozen.springbootpaginatedsearch.model.PageDTO;
import com.mozen.springbootpaginatedsearch.model.Plant;
import com.mozen.springbootpaginatedsearch.repository.PlantRepository;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

@Service
public class PlantService {

    private PlantRepository plantRepository;

    private static final List<String> SEARCHABLE_FIELDS = Arrays.asList("name","scientificName","family");

    public PlantService(PlantRepository plantRepository) {
        this.plantRepository = plantRepository;
    }

    public List<Plant> searchPlants(String text, List<String> fields, int limit) {

        List<String> fieldsToSearchBy = getFieldsToSearchBy(fields);

        return plantRepository.searchBy(
                text, limit, fieldsToSearchBy.toArray(new String[0]));
    }

    public PageDTO<Plant> searchPlantPage(String text, List<String> fields, int limit, int pageOffset) {
        List<String> fieldsToSearchBy = getFieldsToSearchBy(fields);

        return plantRepository.searchPageBy(
                text, limit, pageOffset, fieldsToSearchBy.toArray(new String[0]));
    }

        // We extract the common logic in a separate function
    private List<String> getFieldsToSearchBy(List<String> fields) {
        List<String> fieldsToSearchBy = fields.isEmpty() ? SEARCHABLE_FIELDS : fields;

        boolean containsInvalidField = fieldsToSearchBy.stream(). anyMatch(f -> !SEARCHABLE_FIELDS.contains(f));

        if(containsInvalidField) {
            throw new IllegalArgumentException();
        }
        return fieldsToSearchBy;
    }
}


웹 레이어 확장



이것에서 할 일이 많지 않습니다.

새 PageableSearchRequestDTO를 사용하고 PageDTO를 반환하여 페이지가 매겨진 검색 요청을 수신하려면 새 끝점이 필요합니다.

package com.mozen.springbootpaginatedsearch.controller;

import com.mozen.springbootpaginatedsearch.model.PageDTO;
import com.mozen.springbootpaginatedsearch.model.PageableSearchRequestDTO;
import com.mozen.springbootpaginatedsearch.model.Plant;
import com.mozen.springbootpaginatedsearch.model.SearchRequestDTO;
import com.mozen.springbootpaginatedsearch.service.PlantService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Slf4j
@RestController
@RequestMapping("/plant")
public class PlantController {

    private PlantService plantService;

    public PlantController(PlantService plantService) {
        this.plantService = plantService;
    }

    @GetMapping("/search")
    public List<Plant> searchPlants(SearchRequestDTO searchRequestDTO) {

        log.info("Request for plant search received with data : " + searchRequestDTO);

        return plantService.searchPlants(searchRequestDTO.getText(), searchRequestDTO.getFields(), searchRequestDTO.getLimit());
    }

    @GetMapping("/search/page")
    public PageDTO<Plant> searchPlantPage(PageableSearchRequestDTO pageableSearchRequestDTO) {

        log.info("Request for plant page search received with data : " + pageableSearchRequestDTO);

        return plantService.searchPlantPage(pageableSearchRequestDTO.getText(), pageableSearchRequestDTO.getFields(), pageableSearchRequestDTO.getLimit(), pageableSearchRequestDTO.getPageOffset());
    }
}


수신된 요청 데이터를 기록하고 plantService에 정의된 새 함수를 호출합니다.

함께 모아서



코드를 테스트할 시간입니다!

명령줄로 애플리케이션을 시작할 수 있습니다.

mvn spring-boot:run


첫 번째 기사와 유사하게 Postman을 사용할 수 있습니다.



또는 간단한 cUrl 명령을 사용할 수 있습니다.

// Request page 1 with 2 items per page on all fields

curl -X GET 'http://localhost:9000/plant/search?text=cherry&limit=2&pageOffset=1'

// Request page 2 with 3 items per page on scientificName field

curl -X GET 'http://localhost:9000/plant/search?text=asian&limit=3&fields=name&fields=scientificName&pageOffset=2'


그리고 끝났습니다! 전체 텍스트 검색 구현은 이제 페이지 매김을 지원합니다.

구현에 추가할 수 있는 것이 여전히 많이 있으며 다음 기사에서 추가할 것입니다.

여기에서 이 블로그 게시물의 데모 프로젝트에 액세스할 수 있습니다https://github.com/Mozenn/spring-boot-paginated-search..

좋은 웹페이지 즐겨찾기