ElasticSearch

一、ElasticSearch介绍

1.1 前言

系统中的数据, 随着业务的发展,时间的推移, 将会非常多, 而业务中往往采用模糊查询进行数据的搜索, 而模糊查询会导致查询引擎放弃索引,导致系统查询数据时都是全表扫描,在百万级别的数据库中,查询效率是非常低下的,而我们使用 ElasticSearch 做一个全文索引,将经常查询的系统功能的某些字段,比如说电商系统的商品表中商品名,描述、价格还有 id 这些字段我们放入 ElasticSearch 索引里,可以提高查询速度。

1.2 了解ElasticSearch

Elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。

日常场景:

  • 旅游平台对游记进行搜索,对酒店搜索,对机票搜索

在这里插入图片描述

  • 知识库搜索平台

在这里插入图片描述

  • 电商系统对商品进行检索

在这里插入图片描述

基于地理位置的打车应用,检索当前定位的车辆信息

在这里插入图片描述

Elasticsearch还结合kibana、Logstash、Beats等组件,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.3 ElasticSearch发展历史

2004 年,有一个以色列小伙子,名字叫谢伊·班农( Shay Banon),他成亲不久来到伦敦,因为当时他的夫人正好在伦敦学厨师。

初来乍到,也没有找到工作,于是班农就打算写一个叫作 iCook 的小程序来管理和搜索菜谱,一来练练手,方便找工作;二来这个小工具还可以给其夫人用。

班农在编写 iCook 的过程中,使用了 Lucene,感受到了直接使用 Lucene 开发程序的各种暴击和痛苦,于是他在 Lucene 之上,封装了一个叫作 Compass 的程序框架,与 Hibernate 和 JPA 等 ORM 框架进行集成,通过操作对象的方式来自动地调用 Lucene 以构建索引。

这样做的好处是,可以很方便地实现对‘领域对象’进行索引的创建,并实现‘字段级别’的检索,以及实现‘全文搜索’功能。可以说,Compass 大大简化了给 Java 程序添加搜索功能的开发。Compass 开源出来,变得很流行。

在 Compass 编写到 2.x 版本的时候,社区里面出现了更多需求,比如需要有处理更多数据的能力以及分布式的设计。班农发现只有重写 Compass ,才能更好地实现这些分布式搜索的需求,于是 Compass 3.0 就没有了,取而代之的是一个全新的项目,也就是 Elasticsearch。

2018 年上市后最高市值达到 102.59 亿美元。

1.4 为什么选择ElasticSearch

在这里插入图片描述

ElasticSearch是目前市面上市场份额最大的搜索引擎.

1.5 倒排索引概念

  • 正排索引(Forward Index)

正排索引是将文档的标识符(ID)与其内容一一对应的索引结构。它以文档为单位,存储了每个文档中的所有信息。例如,在一个网页搜索引擎中,正排索引可能包含每个网页的标题、内容、发布日期等信息。这样的索引结构使得系统可以快速地检索和展示文档的详细信息。

举个例子,如果有一个包含网页信息的数据库,正排索引可能如下:

文档ID标题内容
1如何制作披萨从面团到烤箱,教你制作美味的披萨。
2健身计划一个简单但有效的健身计划,帮助你保持健康。
3学习JavaScript了解JavaScript编程语言的基础知识和技巧。
  • 倒排索引(Inverted Index)

倒排索引是一种反转的索引结构,它以词汇为单位,存储了每个词汇出现在哪些文档中的信息。它更侧重于关键词与文档之间的关系。在搜索引擎中,倒排索引用于快速查找包含特定关键词的文档。

以下是一个简化的倒排索引的例子:

词汇文档ID列表
制作1
披萨1
健身2
计划2
学习3
JavaScript3

在这个例子中,你可以看到每个词汇(关键词)都映射到了包含这个词汇的文档ID列表。倒排索引的优势在于可以快速定位包含特定关键词的文档,从而提高搜索效率。

综合起来,正排索引以文档为单位存储信息,而倒排索引以词汇为单位存储信息,使得系统能够高效地进行文档检索。

1.6 ElasticSearch概念类比

MySQLElasticsearch说明
TableIndex索引(index),就是文档的集合,类似数据库的表(table)
RowDocument文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
ColumnField字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQLDSLDSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD
- 数据库负责事务类型操作
- Elasticsearch负责海量数据的搜索、分析、计算

二、ElasticSearch安装部署

