【ElasticSearch】ES案例:旅游酒店搜索

文章目录

  • 一、项目分析
  • 二、需求1:酒店搜索功能
  • 三、需求2:添加过滤功能
  • 四、需求3:我附近的酒店
  • 五、需求4:置顶花广告费的酒店

一、项目分析

启动hotel-demo项目,访问localhost:servicePort,即可访问static下的index.html:

【ElasticSearch】ES案例:旅游酒店搜索_第1张图片

从页面分析,我们需要实现搜索、分页、排序等功能。点击页面,可以看到list接口的传参为:

【ElasticSearch】ES案例:旅游酒店搜索_第2张图片

二、需求1:酒店搜索功能

接下来实现酒店搜索功能,完成关键字搜索和分页。

  • 定义接参的Dto类
@Data
public class RequestParam {

    private String key;

    private Integer page;  //pageNum

    private Integer size;  //pageSize

    private String sortBy;
}


  • 定义返回的结果类
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PageResult {

    private Long total;

    private List<HotelDoc>  hotelDocList;
}
  • 定义controller接口,接收页面请求
@RestController
@RequestMapping("/hotel")
public class HotelSearchController {

    @Resource
    IHotelService hotelService;

    @PostMapping("/list")
    public PageResult searchHotel(@RequestBody RequestParam requestParam){
        return hotelService.search(requestParam);
    }

}
  • Service层要用到JavaRestHighLevelClient对象,在启动类中定义这个Bean
@SpringBootApplication
public class HotelDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(HotelDemoApplication.class, args);
    }

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

}
  • 完成Service层,利用match查询实现根据关键字搜索酒店信息
public interface IHotelService extends IService<Hotel> {
    PageResult search(RequestParam requestParam);
}
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {

    @Resource
    RestHighLevelClient client;  //注入客户端操作的bean

    @Override
    public PageResult search(RequestParam requestParam) {
        try {
            SearchRequest request = new SearchRequest("hotel");
            //搜索关键字
            String key = requestParam.getKey();
            if (StringUtils.isNotEmpty(key)) {   //有key就走全文检索
                request.source().query(QueryBuilders.matchQuery("all", key));
            } else {   //没key就走查所有
                request.source().query(QueryBuilders.matchAllQuery());
            }
            //分页
            request.source().from((requestParam.getPage() - 1) * requestParam.getSize())    //(pageNum-1)*pageSize
                    .size(requestParam.getSize());
                    //发送请求
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            //处理响应结果
            return handleResponse(response);
        } catch (IOException e) {
            throw new RuntimeException();
        }

    }
	
	//处理响应结果的方法
    private PageResult handleResponse(SearchResponse response) {
        SearchHits searchHits = response.getHits();
        long total = searchHits.getTotalHits().value;
        SearchHit[] hits = searchHits.getHits();
        //Stream流将hits中的每条数据都转为HotelDoc对象
        List<HotelDoc> hotelDocList = Arrays.stream(hits).map(t -> {
            String json = t.getSourceAsString();
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            return hotelDoc;
        }).collect(Collectors.toList());
        return new PageResult(total, hotelDocList);
    }
}

重启服务,搜索和分页已实现。

【ElasticSearch】ES案例:旅游酒店搜索_第3张图片

三、需求2:添加过滤功能

接下来添加品牌、城市、星级、价格的过滤功能。这里参与搜索的条件对应着不同的搜索类型,有全文检索,有精确查找,自然要用复合查询Boolean Search

【ElasticSearch】ES案例:旅游酒店搜索_第4张图片

  • 修改接参dto类:
@Data
public class RequestParam {

    private String key;

    private Integer page;  //pageNum

    private Integer size;  //pageSize

    private String sortBy;

	private String brand;  
	  
	private String starName; 
	   
	private String city;    
	
	private Integer minPrice;    
	
