ElasticSearch - Based on the "Dark Horse Tourism" case, it implements search box, paging, conditional filtering, nearby hotels, and advertising top functions

Table of contents

1. Dark Horse Tourism Case

1.1. Implement search box and paging functions

1.1.1. Demand analysis

a) First search box requirements

b) Paging requirements

1.1.2. Define entity classes

1.1.2. Define controller

1.1.3. Inject RestHighLevelClient

1.1.4. Implement the search method of the IHotelService interface

1.1.5. Function display

1.2. Add filtering functions such as brand, city, star rating, price, etc.

1.2.1. Demand analysis

1.2.2. Add parameters to RequestParam

1.2.3. Modify the search method

1.3. Nearby hotels

1.3.1. Demand analysis

1.3.2. Add parameters to RequestParam

1.3.3. Modify the search method

1.3.4. Modify the parsing method

1.4. Advertising at the top (allowing designated hotels to be at the top of search rankings)

1.4.1. Demand analysis

1.4.2. Add attributes to HotelDoc entity class

1.4.3. Modify the query and use function score for sorting.


1. Dark Horse Tourism Case


1.1. Implement search box and paging functions

1.1.1. Demand analysis

a) First search box requirements

Enter "Home Inn" in the search box, then click Search. You can see that the following request was sent with the following parameters:

  • key: the content of the search box
  • page: page number.
  • size: How many pieces of data are displayed on one page.
  • sortBy: Sorting rules (default is sorted in descending order according to the matching degree of the query keyword)

Adjust to the request header and you can see that the request format is JSON, as shown below

Then the requirement is that when the user clicks on the search box, the backend will process it and then return the total number of data searched and the hotel data list (return format is JSON).

Ps: The response data and format are agreed in advance.

b) Paging requirements
After clicking pagination, the same request will be triggered, and the response format will be the same.

1.1.2. Define entity classes

Define entity classes for receiving front-end requests and returning responses.

The request entity class is as follows:

@Data
public class RequestParams {

    private String key;
    private Integer page;
    private Integer size;
    private String sortBy;

}

The response entity class is as follows:

@Data
public class PageResult {

    private Long total;
    private List<HotelDoc> hotels;

    public PageResult() {
    }

    public PageResult(Long total, List<HotelDoc> hotels) {
        this.total = total;
        this.hotels = hotels;
    }
    
}

1.1.2. Define controller

The Controller is responsible for receiving the JSON parameters passed in by the front end, and then calling the search method of the interface IHotelService.

@RestController
@RequestMapping("/hotel")
public class HotelController {

    @Autowired
    private IHotelService hotelService;

    @RequestMapping("/list")
    public PageResult search(@RequestBody RequestParams params) {
        return hotelService.search(params);
    }

}

The IHotelService interface is as follows: 


public interface IHotelService extends IService<Hotel> {
    PageResult search(RequestParams params);
}

1.1.3. Inject RestHighLevelClient

Inject the advanced client of ElasticSearch into the Spring container to facilitate subsequent implementation of search functions through es.

@Component
public class ESComponent {

    @Bean
    public RestHighLevelClient restHighLevelClient() {
        RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://140.143.166.138:9200")
        ));
        return client;
    }

}

1.1.4. Implement the search method of the IHotelService interface

The implementation idea here is the same as the JavaRestClient document operation steps mentioned in the previous chapter~

  1. Create a search request.
  2. Prepare request parameters, the specific parameters are as follows
    1. Build a match query through QueryBuilders (if the user does not enter and clicks search directly, then build match_all here to query all).
    2. Add paging data: from (offset offset) and size (number of data displayed).
  3. Send a request and receive a response.
  4. Parse the response to get the total number of queries and a list of hotel information.

code show as below:

@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {

    @Autowired
    private RestHighLevelClient client;

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public PageResult search(RequestParams params) {
        try {
            //1.创建请求
            SearchRequest request = new SearchRequest("hotel");
            //2.准备参数
            // 1) 查询
            String searchContent = params.getKey();
            if(!StringUtils.hasLength(searchContent)) {
                request.source().query(QueryBuilders.matchAllQuery());
            } else {
                request.source().query(QueryBuilders.matchQuery("all", searchContent));
            }
            // 2) 分页
            Integer page = params.getPage();
            Integer size = params.getSize();
            if(page == null || size == null) {
                throw new IOException("分页数据不能为空!");
            }
            request.source().from((page - 1) * size).size(size);
            //3.发送请求,接收响应
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            //4.解析响应
            return handlerResponse(response);
        } catch (IOException e) {
            System.out.println("[HotelService] 搜索失败!");
            e.printStackTrace();
            return null;
        }
    }

    private PageResult handlerResponse(SearchResponse response) throws JsonProcessingException {
        //1.解析结果
        SearchHits hits = response.getHits();
        long total = hits.getTotalHits().value;
        SearchHit[] hits1 = hits.getHits();
        List<HotelDoc> hotelDocList = new ArrayList<>();
        for(SearchHit searchHit : hits1) {
            //获取source
            String json = searchHit.getSourceAsString();
            hotelDocList.add(objectMapper.readValue(json, HotelDoc.class));
        }
        return new PageResult(total, hotelDocList);
    }

}

