ES学习笔记

RestClient操作ES

一、初始化RestClient

1) 引入依赖

<dependency>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

2)覆盖es版本

因为soringboot默认 7.6.2 ES版本,因此需要覆盖默认版本

<properties>
        <java.version>1.8</java.version>
        <elasticsearch.version>7.12.1</elasticsearch.version>
</properties>

3)初始化RestClient

    //初始化RestHighLevelClient并交由IOC容器管理,需要使用时直接注入
    @Bean
    public RestHighLevelClient restClient(){
        return new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.43.31:9200")));
    }

二、基于Java的索引库操作

注入RestHighLevelClient对象使用即可

基于java操作es,基本分为三步

  • 创建XXXRequest对象 指定索引库

  • 将DSL语句封装到XXXRequest对象

  • 发送http请求,如果是查询还需要接收结果集封装

1)创建索引库

使用CreateIndexRequest对象

    //创建索引库
    @Test
    void testCreateIndex() throws IOException {
        //创建XXXRequest
        CreateIndexRequest request = new CreateIndexRequest("hotel");

        //向XXXRequest中封装DSL语句
        request.source(MAPPING_TEMPLATE, XContentType.JSON);

        //restHighLevelClient.indices()返回对象中包含索引库所有方法
        //使用RestHighLevelClient发送http请求,执行es操作
        restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);

    }

创建索引库的DSL语句

public static final String MAPPING_TEMPLATE = "{\n" +
            "  \"mappings\": {\n" +
            "    \"properties\": {\n" +
            "      \"id\": {\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"name\":{\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\",\n" +
            "        \"copy_to\": \"all\"\n" +
            "      },\n" +
            "      \"address\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"price\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"score\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"brand\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"copy_to\": \"all\"\n" +
            "      },\n" +
            "      \"city\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"copy_to\": \"all\"\n" +
            "      },\n" +
            "      \"starName\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"business\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"location\":{\n" +
            "        \"type\": \"geo_point\"\n" +
            "      },\n" +
            "      \"pic\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"all\":{\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\"\n" +
            "      }\n" +
            "    }\n" +
            "  }\n" +
            "}";

2)删除索引库

//删除索引库
@Test
public void testDeleteIndex() throws IOException {
    //封装XXXRequest对象 指定索引库
    DeleteIndexRequest request = new DeleteIndexRequest("hotel");
    //在XXXRequest封装DSL语句

    //使用RestHighLevelClient发送http请求,执行es操作
    restHighLevelClient.indices().delete(request, RequestOptions.DEFAULT);

}

3)判断索引库是否存在

@Test
public void testExistsHotelIndex() throws IOException {
    //1.创建XXXRequest对象
    GetIndexRequest request = new GetIndexRequest("hotel");
    //封装dsl语句
    //发送http请求,接收返回值
    boolean exists = restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
    System.out.println(exists);
}

4)修改索引库

索引库和mapping一旦创建无法修改,但是可以添加新的字段

PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}

三、基于Java的文档操作

1)新增文档

注入RestHighLevelClient对象使用即可

基于java操作es,基本分为三步

  • 创建XXXRequest对象 指定索引库

  • 将DSL语句封装到XXXRequest对象

  • 发送http请求,如果是查询还需要接收结果集封装

//指定id新增文档
@Test
public void testCreateDoc() throws IOException {
    //封装XXXRequest对象 且指定要操作的索引名字和唯一id
    IndexRequest request = new IndexRequest("hotel").id("1");

    //往XXXRequest中封装DSL语句,添加的数据需要转json
    HotelDoc doc = doc();
    String json = JSON.toJSONString(doc);
    request.source(json,XContentType.JSON);

    //使用RestHighLevelClient发送http请求,执行ES操作
    restHighLevelClient.index(request,RequestOptions.DEFAULT);

}

在这里插入图片描述

2)根据id查询文档

//查询指定文档
@Test
public void testGetDoc() throws IOException {
    //封装XXXRequest对象 指定索引库 指定id
    GetRequest request = new GetRequest("hotel").id("1");

    //往XXXRequest中封装DSL语句

    //执行RestHighLevelClient来发送http请求,执行es操作
    GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
    System.out.println(response.getSourceAsString());
}

在这里插入图片描述

3)查询指定条数文档

//查询文档 默认只查询十条
@Test
public void testGetAllDoc() throws IOException {
    //封装XXXRequest对象 指定索引库
    SearchRequest request = new SearchRequest("hotel");

    //往XXXRequest中封装DSL语句
    request.source().size(100);//默认只查询10条,可以设置size
    //执行RestHighLevelClient来发送http请求,执行es操作
    SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
    for (SearchHit hit : search.getHits().getHits()) {
        System.out.println(hit.getSourceAsString());
    }
}

4)修改文档

方式一:全量修改,会删除旧文档,添加新文档

方式二:增量修改,修改指定字段值

//修改指定文档 增量修改
@Test
public void testUpdateDoc() throws IOException {
    //封装XXXRequest对象 指定索引库 & id
    UpdateRequest request = new UpdateRequest("hotel","1");
    //XXXRequest对象封装DSL语句
    request.doc("name","lmz");

    //发送http请求,执行es操作
    restHighLevelClient.update(request,RequestOptions.DEFAULT);
}