	private Integer maxPrice;

}
  • 修改Service层实现,这里把搜索条件的构建单独抽取成方法,一来方便后面复用,二来让代码看着清爽点
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {

    @Resource
    RestHighLevelClient client;

    @Override
    public PageResult search(RequestParam requestParam) {
        try {
            //准备request
            SearchRequest request = new SearchRequest("hotel");
            //构建查询条件
            buildBasicQuery(requestParam, request);
            //分页
            request.source().from((requestParam.getPage() - 1) * requestParam.getSize())
                    .size(requestParam.getSize());
            //发送请求
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            //处理响应结果
            return handleResponse(response);
        } catch (IOException e) {
            throw new RuntimeException();
        }

    }

    private void buildBasicQuery(RequestParam requestParam, SearchRequest request) {
        BoolQueryBuilder booleanQuery = QueryBuilders.boolQuery();
        //关键字
        String key = requestParam.getKey();
        if (! isEmpty(key)) {
            booleanQuery.must(QueryBuilders.matchQuery("all", key));
        } else {
            booleanQuery.must(QueryBuilders.matchAllQuery());
        }
        //城市
        if (! isEmpty(requestParam.getCity())) {
            booleanQuery.filter(QueryBuilders.termQuery("city", requestParam.getCity()));
        }
        //品牌
        if (! isEmpty(requestParam.getBrand())) {
            booleanQuery.filter(QueryBuilders.termQuery("brand", requestParam.getBrand()));
        }
        //星级
        if (! isEmpty(requestParam.getStarName())) {
            booleanQuery.filter(QueryBuilders.termQuery("startName", requestParam.getStarName()));
        }
        //价格
        if (requestParam.getMaxPrice() != null && requestParam.getMinPrice() != null) {
            booleanQuery.filter(QueryBuilders.rangeQuery("price")
                    .lte(requestParam.getMaxPrice())
                    .gte(requestParam.getMinPrice()));
        }
        request.source().query(booleanQuery);


    }

    private static boolean isEmpty(String str){
        return str == null || "".equals(str);
    }
}

【ElasticSearch】ES案例:旅游酒店搜索_第5张图片

四、需求3:我附近的酒店

前端页面点击定位后,会将你所在的位置发送到后台:
【ElasticSearch】ES案例:旅游酒店搜索_第6张图片
接下来实现根据这个坐标,将酒店结果按照到这个点的距离升序排序。
【ElasticSearch】ES案例:旅游酒店搜索_第7张图片

距离排序与普通字段排序有所差异,对比如下:

【ElasticSearch】ES案例:旅游酒店搜索_第8张图片

开始实现需求:

  • 修改RequestParams参数,接收location字段
@Data
public class RequestParam {

    private String key;

    private Integer page;  //pageNum

    private Integer size;  //pageSize

    private String sortBy;

	private String brand;  
	  
	private String starName; 
	   
	private String city;    
	
	private Integer minPrice;    
	
	private Integer maxPrice;

	private String location;  //经纬度位置
}
  • 修改Service中,在分页前加排序逻辑
@Override
public PageResult search(RequestParam requestParam) {
    try {
        //准备request
        SearchRequest request = new SearchRequest("hotel");
        //构建查询条件
        buildBasicQuery(requestParam, request);
        //排序
        String myLocation = requestParam.getLocation();
        if(! isEmpty(myLocation)){
            request.source().sort(SortBuilders
                    .geoDistanceSort("location",new GeoPoint(myLocation))
                    .order(SortOrder.ASC)
                    .unit(DistanceUnit.KILOMETERS));
        }
        //分页
        request.source().from((requestParam.getPage() - 1) * requestParam.getSize())
                .size(requestParam.getSize());
        //发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //处理响应结果
        return handleResponse(response);
    } catch (IOException e) {
        throw new RuntimeException();
    }

}

但此时发现返回结果中少了距离你xxx千米的信息:

【ElasticSearch】ES案例:旅游酒店搜索_第9张图片

查看DSL返回结果,看到距离是在sort字段中:

【ElasticSearch】ES案例:旅游酒店搜索_第10张图片