2.1 部署ES

  • 创建网络

    我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:

    docker network create es-net
    
  • 创建ElasticSearch容器

    docker run -d \
    	--name es \
        -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
        -e "discovery.type=single-node" \
        -v es-data:/usr/share/elasticsearch/data \
        -v es-plugins:/usr/share/elasticsearch/plugins \
        --privileged \
        --network es-net \
        -p 9200:9200 \
        -p 9300:9300 \
    elasticsearch:7.12.1
    
    • -e "cluster.name=es-docker-cluster":设置集群名称
    • -e "http.host=0.0.0.0":监听的地址,可以外网访问
    • -e "ES_JAVA_OPTS=-Xms512m -Xmx512m":内存大小
    • -e "discovery.type=single-node":非集群模式
    • -v es-data:/usr/share/elasticsearch/data:挂载逻辑卷,绑定es的数据目录
    • -v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷,绑定es的日志目录
    • -v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录
    • --privileged:授予逻辑卷访问权
    • --network es-net :加入一个名为es-net的网络中
    • -p 9200:9200:端口映射配置
  • 验证: 我们在浏览器中输入http://192.168.202.133:9200 看到如下界面,说明部署是没问题的

在这里插入图片描述

2.2 部署Kibana

  • 创建Kibana容器

    docker run -d \
    --name kibana \
    -e ELASTICSEARCH_HOSTS=http://es:9200 \
    --network=es-net \
    -p 5601:5601  \
    kibana:7.12.1
    
    • --network es-net :加入一个名为es-net的网络中,与Elasticsearch在同一个网络中
    • -e ELASTICSEARCH_HOSTS=http://es:9200":设置Elasticsearch的地址,因为kibana已经与Elasticsearch在一个网络,因此可以用容器名直接访问Elasticsearch
    • -p 5601:5601:端口映射配置

设置中文

#1、查看Kibana容器id
docker ps 
#2、进入容器
docker exec -it Kibana容器id bash
#3、进入config目录
cd config/
#4、编辑Kibana.yml
vi kibana.yml 
#5、添加中文配置
i18n.locale: "zh-CN"
#6、退出容器
exit
#7、重启Kibana
docker restart Kibana容器id
  • 验证: 我们在浏览器中输入http://192.168.202.133:5601 看到如下界面,说明部署是没问题的

在这里插入图片描述

ElasticSearch提供了REST接口,可以给客户端调用,我们学习阶段的话,可以使用Postman工具来进行测试,但是没有提示,不太方便。Kibana提供了DevTools可视化界面,具有语法提示功能,比较适合入门学习.

在这里插入图片描述

三、ElasticSearch基础操作

3.1 语法规则

ElasticSearch提供REST的方式,客户端可以非常方便的操作,参数的格式都是以JSON的形式定义.

以下是一个关于分词的操作

GET /_analyze
{
  "text": "god is a girl"
}

语法说明:

  • POST:请求方式
  • /_analyze:请求路径,这里省略了http://192.168.202.133:9200,有kibana帮我们补充
  • 请求参数,json风格:
    • text:要分词的内容

3.2 分词器

3.2.1 默认分词器

我们在前面有提到倒排索引的概念,存储的文档需要按照分词器进行分词,才能编排成倒排索引,所以分词器对于搜索来说是非常重要。我们前面的案例使用的是默认分词器standard,对于英文分词还好,但是如果文本内容是中文的话,分词效果就不太好了,我们使用如下案例测试一下:

GET /_analyze
{
  "text": "上帝是女孩"
}

可以看到默认的分词器,对于中文的分词并不友好,所以我们需要引入额外的分词器IK分词器

https://github.com/medcl/elasticsearch-analysis-ik

3.2.2 安装IK分词器

我们在创建ES容器的时候,设置了一个插件的数据卷目录,我们可以通过命令查看到

docker volume inspect es-plugins

结果如下:

[
    {
        "CreatedAt": "2023-09-04T15:46:27+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
        "Name": "es-plugins",
        "Options": null,
        "Scope": "local"
    }
]

elasticsearch-analysis-ik-7.12.1.zip文件解压到ik目录中,然后将ik整个目录上传到服务器/var/lib/docker/volumes/es-plugins/_data这个路径下

重启容器

docker restart es
  • IK分词器测试
GET /_analyze
{
  "analyzer": "ik_max_word", 
  "text": "上帝是女孩"
}

运行之后,可以看到分词效果明显好很多.

3.2.3 分词模式

  • ik_smart:最少切分,粗粒度。我是程序员 ----> 我 是 程序员

    GET /_analyze
    {
      "analyzer": "ik_smart", 
      "text": "我是程序员"
    }
    
  • ik_max_word:最细切分,细粒度。我是程序员 ----> 我 是 程序员 程序 员

    GET /_analyze
    {
      "analyzer": "ik_max_word", 
      "text": "我是程序员"
    }
    

3.2.4 拓展词库

随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。

比如下面这个案例,广州塔和小蛮腰应该是一个完整的单词,但是被拆开了

GET /_analyze
{
  "analyzer": "ik_smart", 
  "text": "广州塔,也被亲切地称为小蛮腰"
}