在这里插入图片描述

5)删除文档

//删除指定文档
@Test
public void testDeleteDoc() throws IOException {
    //封装XXXRequest对象 索引库 & id
    DeleteRequest request = new DeleteRequest("hotel").id("1");

    //XXXRequest对象封装DSL语句

    //发送http请求,执行es操作
    restHighLevelClient.delete(request,RequestOptions.DEFAULT);
}

6)批量导入文档

从数据库中批量导入数据到ES中

@Test
public void testBatchDoc2() throws IOException {
    //查询数据库中所有酒店信息
    List<Hotel> list = hotelService.list();

    //创建BulkRequest对象,该对象相当于一个Request对象的集合,可以存储后执行批量导入
    BulkRequest bulkRequest = new BulkRequest();

    //将酒店信息封装成hoteldoc对象
    for (Hotel hotel : list) {
        //hotel -> hotelDoc
        HotelDoc doc = new HotelDoc(hotel);
        String json = JSON.toJSONString(doc);
        IndexRequest request = new IndexRequest("hotel").id(doc.getId().toString());
        request.source(json, XContentType.JSON);
        bulkRequest.add(request);

    }
    restHighLevelClient.bulk(bulkRequest,RequestOptions.DEFAULT);
}

四、DSL查询文档

1)常见查询类型

Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:

  • 查询所有:查询出所有数据,一般测试用。例如:match_all
  • 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
    • match_query
    • multi_match_query
  • 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
    • ids
    • range
    • term
  • 地理(geo)查询:根据经纬度查询。例如:
    • geo_distance
    • geo_bounding_box
  • 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
    • bool
    • function_score

查询的语法基本一致:

GET /indexName/_search
{
  "query": {
    "查询类型": {
      "查询条件": "条件值"
    }
  }
}

我们以查询所有为例,其中:

  • 查询类型为match_all
  • 没有查询条件
// 查询所有
GET /indexName/_search
{
  "query": {
    "match_all": {
    }
  }
}
	/**
     * 查询所有 match all   默认查询 10条数据
     */
    @Test
    public void testMatchAll() throws IOException {
        //1.封装XXXRequest对象  指定查询索引库
        SearchRequest request = new SearchRequest("hotel");

        //2.XXXRequest封装DSL语句
        //QueryBuilders构建各种包装对象
        request.source().query(
                QueryBuilders.matchAllQuery()
        );

        //3.发送http请求,执行es操作,获取查询结果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //解析结果集对象
        List<HotelDoc> hotelDocs = parseResult(response);

        //打印
        hotelDocs.forEach(System.out::println);
    }

其它查询无非就是查询类型查询条件的变化。

2)全文检索查询 & 词条检索 & 范围检索

match查询(单字段查询) 和 multi_match(多字段查询)


    /**
     * 单字段全文搜索match  多字段全文搜索 multi_match  词条搜索 term  范围搜索 range
     * @throws IOException
     */
    @Test
    public void testMatch() throws IOException {
        //1.封装Request对象
        SearchRequest request = new SearchRequest("hotel");
        //2.Request对象封装DSL语句
        //request.source().query(QueryBuilders.matchQuery("name","君"));//全文搜索
//        request.source().query(
//                QueryBuilders.multiMatchQuery("君悦","name","brand")
//        );//多字段搜索
//        request.source().query(QueryBuilders.termQuery("brand","君悦"));//词条搜索
        request.source().query(QueryBuilders.rangeQuery("price").gte("500").lte("600"));//范围查询 价格在 500-600

        //3.发送http请求,执行es操作结果,获取查询结果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //结果集解析打印
        parseResult(response).forEach(System.out::println);
    }

在这里插入图片描述

3) 经纬度检索

分为圆形检索 和 长方形检索

在这里插入图片描述

在这里插入图片描述

4) 复合查询

复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑

例如:fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价

布尔查询是一个或多个查询子句的组合。子查询的组合方式有:

  • must:必须匹配每个子查询,类似“与”

  • should:选择性匹配子查询,类似“或”

  • must_not:必须不匹配,不参与算分,类似“非”

  • filter:必须匹配,不参与算分

    /**
     * 复合查询 boolean query
     */
    @Test
    public void testBooleanQuery() throws IOException {
        //封装Request对象
        SearchRequest request = new SearchRequest("hotel");

        //封装boolean对象
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        //查询品牌 必须是 万怡的 价格在 500-600之间
        boolQuery.must(QueryBuilders.termQuery("brand","万怡"));
        boolQuery.filter(QueryBuilders.rangeQuery("price").gte(500).lte(600));

        //Request对象封装DSL语句 将boolean对象传入即可
        request.source().query(boolQuery);

        //发送http请求,执行es操作,获取查询结果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //解析查询结果
        parseResult(response).forEach(System.out::println);
    }

在这里插入图片描述

5) 分页和排序

5.1) 根据坐标距离排序
//距离排序
if (ObjectUtils.isNotEmpty(params.getLocation())){
    request.source().sort(
        //指定es中的经纬度字段 location,自己经纬度地址
        SortBuilders.geoDistanceSort( "location",new GeoPoint(params.getLocation()))
        .order(SortOrder.ASC)//排序方式 升序
        .unit(DistanceUnit.KILOMETERS)//距离单位 km
    );
}
5.2) 分页