1.1.5. Function display

Enter the "Home Inn" keyword in the search box, click Search, and hotel data related to the "Home Inn" keyword will be displayed below.

1.2. Add filtering functions such as brand, city, star rating, price, etc.

1.2.1. Demand analysis

After adding the filter conditions as shown below to the search, click Search to display the filtered data.

Therefore, 5 parameters need to be added to the request:

  1. city: city
  2. starName: star
  3. brand: brand.
  4. minPrice: lower price limit.
  5. maxPrice: price limit.

The backend calculates the response data based on the request: the total number of searched data and the hotel data list.

1.2.2. Add parameters to RequestParam

@Data
public class RequestParams {

    private String key;
    private Integer page;
    private Integer size;
    private String sortBy;
    private String brand;
    private String starName;
    private String city;
    private Integer minPrice;
    private Integer maxPrice;

}

1.2.3. Modify the search method

According to the requirements, compound query (BoolQuery) can be used here.

The fields that need to be filtered are city, brand, starName, and price. The first three fields have one characteristic - inseparable words, all of which are keyword types, so you can use term to query accurately. And price can be queried using range.

Ps: In order to improve the readability of the code, I have encapsulated the query filtering logic into a method.

    private BoolQueryBuilder getHandlerBoolQueryBuilder(RequestParams params) {
        //使用 boolean 查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        // 1) 查询
        String searchContent = params.getKey();
        if(!StringUtils.hasLength(searchContent)) {
            boolQueryBuilder.must(QueryBuilders.matchAllQuery());
        } else {
            boolQueryBuilder.must(QueryBuilders.matchQuery("all", searchContent));
        }
        // 2) 城市过滤
        String city = params.getCity();
        if(StringUtils.hasLength(city)) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("city", city));
        }
        // 3) 品牌过滤
        String brand = params.getBrand();
        if(StringUtils.hasLength(brand)) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("brand", brand));
        }
        // 4) 星级过滤
        String star = params.getStarName();
        if(StringUtils.hasLength(star)) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("starName", star));
        }
        // 5) 价格范围过滤
        if(params.getMinPrice() != null && params.getMaxPrice() != null) {
            boolQueryBuilder.filter(QueryBuilders
                    .rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
        }
        return boolQueryBuilder;
    }

    @Override
    public PageResult search(RequestParams params) {
        try {
            //1.创建请求
            SearchRequest request = new SearchRequest("hotel");
            //2.准备参数
            BoolQueryBuilder boolQuery = getHandlerBoolQueryBuilder(params);
            request.source().query(boolQuery);
            //3.分页
            Integer page = params.getPage();
            Integer size = params.getSize();
            if(page == null || size == null) {
                throw new IOException("分页数据不能为空!");
            }
            request.source().from((page - 1) * size).size(size);
            //4.发送请求,接收响应
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            //5.解析响应
            return handlerResponse(response);
        } catch (IOException e) {
            System.out.println("[HotelService] 搜索失败!");
            e.printStackTrace();
            return null;
        }
    }

1.3. Nearby hotels

1.3.1. Demand analysis

When the user clicks on the positioning point on the mini map, they can automatically locate their location, and then display a list of nearby hotels and show the distance.

After clicking the positioning button, the front end will return the latitude and longitude information of your current location, as follows

Then you need to add a latitude and longitude parameter to the request.

1.3.2. Add parameters to RequestParam

Here you only need to add a String parameter to receive the latitude and longitude information.

@Data
public class RequestParams {

    private String key;
    private Integer page;
    private Integer size;
    private String sortBy;
    private String brand;
    private String starName;
    private String city;
    private Integer minPrice;
    private Integer maxPrice;
    private String location;

}

1.3.3. Modify the search method

You need to add logic for sorting by distance to the search method.

            //4.根据距离排序
            request.source().sort(SortBuilders.geoDistanceSort("location",
                    new GeoPoint(params.getLocation()))
                    .order(SortOrder.ASC)
                    .unit(DistanceUnit.KILOMETERS));

If you are not sure here, you can compare the DSL statements.

1.3.4. Modify the parsing method

It can be seen from the demand analysis that the hotel information also needs to display the information "xx km from you". Therefore, here we also need to parse the sorted "distance between the destination and your current location" attribute in the response. as follows:

Therefore, we also need to add an Object property to HotelDoc to represent the distance.

@Data
@NoArgsConstructor
public class HotelDoc {
    private Long id;
    private String name;
    private String address;
    private Integer price;
    private Integer score;
    private String brand;
    private String city;
    private String starName;
    private String business;
    private String location;
    private String pic;
    private Object distance;