我们希望这些词在分词器中不要被拆分,这时候我们就可以给IK分词器去拓展词典.

要拓展ik分词器的词库,只需要修改一个ik分词器目录中的config目录中的IkAnalyzer.cfg.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!--用户可以在这里配置自己的扩展字典 *** 添加扩展词典-->
        <entry key="ext_dict">ext.dic</entry>
</properties>

然后在名为ext.dic的文件中,添加想要拓展的词语即可:

广州塔
小蛮腰

重启容器即可

3.2.5 停用词库

比如有些语气助词希望在分词的时候不出现,或者禁用某些敏感词条,那么可以配置停用词库,

只需要修改一个ik分词器目录中的config目录中的IkAnalyzer.cfg.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!--用户可以在这里配置自己的扩展字典-->
        <entry key="ext_dict">ext.dic</entry>
         <!--用户可以在这里配置自己的扩展停止词字典  *** 添加停用词词典-->
        <entry key="ext_stopwords">stopword.dic</entry>
</properties>

然后在名为stopword.dic的文件中,添加想要拓展的词语即可:

也
被
地

重启容器即可

3.3 索引库操作

我们在操作MySQL的时候,首先是需要创建一张表,比如通过create table命令创建,然后我们需要定义有哪些字段,字段的类型是哪些? 现在我们在操作ES的索引库也是需要先定义文档都有哪些字段?都有哪些类型?

在ES中mapping是对索引库中文档的约束,常见的mapping属性包括:

  • type:字段数据类型,常见的简单类型有:

    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
    • 数值:long、integer、short、byte、double、float、
    • 布尔:boolean
    • 日期:date
    • 对象:object
  • index:是否创建索引,默认为true

  • analyzer:使用哪种分词器

  • properties:该字段的子字段

需求: 比如我需要做一个酒店搜索的功能,里面包含这些字段:

CREATE TABLE `tb_hotel` (
  `id` bigint(20) NOT NULL COMMENT '酒店id',
  `name` varchar(255) NOT NULL COMMENT '酒店名称',
  `address` varchar(255) NOT NULL COMMENT '酒店地址',
  `price` int(10) NOT NULL COMMENT '酒店价格',
  `score` int(2) NOT NULL COMMENT '酒店评分',
  `brand` varchar(32) NOT NULL COMMENT '酒店品牌',
  `city` varchar(32) NOT NULL COMMENT '所在城市',
  `star_name` varchar(16) NOT NULL COMMENT '酒店星级,1星到5星,1钻到5钻',
  `business` varchar(255) DEFAULT NULL COMMENT '商圈',
  `latitude` varchar(32) NOT NULL COMMENT '纬度',
  `longitude` varchar(32) NOT NULL COMMENT '经度',
  `pic` varchar(255) DEFAULT NULL COMMENT '酒店图片',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;

我们在创建ES的索引库映射mapping的时候,就需要考虑哪些字段需要分词?分词的话使用什么分词器?哪些不需要分词?哪些字段需要被搜索到?

- 酒店id,需要搜索,不需要分词
- 酒店名称,需要搜索,需要分词
- 酒店地址,不需要搜索,不需要分词
- 酒店价格,需要范围查询,不需要分词
- 酒店评分,需要范围查询,不需要分词
- 酒店品牌,需要搜索,不需要分词
- 所在城市,需要搜索,不需要分词
- 酒店星级,需要搜索,不需要分词
- 商圈,需要搜索,不需要分词
- 经度/纬度,需要基于地理位置搜索,不需要分词
- 酒店图片,不需要搜索,不需要分词

3.3.1 REST方式

3.3.1.1 新增

ES中通过RESTful请求操作索引库、文档。请求内容用DSL语句来表示。创建索引库和mapping的DSL语法如下:

PUT /hotel
{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "copy_to": "all"
      },
      "address":{
        "type": "keyword",
        "index": false
      },
      "price":{
        "type": "integer"
      },
      "score":{
        "type": "integer"
      },
      "brand":{
        "type":"keyword",
        "copy_to": "all"
      },
      "starName":{
        "type":"keyword"
      },
      "city":{
        "type":"keyword"
      },
      "business":{
        "type":"keyword",
        "copy_to": "all"
      },
      "location":{
        "type":"geo_point"
      },
      "pic":{
        "type": "keyword",
        "index": false
      },
      "all": {
        "type": "text",
        "analyzer": "ik_max_word"
      }
    }
  }
}
3.3.1.2 查看

查看索引库

GET /hotel
3.3.1.3 修改

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

PUT /hotel/_mapping
{
  "properties":{
    "info":{
      "type":"keyword"
    }
  }
}
3.3.1.4 删除
DELETE /hotel