因此需要修改结果处理的方法,且最后pageResult中是HotelDoc对象的集合,因此,修改Hoteldoc类,加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();
    }
}
private PageResult handleResponse(SearchResponse response) {
    SearchHits searchHits = response.getHits();
    long total = searchHits.getTotalHits().value;
    SearchHit[] hits = searchHits.getHits();
    List<HotelDoc> hotelDocList = Arrays.stream(hits).map(t -> {
        String json = t.getSourceAsString();
        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        //开始加入距离
        Object[] sortValues = t.getSortValues();   //排序字段可能不止一个
        if(sortValues.length > 0 ){
            Object sortValue = sortValues[0];
            hotelDoc.setDistance(sortValue); 拿到sort值赋值给距离
        }
        return hotelDoc;
    }).collect(Collectors.toList());
    return new PageResult(total, hotelDocList);
}

到此,需求实现:

【ElasticSearch】ES案例:旅游酒店搜索_第11张图片

五、需求4:置顶花广告费的酒店

实现让指定的酒店在搜索结果中排名置顶:

【ElasticSearch】ES案例:旅游酒店搜索_第12张图片
实现思路为:

  • HotelDoc类添加标记字段isAD,Boolean类型
  • 对于出广告费的酒店,isAD为true,前端可用这个字段给酒店打广告标签
  • 使用function score给花钱的酒店人为增加权重,干涉排序

代码实现:

  • hotelDoc类:
@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;
  • 更新ES数据,模拟某酒店出广告费

【ElasticSearch】ES案例:旅游酒店搜索_第13张图片

  • 加入function score算分认为控制,给isAD为true的加权
private void buildBasicQuery(RequestParam requestParam, SearchRequest request) {
    //BoolQuery原始查询条件,原始算分
    BoolQueryBuilder booleanQuery = QueryBuilders.boolQuery();
    //关键字
    String key = requestParam.getKey();
    if (!isEmpty(key)) {
        booleanQuery.must(QueryBuilders.matchQuery("all", key));
    } else {
        booleanQuery.must(QueryBuilders.matchAllQuery());
    }
    //城市
    if (!isEmpty(requestParam.getCity())) {
        booleanQuery.filter(QueryBuilders.termQuery("city", requestParam.getCity()));
    }
    //品牌
    if (!isEmpty(requestParam.getBrand())) {
        booleanQuery.filter(QueryBuilders.termQuery("brand", requestParam.getBrand()));
    }
    //星级
    if (!isEmpty(requestParam.getStarName())) {
        booleanQuery.filter(QueryBuilders.termQuery("startName", requestParam.getStarName()));
    }
    //价格
    if (requestParam.getMaxPrice() != null && requestParam.getMinPrice() != null) {
        booleanQuery.filter(QueryBuilders.rangeQuery("price")
                .lte(requestParam.getMaxPrice())
                .gte(requestParam.getMinPrice()));
    }
    
    //function score算分控制
    FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
            booleanQuery,  //第一个参数传入booleanQuery为原始查询,对应原始的相关性算分
            new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{  //第二个形参,function score数组,里面有个function score元素
                    new FunctionScoreQueryBuilder.FilterFunctionBuilder(  //function score元素对象,第一个参数传入筛选字段
                            QueryBuilders.termQuery("isAD", true),   //不再用酒店品牌筛选,而是isAD字段
                            ScoreFunctionBuilders.weightFactorFunction(10)  //算分函数,用默认的乘法,权重为10
                    )
            });
    request.source().query(functionScoreQuery);


}

实现效果;

【ElasticSearch】ES案例:旅游酒店搜索_第14张图片


Function Score查询可以控制文档的相关性算分,使用方式如下:

【ElasticSearch】ES案例:旅游酒店搜索_第15张图片

最后贴上以上四个需求Service层代码:

import cn.itcast.hotel.domain.dto.RequestParam;
import cn.itcast.hotel.domain.pojo.HotelDoc;
import cn.itcast.hotel.domain.vo.PageResult;
import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.domain.pojo.Hotel;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;