    public HotelDoc(Hotel hotel) {
        this.id = hotel.getId();
        this.name = hotel.getName();
        this.address = hotel.getAddress();
        this.price = hotel.getPrice();
        this.score = hotel.getScore();
        this.brand = hotel.getBrand();
        this.city = hotel.getCity();
        this.starName = hotel.getStarName();
        this.business = hotel.getBusiness();
        this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
        this.pic = hotel.getPic();
    }
}

The parsing code is as follows: 

    private PageResult handlerResponse(SearchResponse response) throws JsonProcessingException {
        //1.解析结果
        SearchHits hits = response.getHits();
        long total = hits.getTotalHits().value;
        SearchHit[] hits1 = hits.getHits();
        List<HotelDoc> hotelDocList = new ArrayList<>();
        for(SearchHit searchHit : hits1) {
            //获取source
            String json = searchHit.getSourceAsString();
            HotelDoc hotelDoc = objectMapper.readValue(json, HotelDoc.class);
            Object[] sortValues = searchHit.getSortValues();
            //通过 getSortValues 得到的是一个 Object 类型数组,因为有可能是根据多个条件排序(价格、评价、距离...)
            //获取的下标,就要看你先给谁排序,谁就是 0 下标(以此类推)
            if(sortValues != null && sortValues.length > 0) {
                hotelDoc.setDistance(sortValues[0]);
            }
            hotelDocList.add(hotelDoc);
        }
        return new PageResult(total, hotelDocList);
    }

Ps: What you get through getSortValues ​​is an Object type array, because it may be sorted according to multiple conditions (price, evaluation, distance...). The subscript obtained depends on who you sort first, who will have a 0 subscript (and so on).

1.3.5. Demonstration effect

After clicking on the anchor point, it will be sorted in ascending order according to the distance from you, and then the corresponding hotel data will be displayed through paging.

Ps: The distance in the picture below is too far because I did not add hotels near my current location in the database (all across provinces). 

1.4. Advertising at the top (allowing designated hotels to be at the top of search rankings)

1.4.1. Demand analysis

After the user clicks on the search, the advertising data (specially marked hotel information) will be prioritized at the top.

Therefore, it is necessary to add a field to HotelDoc to mark whether the current hotel information exists as an advertisement, and then the backend assigns more weight to the advertising information to keep the advertising data at the top.

1.4.2. Add attributes to HotelDoc entity class

Add a Boolean type field to HotelDoc to mark whether the current hotel information exists as an advertisement.

@Data
@NoArgsConstructor
public class HotelDoc {
    private Long id;
    private String name;
    private String address;
    private Integer price;
    private Integer score;
    private String brand;
    private String city;
    private String starName;
    private String business;
    private String location;
    private String pic;
    private Object distance;
    private Boolean isAD;

    public HotelDoc(Hotel hotel) {
        this.id = hotel.getId();
        this.name = hotel.getName();
        this.address = hotel.getAddress();
        this.price = hotel.getPrice();
        this.score = hotel.getScore();
        this.brand = hotel.getBrand();
        this.city = hotel.getCity();
        this.starName = hotel.getStarName();
        this.business = hotel.getBusiness();
        this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
        this.pic = hotel.getPic();
    }
}

1.4.3. Modify the query and use function score for sorting.

Function score is used to filter the query. You can use term to accurately query whether the value of isAD is true. If so, modify the weight through weight (there is no weighting mode specified here, so the default is multiplication).

    private void HandlerBoolQueryBuilder(SearchRequest request, RequestParams params) {
        //1.使用 boolean 查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        // 1) 查询
        String searchContent = params.getKey();
        if(!StringUtils.hasLength(searchContent)) {
            boolQueryBuilder.must(QueryBuilders.matchAllQuery());
        } else {
            boolQueryBuilder.must(QueryBuilders.matchQuery("all", searchContent));
        }
        // 2) 城市过滤
        String city = params.getCity();
        if(StringUtils.hasLength(city)) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("city", city));
        }
        // 3) 品牌过滤
        String brand = params.getBrand();
        if(StringUtils.hasLength(brand)) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("brand", brand));
        }
        // 4) 星级过滤
        String star = params.getStarName();
        if(StringUtils.hasLength(star)) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("starName", star));
        }
        // 5) 价格范围过滤
        if(params.getMinPrice() != null && params.getMaxPrice() != null) {
            boolQueryBuilder.filter(QueryBuilders
                    .rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
        }

        //2.算分控制
        FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
                //原始查询,相关性算分查询
                boolQueryBuilder,
                //function score 的数组
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                        //其中的一个 function score 元素
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                //过滤条件
                                QueryBuilders.termQuery("isAD", true),
                                //算分函数
                                ScoreFunctionBuilders.weightFactorFunction(10)
                        )
                }
        );
        request.source().query(functionScoreQuery);
    }

Here you can see it corresponding to the DSL statement

Guess you like

Origin blog.csdn.net/CYK_byte/article/details/133314299