3.3.2 项目环境准备

  1. 创建一个名字为tb_hotel的数据库

  2. 导入资料中的初始项目

  3. 添加RestHighLevelClient的bean

    @Bean
    public RestHighLevelClient restHighLevelClient(){
        return new RestHighLevelClient(
                RestClient.builder(HttpHost.create("http://192.168.202.133:9200")));
    }
    
  4. 注册高德账号

    因为我们需要用到高德地图(JSAPI),需要先申请高德地图的appKey,同学们自行去这个网站进行注册

    高德开放平台 | 高德地图API (amap.com)

    地图JS API接口免费配额调整 | 高德地图API (amap.com)

    • 创建应用

    登录之后,在 管控台-->应用管理-->我的应用

在这里插入图片描述

在这里插入图片描述

  • 添加Key

在这里插入图片描述

在这里插入图片描述

把这个key覆盖项目中的key

3.3.3 RestClient方式

ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-getting-started.html

3.3.3.1 新增
@Test
void testCreateIndex() throws IOException {
    //1.定义请求对象
    CreateIndexRequest request = new CreateIndexRequest("hotel");
    //2.设置内容
    request.source(HotelConstants.HOTEL_MAPPING, XContentType.JSON);
    //3.发起请求
    client.indices().create(request, RequestOptions.DEFAULT);
}

将mapping内容封装到静态变量中

public class HotelConstants {
    public final static String HOTEL_MAPPING = "{\n" +
            "  \"mappings\": {\n" +
            "    \"properties\": {\n" +
            "      \"id\": {\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"name\": {\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\"\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" +
            "      },\n" +
            "      \"starName\":{\n" +
            "        \"type\":\"keyword\"\n" +
            "      },\n" +
            "      \"city\":{\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" +
            "    }\n" +
            "  }\n" +
            "}";
}
3.3.3.2 查看
@Test
void testGetIndex() throws IOException {
    //1.定义请求对象
    GetIndexRequest request = new GetIndexRequest("hotel");
    //2.发起请求
    GetIndexResponse response = client.indices().get(request, RequestOptions.DEFAULT);
    System.out.println(JSON.toJSONString(response.getMappings()));
}
3.3.3.3 修改
@Test
void testUpdateIndex() throws IOException {
    //1.定义请求对象
    PutMappingRequest request = new PutMappingRequest("hotel");
    //2.设置内容
    request.source(HotelConstants.HOTEL_MAPPING_UPDATE, XContentType.JSON);
    //3.发起请求
    client.indices().putMapping(request, RequestOptions.DEFAULT);
}

将mapping内容封装到静态变量中

public final static String HOTEL_MAPPING_UPDATE = "{\n" +
            "  \"properties\":{\n" +
            "    \"info\":{\n" +
            "      \"type\":\"keyword\"\n" +
            "    }\n" +
            "  }\n" +
            "}";
}
3.3.3.4 删除
@Test
void testDeleteIndex() throws IOException {
    //1.定义请求对象
    DeleteIndexRequest request = new DeleteIndexRequest("hotel");
    //2.发起请求
   client.indices().delete(request, RequestOptions.DEFAULT);
}

3.4 文档操作

3.4.1 REST方式

3.4.1.1 新增
POST /hotel/_doc/38812
{
	"name": "7天连锁酒店(上海漕溪路地铁站店)",
	"address": "徐汇龙华西路315弄58号",
	"price": 298,
	"score": 37,
	"pic": "t0.jpg",
	"brand": "7天酒店",
	"star_name": "二钻",
	"business": "八万人体育场地区",
	"city": "上海",
	"location:": "31.174377,121.442875"
}
3.4.1.2 查看
GET /hotel/_doc/38812
3.4.1.3 修改
  • 方式一: 全量修改,会删除旧文档,添加新文档
PUT /hotel/_doc/38812
{
	"name": "7天连锁酒店(上海漕溪路地铁站店)",
	"address": "徐汇龙华西路315弄58号",
	"price": 398,
	"score": 37,
	"pic": "t0.jpg",
	"brand": "7天酒店",
	"star_name": "二钻",
	"business": "八万人体育场地区",
	"city": "上海",
	"location:": "31.174377,121.442875"
}
  • 方式二: 增量修改,修改指定字段值
POST /hotel/_update/38812
{
  "doc": {
    "price": 298
  }
}
3.4.1.4 删除
DELETE /hotel/_doc/38812

3.4.2 RestClient方式