分页方式有三种:

  • from + size
    • 支持随机翻页,深度分页越深性能越差
  • after search
    • 没有查询上限(单次查询的size不超过10000),但不支持随机翻页
  • scroll
    • 没有查询上限(单次查询的size不超过10000),不建议使用
/**
     * 分页和排序
     */
@Test
public void testPageSort() throws IOException {
    //封装Request对象
    SearchRequest request = new SearchRequest("hotel");

    //Request对象封装DSL语句
    request.source().query(QueryBuilders.termQuery("brand","君悦"));
    //指定分页数 & 排序规则
    request.source().from(0).size(2).sort("price", SortOrder.ASC);

    //发送http请求,执行es操作,接收返回值
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    //返回值解析
    parseResult(response).forEach(System.out::println);

}

6) 相关性算分

在这里插入图片描述

        //相关性加分
        FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(
                boolQuery,//原始查询条件
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                QueryBuilders.termQuery("name","喜来登"),//筛选条件
                                ScoreFunctionBuilders.weightFactorFunction(10)//权重加分
                        )
                }
        ).boostMode(CombineFunction.SUM);//运算方式

在这里插入图片描述

7) 高亮

    /**
     * 高亮 highLight
     */
    @Test
    public void testHighLight() throws IOException {
        //封装Request对象
        SearchRequest request = new SearchRequest("hotel");
        //Request对象封装DSL语句
        request.source().query(QueryBuilders.matchQuery("name","如家"));
        //开启高亮字段查询  指定name字段的词条高亮 且 包裹标签为 <ee>高亮</ee>
        request.source().highlighter(
                new HighlightBuilder().field("name").preTags("<ee>").postTags("</ee>")
        );

        //发送http请求,执行es,接收返回值
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //解析结果
        parseResultAndHighLight(response).forEach(System.out::println);

    }
7.1)高亮结果集封装
//结果集解析 + 高亮解析
    public List<HotelDoc> parseResultAndHighLight(SearchResponse response){
        //获取hits属性的对象  可以获取查询到的总记录数
        SearchHits hits = response.getHits();
        System.out.println(hits.getTotalHits().value);//总条数

        ArrayList<HotelDoc> docs = new ArrayList<>();
        //获取hits.hits属性并遍历  可以获取高亮数据和未高亮的结果数据
        for (SearchHit hit : hits.getHits()) {
            //获取hits.hits.source 未高亮的数据
            String json = hit.getSourceAsString();
            //对象 -》 HotelDoc
            HotelDoc doc = JSON.parseObject(json, HotelDoc.class);

            //获取hits.hits.highlight 高亮的数据
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            //判断是否存在高亮数据,存在就获取并且设置到结果对象中
            if (highlightFields != null && highlightFields.get("name") != null){
//                //获取name键的值,值为一个 HighlightField对象 对象中fragments属性是结果,是一个数组
//                HighlightField highlightField = highlightFields.get("name");
//                //获取高亮数组
//                Text[] fragments = highlightField.getFragments();
//                //拼接高亮数组
//                StringBuffer buffer = new StringBuffer();
//                for (Text fragment : fragments) {
//                    buffer.append(fragment);
//                }
//
//                //高亮数组设置进HotelDoc对象
//                doc.setName(buffer.toString());
                //拼接高亮数组的结果
                String join = StringUtils.join(highlightFields.get("name").getFragments(), "...");
                doc.setName(join);
            }
            docs.add(doc);
        }

        return docs;
    }

在这里插入图片描述

五、数据聚合

**聚合(aggregations**可以让我们极其方便的实现对数据的统计、分析、运算。例如:

  • 什么品牌的手机最受欢迎?
  • 这些手机的平均价格、最高价格、最低价格?
  • 这些手机每月的销售情况如何?

实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。

5.1.聚合的种类

聚合常见的有三类:

  • **桶(Bucket)**聚合:用来对文档做分组
    • TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
    • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
  • **度量(Metric)**聚合:用以计算一些值,比如:最大值、最小值、平均值等
    • Avg:求平均值
    • Max:求最大值
    • Min:求最小值
    • Stats:同时求max、min、avg、sum等
  • **管道(pipeline)**聚合:其它聚合的结果为基础做聚合

**注意:**参加聚合的字段必须是keyword、日期、数值、布尔类型

5.2.DSL实现聚合

现在,我们要统计所有数据中的酒店品牌有几种,其实就是按照品牌对数据分组。此时可以根据酒店品牌的名称做聚合,也就是Bucket聚合。

5.2.1.Bucket聚合语法

语法如下:

GET /hotel/_search
{
  "size": 0,  // 设置size为0,结果中不包含文档,只包含聚合结果
  "aggs": { // 定义聚合
    "brandAgg": { //给聚合起个名字
      "terms": { // 聚合的类型,按照品牌值聚合,所以选择term
        "field": "brand", // 参与聚合的字段
        "size": 20 // 希望获取的聚合结果数量
      }
    }
  }
}

结果如图:

在这里插入图片描述

5.2.2.聚合结果排序

默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。

我们可以指定order属性,自定义聚合的排序方式:

GET /hotel/_search
{
  "size": 0, 
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "order": {
          "_count": "asc" // 按照_count升序排列
        },
        "size": 20
      }
    }
  }
}
5.2.3.限定聚合范围

