• Elasticsearch搜索引擎


    目录

    初识elasticsearch

    了解ES

    什么是elasticsearch

    elasticsearch的发展

    搜索引擎技术排名:

    总结

    倒排索引

    正向索引和倒排索引

    正向索引

    倒排索引

    总结

    es的一些概念

    文档

    索引

    概念对比

    架构

    总结

    安装es,kibana

    安装es

    安装kibana

    安装分词器

    分词器

    安装IK分词器

    查看数据卷

    上传ik安装包​编辑

    重启docker容器

    测试

    IK分词器扩展和停用词典​编辑

    总结

    索引库操作

    mapping映射属性

    mapping属性​编辑

    总结

    索引库CRUD

    创建索引库

    查看,删除索引库​编辑

    示例:查询

    示例:删除

    修改索引库

    示例:修改

    总结

    文档操作

    新增文档

    新增文档的DSL语法如下:​编辑

    示例

    查询文档

    DSL语法​编辑

    示例

    删除文档

    DSL语法​编辑

    示例

    修改文档

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

    示例

    方式二:增加修改,修改指定字段值​编辑

    示例

    总结

    RestAPI

    RestClient操作索引库

    初始化JavaRestClient

    创建索引库​编辑

    删除索引库

    判断索引库是否存在

    总结

    RestClient操作文档

    基本步骤

    新增文档

    示例

    运行结果​编辑​编辑

    查询文档

    示例

    运行结果​编辑

    修改文档

    示例

    运行结果

    修改前​编辑

    修改后​编辑

    删除文档

    示例

    运行结果

    删除前

    删除后​编辑

    总结

    批量导入文档

    利用JavaRestClient批量导入索引库中

    DSL查询文档

    DSL查询分类

    DSL Query基本语法​编辑

    总结

    全文检索查询​编辑

    match查询:

    multi_match查询:

    总结

    精准查询

    term查询:

    range查询:

    总结

    地理坐标查询

    geo_bounding_box:

    geo_distance:

    复合查询

    function score

    总结

    Bloolean Query

    案例:

    总结

    搜索结果处理

    排序

    示例:对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序

    示例:实现对酒店数据按照你的位置坐标的距离升序排序

    分页

    针对深度分页,ES提供了两种解决方案:

    总结

    高亮​编辑

    总结​编辑

    RestClient查询文档

    快速入门

    我们通过match_all来演示下基本api,先看DSL的组织:​编辑

    代码

    运行结果​编辑

    我们通过match_all来演示下基本的API,再看结果的解析:​编辑

    代码

    运行结果​编辑

    总结

    match查询

    代码

    运行结果​编辑

    精确查询​编辑

    复合查询

    总结

    排序和分页

    代码

    运行结果​编辑

    高亮

    高亮结果处理​编辑

    代码

    运行结果​编辑

    总结

    黑马旅游案例

    案例1:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页

    步骤:

    案例2:添加品牌,城市,星际,价格等过滤功能

    步骤:

    案例3:我附近的酒店

    步骤

    案例4:让指定的酒店再搜索中排名位置置顶

    步骤

    数据聚合

    聚合的种类

    聚合分类

    总结

    什么是聚合?

    聚合的常见种类有哪些?

    参与聚合的字段类型必须是:

    DSL实现聚合

    DSL实现Bucket聚合

    总结

    DSL实现Metrics聚合

    RestAPI实现聚合

    案例:在IUserService中定义方法,实现对品牌,城市,星级的聚合

    自动补全

    拼音分词器

    测试​编辑

    自定义分词器

    总结

    自动补全查询

    总结

    实现酒店搜索框自动补全

    RestAPI实现自动补全

    自动补全​编辑

    数据同步

    数据同步思路分析

    方案一:同步调用​编辑

    方案二:异步调用​编辑

    方案三:监听binlog​编辑

    总结

    实现elasticsearch与数据库同步

    利用MQ实现mysql于elasticsearch中数据也要完成操作。

    elasticsearch集群

    搭建ES集群

    ES集群结构

    集群脑裂问题

    ES集群的脑裂

    总结

    集权故障转移

    集群分布式存储

    集群分布式查询​编辑

    elasticsearch的查询分成两个阶段:

    总结

    ES集群故障转移

    总结


    初识elasticsearch

    了解ES

    什么是elasticsearch

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

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

    elasticsearch是elastic stack的核心,负责存储,搜索,分析数据。

    Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,有DougCutting于1999年研发。官网地址:Apache Lucene - Welcome to Apache Lucene

    Lucene的优势:

    1. 易扩展

    2. 高性能(基于倒排索引

    Lucene的缺点:

    1. 只限于Java语言开发

    2. 学习曲线陡峭

    3. 不支持水平扩展

    elasticsearch的发展

    2004年Shay Banon基于Lucene开发Compass

    2010年Shay Banon重写了Compass,取名为Elasticsearch

    官网地址:Elasticsearch 平台 — 大规模查找实时答案 | Elastic

    相比与lucene,elasticsearch具备下列优势:

    1. 支持分布式,可水平扩展

    2. 提供Restful接口,可被任何语言调用

    搜索引擎技术排名:

    1. Elasticsearch:开源的分布式搜索引擎

    2. Splunk:商业项目

    3. Solr:Apache的开源搜索引擎

    总结

    什么是elaticsearch?

    一个开源的分布式搜索引擎,可以用来实现搜索,日志统计,分析,系统监控等功能

    什么是elastic stack(ELK)?

    是以elaticsearch为核心的技术栈,包括bears,Logstash,kibana,elaticsearch

    什么是Lucene?

    是Apache的开源搜索引擎类库,提供了搜索引擎的核心API

    倒排索引

    正向索引和倒排索引

    正向索引

    传统数据库(如MySQL)采用正向索引,例如给下表(tb_goods)中的id创建索引:

    倒排索引

    elasticsarch采用倒排索引:

    1. 文档(document):每条数据就是一个文档

    2. 词条(term):文档按照语义分成的词语

    总结

    什么是文档和词条?

    1. 每一条数据就是一个文档

    2. 对文档中的内容分词,得到的词语就是词条

    什么是正向索引?

    1. 基于文档id创建索引,查询词条时必须先找到文档,而后判断是否包含词条

    什么是倒排索引?

    1. 对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先更具词条查询到文档id,而后获取到文档

    es的一些概念

    文档

    elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。

    文档数据会被序列化为json格式后存储在elasticsearch中。

    索引

    索引(index):相同类型的文档的集合

    映射(mapping):索引中文档的字段约束信息,类似表的结构约束

    概念对比

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

    架构

    MySQL:擅长事物类型操作,可以确保数据的安全和一致性

    Elaticsearch:擅长海量数据的搜索,分析,计算

    总结

    文档:一条数据就是一个文档,es中是Json格式

    字段:Json文档中的字段

    索引:同类型文档的集合

    映射:索引中文档的约束,比如字段名称,类型

    elasticaserch与数据库的关系:

    1. 数据库负责事物类型操作

    2. elasticsearch负责海量数据的搜索,分析,计算

    安装es,kibana

    安装es

    1. 创建网络

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

      docker network create es-net

    2. 加载镜像

      docker pull elasticsearch:7.12.1

    3. 运行es

      1. docker run -d \
      2. --name es \
      3.   -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
      4.   -e "discovery.type=single-node" \
      5.   -v es-data:/usr/share/elasticsearch/data \
      6.   -v es-plugins:/usr/share/elasticsearch/plugins \
      7.   --privileged \
      8.   --network es-net \
      9.   -p 9200:9200 \
      10.   -p 9300:9300 \
      11. 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:端口映射配置

    4. 开放9200端口,访问端口

      8.137.59.245:9200

    安装kibana

    kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。

    1. 部署kibana

      1. docker run -d \
      2. --name kibana \
      3. -e ELASTICSEARCH_HOSTS=http://es:9200 \
      4. --network=es-net \
      5. -p 5601:5601 \
      6. kibana:7.12.1
      • --network es-net :加入一个名为es-net的网络中,与elasticsearch在同一个网络中

      • -e ELASTICSEARCH_HOSTS=http://es:9200":设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch

      • -p 5601:5601:端口映射配置

      kibana启动一般比较慢,需要多等待一会,可以通过命令:

      docker logs -f kibana

      查看运行日志,当查看到下面的日志,说明成功:

      此时,在浏览器输入地址访问:http://192.168.150.101:5601,即可看到结果

    安装分词器

    分词器

    es在创建倒排索引时需要对文档分词;在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好。

    我们在kibana中DevTools中测试:

    1. POST /_analyze
    2. {
    3.  "analyzer": "standard",
    4.  "text": "黑马程序员学习java太棒了"
    5. }

    安装IK分词器

    查看数据卷

    上传ik安装包
    重启docker容器
    docker restart es
    测试

    IK分词器包含两种模式:

    1. ik_smart:最少切分

    2. ik_max_word:最细切分

      1. POST /_analyze
      2. {
      3. "analyzer": "ik_max_word",
      4. "text": "黑马程序员学习java太棒了"
      5. }

    IK分词器扩展和停用词典

    总结

    分词器的作用是什么?

    1. 创建倒排索引时对文档分词

    2. 用户搜索时,对输入的内容分词

    IK分词器又几种模式?

    1. ik_smart:智能切分,粗粒度

    2. ik_max_word:最细切分,细粒度

    IK分词器如何扩展词条?如何停用词条?

    1. 利用config目录的IKAnalyzer.cfg.xml文件添加扩展词条和停用词典

    2. 在词典中添加扩展词条或者停用词条

    索引库操作

    mapping映射属性

    mapping属性

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

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

      1. 字符串:text(可分词的文本),keyword(精确值,例如:品牌,国家,ip地址)

      2. 数值:long,integer,short,byte,double,float

      3. 布尔:boolean

      4. 日期:date

      5. 对象:object

    2. index:是否创建索引,默认为true

    3. analyzer:使用哪种分词器

    4. properties:该字段的子字段

    总结

    mapping常见属性有哪些?

    1. type:数据类型

    2. index:是否索引

    3. analyzer:分词器

    4. prperties:子字段

    type常见的有哪些?

    1. 字符串:text,keyword

    2. 数字:long,integer,short,byte,double,float

    3. 布尔:boolean

    4. 日期:date

    5. 对象:object

    索引库CRUD

    创建索引库

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

    1. #创建索引库
    2. PUT /heima
    3. {
    4.  "mappings": {
    5.    "properties": {
    6.      "info" : {
    7.        "type": "text",
    8.        "analyzer": "ik_smart"
    9.     },
    10.      "email":{
    11.        "type" : "keyword",
    12.        "index": false
    13.     },
    14.      "name" : {
    15.        "type": "object",
    16.        "properties": {
    17.          "firstName" : {
    18.            "type" : "keyword"
    19.         },
    20.          "lastName" : {
    21.            "type" : "keyword"
    22.         }
    23.       }
    24.     }
    25.   }
    26. }
    27. }

    成功运行

    查看,删除索引库

    示例:查询

    示例:删除

    修改索引库

    DELETE /heima
     
    
    示例:修改

    总结

    索引库操作有哪些?

    1. 创建索引库:PUT/索引库名

    2. 查询索引库:GET/索引库名

    3. 删除索引库:DELETE/索引库名

    4. 添加字段:PUT/索引库名/_mapping (可以添加字段但不能修改以前的字段)

    文档操作

    新增文档

    新增文档的DSL语法如下:

    示例

    1. #插入文档
    2. POST /heima/_doc/1
    3. {
    4.  "info" : "黑马程序员Java讲师",
    5.  "email" : "zy@itcast.cn",
    6.  "name" : {
    7.    "firstName" : "云",
    8.    "lastName" : "赵"
    9. }
    10. }

    运行结果

    1. {
    2.  "_index" : "heima",
    3.  "_type" : "_doc",
    4.  "_id" : "1",
    5.  "_version" : 1,
    6.  "result" : "created",
    7.  "_shards" : {
    8.    "total" : 2,
    9.    "successful" : 1,
    10.    "failed" : 0
    11. },
    12.  "_seq_no" : 0,
    13.  "_primary_term" : 1
    14. }

    查询文档

    DSL语法

    示例
    1. #查询文档
    2. GET /heima/_doc/1

    运行结果

    1. {
    2.  "_index" : "heima",
    3.  "_type" : "_doc",
    4.  "_id" : "1",
    5.  "_version" : 1,
    6.  "_seq_no" : 0,
    7.  "_primary_term" : 1,
    8.  "found" : true,
    9.  "_source" : {
    10.    "info" : "黑马程序员Java讲师",
    11.    "email" : "zy@itcast.cn",
    12.    "name" : {
    13.      "firstName" : "云",
    14.      "lastName" : "赵"
    15.   }
    16. }
    17. }

    删除文档

    DSL语法

    示例
    1. #删除文档
    2. DELETE /heima/_doc/1

    运行结果

    1. {
    2. "_index" : "heima",
    3. "_type" : "_doc",
    4. "_id" : "1",
    5. "_version" : 2,
    6. "result" : "deleted",
    7. "_shards" : {
    8. "total" : 2,
    9. "successful" : 1,
    10. "failed" : 0
    11. },
    12. "_seq_no" : 1,
    13. "_primary_term" : 1
    14. }

    修改文档

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

    示例
    1. #全量修改文档
    2. PUT /heima/_doc/1
    3. {
    4. "info" : "黑马程序员Java讲师",
    5. "email" : "ZhaoYun@itcast.cn",
    6. "name" : {
    7. "firstName" : "云",
    8. "lastName" : "赵"
    9. }
    10. }

    运行结果(版本增加1)

    1. {
    2.  "_index" : "heima",
    3.  "_type" : "_doc",
    4.  "_id" : "1",
    5.  "_version" : 3,
    6.  "result" : "updated",
    7.  "_shards" : {
    8.    "total" : 2,
    9.    "successful" : 1,
    10.    "failed" : 0
    11. },
    12.  "_seq_no" : 4,
    13.  "_primary_term" : 1
    14. }

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

    示例
    1. #局部修改文档字段
    2. POST /heima/_update/1
    3. {
    4.  "doc" : {
    5.    "email" : "ZYun@itcast.cn"
    6. }
    7. }

    运行结果

    1. {
    2.  "_index" : "heima",
    3.  "_type" : "_doc",
    4.  "_id" : "1",
    5.  "_version" : 4,
    6.  "result" : "updated",
    7.  "_shards" : {
    8.    "total" : 2,
    9.    "successful" : 1,
    10.    "failed" : 0
    11. },
    12.  "_seq_no" : 5,
    13.  "_primary_term" : 1
    14. }
    总结

    文档操作有哪些?

    1. 创建文档:POST /索引库名/_doc/文档id {json文档}

    2. 查询文档:GET /索引库名/_doc/文档id

    3. 修改文档:DELETE /索引库名/_doc/文档id

    4. 修改文档:

      1. 全量修改: PUT /索引库名/_doc/文档id {json文档}

      2. 增量修改: POST/索引库名/_update/文档id {"doc":{字段}}

    RestAPI

    什么是RestClient

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

    RestClient操作索引库

    mapping要考虑的问题:

    字段名,数据类型,是否参与搜索,是否分词,如果分词,分词器是什么?

    ES中支持两种地理

    1. geo_point:由纬度(latitude)和经度(longitude)确定的一个点。例如:"32.83232,120.233231"

    2. geo_shape:有多个geo_point组成的复杂几何图形。例如一条直线,"LINESTRING(-77.2344232 36.421231,-77.009099 38.8821384)"

    字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段。示例:

    "business" : { ​ "type": "keyword", ​ "copy_to": "all" ​ }, "all" : { ​ "type": "text", ​ "analyzer": "ik_max_word" ​ }

    初始化JavaRestClient

    1. 引入es的RestHighLeveClient依赖:

      1. <dependency>
      2.   <groupId>org.elasticsearch.clientgroupId>
      3.   <artifactId>elasticsearch-rest-high-level-clientartifactId>
      4.   <version>7.12.1version>
      5. dependency>

    2. 因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:

      1. <properties>
      2.   <java.version>1.8java.version>
      3.   <elasticsearch.version>7.12.1elasticsearch.version>
      4. properties>

    3. 初始化RestHighLevelClient:

      新建一个测试类

      1. public class HotelIndexTest {
      2.    private RestHighLevelClient client;
      3.    @Test
      4.    void name(){
      5.        System.out.println(client);
      6.   }
      7.    //初始化
      8.    @BeforeEach
      9.    void setUp(){
      10.        this.client = new RestHighLevelClient(RestClient.builder(
      11.                HttpHost.create("http://8.137.59.245:9200")
      12.       ));
      13.   }
      14.    //清理
      15.    @Test
      16.    @AfterEach
      17.    void tearDown() throws IOException {
      18.        this.client.close();
      19.   }
      20. }

    创建索引库

    编写DSL语句

    1. public class HotelConstants {
    2. public static final String MAPPING_TEMPLATE = "{\n" +
    3. " \"mappings\": {\n" +
    4. " \"properties\": {\n" +
    5. " \"id\" : {\n" +
    6. " \"type\": \"keyword\"\n" +
    7. " },\n" +
    8. " \"name\" : {\n" +
    9. " \"type\": \"text\",\n" +
    10. " \"analyzer\": \"ik_max_word\",\n" +
    11. " \"copy_to\": \"all\"\n" +
    12. " },\n" +
    13. " \"addres\" : {\n" +
    14. " \"type\": \"keyword\",\n" +
    15. " \"index\": false\n" +
    16. " },\n" +
    17. " \"price\" : {\n" +
    18. " \"type\": \"integer\"\n" +
    19. " },\n" +
    20. " \"score\" : {\n" +
    21. " \"type\": \"integer\"\n" +
    22. " },\n" +
    23. " \"brand\" : {\n" +
    24. " \"type\": \"keyword\",\n" +
    25. " \"copy_to\": \"all\"\n" +
    26. " },\n" +
    27. " \"city\" : {\n" +
    28. " \"type\": \"keyword\"\n" +
    29. " },\n" +
    30. " \"starName\" : {\n" +
    31. " \"type\": \"keyword\"\n" +
    32. " },\n" +
    33. " \n" +
    34. " \"location\" : {\n" +
    35. " \"type\": \"geo_point\"\n" +
    36. " },\n" +
    37. " \"pic\" : {\n" +
    38. " \"type\": \"keyword\",\n" +
    39. " \"index\": false\n" +
    40. " },\n" +
    41. " \"all\" : {\n" +
    42. " \"type\": \"text\",\n" +
    43. " \"analyzer\": \"ik_max_word\"\n" +
    44. " }\n" +
    45. " }\n" +
    46. " }\n" +
    47. "}";
    48. }

    编写测试类

    1. @Test
    2. void createHotelIndex() throws IOException {
    3. //1.创建Request对象
    4. CreateIndexRequest request = new CreateIndexRequest("hotel");
    5. //2.准备请求的参数:DSL语句
    6. request.source(MAPPING_TEMPLATE, XContentType.JSON);
    7. //3.发送请求
    8. client.indices().create(request, RequestOptions.DEFAULT);
    9. }

    查看运行结果

    删除索引库

    1. //   删除索引
    2.    @Test
    3.    void testDeleteHotelIndex() throws IOException {
    4.        //1.创建Request对象
    5.        DeleteIndexRequest request = new DeleteIndexRequest("hotel");
    6.        //2.发送请求
    7.        client.indices().delete(request, RequestOptions.DEFAULT);
    8.   }

    运行结果

    判断索引库是否存在

    1. // 判断索引是否存在
    2. @Test
    3. void testExistHotelIndex() throws IOException {
    4. //1.创建Request对象
    5. GetIndexRequest request = new GetIndexRequest("hotel");
    6. //2.发送请求
    7. boolean exist = client.indices().exists(request, RequestOptions.DEFAULT);
    8. //3.判断结果
    9. System.out.println(exist ? "索引库已经存在!" : "索引库不存在!");
    10. }

    运行结果

    总结

    索引库操作的基本步骤:

    1. 初始化RestHighLevelClient

    2. 创建XxxIndexRequest。xxx是CREATE,Get,Delete

    3. 准备DSL(CREATE时需要)

    4. 发送请求。调用RestHighLevelClient#indices().xx()方法,xxx时create, exists, delete

    RestClient操作文档

    基本步骤

    利用JavaRestClient实现文档的CRUD

    去数据库查询酒店数据,导入到hotel索引库,实现酒店数据的CRUD。

    基本步骤如下:

    1. 初始化JavaRestClient

    2. 利用JavaRestClient新增酒店数据

    3. 利用JavaRestClient根据id查询酒店数据

    4. 利用javaRestClient删除酒店数据

    5. 利用JavaRestClient修改酒店数据

    新增文档

    先查询酒店数据,然后给这条数据创建倒排索引,即可完成添加:

    示例
    1. //RestClient的新增数据
    2. @Test
    3. void testAddDocument() throws IOException {
    4.    // 根据id查询酒店数据
    5.    Hotel hotel = hotelService.getById(39141L);
    6.    // 转换为文档类型
    7.    HotelDoc hotelDoc = new HotelDoc(hotel);
    8.    //1.准备Request对象
    9.    IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
    10.    //2.准备Json文档
    11.    request.source(JSON.toJSONString(hotelDoc),XContentType.JSON);
    12.    //3.发送请求
    13.    client.index(request,RequestOptions.DEFAULT);
    14. }
    运行结果

    查询文档

    根据id查询到文档数据是json,需要反序列化为java对象:

    示例
    1. //RestClient的查询数据
    2. @Test
    3. void testGetDocument() throws IOException {
    4. // 1.准备Request
    5. GetRequest request = new GetRequest("hotel", "39141");
    6. // 2.发送请求,得到响应
    7. GetResponse response = client.get(request, RequestOptions.DEFAULT);
    8. // 3.解析响应结果
    9. String json = response.getSourceAsString();
    10. HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    11. System.out.println(hotelDoc);
    12. }
    运行结果

    修改文档

    修改文档数据有两种方式:

    方式一:全局更新。再次写入id一样的文档,就会删除旧文档,添加新文档

    方式二:局部更新。只跟新局部字段,我们演示方式二

    示例
    1. //RestClient的修改数据
    2. @Test
    3. void testUpdateDocument() throws IOException {
    4.    // 1.准备Request
    5.    UpdateRequest request = new UpdateRequest("hotel", "39141");
    6.    // 2.准备请求参数
    7.    request.doc(
    8.            "price" , "952",
    9.            "starName" , "四钻"
    10.   );
    11.    // 3.发送请求
    12.    client.update(request, RequestOptions.DEFAULT);
    13. }
    运行结果
    修改前
    修改后

    删除文档

    示例
    1. //RestClient的删除数据
    2. @Test
    3. void testDeleteDocument() throws IOException {
    4. //准备Request
    5. DeleteRequest request = new DeleteRequest("hotel", "39141");
    6. //发送请求参数
    7. client.delete(request, RequestOptions.DEFAULT);
    8. }
    运行结果
    删除前
    删除后
    总结

    文档操作的基本步骤

    1. 初始化RestHighLevelClient

    2. 创建XxxRequest。XXX是Index,Get,Update,Delete

    批量导入文档

    利用JavaRestClient批量导入索引库中

    思路:

    1. 利用mybatis-plus查询酒店数据

    2. 将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)

    3. 利用JavaRestClient总的Bulk批量处理,实现批量新增文档

      1. //批量导入es
      2. @Test
      3. void testBulkRequest() throws IOException {
      4.    // 批量查询酒店数据
      5.    List hotels = hotelService.list();
      6.    // 1.创建Request
      7.    BulkRequest request = new BulkRequest();
      8.    // 2.准备参数,添加多个新增的Request
      9.    //转换为文档类型HotelDoc
      10.    for (Hotel hotel : hotels) {
      11.        HotelDoc hotelDoc = new HotelDoc(hotel);
      12.        //创建新增文档的Request对象
      13.        request.add(new IndexRequest("hotel")
      14.               .id(hotelDoc.getId().toString())
      15.               .source(JSON.toJSONString(hotelDoc), XContentType.JSON));
      16.   }
      17.    // 3.发送请求
      18.    client.bulk(request, RequestOptions.DEFAULT);
      19. }

      运行结果

    DSL查询文档

    DSL查询分类

    Elasticashearch提供了基于JSON的DSL来定义查询。常见查询类型包括:

    1. 查询所有:查询出所有数据,一般测试用。例如:mathc_all

    2. 全文检索(full text)查询:利用分词器对用户输入内容分词,然后倒排索引库中匹配。例如:

      1. match_query

      2. multi_match_query

    3. 精准查询:根据精准词条查找数据,一般是查找keyword,数值,日期,boolean等类型字段。例如

      1. ids

      2. range

      3. term

    4. 地理(geo)查询:根据经纬度查询。例如

      1. geo_distance

      2. geo_bounding_box

    5. 复合(compound)查询:复合查询可以将上述各种条件组合起来,合并查询条件。例如:

      1. bool

      2. funcation_score

    DSL Query基本语法
    1. #查询所有
    2. GET /hotel/_search
    3. {
    4.  "query": {
    5.    "match_all": {}
    6. }
    7. }

    运行结果

    总结

    查询DSL的基本语法是什么?

    1. GET /索引库名/_search

    2. { "query" : { "查询类型" : { "FIELD" : "TEXT" } } }

    全文检索查询

    match查询:

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

    1. # match查询
    2. GET /hotel/_search
    3. {
    4.  "query": {
    5.    "match": {
    6.      "all": "外滩如家"
    7.   }
    8. }
    9. }
    multi_match查询:

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

    1. # multi_match查询
    2. GET /hotel/_search
    3. {
    4. "query": {
    5. "multi_match": {
    6. "query": "外滩如家",
    7. "fields": ["brand","name","business"]
    8. }
    9. }
    10. }
    总结

    match和multi_match的区别是什么?

    1. match:更具一个字段查询

    2. muti_match:根据多个字段查询,参与查询字段越多,查阅性能越差

    精准查询

    准确查询一般是查找keyword,数值,日期,boolean等类型字段。所以不会对搜索条件分词。常见的有:

    term查询:
    1. #term查询
    2. GET /hotel/_search
    3. {
    4. "query": {
    5. "term": {
    6. "city": {
    7. "value": "上海"
    8. }
    9. }
    10. }
    11. }

    range查询:
    1. # range查询
    2. GET /hotel/_search
    3. {
    4. "query": {
    5. "range": {
    6. "price": {
    7. "gte": 100,
    8. "lte": 200
    9. }
    10. }
    11. }
    12. }

    总结

    精确查询常见的有哪些?

    1. term查询:根据词条精确匹配,一般搜索keywored类型,数值类型,布尔类型,日期类型字段

    2. range查询:根据数值查询范围,可以是数值,日期的范围

    地理坐标查询

    根据经纬度查询。常见的使用场景包括:

    1. 携程:搜索我的附近的酒店

    2. 滴滴:搜索我附近的出租车

    3. 微信:搜索我附近的人

    根据经纬度查询。例如:

    geo_bounding_box:

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

    geo_distance:

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

    # distance查询
    GET /hotel/_search
    {
      "query": {
        "geo_distance": {
          "distance": "5km",
          "location": "31.21, 121.5"
        }
      }
    }

    复合查询

    复合(compound)查询:复合查询可以将其他简单查询组合起来,实现更复杂的搜索逻辑,例如:

    function score

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

    当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时安装分值降序排列。

    例如,我们搜索”虹桥如家“,结果如下:

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

    1. # function score查询
    2. GET /hotel/_search
    3. {
    4. "query": {
    5. "function_score": {
    6. "query": {
    7. "match": {
    8. "all": "外滩"
    9. }
    10. },
    11. "functions": [
    12. {
    13. "filter": {
    14. "term": {
    15. "brand": "如家"
    16. }
    17. },
    18. "weight": 10
    19. }
    20. ],
    21. "boost_mode": "sum"
    22. }
    23. }
    24. }
    总结

    function score query定义得三要素是什么?

    1. 过滤条件:哪些文档要加分

    2. 算分函数:如何计算function score

    3. 加权方式:function score 与 query score如何运算

    Bloolean Query

    参与算分越多,越影响性能。

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

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

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

    3. must_not:必须不匹配,不参与算法,类似”非“

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

    案例:

    利用bool查询名字包含 ”如家“,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。

    1. # bool 查询
    2. GET /hotel/_search
    3. {
    4. "query": {
    5. "bool": {
    6. "must": [
    7. {
    8. "match": {
    9. "name": "如家"
    10. }
    11. }
    12. ],
    13. "must_not": [
    14. {
    15. "range": {
    16. "price": {
    17. "gt": 400
    18. }
    19. }
    20. }
    21. ],
    22. "filter": [
    23. {
    24. "geo_distance": {
    25. "distance": "10km",
    26. "location": {
    27. "lat": 31.21,
    28. "lon": 121.5
    29. }
    30. }
    31. }
    32. ]
    33. }
    34. }
    35. }

    总结

    elasticsearch中的相关性打分算法时什么?

    1. TF-IDF:在elasticserch5.0之前,会随着词频增加反而越来越大

    2. BM25:在elasticsearch5.0之后,会随着词频增大而增大,但增长曲线会趋于水平

    搜索结果处理

    排序

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

    示例:对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序
    1. # sort排序
    2. GET /hotel/_search
    3. {
    4.  "query": {
    5.    "match_all": {}
    6. },
    7.  "sort": [
    8.   {
    9.      "score": "desc"
    10.   },
    11.   {
    12.      "price": "asc"
    13.   }
    14. ]
    15. }
    示例:实现对酒店数据按照你的位置坐标的距离升序排序

    获取经纬度的方式:获取鼠标点击经纬度-地图属性-示例中心-JS API 2.0 示例 | 高德地图API

     
    
    1. # 找到121.612282,31.034661周围的酒店,距离升序排序
    2. GET /hotel/_search
    3. {
    4.  "query": {
    5.    "match_all": {
    6.   }
    7. },
    8.    "sort" : [
    9.     {
    10.       "_geo_distance":{
    11.         "location":{
    12.           "lat": "31.034661",
    13.           "lon": "121.612282"
    14.         },
    15.         "order": "asc",
    16.         "unit" : "km"
    17.     }
    18.   }
    19. ]
    20. }

    分页

    elatcsearch默认情况下只返回top10的数据,而如果要查询更多数据就需要修改分页参数了。

    elatcsearch中通过修改from,size参数来控制返回的分页结果:

    1. # 分页查询
    2. GET /hotel/_search
    3. {
    4. "query": {
    5. "match_all": {}
    6. },
    7. "sort": [
    8. {
    9. "price": "asc"
    10. }
    11. ],
    12. "from": 10,
    13. "size": 10
    14. }

    深度ES是分布式的,所以会面临深度分页问题。例如按price排序后,后去from = 990,size = 10的数据:

    1. 首先在每个数据分片上都排序并查询前1000条文档。

    2. 然后将所有结点的结果聚合,在内存中重新排序选出前1000条文档

    3. 最后从这1000条中,选取从990开始的10条文档

    如果搜索页数过深,或者结果集(from+size)越大,对内存和CPU的消耗越高。硬扯ES设定结果查询上限时10000

    针对深度分页,ES提供了两种解决方案:
    1. search after:分页时需要排序,原理上是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。

    2. scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用。

    总结

    from + size:

    1. 优点:支持随机翻页

    2. 缺点:深度分页问题,默认查询上限(from+size)是10000

    3. 场景:百度,京东,谷歌,淘宝这样的随机翻页搜索

    after search:

    1. 优点:没有查询上线(单次查询的size不超过10000)

    2. 缺点:智能下岗后逐页查询,不支持随机翻页

    3. 场景:没有随机分页需求的搜索,例如手机向下滚动翻页

    scroll:

    1. 优点:没有查询上限(单次查询的size不超过10000)

    2. 缺点:会有额外内存损耗,并且搜索结果是非实时的

    3. 场景:海量数据的获取和迁移。重ES7.1开始不推荐使用,建议用after search 方案

    高亮

    高亮:就是在搜索结果中把收索关键字突出显示。

    原理是这样的:

    1. 将搜索结果中的关键字用标签标记出来

    2. 在页面中给标签添加css样式

    语法:

    总结

    RestClient查询文档

    快速入门
    我们通过match_all来演示下基本api,先看DSL的组织:
    代码
    1. @Test
    2. void testMatchAll() throws IOException {
    3.    // 1.准备Request
    4.    SearchRequest request = new SearchRequest("hotel");
    5.    // 2.准备DSL
    6.    request.source().query(QueryBuilders.matchAllQuery());
    7.    // 3.发送请求
    8.    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    9.    System.out.println(response);
    10. }
    运行结果
    我们通过match_all来演示下基本的API,再看结果的解析:
    代码
    1. @Test
    2. void testMatchAll() throws IOException {
    3. // 1.准备Request
    4. SearchRequest request = new SearchRequest("hotel");
    5. // 2.准备DSL
    6. request.source().query(QueryBuilders.matchAllQuery());
    7. // 3.发送请求
    8. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    9. System.out.println(response);
    10. //4.解析响应
    11. SearchHits searchHits = response.getHits();
    12. //4.1. 获取总条数
    13. long total = searchHits.getTotalHits().value;
    14. System.out.println("共搜索到"+ total+"条数据");
    15. //4.2. 文档数组
    16. SearchHit[] hits = searchHits.getHits();
    17. //4.3. 便利
    18. for (SearchHit hit : hits){
    19. // 获取文档source
    20. String json = hit.getSourceAsString();
    21. //反序列化
    22. HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    23. System.out.println("hotelDoc"+hotelDoc);
    24. }
    25. System.out.println(response);
    26. }
    运行结果

    RestAPI其中构建DSL是通过HighLevelRestClient中的resource()来实现的,其中包含了查询,排序,分页,高亮等所有功能

    RestAPI中其中构建查询条件的核心是由一个名为QueryBuilders的工具类提供的,其中包含了各种查询方法:

    总结
    1. 创建SearchRequest对象

    2. 准备Request.source(),也就是DSL.

      1. QueryBuilders来构建查询条件

      2. 传入Request.source()的query()方法

    3. 发送请求,得到结果

    4. 解析结果(参考JSON结果,从外到内,逐层解析)

    match查询
    代码
    1. @Test
    2. void testMatch() throws IOException {
    3. // 1.准备Request
    4. SearchRequest request = new SearchRequest("hotel");
    5. // 2.准备DSL
    6. request.source().query(QueryBuilders.matchQuery("all","如家"));
    7. // 3.发送请求
    8. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    9. System.out.println(response);
    10. extracted(response);
    11. System.out.println(response);
    12. }
    13. private void extracted(SearchResponse response) {
    14. //4.解析响应
    15. SearchHits searchHits = response.getHits();
    16. //4.1. 获取总条数
    17. long total = searchHits.getTotalHits().value;
    18. System.out.println("共搜索到"+ total+"条数据");
    19. //4.2. 文档数组
    20. SearchHit[] hits = searchHits.getHits();
    21. //4.3. 便利
    22. for (SearchHit hit : hits){
    23. // 获取文档source
    24. String json = hit.getSourceAsString();
    25. //反序列化
    26. HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    27. System.out.println("hotelDoc"+hotelDoc);
    28. }
    29. }
    运行结果

    精确查询

    复合查询

    精确查询常见的有term查询和range查询,同样利用QueryBuilders实现:

    1.    @Test
    2.    void testBool() throws IOException {
    3.        // 1.准备Request
    4.        SearchRequest request = new SearchRequest("hotel");
    5.        // 2.准备DSL
    6.        // 2.1. 准备BooleanQuery
    7.        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    8.        //2.2. 添加term
    9.        boolQuery.must(QueryBuilders.termQuery("city","上海"));
    10.        // 2.3. 添加range
    11.        boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
    12.        request.source().query(boolQuery);
    13.        // 3.发送请求
    14.        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    15.        System.out.println(response);
    16.        extracted(response);
    17.   }
    18.    private void extracted(SearchResponse response) {
    19.        //4.解析响应
    20.        SearchHits searchHits = response.getHits();
    21.        //4.1. 获取总条数
    22.        long total = searchHits.getTotalHits().value;
    23.        System.out.println("共搜索到"+ total+"条数据");
    24.        //4.2. 文档数组
    25.        SearchHit[] hits = searchHits.getHits();
    26.        //4.3. 便利
    27.        for (SearchHit hit : hits){
    28.            // 获取文档source
    29.            String json = hit.getSourceAsString();
    30.            //反序列化
    31.            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    32.            System.out.println("hotelDoc"+hotelDoc);
    33.       }
    34.   }

    总结

    要构建查询条件,只要记住一个类:QueryBuilders

    排序和分页
    代码
    1. @Test
    2. void testPageAndSort() throws IOException {
    3.    //页码,每页大小
    4.    int page = 2,size = 5;
    5.    // 1.准备Request
    6.    SearchRequest request = new SearchRequest("hotel");
    7.    // 2.准备DSL
    8.    // 2.1. 准备Query
    9.    request.source().query(QueryBuilders.matchAllQuery());
    10.    // 2.2. 排序 sort
    11.    request.source().sort("price", SortOrder.ASC);
    12.    // 2.3. 分页 from,size
    13.    request.source().from((page-1)*size).size(5);
    14.    // 3.发送请求
    15.    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    16.    System.out.println(response);
    17.    extracted(response);
    18. }
    运行结果

    高亮

    高亮API包括请求DSL构建和结果解析两部分,我们先看请求的DSL构建:

    高亮结果处理
    代码
    1. @Test
    2. void testHighlight() throws IOException {
    3.    // 1.准备Request
    4.    SearchRequest request = new SearchRequest("hotel");
    5.    // 2.准备DSL
    6.    // 2.1. 准备Query
    7.    request.source().query(QueryBuilders.matchQuery("all","如家"));
    8.    // 2.2. 高亮
    9.    request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
    10.    // 3.发送请求
    11.    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    12.    System.out.println(response);
    13.    handleResponse(response);
    14. }
    15. private void handleResponse(SearchResponse response) {
    16.    //4.解析响应
    17.    SearchHits searchHits = response.getHits();
    18.    //4.1. 获取总条数
    19.    long total = searchHits.getTotalHits().value;
    20.    System.out.println("共搜索到"+ total+"条数据");
    21.    //4.2. 文档数组
    22.    SearchHit[] hits = searchHits.getHits();
    23.    //4.3. 遍历
    24.    for (SearchHit hit : hits){
    25.        // 获取文档source
    26.        String json = hit.getSourceAsString();
    27.        //反序列化
    28.        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    29.        // 获取高亮结果
    30.        Map highlightFields = hit.getHighlightFields();
    31.        if (!CollectionUtils.isEmpty(highlightFields)){
    32.            // 根据字段名称获取高亮结果
    33.            HighlightField highlightField = highlightFields.get("name");
    34.            if (highlightField != null){
    35.                //获取高亮值
    36.                String name = highlightField.getFragments()[0].string();
    37.                // 覆盖非高亮结果
    38.                hotelDoc.setName(name);
    39.           }
    40.       }
    41.        System.out.println("hotelDoc"+hotelDoc);
    42.   }
    43. }
    运行结果
    总结
    1. 所有的搜索DSL的构建,记住一个API:SearchRequest的source()方法

    2. 高亮结果解析是参考JSON结果,逐层解析

    黑马旅游案例

    案例1:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页

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

    2. 定义controller接口,接收页面请求,调用IHotelService的search方法

    3. 定义IHotelService中的search方法,利用match查询实现根据关键字搜索酒店信息

    1. @Override
    2. public PageResult search(RequestParams params) {
    3.    try {
    4.        // 1.准备Request
    5.        SearchRequest request = new SearchRequest("hotel");
    6.        // 2.准备DSL
    7.        // 2.1 query
    8.        String key = params.getKey();
    9.        if (key == null || key.trim().length() == 0){
    10.            request.source().query(QueryBuilders.matchAllQuery());
    11.       } else {
    12.            request.source().query(QueryBuilders.matchQuery("all",key));
    13.       }
    14.        // 2.2. 分页
    15.        int page = params.getPage();
    16.        int size = params.getSize();
    17.        request.source().from((page - 1) * size).size(size);
    18.        // 3.发送请求
    19.        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    20.        System.out.println(response);
    21.        handleResponse(response);
    22.        return handleResponse(response);
    23.   } catch (IOException e) {
    24.        throw new RuntimeException(e);
    25.   }
    26. }
    27. private PageResult handleResponse(SearchResponse response) {
    28.    //4.解析响应
    29.    SearchHits searchHits = response.getHits();
    30.    //4.1. 获取总条数
    31.    long total = searchHits.getTotalHits().value;
    32.    //4.2. 文档数组
    33.    SearchHit[] hits = searchHits.getHits();
    34.    //4.3. 遍历
    35.    List hotels = new ArrayList<>();
    36.    for (SearchHit hit : hits){
    37.        // 获取文档source
    38.        String json = hit.getSourceAsString();
    39.        //反序列化
    40.        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    41.        hotels.add(hotelDoc);
    42.   }
    43.    // 4.4.封装返回
    44.    return new PageResult(total, hotels);
    45. }

    案例2:添加品牌,城市,星际,价格等过滤功能

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

    2. 修改search方法的实现类,再关键字搜索时,如果brand等参数存在,对其做过滤

      1. city精确匹配

      2. brand精确匹配

      3. starNmae精确匹配

      4. price范围过滤

      5. 注意事项

        1. 多个条件之间时AND关系,组合多条件用BooleanQuery

        2. 参数存在才需要过滤,做好非空判断

    1. @Autowired
    2. private RestHighLevelClient client;
    3. @Override
    4. public PageResult search(RequestParams params) {
    5.    try {
    6.        // 1.准备Request
    7.        SearchRequest request = new SearchRequest("hotel");
    8.        // 2.准备DSL
    9.        // 2.1 query
    10.        // 构建BooleanQuery
    11.        buildBasicQuery(params,request);
    12.        // 2.2. 分页
    13.        int page = params.getPage();
    14.        int size = params.getSize();
    15.        request.source().from((page - 1) * size).size(size);
    16.        // 3.发送请求
    17.        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    18.        //4.
    19.        return handleResponse(response);
    20.   } catch (IOException e) {
    21.        throw new RuntimeException(e);
    22.   }
    23. }
    24. private void buildBasicQuery(RequestParams params,SearchRequest request) {
    25.    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    26.    //关键字搜索
    27.    String key = params.getKey();
    28.    if (key == null || "".equals(key)){
    29.        boolQuery.must(QueryBuilders.matchAllQuery());
    30.   } else {
    31.        boolQuery.must(QueryBuilders.matchQuery("all",key));
    32.   }
    33.    // 城市条件
    34.    if (params.getCity() != null && !params.getCity().equals("")){
    35.        boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
    36.   }
    37.    // 品牌条件
    38.    if (params.getBrand() != null && !params.getBrand().equals("")){
    39.        boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
    40.   }
    41.    // 星级条件
    42.    if (params.getStarName() != null && !params.getStarName().equals("")){
    43.        boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
    44.   }
    45.    // 价格条件
    46.    if (params.getMinPrice() != null && !params.getMinPrice().equals("")){
    47.        boolQuery.filter(QueryBuilders
    48.               .rangeQuery("price").gte(params.getMaxPrice()).lte(params.getMaxPrice()));
    49.   }
    50.    request.source().query(boolQuery);
    51. }
    52. private PageResult handleResponse(SearchResponse response) {
    53.    //4.解析响应
    54.    SearchHits searchHits = response.getHits();
    55.    //4.1. 获取总条数
    56.    long total = searchHits.getTotalHits().value;
    57.    //4.2. 文档数组
    58.    SearchHit[] hits = searchHits.getHits();
    59.    //4.3. 遍历
    60.    List hotels = new ArrayList<>();
    61.    for (SearchHit hit : hits){
    62.        // 获取文档source
    63.        String json = hit.getSourceAsString();
    64.        //反序列化
    65.        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    66.        hotels.add(hotelDoc);
    67.   }
    68.    // 4.4.封装返回
    69.    return new PageResult(total, hotels);
    70. }

    案例3:我附近的酒店

    步骤
    1. 前端页面定位后,会将你所有的位置发送到后台:

    2. 我们根据这个坐标,将酒店结果按照这个点的距离升序排序。

    3. 思路如下:

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

      2. 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能

    案例4:让指定的酒店再搜索中排名位置置顶

    步骤

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

    实现步骤分析:

    1. 给HotelDoc类添加isAD字段,Boolean类型

    2. 挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true

    3. 修改search方法,添加function score功能,给isAD值为true的酒店增加权重

    数据聚合

    聚合的种类

    聚合分类

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

    1. 桶(Bucket)聚合:用来对文档做分组

      1. TermaAggregation:按照文档字段值分组

      2. Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组

    2. 度量(Metric)聚合:用以计算一些值,比如:最大值,最小值,平均值等

      1. Avg:求平均值

      2. Max:求最大值

      3. Min:求最小值

      4. Stats:同时求max,min,avg,sum等

    3. 管道(pipeline)聚合:其它聚合的结果为基础做聚合

    总结
    什么是聚合?

    聚合是对文档数据的统计,分析,计算

    聚合的常见种类有哪些?
    1. Bucket:对文档数据分组,并统计每组数量

    2. Meric:最文档数做计算,例如avg

    3. Pipeline:基于其他聚合结果在做聚合

    参与聚合的字段类型必须是:
    1. keword

    2. 数值

    3. 日期

    4. 布尔

    DSL实现聚合

    DSL实现Bucket聚合

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

    类型为term类型,DSL示例:

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

    我们可以修改结果排序方式:

    1. #聚合功能,自定义排序规则
    2. GET /hotel/_search
    3. {
    4.  "size": 0,
    5.  "aggs": {
    6.    "brandAgg": {
    7.      "terms": {
    8.        "field": "brand",
    9.        "size": 20,
    10.        "order": {
    11.          "_count": "asc"
    12.       }
    13.     }
    14.   }
    15. }
    16. }

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

    1. #聚合功能,限定聚合范围
    2. GET /hotel/_search
    3. {
    4.  "query": {
    5.    "range": {
    6.      "price": {
    7.        "lte": 200
    8.     }
    9.   }
    10. },
    11.  "size": 0,
    12.  "aggs": {
    13.    "brandAgg": {
    14.      "terms": {
    15.        "field": "brand",
    16.        "size": 20
    17.     }
    18.   }
    19. }
    20. }
    总结

    aggs代表聚合,与query同级,此时query的作用是?

    限定聚合的文档范围

    聚合必须的三要素

    1. 聚合名称

    2. 聚合类型

    3. 聚合字段

    聚合可配置属性有:

    1. size:指定聚合结果数量

    2. order:指定聚合结果排序方式

    3. field:指定聚合字段

    DSL实现Metrics聚合

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

    我们可以利用stats聚合:

    1. #嵌套聚合metric
    2. GET /hotel/_search
    3. {
    4.  "size": 0,
    5.  "aggs": {
    6.    "brandAgg": {
    7.      "terms": {
    8.        "field": "brand",
    9.        "size": 20,
    10.        "order": {
    11.          "scoreAgg.avg": "desc"
    12.       }
    13.     },
    14.      "aggs": {
    15.        "scoreAgg": {
    16.          "stats": {
    17.            "field": "score"
    18.         }
    19.       }
    20.     }
    21.   }
    22. }
    23. }

    RestAPI实现聚合

    我们以品牌聚合为例,演示以下Java的RestClient使用,先看请求组装:

    再看下聚合结果解析

    1.    @Test
    2.    void testAggregation() throws IOException {
    3.        // 1. 准备Request
    4.        SearchRequest request = new SearchRequest("hotel");
    5.        // 2. 准备DSL
    6.        // 2.1 设置size
    7.        request.source().size(0);
    8.        // 2.2. 聚合
    9.        request.source().aggregation(AggregationBuilders
    10.                .terms("brandAgg")
    11.                .field("brand")
    12.                .size(10)
    13.       );
    14.        // 3. 发出请求
    15.        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    16.        // 4. 解析结果
    17.        Aggregations aggregations = response.getAggregations();
    18.        Terms brandTerms = aggregations.get("brandAgg");
    19.        Listextends Terms.Bucket> buckets = brandTerms.getBuckets();
    20.        // 4.3遍历
    21.        for (Terms.Bucket bucket : buckets) {
    22.            String key = bucket.getKeyAsString();
    23.            System.out.println(key);
    24.       }
    25.   }

    案例:在IUserService中定义方法,实现对品牌,城市,星级的聚合

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

    1. @Override
    2. public Map> filters() {
    3.    try {
    4.        // 1. 准备Request
    5.        SearchRequest request = new SearchRequest("hotel");
    6.        // 2. 准备DSL
    7.        // 2.1 设置size
    8.        request.source().size(0);
    9.        // 2.2. 聚合
    10.        buildAggregation(request);
    11.        // 3. 发出请求
    12.        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    13.        Map> result = new HashMap<>();
    14.        // 4. 解析结果
    15.        Aggregations aggregations = response.getAggregations();
    16.        // 4.1. 根据品牌名称,获取品牌结果
    17.        List brandList = getAggByName(aggregations,"brandAgg");
    18.        // 4.4. 放入map
    19.        result.put("品牌",brandList);
    20.        List cityList = getAggByName(aggregations,"cityAgg");
    21.        // 4.4. 放入map
    22.        result.put("城市",cityList);
    23.        List starList = getAggByName(aggregations,"starAgg");
    24.        // 4.4. 放入map
    25.        result.put("星级",starList);
    26.        return result;
    27.   } catch (IOException e) {
    28.        throw new RuntimeException(e);
    29.   }
    30. }
    31. private List getAggByName(Aggregations aggregations,String aggName) {
    32.    // 4.1. 根据聚合名称获取聚合结果
    33.    Terms brandTerms = aggregations.get(aggName);
    34.    // 4.2. 获取buckets
    35.    Listextends Terms.Bucket> buckets = brandTerms.getBuckets();
    36.    // 4.3遍历
    37.    List brandList = new ArrayList<>();
    38.    for (Terms.Bucket bucket : buckets) {
    39.        String key = bucket.getKeyAsString();
    40.        System.out.println(key);
    41.        brandList.add(key);
    42.   }
    43.    return brandList;
    44. }
    45. private void buildAggregation(SearchRequest request) {
    46.    request.source().aggregation(AggregationBuilders
    47.           .terms("brandAgg")
    48.           .field("brand")
    49.           .size(100)
    50.   );
    51.    request.source().aggregation(AggregationBuilders
    52.           .terms("cityAgg")
    53.           .field("city")
    54.           .size(100)
    55.   );
    56.    request.source().aggregation(AggregationBuilders
    57.           .terms("starAgg")
    58.           .field("starName")
    59.           .size(100)
    60.   );
    61. }

    前端页面会向服务端发起请求,查询品牌,城市,星级等字段的聚合结果:

    自动补全

    拼音分词器

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

    GitHub - infinilabs/analysis-pinyin: 🛵 This Pinyin Analysis plugin is used to do conversion between Chinese characters and Pinyin.

    测试

    自定义分词器

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

    1. character fiters:在tokenizer之前对文本进行处理。例如删除字符,替换字符

    2. tokenizer:将文本按照按照一定的规则切割词条(term)。例如keyword,就是不分词;还有ik_smart

    3. tokenizer filter :将tokenizer输出的词条做进一步处理。例如大小写转换,同义词处理,拼音处理等

    拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用。

    1. # 自定义分词器
    2. PUT /test
    3. {
    4.  "settings": {
    5.    "analysis": {
    6.      "analyzer": {
    7.        "my_analyzer": {
    8.          "tokenizer": "ik_max_word",
    9.          "filter": "py"
    10.       }
    11.     },
    12.      "filter": {
    13.        "py": {
    14.          "type": "pinyin",
    15.          "keep_full_pinyin": false,
    16.          "keep_joined_full_pinyin": true,
    17.          "keep_original": true,
    18.          "limit_first_letter_length": 16,
    19.          "remove_duplicated_term": true,
    20.          "none_chinese_pinyin_tokenize":false
    21.          
    22.       }
    23.     }
    24.   }
    25. },
    26.  "mappings": {
    27.    "properties": {
    28.      "name": {
    29.        "type": "text",
    30.        "analyzer": "my_analyzer"
    31.       , "search_analyzer": "ik_smart"
    32.     }
    33.   }
    34. }
    35. }
    36. POST /test/_doc/1
    37. {
    38.  "id": 1,
    39.  "name": "狮子"
    40. }
    41. POST /test/_doc/2
    42. {
    43.  "id": 2,
    44.  "name": "虱子"
    45. }
    46. GET /test/_search
    47. {
    48.  "query": {
    49.    "match": {
    50.      "name": "调入狮子笼子咋办"
    51.   }
    52. }
    53. }
    总结

    如何使用拼音分词器?

    1. 下载pinyin分词器

    2. 解压并放到elasticsearch的plugin目录

    3. 重启即可

    如何自定义分词器?

    1. 创建索引库,在settings中配置,可以包含三部分

    2. character filter

    3. tokenizer

    4. filter

    拼音分词器注意思事项?

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

    自动补全查询

    completion suggester查询

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

    1. 参与补全查询的字段必须时completion类型。

    2. 字段的类容一般是用来补全的多词条形成的数组

    1. #自动补全索引库
    2. PUT test2
    3. {
    4.  "mappings": {
    5.    "properties": {
    6.      "title":{
    7.        "type": "completion"
    8.     }
    9.   }
    10. }
    11. }
    12. # 示例数据
    13. POST test2/_doc
    14. {
    15.  "title" : ["Sony", "WH-1000XM3"]
    16. }
    17. POST test2/_doc
    18. {
    19.  "title": ["SK-II", "PITERA"]
    20. }
    21. POST test2/_doc
    22. {
    23.  "title": ["Nintendo", "switch"]
    24. }
    25. # 自动补全查询
    26. GET /test2/_search
    27. {
    28.  "suggest": {
    29.    "titleSuggest": {
    30.      "text": "s",
    31.      "completion": {
    32.        "field": "title",
    33.        "skip_duplicates": true,
    34.        "size": 10
    35.     }
    36.   }
    37. }
    38. }
    总结

    自动补全对字段的要求:

    1. 类型是completion类型

    2. 字段值是多词条的数组

    实现酒店搜索框自动补全

    实现思路如下:

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

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

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

    4. 给HoteDoc类添加suggestio字段,内容包含brand,susiness

    5. 重新导入数据到hotel库

    1. @Override
    2. public List getSuggestions(String prefix) {
    3.    try {
    4.        // 1. 准备Request
    5.        SearchRequest request = new SearchRequest("hotel");
    6.        // 2. 准备SDL
    7.        request.source().suggest(new SuggestBuilder().addSuggestion(
    8.                "suggestions",
    9.                SuggestBuilders.completionSuggestion("suggestion")
    10.                       .prefix(prefix)
    11.                       .skipDuplicates(true)
    12.                       .size(10)
    13.       ));
    14.        // 3. 发送请求
    15.        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    16.        // 4. 解析响应结果
    17.        Suggest suggest = response.getSuggest();
    18.        // 4.1. 根据补全查询名称,获取补全结果
    19.        CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
    20.        //4.2. 获取options
    21.        List options = suggestions.getOptions();
    22.        //4.3. 遍历
    23.        List list = new ArrayList<>(options.size());
    24.        for (CompletionSuggestion.Entry.Option option : options){
    25.            String text = option.getText().toString();
    26.            list.add(text);
    27.       }
    28.        return list;
    29.   } catch (IOException e) {
    30.        throw new RuntimeException();
    31.   }
    32. }

    RestAPI实现自动补全

    先看请求参数构造API

    1. @Test
    2. void testSuggest() throws IOException {
    3.    // 1. 准备Request
    4.    SearchRequest request = new SearchRequest("hotel");
    5.    // 2. 准备SDL
    6.    request.source().suggest(new SuggestBuilder().addSuggestion(
    7.            "suggestions",
    8.            SuggestBuilders.completionSuggestion("suggestion")
    9.                   .prefix("h")
    10.                   .skipDuplicates(true)
    11.                   .size(10)
    12.   ));
    13.    // 3. 发送请求
    14.    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    15.    // 4. 解析响应结果
    16.    System.out.println(response);
    17. }

    自动补全

    数据同步

    数据同步思路分析

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

    方案一:同步调用

    方案二:异步调用

    方案三:监听binlog

    总结

    方式一:同步调用

    1. 优点:实现简单,粗暴

    2. 缺点:业务耦合度高

    方式二:异步通知

    1. 优点:低耦合,实现难度一般

    2. 缺点:依赖mq的可靠性

    方式三:监听binlog

    1. 优点:完全解除服务耦合

    2. 缺点:开启binlog增加数据库负担,实现复杂度高

    实现elasticsearch与数据库同步

    利用课前资料提供的hotel-admin项目作为酒店管理的微服务。当酒店数据发生增,删,查,改时,要求对elasticsearch中数据也要完成相同操作。

    利用MQ实现mysql于elasticsearch中数据也要完成操作。

    步骤:

    1. 导入课前资料提供的hotel-admin项目,启动并测试酒店数据的CRUD

    2. 声明exchange,queue,RoutingKey

    3. 在hotel-admin中的增,删,改业务中完成发送消息

    4. 在hotel-demo中完成消息监听,并更新elasticsearch中数据

    5. 启动并测试数据同步功能

    elasticsearch集群

    搭建ES集群

    ES集群结构

    单机的elasticsearch做数据存储,必然面临两个问题:海量数据存储问题,单点故障问题。

    1. 海量数据存储问题:将索引从逻辑上拆分为N个分片(shard),存储到多个结点

    2. 单点故障问题:将分片数据在不同结点备份(replica)

    文件资料里有对应文档。

    集群脑裂问题

    elasticsearch中集群结点有不同的职责划分:

    结点类型配置参数默认值节点职责
    master eligiblenode.mastertrue备选主节点:主节点可以管理和记录集群状态,决定分片在哪个节点,处理创建和删除索引库的请求
    datanode.datatrue数据节点:存储数据,搜索,聚合,CRUD
    ingestnode.ingesttrue数据存储之前的预处理
    coordinating上面3个参数都是为false则为coordinating节点路由请求到其他节点,合并其它节点处理的结果,返回给用户

    ES集群的脑裂

    默认情况下,每个节点都是master eligible节点,因此一旦master节点宕机,其它候选节点会选举一个成为主节点。当主节点于其它节点网路解耦故障时,可能发生脑裂问题。

    为了避免脑裂,需要要求选票超过(eligible节点数量+1)/ 2 才能当选为主节点,因此eligible节点数量组好是奇数。对于配置项discovery.zen.minimum_master_nodes,在es7.0以后,已经成为我们配置,因此一般不会发生脑裂问题

    总结

    master eligible节点的作用是上面?

    1. 参与集群选主

    2. 主节点可以管理集群状态,管理分片信息,处理创建和删除索引库的请求

    3. data节点的作用是上面?

    data节点的作用是上面?

    1. 数据的CRUD

    coordinator节点的作用是什么?

    1. 路由请求到其它节点

    2. 合并查询到的结果,返回给用户

    集权故障转移

    集群分布式存储

    当新增文档时,应该保存不同分片,保证数据均衡,那么coordinating node如何确定数据存储到哪个分片呢?

    elasticsearch会通过hash算法来计算文档应该存储到哪个分片:

    shard = hash(_routing) % number_of_shards

    说明:

    1. _routing默认是文旦的id

    2. 算法与分片数量有关,因此索引库一旦创建,分片数量不能修改!

    集群分布式查询

    elasticsearch的查询分成两个阶段:

    1. scatter phase:分散阶段,coordinating node会把请求分发到每一个分片

    2. gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户

    总结

    分布式新增如何确定分片?

    1. coordinating node根据id做hash运算,得到结果对shard数量取余,余数就是对应的分片

    分布式查询:

    1. 分散阶段:coordinating node将查询请求分发给不同分片

    2. 收集阶段:将查询结果汇总到coordinating node,整理并返回给用户

    ES集群故障转移

    集群的master节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到其它急待你,确保数据安全,这个叫做故障转移

    总结

    故障转移

    1. master宕机后,EligibleMaster选举为新的主节点。

    2. master节点监控分片,阶段状态,将故障节点上的分片转移到正常节点,确保数据安全。

  • 相关阅读:
    攻防世界MISC练习区(gif 掀桌子 ext3 )
    力扣题库3.无重复字符的最长子串
    One class learning(SVDD)
    过去将来时习题
    win11安装ubuntu(by wsl2)
    四路定时控制器设计原理
    数据化运营07 分析突破口:如何通过指标抓取数据的主要特征?
    [附源码]Python计算机毕业设计Django网咖管理系统
    中国互联网众筹行业
    前端知识点、技巧、webpack、性能优化(持续更新~)
  • 原文地址:https://blog.csdn.net/qq_53573029/article/details/136421997