3.4.2.1 新增
@Test
void testAddDocument() throws IOException {
    //1.定义请求对象
    IndexRequest request = new IndexRequest("hotel").id("38812");
    String data = "{\n" +
            "\t\"name\": \"7天连锁酒店(上海漕溪路地铁站店)\",\n" +
            "\t\"address\": \"徐汇龙华西路315弄58号\",\n" +
            "\t\"price\": 298,\n" +
            "\t\"score\": 37,\n" +
            "\t\"pic\": \"t0.jpg\",\n" +
            "\t\"brand\": \"7天酒店\",\n" +
            "\t\"star_name\": \"二钻\",\n" +
            "\t\"business\": \"八万人体育场地区\",\n" +
            "\t\"city\": \"上海\",\n" +
            "\t\"location:\": \"31.174377,121.442875\"\n" +
            "}";
    //2.设置内容
    request.source(data,XContentType.JSON);
    //3,发起请求
    client.index(request,RequestOptions.DEFAULT);
}
3.4.2.2 查看
@Test
void testGetDocument() throws IOException {
    //1.定义请求对象
    GetRequest request = new GetRequest("hotel","38812");
    //2.发起请求
    GetResponse response = client.get(request, RequestOptions.DEFAULT);
    //3.输入内容
    System.out.println(response.getSourceAsString());
}
3.4.2.3 修改
@Test
void testUpdateDocument() throws IOException {
    //1.定义请求对象
    UpdateRequest request = new UpdateRequest("hotel","38812");
    String data = "{\"price\": 398}";
    //2.设置内容
    request.doc(data,XContentType.JSON);
    //3.发起请求
    client.update(request,RequestOptions.DEFAULT);
}
3.4.2.4 删除
@Test
void testDeleteDocument() throws IOException {
    //1.定义请求对象
    DeleteRequest request = new DeleteRequest("hotel","38812");
    //2.发起请求
    client.delete(request,RequestOptions.DEFAULT);
}
3.4.2.5 批量操作
@Test
void testBulkRequest() throws IOException {
    //1.查询数据
    List<Hotel> hotelList = hotelService.list();
    //2.设置批量请求
    BulkRequest request = new BulkRequest();
    //3.批量设置数据
    hotelList.forEach(hotel -> {
        request.add(new IndexRequest().index("hotel")
                .id(hotel.getId().toString())
                .source(JSON.toJSONString(new HotelDoc(hotel)),XContentType.JSON)
        );
    });
    //发起请求
    client.bulk(request,RequestOptions.DEFAULT);
}

3.5 文档查询操作

Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。

3.5.1 REST方式

3.5.1.1查询所有

查询出所有数据,一般测试用。

match_all查询
GET /hotel/_search
{
  "query": {
    "match_all": {}
  }
}
3.5.1.2全文检索查询

利用分词器对用户输入内容分词,然后去倒排索引库中匹配。

match查询

全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,语法:

GET /hotel/_search
{
  "query": {
    "match": {
      "all": "如家"
    }
  }
}
multi_match查询

与match查询类似,只不过允许同时查询多个字段,语法:

GET /hotel/_search
{
  "query": {
    "multi_match": {
      "query": "如家",
      "fields": ["brand","city","name"]
    }
  }
}

参与查询字段越多,查询性能越差

3.5.1.3精确查询
term查询

term属于精确查询,根据词条精确值查询,一般是查找keyword、数值、日期、boolean等类型字段,所以不会对搜索条件分词。

GET /hotel/_search
{
  "query": {
    "term": {
      "city": {
        "value": "北京"
      }
    }
  }
}
range查询

range属于精确查询,根据值的范围查询

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 100,
        "lte": 400
      }
    }
  }
}
ids查询

range属于精确查询,根据Id进行查询

GET /hotel/_search
{
  "query": {
    "ids": {
      "values": ["36934","38609"]
    }
  }
}
3.5.1.4地理(geo)查询

根据经纬度查询

geo_distance查询

询到指定中心点小于某个距离值的所有文档

在这里插入图片描述

GET /hotel/_search
{
  "query": {
    "geo_distance":{
      "distance":"10km",
      "location":"30.921659, 121.575572"
    }
  }
}
geo_bounding_box查询

查询geo_point值落在某个矩形范围的所有文档

在这里插入图片描述

GET /hotel/_search
{
  "query": {
    "geo_bounding_box":{
      "location":{
        "top_left":{
          "lat":31.1,
          "lon":121.5
        },
        "bottom_right":{
          "lat":30.9,
          "lon":121.7
        }
      }
    }
  }
}
3.5.1.5 复合(compound)查询

复合查询可以将上述各种查询条件组合起来,合并查询条件。

Function Score Query查询

使用 function score query,可以修改文档的相关性算分(query score),根据新得到的算分排序。