默认情况下,Bucket聚合是对索引库的所有文档做聚合,但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。

我们可以限定要聚合的文档范围,只要添加query条件即可:

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "lte": 200 // 只对200元以下的文档聚合
      }
    }
  }, 
  "size": 0, 
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 20
      }
    }
  }
}

这次,聚合得到的品牌明显变少了:

在这里插入图片描述

5.2.4.Metric聚合语法

上节课,我们对酒店按照品牌分组,形成了一个个桶。现在我们需要对桶内的酒店做运算,获取每个品牌的用户评分的min、max、avg等值。

这就要用到Metric聚合了,例如stat聚合:就可以获取min、max、avg等结果。

语法如下:

GET /hotel/_search
{
  "size": 0, 
  "aggs": {
    "brandAgg": { 
      "terms": { 
        "field": "brand", 
        "size": 20
      },
      "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
        "score_stats": { // 聚合名称
          "stats": { // 聚合类型,这里stats可以计算min、max、avg等
            "field": "score" // 聚合字段,这里是score
          }
        }
      }
    }
  }
}

这次的score_stats聚合是在brandAgg的聚合内部嵌套的子聚合。因为我们需要在每个桶分别计算。

另外,我们还可以给聚合结果做个排序,例如按照每个桶的酒店平均分做排序:

在这里插入图片描述

5.3.RestAPI实现聚合

    //Bucket聚合测试  类似与分组
    @Test
    public void bucketTest() throws IOException {
        //创建XXXRequest对象
        SearchRequest request = new SearchRequest("hotel");//指定索引库

        //XXXRequest对象封装dsl语句
        request.source().size(0);//不获取查询到的结果
        TermsAggregationBuilder size = AggregationBuilders.terms("brandAgg")//指定聚合查询后结果的字段名
                .field("brand")//聚合查询的列
                .size(6)//显示条数
                .order(BucketOrder.count(false));//结果按照count来进行排序 true升序 false 降序
        request.source().aggregation(size);//封装聚合查询

        //发送http请求,接收查询结果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //聚合查询结果集解析
        ParsedStringTerms brandAgg = response.getAggregations().get("brandAgg");//这里为聚合查询的结果字段名
        for (Terms.Bucket bucket : brandAgg.getBuckets()) {
            System.out.println("key-->" + bucket.getKeyAsString());
            System.out.println("count-->" + bucket.getDocCount());
        }
    }

六、自动补全

当用户在搜索框输入字符时,我们应该提示出与该字符有关的搜索项,如图:

在这里插入图片描述

这种根据用户输入的字母,提示完整词条的功能,就是自动补全了。

因为需要根据拼音字母来推断,因此要用到拼音分词功能。

1) 拼音分词器

要实现根据字母做补全,就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。地址:https://github.com/medcl/elasticsearch-analysis-pinyin

在这里插入图片描述

课前资料中也提供了拼音分词器的安装包:

在这里插入图片描述

安装方式与IK分词器一样,分三步:

​ ①解压

​ ②上传到虚拟机中,elasticsearch的plugin目录

​ ③重启elasticsearch

​ ④测试

详细安装步骤可以参考IK分词器的安装过程。

测试用法如下:

POST /_analyze
{
  "text": "如家酒店还不错",
  "analyzer": "pinyin"
}

结果:

在这里插入图片描述

2) 自定义分词器

默认的拼音分词器会将每个汉字单独分为拼音,而我们希望的是每个词条形成一组拼音,需要对拼音分词器做个性化定制,形成自定义分词器。

elasticsearch中分词器(analyzer)的组成包含三部分:

  • character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
  • tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
  • tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等

文档分词时会依次由这三部分来处理文档:

在这里插入图片描述

声明自定义分词器的语法如下:

PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": { // 自定义分词器
        "my_analyzer": {  // 分词器名称
          "tokenizer": "ik_max_word",
          "filter": "py"
        }
      },
      "filter": { // 自定义tokenizer filter
        "py": { // 过滤器名称
          "type": "pinyin", // 过滤器类型,这里是pinyin
		  "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_analyzer",
        "search_analyzer": "ik_smart"
      }
    }
  }
}

测试:

在这里插入图片描述

完整代码;

PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "tokenizer": "ik_max_word",
          "filter": "py"
        }
      },
      "filter": {
        "py": {
          "type": "pinyin",
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_analyzer",
        "search_analyzer": "ik_smart"
      }
    }
  }
}

总结:

如何使用拼音分词器?

  • ①下载pinyin分词器
  • ②解压并放到elasticsearch的plugin目录
  • ③重启即可

如何自定义分词器?

  • ①创建索引库时,在settings中配置,可以包含三部分
  • ②character filter
  • ③tokenizer
  • ④filter

拼音分词器注意事项?

  • 为了避免搜索到同音字,搜索时不要使用拼音分词器

3) 自动补全查询

elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:

  • 参与补全查询的字段必须是completion类型。
  • 字段的内容一般是用来补全的多个词条形成的数组。

