Spring Boot ElasticSearch – builder pattern and DTO search criteria object

S

Hi, and welcome to the 4th article devoted to the theme:  “How to work with ElasticSearch using Java Spring Boot”. Previous article (Part 3: Spring Boot – Front Controller and API documentation) is located here. At that article we will speak about how to transform our requests parameters at the DTO criteria object. That will create an abstract layer between the front world and our backend logic and will allow us to be flexible with coming changes from the front side or in case some additional outside integrations. Here we will use the builder pattern.

Builder design pattern
Builder design pattern

We will have a director class. You may treat it as the builder ‘s manager. And HoteSearchCriteria builder that will take url parameters and transform it to a DTO object. Let’s open our project and look at realization. All the classes responsible for transformation logic are located at the dependencies -> hotel_search_criteria folder.

HotelSearchCriteria

Here you see that we define abstract classes for our directors and builders. While looking at abstract criteria builder you can find get_criteria and create_criteria methods which are synonymous of getResult and and buildPart from builder design scheme presented above. Logic is the same – simply naming is adjusted for our requirements.

package com.udemy_sergii_java.spring_boot_es.dependencies.hotel_search_criteria;

import com.udemy_sergii_java.spring_boot_es.model.criteria.HotelSearchCriteria;

abstract class AbstractCriteriaBuilder {

    abstract HotelSearchCriteria getCriteria();

    abstract void createCriteria();
}

And here is our Director class

package com.udemy_sergii_java.spring_boot_es.dependencies.hotel_search_criteria;

import com.udemy_sergii_java.spring_boot_es.model.criteria.HotelSearchCriteria;

abstract class AbstractCriteriaDirector {
    protected AbstractCriteriaBuilder builder;

    public AbstractCriteriaDirector (AbstractCriteriaBuilder builder) {
        this.builder = builder;
    }

    abstract void buildCriteria();

    abstract HotelSearchCriteria getCriteria();
}

Now lets investigate realization of abstracts. In case HotelSearchCriteriaDirector – there is nothing special, it is rather simple logic. At real practice it can be more complicated and build final output using additional operations e.g getting some additional data form microservices.

package com.udemy_sergii_java.spring_boot_es.dependencies.hotel_search_criteria;

import com.udemy_sergii_java.spring_boot_es.model.criteria.HotelSearchCriteria;

public class HotelSearchCriteriaDirector extends AbstractCriteriaDirector {

    public HotelSearchCriteriaDirector(AbstractCriteriaBuilder builder) {
        super(builder);
    }

    @Override
    public void buildCriteria() {
        this.builder.createCriteria();
    }

    @Override
    public HotelSearchCriteria getCriteria() {
        return this.builder.getCriteria();
    }
}

Now lets have a look at our criteria builder realization

package com.udemy_sergii_java.spring_boot_es.dependencies.hotel_search_criteria;

import com.udemy_sergii_java.spring_boot_es.model.criteria.HotelSearchCriteria;
import com.udemy_sergii_java.spring_boot_es.model.request.SearchRequestModel;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;

public class HotelSearchCriteriaUrlBuilder extends AbstractCriteriaBuilder {

    private SearchRequestModel searchRequestModel;
    private HotelSearchCriteria hotelSearchCriteria;

    public HotelSearchCriteriaUrlBuilder(SearchRequestModel searchRequestModel) {
        this.searchRequestModel = searchRequestModel;
        this.hotelSearchCriteria = new HotelSearchCriteria();
    }

    @Override
    public HotelSearchCriteria getCriteria() {
        return this.hotelSearchCriteria;
    }

    @Override
    public void createCriteria() {
        if (searchRequestModel.getPage() >= 1) {
            this.hotelSearchCriteria.setPage(searchRequestModel.getPage() - 1);
        }

        this.hotelSearchCriteria.setHotelAge(searchRequestModel.getAge());
        this.hotelSearchCriteria.setCityName(searchRequestModel.getCity());
        this.hotelSearchCriteria.setHotelName(searchRequestModel.getHotel());
        this.hotelSearchCriteria.setFreePlacesAtNow(searchRequestModel.getFpn());

        if (searchRequestModel.getSize() <= HotelSearchCriteria.SIZE_MAX &&
            searchRequestModel.getSize() >= HotelSearchCriteria.SIZE_MIN
        ) {
            this.hotelSearchCriteria.setSize(searchRequestModel.getSize());
        }

        if (searchRequestModel.getLat() != null && searchRequestModel.getLng() != null) {
            hotelSearchCriteria.setGeoCoordinates(new GeoPoint(
                    searchRequestModel.getLat(),
                    searchRequestModel.getLng()
            ));
        }
    }
}

