Создание Spring Boot REST API с постраничным полнотекстовым поиском с помощью Hibernate Search

В предыдущей статье мы узнали, как добавить полнотекстовый поиск в Spring Boot Rest API с помощью Hibernate Search.

В этой статье мы будем развивать эту идею и узнаем, как добавить постраничный поиск в существующий 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;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Расширение слоя данных

Мы объявляем новую функцию searchPageBy в интерфейсе SearchRepository.

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, добавив новый аргумент «offset». Затем мы используем этот аргумент в методе 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.

Оцените статью
Procodings.ru
Добавить комментарий