比如,一个这样的索引库:

// 创建索引库
PUT /test
{
  "mappings": {
    "properties": {
      "title": {
        "type": "completion"
      }
    }
  }
}

然后插入下面的数据:

// 示例数据
POST test/_doc
{
 "title": ["Sony", "WH-1000XM3"]
}
POST test/_doc
{
 "title": ["SK-II", "PITERA"]
}
POST test/_doc
{
 "title": ["Nintendo", "switch"]
}

查询的DSL语句如下:

// 自动补全查询
GET /test/_search
{
  "suggest": {
    "title_suggest": {
      "text": "s", // 关键字
      "completion": {
        "field": "title", // 补全查询的字段
        "skip_duplicates": true, // 跳过重复的
        "size": 10 // 获取前10条结果
      }
    }
  }
}

4) 实现酒店搜索框自动补全

现在,我们的hotel索引库还没有设置拼音分词器,需要修改索引库中的配置。但是我们知道索引库是无法修改的,只能删除然后重新创建。

另外,我们需要添加一个字段,用来做自动补全,将brand、suggestion、city等都放进去,作为自动补全的提示。

因此,总结一下,我们需要做的事情包括:

  1. 修改hotel索引库结构,设置自定义拼音分词器
  2. 修改索引库的name、all字段,使用自定义分词器
  3. 索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器
  4. 给HotelDoc类添加suggestion字段,内容包含brand、business
  5. 重新导入数据到hotel库
4.1) 修改酒店映射结构

代码如下:

// 酒店数据索引库
PUT /hotel
{
  "settings": {
    "analysis": {
      "analyzer": {
        "text_anlyzer": {
          "tokenizer": "ik_max_word",
          "filter": "py"
        },
        "completion_analyzer": {
          "tokenizer": "keyword",
          "filter": "py"
        }
      },
      "filter": {
        "py": {
          "type": "pinyin",
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "id":{
        "type": "keyword"
      },
      "name":{
        "type": "text",
        "analyzer": "text_anlyzer",
        "search_analyzer": "ik_smart",
        "copy_to": "all"
      },
      "address":{
        "type": "keyword",
        "index": false
      },
      "price":{
        "type": "integer"
      },
      "score":{
        "type": "integer"
      },
      "brand":{
        "type": "keyword",
        "copy_to": "all"
      },
      "city":{
        "type": "keyword"
      },
      "starName":{
        "type": "keyword"
      },
      "business":{
        "type": "keyword",
        "copy_to": "all"
      },
      "location":{
        "type": "geo_point"
      },
      "pic":{
        "type": "keyword",
        "index": false
      },
      "all":{
        "type": "text",
        "analyzer": "text_anlyzer",
        "search_analyzer": "ik_smart"
      },
      "suggestion":{
          "type": "completion",
          "analyzer": "completion_analyzer"
      }
    }
  }
}
4.2) 修改HotelDoc实体

HotelDoc中要添加一个字段,用来做自动补全,内容可以是酒店品牌、城市、商圈等信息。按照自动补全字段的要求,最好是这些字段的数组。

因此我们在HotelDoc中添加一个suggestion字段,类型为List<String>,然后将brand、city、business等信息放到里面。

代码如下:

package cn.itcast.hotel.pojo;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@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;
    private List<String> suggestion;

    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();
        // 组装suggestion
        if(this.business.contains("/")){
            String[] split = this.business.split("/");
            this.suggestion=new ArrayList();
            this.suggestion.add(this.brand);
            Collections.addAll(this.suggestion,split);
        }
        if(this.business.contains("、")){
            String[] split = this.business.split("、");
            this.suggestion=new ArrayList();
            this.suggestion.add(this.brand);
            Collections.addAll(this.suggestion,split);
        }
        else{
            this.suggestion = Arrays.asList(this.brand, this.business);
        }
    }
}
4.3) 重新导入

重新执行之前编写的导入数据功能,可以看到新的酒店数据中包含了suggestion:

在这里插入图片描述

4.4) 自动补全查询的JavaAPI
    //自动补全测试
    @Test
    public void suggestTest() throws IOException {
        //SearchRequest对象
        SearchRequest request = new SearchRequest("hotel");

        //自动补全查询DSL语句封装
        request.source().suggest(new SuggestBuilder().addSuggestion(
                "suggestion",//查询后的列名称
                SuggestBuilders.completionSuggestion("suggestion") //查询字段
                        .prefix("sd") //查询的key
                        .skipDuplicates(true) //跳过重复
                        .size(10)//获取前10条
        ));

        //发送请求,结果集解析
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        CompletionSuggestion titleSuggest = response.getSuggest().getSuggestion("suggestion");
        titleSuggest.getOptions().stream().forEach(text -> System.out.println(text.getText()));
    }

七、数据同步

elasticsearch中的酒店数据来自于mysql数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是elasticsearch与mysql之间的数据同步

1) 思路分析

常见的数据同步方案有三种:

  • 同步调用
  • 异步通知
  • 监听binlog
1.1) 同步调用

方案一:同步调用

在这里插入图片描述