At constructor we a recreating our DTO object which is called HotelSearchCriteria. I created it as simple standalone model class. Though it also can be done as ORM entity which can be persisted at database at e.g for logging and further analytics.

package com.udemy_sergii_java.spring_boot_es.model.criteria;

import org.springframework.data.elasticsearch.core.geo.GeoPoint;

public class HotelSearchCriteria {
    public static final int SIZE_MIN = 1;
    public static final int SIZE_MAX = 20;

    private Integer page = 1;
    private Integer size = 10;
    private Boolean freePlacesAtNow = false;
    private String hotelName;
    private String cityName;
    private Integer hotelAge;
    private Integer hotelStars;
    private GeoPoint geoCoordinates;

    public Integer getPage() {
        return page;
    }

    public void setPage(Integer page) {
        this.page = page;
    }

    public Integer getSize() {
        return size;
    }

    public void setSize(Integer size) {
        this.size = size;
    }

    public Boolean getFreePlacesAtNow() {
        return freePlacesAtNow;
    }

    public void setFreePlacesAtNow(Boolean freePlacesAtNow) {
        this.freePlacesAtNow = freePlacesAtNow;
    }

    public String getHotelName() {
        return hotelName;
    }

    public void setHotelName(String hotelName) {
        this.hotelName = hotelName;
    }

    public String getCityName() {
        return cityName;
    }

    public void setCityName(String cityName) {
        this.cityName = cityName;
    }

    public Integer getHotelAge() {
        return hotelAge;
    }

    public void setHotelAge(Integer hotelAge) {
        this.hotelAge = hotelAge;
    }

    public Integer getHotelStars() {
        return hotelStars;
    }

    public void setHotelStars(Integer hotelStars) {
        this.hotelStars = hotelStars;
    }

    public GeoPoint getGeoCoordinates() {
        return geoCoordinates;
    }

    public void setGeoCoordinates(GeoPoint geoCoordinates) {
        this.geoCoordinates = geoCoordinates;
    }
}

Now lets return to our HotelSearchCriteriaUrlBuilder -> createCriteria method. It performs the main work and transforms the url parameters we get at request to DTO object. In that case it is rather simple transformations – small validation and map operations. But in real practice create criteria method can much be more complicated.

Now lets see how put together all that puzzle. Lets return to Front Controller – here I will represent only main method code without annotations (you may find whole code at previous article)

public ResponseEntity<List<SearchResponseModel>> search(
            @Validated @ModelAttribute SearchRequestModel searchRequestModel
    ) {
        try {
            HotelSearchCriteriaUrlBuilder builder = new HotelSearchCriteriaUrlBuilder(
                    searchRequestModel
            );
            HotelSearchCriteriaDirector director = new HotelSearchCriteriaDirector(
                     builder
            );
            director.buildCriteria();

            HotelSearchCriteria criteria = director.getCriteria();

            SearchPage<HotelBookingDocument> searchPage = searchService.search(criteria);
            Iterator<SearchHit<HotelBookingDocument>> iterator = searchPage.iterator();
            List<SearchResponseModel> results = new ArrayList<>();

            while (iterator.hasNext()) {
                HotelBookingDocument hotel = iterator.next().getContent();
                results.add(new SearchResponseModel(
                        hotel.getName(), hotel.getCityNameEn())
                );
            }

            return ResponseEntity.ok(results);

        } catch (Throwable t) {
            .....
        }

So, using builder pattern we are creating HotelSearchCriteria DTO object from request. HotelSearchCriteria would represent the interface for our search service, which should know nothing about where search parameters came from and how they were created. Out search service should perform only one responsibility – make a search using DTO parameters.

At the next lecture (Part5: Spring Boot ElasticSearch – model data layer) we will go further with creating our search microservice and speak about Elasticsearch model data layer. If you would like to pass all material more fast, then I propose you to view my on-line course at udemy where you will also find full project skeleton. Below is the link to the course. As the reader of that blog you are also getting possibility to use coupon for the best possible low price. Otherwise, please wait at next articles. Thank you for you attention.


architecture AWS cluster cyber-security devops devops-basics docker elasticsearch flask geo high availability java machine learning opensearch php programming languages python recommendation systems search systems spring boot symfony