GET /hotel/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "all": "外滩"
        }
      },
      "functions": [
        {
          "filter": {"term": {"brand": "如家"}},
          "weight": 10
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

算分函数:算分函数的结果称为function score ,将来会与query score运算,得到新算分,常见的算分函数有:

  • weight:给一个常量值,作为函数结果(function score)

  • field_value_factor:用文档中的某个字段值作为函数结果

  • random_score:随机生成一个值,作为函数结果

  • script_score:自定义计算公式,公式结果作为函数结果

加权模式:定义function score与query score的运算方式,包括:

  • multiply:两者相乘。默认就是这个
  • replace:用function score 替换 query score
    其它:sum、avg、max、min
Boolean Query查询

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

  • must:必须匹配每个子查询,类似“与”
  • should:选择性匹配子查询,类似“或”
  • must_not:必须不匹配,不参与算分,类似“非”
  • filter:必须匹配,不参与算分

需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。

GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "如家"
          }
        }
      ],
      "must_not": [
        {
          "range": {
            "price": {
              "gt": 400
            }
          }
        }
      ],
      "filter": [
        {
          "geo_distance": {
            "distance": "10km",
            "location": {
              "lat": 31.21,
              "lon": 121.5
            }
          }
        }
      ]
    }
  }
}
3.5.1.6 文档结果处理
排序处理

Elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等

  • 按照分数排序
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "score": {
        "order": "desc"
      }
    }
  ]
}
  • 按照距离排序
GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "_geo_distance": {
        "location": {
          "lat": 22.507276,
          "lon": 113.931251
        },
        "order": "asc", 
        "unit": "km"
      }
    }
  ]
}
分页处理

Elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
Elasticsearch中通过修改from、size参数来控制要返回的分页结果:

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0, 
  "size": 10, 
  "sort": [
    {
      "score": {
        "order": "desc"
      }
    }
  ]
}
高亮处理

必须要有搜索的内容才能进行高亮处理

GET /hotel/_search
{
  "query": {
    "match": {
      "all": "如家"
    }
  },
  "highlight": {
    "fields": {
      "name": {
        "pre_tags": "<em>",
        "post_tags": "</em>"
      }
    },
    "require_field_match": "false"
  }
}

3.5.2 RestClient方式

3.5.2.1 查询所有
match_all查询
@Test
void testMathAllQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.matchAllQuery());
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
    });
}
3.5.2.2 全文检索查询
math查询
@Test
void testMatchQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.matchQuery("name","如家"));
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
    });
}
multi_match查询
@Test
void testMultiMatchQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.multiMatchQuery("如家","brand","name"));
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
    });
}
3.5.2.3 精确查询
term查询
@Test
void testTermQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.termQuery("city","北京"));
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
    });
}
range查询
@Test
void testRangeQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.rangeQuery("price").gte(100).lte(400));
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
    });
}
ids查询
@Test
void testIdsQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.idsQuery().addIds("36934","38609"));
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
    });
}
3.5.2.4地理(geo)查询
geo_distance查询
@Test
void testGeoDistanceQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.geoDistanceQuery("location").distance("10km").point(30.921659,121.575572));
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
    });
}
geo_bounding_box查询
@Test
void testGeoBoundingBoxQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.geoBoundingBoxQuery("location").setCorners(31.1,121.5,30.9,121.7));
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
    });
}
3.5.2.5 复合(compound)查询
Function Score Query
@Test
void testFunctionScoreQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.functionScoreQuery(QueryBuilders.matchQuery("all","外滩"),new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
            new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery("brand","如家"), ScoreFunctionBuilders.weightFactorFunction(10))
    }).boostMode(CombineFunction.MULTIPLY));
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
    });
}
Boolean Query
@Test
void testBoolQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    boolQuery.must(QueryBuilders.matchQuery("name","如家"));
    boolQuery.mustNot(QueryBuilders.rangeQuery("price").gt(400));
    boolQuery.filter(QueryBuilders.geoDistanceQuery("location")
            .distance("10km").point(31.21,121.5));
    //2.设置请求
    request.source().query(boolQuery);
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
    });
}
3.5.2.6 文档结果处理
排序分页
@Test
void testScoreAndSortQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.matchAllQuery());
    request.source().from(0).size(10).sort("score", SortOrder.DESC);
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
    });
}
高亮处理
@Test
void testHighLightQuery() throws IOException {
    //1.定义查询请求
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.matchQuery("all","如家"));
    request.source().highlighter(new HighlightBuilder().field("name").preTags("<em>").postTags("</em>").requireFieldMatch(false));
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    SearchHits hits = response.getHits();
    System.out.println("结果总数:"+hits.getTotalHits().value);
    hits.forEach(hit->{
        System.out.println(hit.getSourceAsString());
        HighlightField nameField = hit.getHighlightFields().get("name");
        System.out.println("高亮内容:"+nameField.getFragments()[0]);
    });
}

3.6 案例操作

3.6.1 搜索/分页功能

  1. 定义实体类,接收前端请求

    @Data
    public class RequestParams {
        private String key;
        private Integer page;
        private Integer size;
        private String sortBy;
    }
    
  2. 处理前端/hotel/list请求,需要返回RequestParam

    @Data
    public class PageResult {
        private Long total;
        private List<HotelDoc> hotels = new ArrayList<>();
    }
    
  3. 在Service中,使用all查询,然后进行排序分页