基本步骤如下:

  • hotel-demo对外提供接口,用来修改elasticsearch中的数据
  • 酒店管理服务在完成数据库操作后,直接调用hotel-demo提供的接口,
1.2) 异步通知

方案二:异步通知

在这里插入图片描述

流程如下:

  • hotel-admin对mysql数据库数据完成增、删、改后,发送MQ消息
  • hotel-demo监听MQ,接收到消息后完成elasticsearch数据修改
1.3) 监听binlog

方案三:监听binlog

在这里插入图片描述

流程如下:

  • 给mysql开启binlog功能
  • mysql完成增、删、改操作都会记录在binlog中
  • hotel-demo基于canal监听binlog变化,实时更新elasticsearch中的内容
1.4) 选择

方式一:同步调用

  • 优点:实现简单,粗暴
  • 缺点:业务耦合度高

方式二:异步通知

  • 优点:低耦合,实现难度一般
  • 缺点:依赖mq的可靠性

方式三:监听binlog

  • 优点:完全解除服务间耦合
  • 缺点:开启binlog增加数据库负担、实现复杂度高

2) 代码实现(这里实现第二种)

2.1) 后台增删改
    @PostMapping
    public void saveHotel(@RequestBody Hotel hotel){
        hotel.setId((long) (Math.random() * 10000));
        hotelService.save(hotel);

        //发送消息到mq指定队列中
        rabbitTemplate.convertAndSend(MQConstants.HOTEL_EXCHANGE,MQConstants.HOTEL_ADD_KEY, JSON.toJSONString(hotel));
    }

    @PutMapping()
    public void updateById(@RequestBody Hotel hotel){
        if (hotel.getId() == null) {
            throw new InvalidParameterException("id不能为空");
        }
        hotelService.updateById(hotel);

        //发送消息到mq指定队列中
        rabbitTemplate.convertAndSend(MQConstants.HOTEL_EXCHANGE,MQConstants.HOTEL_UPDATE_KEY, JSON.toJSONString(hotel));

    }

    @DeleteMapping("/{id}")
    public void deleteById(@PathVariable("id") Long id) {
        hotelService.removeById(id);

        //发送消息到mq指定队列中
        rabbitTemplate.convertAndSend(MQConstants.HOTEL_EXCHANGE,MQConstants.HOTEL_DELETE_KEY, JSON.toJSONString(id));

    }
2.2) es同步
2.2.1) 监听器
@Component
public class HotelMqListen {

    @Autowired
    private IHotelService service;

    //监听添加酒店
    @RabbitListener(bindings = @QueueBinding(//绑定队列 和 交换机
            value = @Queue(MQConstants.HOTEL_ADD_QUERY),//定义添加时队列
            exchange = @Exchange(value = MQConstants.HOTEL_EXCHANGE,type = ExchangeTypes.DIRECT),//定义添加时交换机
            key = {MQConstants.HOTEL_ADD_KEY}//定义添加路由标识
    ))
    public void hotelAdd(String msg) throws IOException {
        //解析消息为 HotelDoc对象
        Hotel hotel = JSON.parseObject(msg, Hotel.class);
        HotelDoc doc = new HotelDoc(hotel);
        //添加
        service.add(doc);
    }

    //监听修改酒店
    @RabbitListener(bindings = @QueueBinding(//绑定队列 和 交换机
            value = @Queue(MQConstants.HOTEL_UPDATE_QUERY),
            exchange = @Exchange(MQConstants.HOTEL_EXCHANGE),
            key = {MQConstants.HOTEL_UPDATE_KEY}
    ))
    public void hoteUpdate(String msg) throws IOException {
        //解析消息为 HotelDoc对象
        Hotel hotel = JSON.parseObject(msg, Hotel.class);
        HotelDoc doc = new HotelDoc(hotel);
        //添加
        service.updateHotel(doc);
    }

    //监听删除酒店
    @RabbitListener(bindings = @QueueBinding(//绑定队列 和 交换机
            value = @Queue(MQConstants.HOTEL_DELETE_QUERY),
            exchange = @Exchange(value = MQConstants.HOTEL_EXCHANGE),
            key = {MQConstants.HOTEL_DELETE_KEY}
    ))
    public void hotelDelete(String id) throws IOException {
        service.deleteHotel(id);
    }

}
2.2.2) 服务端
    //同步数据库修改操作
    @Override
    public void updateHotel(HotelDoc doc) throws IOException {
        //封装XXXRequest对象 指定索引库 & id
        UpdateRequest request = new UpdateRequest("hotel",doc.getId().toString());
        //XXXRequest对象封装DSL语句
        request.doc(JSON.toJSONString(doc),XContentType.JSON);
        //发送http请求,执行es操作
        client.update(request,RequestOptions.DEFAULT);
    }

    //同步数据库删除操作
    @Override
    public void deleteHotel(String id) throws IOException {

        DeleteRequest request = new DeleteRequest("hotel").id(id);

        client.delete(request,RequestOptions.DEFAULT);

    }

    //同步数据库添加操作
    @Override
    public void add(HotelDoc doc) throws IOException {
        //IndexRequest
        IndexRequest request = new IndexRequest("hotel").id(doc.getId().toString());

        //封装dsl语句
        request.source(JSON.toJSONString(doc), XContentType.JSON);

        //发送请求
        client.index(request,RequestOptions.DEFAULT);
    }