import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

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

    @Resource
    RestHighLevelClient client;

    @Override
    public PageResult search(RequestParam requestParam) {
        try {
            //准备request
            SearchRequest request = new SearchRequest("hotel");
            //构建查询条件
            buildBasicQuery(requestParam, request);
            //排序
            String myLocation = requestParam.getLocation();
            if (!isEmpty(myLocation)) {
                request.source().sort(SortBuilders
                        .geoDistanceSort("location", new GeoPoint(myLocation))
                        .order(SortOrder.ASC)
                        .unit(DistanceUnit.KILOMETERS));
            }
            //分页
            request.source().from((requestParam.getPage() - 1) * requestParam.getSize())
                    .size(requestParam.getSize());
            //发送请求
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            //处理响应结果
            return handleResponse(response);
        } catch (IOException e) {
            throw new RuntimeException();
        }

    }

    private void buildBasicQuery(RequestParam requestParam, SearchRequest request) {
        //BoolQuery原始查询条件,原始算分
        BoolQueryBuilder booleanQuery = QueryBuilders.boolQuery();
        //关键字
        String key = requestParam.getKey();
        if (!isEmpty(key)) {
            booleanQuery.must(QueryBuilders.matchQuery("all", key));
        } else {
            booleanQuery.must(QueryBuilders.matchAllQuery());
        }
        //城市
        if (!isEmpty(requestParam.getCity())) {
            booleanQuery.filter(QueryBuilders.termQuery("city", requestParam.getCity()));
        }
        //品牌
        if (!isEmpty(requestParam.getBrand())) {
            booleanQuery.filter(QueryBuilders.termQuery("brand", requestParam.getBrand()));
        }
        //星级
        if (!isEmpty(requestParam.getStarName())) {
            booleanQuery.filter(QueryBuilders.termQuery("startName", requestParam.getStarName()));
        }
        //价格
        if (requestParam.getMaxPrice() != null && requestParam.getMinPrice() != null) {
            booleanQuery.filter(QueryBuilders.rangeQuery("price")
                    .lte(requestParam.getMaxPrice())
                    .gte(requestParam.getMinPrice()));
        }

        //function score算分控制
        FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
                booleanQuery,  //第一个参数传入booleanQuery为原始查询,对应原始的相关性算分
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{  //第二个形参,function score数组,里面有个function score元素
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(  //function score元素对象,第一个参数传入筛选字段
                                QueryBuilders.termQuery("isAD", true),   //不再用酒店品牌筛选,而是isAD字段
                                ScoreFunctionBuilders.weightFactorFunction(10)  //算分函数,用默认的乘法,权重为10
                        )
                });
        request.source().query(functionScoreQuery);


    }

    private PageResult handleResponse(SearchResponse response) {
        SearchHits searchHits = response.getHits();
        long total = searchHits.getTotalHits().value;
        SearchHit[] hits = searchHits.getHits();
        List<HotelDoc> hotelDocList = Arrays.stream(hits).map(t -> {
            String json = t.getSourceAsString();
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            //开始加入距离
            Object[] sortValues = t.getSortValues();
            if (sortValues.length > 0) {
                Object sortValue = sortValues[0];
                hotelDoc.setDistance(sortValue);
            }
            return hotelDoc;
        }).collect(Collectors.toList());
        return new PageResult(total, hotelDocList);
    }

    private static boolean isEmpty(String str) {
        return str == null || "".equals(str);
    }
}

最后,页面上其他地方的需求实现思路:

排序:

【ElasticSearch】ES案例:旅游酒店搜索_第16张图片
前端会传递sortBy参数,就是排序方式,后端需要判断sortBy值是什么:

  • default:相关度算分排序,这个不用管,es的默认排序策略
  • score:根据酒店的score字段排序,也就是用户评价,降序
  • price:根据酒店的price字段排序,就是价格,升序
高亮:

【ElasticSearch】ES案例:旅游酒店搜索_第17张图片

request.source()
        .query(QueryBuilders.matchQuery("all",requestParam.getKey()))
        .highlighter(new HighlightBuilder().field("name")
                .requireFieldMatch(false)
                .preTags("")
                .postTags(")
        );

你可能感兴趣的:(ElasticSearch,SpringCloud,elasticsearch,旅游,大数据)