3.6.2 多条件查询

需求: 添加品牌、城市、星级、价格等过滤功能

  1. 修改RequestParams类,添加brand、city、starName、minPrice、maxPrice等参数

    @Data
    public class RequestParams {
        private String key;
        private Integer page;
        private Integer size;
        private String sortBy;
        private String city;
        private String brand;
        private String starName;
        private Integer minPrice;
        private Integer maxPrice;
    }
    
  2. 修改search方法的实现,在关键字搜索时,如果brand等参数存在,对其做过滤

    过滤条件包括:

    • city精确匹配
    • brand精确匹配
    • starName精确匹配
    • price范围过滤

    注意事项:

    • 多个条件之间是AND关系,组合多条件用BooleanQuery
    • 参数存在才需要过滤,做好非空判断

3.6.3 基于地理位置排序

需求: 按照当前位置,然后基于距离进行排序

前端页面点击定位后,会将你所在的位置发送到后台,我们要根据这个坐标,将酒店结果按照到这个点的距离升序排序,按照距离排序后,还需要显示具体的距离值:

  1. 修改RequestParams参数,接收location字段

    @Data
    public class RequestParams {
        private String key;
        private Integer page;
        private Integer size;
        private String sortBy;
        private String city;
        private String brand;
        private String starName;
        private Integer minPrice;
        private Integer maxPrice;
        private String location;
    }
    
  2. 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能

3.6.4 文档权重设置

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

我们给需要置顶的酒店文档添加一个标记。然后利用function score给带有标记的文档增加权重。

  1. 给HotelDoc类添加isAD字段,Boolean类型
  2. 挑选几个酒店,给它的文档数据添加isAD字段,值为true
  3. 修改search方法,添加function score功能,给isAD值为true的酒店增加权重

3.6.5 高亮功能

当搜索的时候,我们需要让酒店名称高亮

3.6.6 排序功能

可以按照评分降序和价格升序排序

3.7 聚合操作

聚合(aggregations)可以实现对文档数据的统计、分析、运算。聚合常见的有三类:

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

3.7.1 REST方式

3.7.1.1 Bucket聚合

现在,我们要统计所有数据中的酒店品牌有几种,此时可以根据酒店品牌的名称做聚合。

GET /hotel/_search
{
  "size": 0,
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 10
      }
    }
  }
}

默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。
我们可以修改结果排序方式:

GET /hotel/_search
{
  "size": 0,
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 10,
        "order": {
          "_count": "asc"
        }
      }
    }
  }
}

默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加query条件即可:

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "lte": 200
      }
    }
  }, 
  "size": 0,
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 10,
        "order": {
          "_count": "asc"
        }
      }
    }
  }
}
3.7.1.2 Metrics 聚合

例如,我们要求获取每个品牌的用户评分的min、max、avg等值.

GET /hotel/_search
{
  "size": 0,
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 10
      },
      "aggs": {
        "score_stat": {
          "stats": {
            "field": "score"
          }
        }
      }
    }
  }
}

3.7.2 RestClient方式

3.7.2.1 Bucket聚合
@Test
void testBucket() throws IOException {
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().query(QueryBuilders.rangeQuery("price").lte(200));
    request.source().size(0);
    request.source().aggregation(AggregationBuilders.terms("brandAgg")
            .field("brand")
            .size(20)
            .order(BucketOrder.count(true)));
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    Aggregations aggregations = response.getAggregations();
    Terms terms = aggregations.get("brandAgg");
    List<? extends Terms.Bucket> buckets = terms.getBuckets();
    buckets.stream().forEach(bucket->{
        System.out.println(bucket.getKeyAsString());
        System.out.println(bucket.getDocCount());
    });
}
3.7.2.2 Metrics聚合
@Test
void testMetrics() throws IOException {
    SearchRequest request = new SearchRequest("hotel");
    //2.设置请求
    request.source().size(0);
    request.source().aggregation(AggregationBuilders.terms("brandAgg")
            .field("brand")
            .size(20)
            .subAggregation(AggregationBuilders.stats("score_stat").field("score"))
    );
    //3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //4.处理结果
    Aggregations aggregations = response.getAggregations();
    Terms terms = aggregations.get("brandAgg");
    List<? extends Terms.Bucket> buckets = terms.getBuckets();
    buckets.stream().forEach(bucket->{
        System.out.println(bucket.getKeyAsString());
        ParsedStats parsedStats = bucket.getAggregations().get("score_stat");
        System.out.println("平均值:"+parsedStats.getAvg());
        System.out.println("最大值:"+parsedStats.getMax());
        System.out.println("最小值:"+parsedStats.getMin());
        System.out.println("总数:"+parsedStats.getCount());
        System.out.println("求和:"+parsedStats.getSum());
        System.out.println("===================");
    });
}