2.3) 常量类
//MQ常量类
public class MQConstants {

    public final static String HOTEL_EXCHANGE = "hotel.router";
    
    public final static String HOTEL_ADD_QUERY = "hotel.add.query";
    public final static String HOTEL_UPDATE_QUERY = "hotel.update.query";
    public final static String HOTEL_DELETE_QUERY = "hotel.delete.query";

    public final static String HOTEL_ADD_KEY = "hotel.add";
    public final static String HOTEL_UPDATE_KEY = "hotel.update";
    public final static String HOTEL_DELETE_KEY = "hotel.delete";

}

八、黑马旅游案例

案例

  1. 实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
  2. 添加品牌、城市、星级、价格等过滤功能
  3. 显示我附近的酒店并显示距离
  4. 让指定的酒店在搜索结果中排名置顶
  5. 给黑马旅游添加排序功能
  6. 给黑马旅游添加搜索关键字高亮效果
  7. 实现酒店搜索页面输入框的自动补全
  8. 利用MQ实现mysql与elasticsearch数据同步

7.1业务层代码

package cn.itcast.hotel.service.impl;

import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import cn.itcast.hotel.pojo.HotelListDto;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.lucene.search.function.CombineFunction;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentType;
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.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.BucketOrder;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

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

    @Autowired
    private RestHighLevelClient client;

    //同步数据库修改操作
    @Override
    public void updateHotel(HotelDoc doc) throws IOException {
        //封装XXXRequest对象 指定索引库 & id
        UpdateRequest request = new UpdateRequest("hotel",doc.getId().toString());
        //XXXRequest对象封装DSL语句
        request.doc(JSON.toJSONString(doc),XContentType.JSON);
        //发送http请求,执行es操作
        client.update(request,RequestOptions.DEFAULT);
    }

    //同步数据库删除操作
    @Override
    public void deleteHotel(String id) throws IOException {

        DeleteRequest request = new DeleteRequest("hotel").id(id);

        client.delete(request,RequestOptions.DEFAULT);

    }

    //同步数据库添加操作
    @Override
    public void add(HotelDoc doc) throws IOException {
        //IndexRequest
        IndexRequest request = new IndexRequest("hotel").id(doc.getId().toString());

        //封装dsl语句
        request.source(JSON.toJSONString(doc), XContentType.JSON);

        //发送请求
        client.index(request,RequestOptions.DEFAULT);
    }

    @Override
    public Map<String, List<String>> filters(HotelListDto hotelListDto) throws IOException {
        //创建XXXRequest对象
        SearchRequest request = new SearchRequest("hotel");
        //封装DSL语句
        //1.基本查询
        basicBuild(hotelListDto,request);

        //2.聚合查询
        //2.0 不返回查询的结果,只要聚合结果
        request.source().size(0);
        //2.1 城市查询
        SearchSourceBuilder source = request.source();
        aggBuild("cityAgg","city",source);

        //2.2 品牌查询
        aggBuild("brandAgg","brand",source);

        //2.3 星级查询
        aggBuild("starNameAgg","starName",source);

        //发送http请求查询
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //解析结果集然后返回
        Map<String,List<String>> map = new HashMap<>();
        map.put("city",parseFilterResult(response,"cityAgg"));
        map.put("brand",parseFilterResult(response,"brandAgg"));
        map.put("starName",parseFilterResult(response,"starNameAgg"));
        return map;
    }

    @Override
    public List<String> suggestion(String key) throws IOException {
        //SearchRequest
        SearchRequest request = new SearchRequest("hotel");

        //自动补全语句封装
        request.source().suggest(new SuggestBuilder().addSuggestion(
                "titleSuggestion",
                SuggestBuilders.completionSuggestion("suggestion")
                        .prefix(key)
                        .skipDuplicates(true)
                        .size(10)
        ));

        //发送http请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //结果集解析
        CompletionSuggestion suggestion = response.getSuggest().getSuggestion("titleSuggestion");
        List<String> list = suggestion.getOptions().stream()
                .map(text -> text.getText().toString())
                .collect(Collectors.toList());

        return list;
    }



    //聚合查询构造
    private void aggBuild(String aggName,String field,SearchSourceBuilder source){
        source.aggregation(AggregationBuilders
                .terms(aggName)
                .field(field)
                .size(6)
                .order(BucketOrder.count(false)));
    }

    //解析filter结果集
    private List<String> parseFilterResult(SearchResponse response, String cityAgg){
        ParsedStringTerms agg = response.getAggregations().get(cityAgg);
        return agg.getBuckets().stream()
                .map(k -> k.getKeyAsString())
                .collect(Collectors.toList());
    }

    @Override
    public PageResult hotelList(HotelListDto hotelListDto) throws IOException {
        //封装Request对象 指定索引库
        SearchRequest request = new SearchRequest("hotel");

        //Request对象封装DSL语句 match
        basicBuild(hotelListDto,request);

        //设置分页
        //起始页 (page - 1)*size
        int page = (hotelListDto.getPage() - 1) * hotelListDto.getSize();
        request.source().from(page).size(hotelListDto.getSize());

        //排序

        //发送http请求 执行es操作,获取结果
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //解析结果返回
        return parseResult(response, hotelListDto.getSize(),hotelListDto);
    }


    //搜索条件构造
    private void basicBuild(HotelListDto params,SearchRequest request){

        //复合查询
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();


        //判断用户是否输入
        if (ObjectUtils.isEmpty(params.getKey())){
            //没有输入就查询所有
//            request.source().query(QueryBuilders.matchAllQuery());
            boolQuery.must(QueryBuilders.matchAllQuery());
        }else{
            //输入则根据key查询
//            request.source().query(QueryBuilders.matchQuery("name",hotelListDto.getKey()));
            boolQuery.must(QueryBuilders.matchQuery("name",params.getKey()));
        }

        //品牌过滤
        if (ObjectUtils.isNotEmpty(params.getBrand())){
            boolQuery.filter(QueryBuilders.termQuery("brand",params.getBrand()));
        }

        //城市过滤
        if (ObjectUtils.isNotEmpty(params.getCity())){
            boolQuery.filter(QueryBuilders.termQuery("city",params.getCity()));
        }

        //星级过滤
        if (ObjectUtils.isNotEmpty(params.getStarName())){
            boolQuery.filter(QueryBuilders.termQuery("starName",params.getStarName()));
        }

        //价格过滤
        if (ObjectUtils.allNotNull(params.getMinPrice(),params.getMaxPrice())){
            boolQuery.filter(QueryBuilders.rangeQuery("price")
                    .gt(Integer.valueOf(params.getMinPrice().toString()))
                    .lte(Integer.valueOf(params.getMaxPrice().toString()))
            );
        }

        //距离排序
        if (ObjectUtils.isNotEmpty(params.getLocation())){
            request.source().sort(
                    //指定es中的经纬度字段 location,自己经纬度地址
                    SortBuilders.geoDistanceSort( "location",new GeoPoint(params.getLocation()))
                    .order(SortOrder.ASC)//排序方式 升序
                    .unit(DistanceUnit.KILOMETERS)//距离单位 km
            );
        }

        //用户评价降序 || 价格升序
        if (StringUtils.equals(params.getSortBy(),"score")){
            request.source().sort("score",SortOrder.DESC);
        }

        if (StringUtils.equals(params.getSortBy(),"price")){
            request.source().sort("price",SortOrder.ASC);
        }

        //高亮
        request.source().highlighter(
                new HighlightBuilder().field("name").preTags("<em>").postTags("</em>")
        );

        //相关性加分
        FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(
                boolQuery,//原始查询条件
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                QueryBuilders.termQuery("name","喜来登"),//筛选条件
                                ScoreFunctionBuilders.weightFactorFunction(10)//权重加分
                        )
                }
        ).boostMode(CombineFunction.SUM);//运算方式

        request.source().query(functionScoreQueryBuilder);
    }

    //解析分页查询结果
    private PageResult parseResult(SearchResponse response,int size,HotelListDto hotelListDto){
        //获取hits
        SearchHits hits = response.getHits();
        long total = hits.getTotalHits().value;//总记录数

        //获取hits.hits集合数据
        ArrayList<HotelDoc> docs = new ArrayList<>();
        for (SearchHit hit : hits.getHits()) {
            //获取每个对象的source
            String json = hit.getSourceAsString();
            HotelDoc doc = JSON.parseObject(json, HotelDoc.class);

            //获取距离
            //判断前端是否传入了距离,有就获取距离排序
            if (ObjectUtils.allNotNull(hotelListDto.getLocation())){
                Object[] sortValues = hit.getSortValues();
                doc.setDistance(sortValues[0]);
            }

            //获取高亮数据,判断是否有高亮数据,有就获取
            if(ObjectUtils.allNotNull(hit.getHighlightFields(),hit.getHighlightFields().get("name"))){
                String highName = StringUtils.join(
                        hit.getHighlightFields().get("name").getFragments(), "....");
                //赋值给HotelDoc对象
                doc.setName(highName);

            }

            docs.add(doc);
        }

        return new PageResult(total,docs);
    }
}

7.2 实体类

package cn.itcast.hotel.pojo;

import lombok.Data;
import lombok.NoArgsConstructor;

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

@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;//该字段用于相关性加分,当为true就加分

    //自动补全字段
    private List<String> suggestion;

    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();

        //自动补全字段
        if (this.business.contains("/")){
            String[] split = this.business.split("/");
            this.suggestion = new ArrayList<>();
            this.suggestion.add(this.brand);
            this.suggestion.addAll(Arrays.asList(split));
        }

        if (this.business.contains("、")){
            String[] split = this.business.split("、");
            this.suggestion = new ArrayList<>();
            this.suggestion.add(this.brand);
            this.suggestion.addAll(Arrays.asList(split));
        }else{
            this.suggestion = Arrays.asList(this.brand,this.business);
        }
    }
}

package cn.itcast.hotel.pojo;

import lombok.Data;

@Data
public class HotelListDto {
    //搜索词条
    private String key;
    private Integer page;
    private Integer size;
    private String sortBy;
    private String brand;
    private String city;
    private Integer maxPrice;
    private Integer minPrice;
    private String starName;
    private String location;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值