3.7.3 案例集成聚合操作

需求:搜索页面的品牌、城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的

完成/hotel/filters接口,

返回数据如下:

{"城市": ["上海", "北京"], "品牌": ["如家", "希尔顿"]}

3.8 自动补全

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

3.8.1 安装拼音分词器

要实现根据字母做补全,就必须对文档按照拼音分词。在GitHub上恰好有Elasticsearch的拼音分词插件。地址:

https://github.com/medcl/elasticsearch-analysis-pinyin

安装步骤:

  1. 解压elasticsearch-analysis-pinyin-7.12.1.zip
  2. 上传到虚拟机中,Elasticsearch的plugin目录
  3. 重启Elasticsearch
  4. 测试
POST _analyze
{
  "text": "今天天气真不错",
  "analyzer": "pinyin"
}

3.8.2 自定义分词器

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

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

在这里插入图片描述

我们针对分词器进行设置

PUT /test
{
  "mappings": {
    "properties": {
      "name":{
        "type": "text",
        "analyzer": "my_analyzer"
      }
    }
  }, 
  "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
        }
      }
    }
  }
}

测试:

POST /test/_analyze
{
  "text": "今天天气真不错",
  "analyzer": "my_analyzer"
}
  • 拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用。

比如我们先看看下面的例子:

POST /test/_doc/1
{
  "id": 1,
  "name": "狮子"
}
POST /test/_doc/2
{
  "id": 2,
  "name": "虱子"
}

GET /test/_search
{
  "query": {
    "match": {
      "name": "掉入狮子笼咋办"
    }
  }
}

运行案例之后,我们会发现id=2的文档也会被搜索出来,原因如下:

id=1中name=“狮子”====>分词结果为[“狮子”,“shizi”,“sz”]

id=2中name=“虱子”====>分词结果为:[“虱子”,“shizi”,“sz”]

词条文档编号
狮子1
虱子2
shizi1,2
sz1,2

当我们搜索的时候,也会使用拼音分词器进行分词处理.

name:=“掉入狮子笼咋办”====>分词结果为[“狮子”,“shizi”,“sz”,“掉入”,“diaoru”,“dr”,…],所以就会被搜索到我们需要修改一下name的分词配置

DELETE /test
PUT /test
{
  "mappings": {
    "properties": {
      "name":{
        "type": "text",
        "analyzer": "my_analyzer",
        "search_analyzer": "ik_smart"
      }
    }
  }, 
  "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
        }
      }
    }
  }
}

我们重新再执行一次案例就不会有这些问题了.

3.8.3 completion suggester查询

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

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

先做一下数据的初始化:

  • 删除之前的索引库
DELETE /test
  • 新增索引库
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"]
}
3.8.3.1 REST方式
GET /test/_search
{
  "suggest": {
    "title_suggest": {
      "text": "w",
      "completion":{
        "field":"title",
        "skip_duplicates":true,
        "size":10
      }
    }
  }
}
3.8.3.2 RestClient方式
@Test
void testCompletion() throws IOException {
    //1.定义请求对象
    SearchRequest request = new SearchRequest("test");
    //2.设置补全参数
    request.source().suggest(new SuggestBuilder().addSuggestion("title_suggest",
            SuggestBuilders.completionSuggestion("title").prefix("w").skipDuplicates(true).size(10)));
    //3.发起请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    //处理请求
    Suggest suggest = response.getSuggest();
   CompletionSuggestion suggestion = suggest.getSuggestion("title_suggest");
    suggestion.getOptions().stream().forEach(option -> {
        System.out.println(option.getText());
    });
}

3.8.4 案例集成自动补全

思路步骤:

  1. 修改hotel索引库结构,设置自定义拼音分词器

  2. 修改索引库的name、all字段,使用自定义分词器

  3. 索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器

    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_max_word",
            "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_max_word"
          },
          "suggestion":{
              "type": "completion",
              "analyzer": "completion_analyzer"
          }
        }
      }
    }
    
  4. 给HotelDoc类添加suggestion字段,内容包含brand、business

    @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();
            if(this.business.contains("、")){
                String[] arr = this.business.split("、");
                this.suggestion = new ArrayList<>();
                this.suggestion.add(this.brand);
                Collections.addAll(this.suggestion,arr);
            }else if(this.business.contains("/")){
                String[] arr = this.business.split("/");
                this.suggestion = new ArrayList<>();
                this.suggestion.add(this.brand);
                Collections.addAll(this.suggestion,arr);
            }else{
                this.suggestion = Arrays.asList(this.brand,this.business);
                this.suggestion = Arrays.asList(this.brand,this.business);
            }
        }
    }
    
  5. 重新导入数据到hotel库

  6. 完成/hotel/suggestion请求处理

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值