分布式搜索学习

1.elasticsearch

1.什么是elasticsearch

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

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

        elasticsearch是elastic stack的核心,负责存储,搜索,分析数据,底层是Lucene技术。Logstash,Beats负责数据抓取。Kibana负责数据可视化。

        Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目。优点:1.易扩展,2.高性能。缺点:1.只限于Java语言开发,2.学习曲线陡峭,3.不支持水平扩展

        相比于lucene,elasticsearch优势:1.支持分布式,可水平扩展 2.提供Restful接口,可被任何语言调用。

2.正向索引和倒排索引

        传统数据库(如MySQL)采用正向索引(通过ID找内容)。

        elasticsearch采用倒排索引(通过内容找ID):

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

                词条(term):文档按照语义分成的词语,词条不重复,为词条做索引,对应的数据是文档ID。按词条进行搜索,然后通过词条得到文档ID,再通过文档ID去获取对应的文档数据。

3.文档

        elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中(类似于mongodb)。

4.索引(Index)

        相同类型的文档的集合。比如字段都相同的json数据,就像数据库中的表结构,一个索引映射一张表

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

 5.架构

MySQL:擅长事务类型操作,可以确保数据的安全和一致性。数据量少用这个。

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

2.部署单点es

1.创建网络

docker network create es-net

2.安装elasticsearch

docker pull elasticsearch
docker run -d \
	--name es \   # 容器名字
    -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \   # 环境变量  JVM的堆内存大小
    -e "discovery.type=single-node" \    # 运行模式-单独模式
    -v es-data:/usr/share/elasticsearch/data \   # 数据卷挂载
    -v es-plugins:/usr/share/elasticsearch/plugins \
    --privileged \
    --network es-net \  # 让es容器加入这个容器中
    -p 9200:9200 \   # http协议端口,用户访问
    -p 9300:9300 \   # es容器各个节点互联的端口
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:端口映射配置

访问IP:9200

常见问题:

1.9200网址无法访问或报错。可能原因:服务器没有安装java jdk

查看是否安装jdk

java -version

 如果未安装,装一个

服务器安装jdk教程

yum search java | grep -i --color JDK        #  查看JDK软件包列表
yum  install  java-1.8.0-openjdk   java-1.8.0-openjdk-devel      
# 安装JDK,如果没有java-1.8.0-openjdk-devel就没有javac命令 

 3.安装Kibana

docker pull kibana
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \  # 和es在同一个服务器,所以可以使用容器名互联
--network=es-net \
-p 5601:5601  \
kibana  # 版本要和es保持一致

访问IP:5601

GET _search  // get请求方式,搜索 下面是搜索内容
{
  "query": {
    "match_all": {}
  }
}

# 模拟请求模拟请求请求
GET /

常见问题:

1.访问失败

Unable to connect to Elasticsearch at http://elasticsearch:9200.

如下图:

 解决方案:修改kibana的配置文件kibana.yml

docker exec -it 容器名 /bin/bash
cd /etc/kibana
cat kibana.yml  # 把文件中的内容复制出来,然后进行修改
cat > kibana.yml # 把修改好的内容在粘贴进去  ctrl + c 退出

修改完成之后重启kibana容器

docker restart kibana

 在容器中修改文件方法(推荐用方法1,简单):在容器中修改文件方法1

容器中修改文件方法2

本问题处理其它方法:

Kibana无法访问问题解决方法1

Kibana无法访问问题解决方法2

4.分词器

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

        处理中文分词,一般会使用IK分词器。

1.在线安装ik插件

# 进入容器内部
docker exec -it es /bin/bash

# 在线下载并安装
./bin/elasticsearch-plugin  install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip

#退出
exit
#重启容器
docker restart es

2.离线安装ik插件

1.查看数据卷目录

docker volume inspect es-plugins

2.解压缩分词器安装包

        在数据卷目录解压安装包

3.重启容器

3.测试

IK分词器包含两种模式:

        ik_smart:最少切分  粗粒度

        ik_max_word:最细切分  细粒度

# 测试分词器
POST /_analyze
{
  "text": "123",
  "analyzer": "ik_max_word"
}

4.分词器的扩展和停用词典

在ik分词器的config文件加下的IKAnalyzer.cfg文件中编辑

<properties>
<comment>IK Analyzer 扩展配置</comment>
<!-- 用户可以在这里配置自己的扩展字典  -->
<entry key="ext_dict"/>
<!-- 用户可以在这里配置自己的扩展停止词字典 -->
<entry key="ext_stopwords"/>
<!-- 用户可以在这里配置远程扩展字典  -->
<!--  <entry key="remote_ext_dict">words_location</entry>  -->
<!-- 用户可以在这里配置远程扩展停止词字典 -->
<!--  <entry key="remote_ext_stopwords">words_location</entry>  -->
</properties>

在对应的词典文件中修改内容

修改之后重启容器 

3.索引库操作

1.mapping属性

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

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

        字符串:text(可分词的文本),keyword(精确值,例如:品牌,国家,ip地址。 无法进行分词的专有名词)

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

        布尔:boolean

        日期:date

        对象:object

2.index:是否创建索引(是否参与搜索),默认为true

3.analyzer:使用哪种分词器

4.properties:该字段的子字段

2.创建索引库

DSL语句

PUT /index_name  # 索引名称
{
  "mappings": {  # 映射
    "properties": {  # 字段
      "info": {  # 字段1名称
        "type":"text",  # 字段1类型
        "analyzer":"ik_smart"  # 分词模式
      },
      "email":{
        "type":"keyword",
        "index": false
      },
      "name": {
        "type":"object",
        "properties": {  # 子字段数据
          "firstName": {  # 子字段1
            "type":"keyword"
          },
          "lastName": {
            "type":"keyword"
          }
        }
      }
    }
  }
}

3.索引库操作

查看索引库

GET /索引库名称

删除索引库

DELETE /索引库

修改索引库

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

PUT /index_name/_mapping
{
  "properties": {
    "age":{
      "type":"integer"
    }
  }
}

4.文档操作

没进行一次操作,文档版本号都会加1

1.添加文档

POST /index_name/_doc/1  # 索引名称/_doc/id  id不给会自动生成
{
  "info":"字段值",
  "email":"hhh@qq.cn",
  "name":{
    "firstName":"value1",
    "lastName":"value2"
  }
}

2.查询文档

GET /索引库名称/_doc/id

3.删除文档

DELETE /索引库名称/_doc/id

4.修改文档

1.全量修改,会删除旧文档,添加新文档  如果存在则修改,不存在则新增

PUT /index_name/_doc/1  # 索引名称/_doc/id 先删除后新增
{
  "info":"字段值",
  "email":"hhh@qq.cn",
  "name":{
    "firstName":"value1",
    "lastName":"value2"
  }
}

2.增量修改,修改指定字段

POST /index_name/_update/1
{
  "doc":{
    "email":"new_value"
  }
}

4.RestClient操作索引库

1.什么是RestClient

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

2.基本步骤

1.引入es的RestHighLevelClient依赖

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

2.springBoot默认的ES版本是7.6.2,所以需要覆盖默认的版本

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

3.初始化RestHighLevelClient

package cn.itcast.hotel;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

public class HotelIndexTest {
    private RestHighLevelClient client;

    @Test
    void testInit() {
        System.out.println(client);
    }

    /* 创建连接  初始化 */
    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://192.168.1.52:9200")
        ));
    }

    /* 销毁连接 */
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}

 4.创建索引库

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

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

5.删除索引库

    @Test
    void deleteHotelIndex() throws IOException {
        DeleteIndexRequest request = new DeleteIndexRequest("hotel");
        
        client.indices().delete(request, RequestOptions.DEFAULT);
    }

6.判断索引库是否存在

    @Test
    void existsHotelIndex() throws IOException {
        GetIndexRequest request = new GetIndexRequest("hotel");

        boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);

        System.out.println(exists ? "索引库已经存在" : "索引库不存在");
    }

5.RestClient操作文档

1.基本操作

1.插入文档

    @Test
    void testAddDocument() throws IOException {
        // 根据id查询数据库中数据
        Hotel hotel = hotelService.getById(1);
        // 转换为文档类型
        HotelDoc hotelDoc = new HotelDoc(hotel);
        // 1.准备Request对象
        IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
        // 2.准备Json文档
        request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
        // 3.发送请求
        client.index(request, RequestOptions.DEFAULT);
    }

2.查询文档

    @Test
    void testGetDocumentById() throws IOException {
        // 1.创建request对象
        GetRequest request = new GetRequest("hotel", "1");
        // 2.发送请求,获取响应
        GetResponse response = client.get(request, RequestOptions.DEFAULT);
        // 3.解析结果
        String json = response.getSourceAsString();
        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        System.out.println(hotelDoc);
    }

3.修改文档

1.全量更新

        再次写入id一样的文档,就会删除旧文档,添加新文档

2.局部更新

    @Test
    void testUpdateDocumentById() throws IOException {
        // 1.创建request对象
        UpdateRequest request = new UpdateRequest("hotel", "1");
        // 2.准备请求参数
        request.doc(
                "price", "100",
                "startName", "五钻"
        );
        // 发送请求
        client.update(request, RequestOptions.DEFAULT);
    }

4.删除文档

    @Test
    void testDeleteDocument() throws IOException {
        // 1.准备request
        DeleteRequest request = new DeleteRequest("hotel", "1");
        // 2.发送请求
        client.delete(request, RequestOptions.DEFAULT);
    }

2.批量导入数据到es

@Test
    void testBulkRequest() throws IOException {
        // 批量查询数据
        List<Hotel> hotels = hotelService.list();  // 随便什么对象都行
        // 1.创建request
        BulkRequest request = new BulkRequest();
        // 2.准备参数,添加多个request请求
        // 转换为文档类型HotelDoc
        for (Hotel hotel : hotels) {
            HotelDoc hotelDoc = new HotelDoc(hotel);  // 随便什么对象都行
            request.add(new IndexRequest("hotel")
                    .id(hotelDoc.getId().toString())
                    .source(JSON.toJSONString(hotelDoc), XContentType.JSON));
        }
        // 3.发送请求
        client.bulk(request, RequestOptions.DEFAULT);
    }

6.DSL查询

DSL Query的分类

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

1.查询基本语法

GET /index_name/_search
{
  "query": {
    "查询类型": {
       "查询字段" : "查询值"
    }
  }
}

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

GET /index_name/_search
{
  "query": {
    "match_all": {  # 查询所有,不用写东西
      
    }
  }
}

2.全文检索(full text)查询

利用分词器对用户输入的内容分词,然后去倒排索引库中匹配,常用于搜索框搜索。例如

1.match查询

对用户输入内容进行分词,去倒排索引库去查,一次只能一个字段

GET /index_name/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"  # FIELD:字段名  TEXT: 搜索内容
    }
  }
}

2.multi_match

允许同时查询多个字段

GET /index_name/_search
{
  "query": {
    "multi_match": {
      "query": "value",
      "fields": ["key1", "key2", "age"]   # 多个字段搜索同一个值
    }
  }
}

3.精确查询

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

 1. range 数值范围

GET /index_name/_search
{
  "query": {
    "range": {
      "FIELD": {   # 字段名
        "gte": 10,   # 大于等于
        "lte": 20    # 小于等于
      }
    }
  }
}

 2.trem 根据词条精确值查询

GET /index_name/_search
{
  "query": {
    "term": {
      "key": {   # 字段名
        "value": "搜索值"  
      }
    }
  }
}

3.地理(geo)查询

根据经纬度查询。例如:

1.geo_distance

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

GET /index_name/_search
{
  "query": {
    "geo_distance": {
      "distance": "15km",
      "location": 32.2, 124.5
    }
  }
}
2.geo_bounding_box

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

GET /index_name/_search
{
  "query": {
    "geo_bounding_box": {
      "FIELD": {    # 类型为geo_point的字段名称
        "top_left": {
          "lat": 30.1,
          "lon": 123.2
        },
        "bottom_right": {
          "lat": 30.5,
          "lon": 123.2
        }
      }
    }
  }
}

4.复合(compound)查询

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

1.Boolean Query

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

must:必须匹配每个子查询, 与

should:选择性匹配子查询, 或

must_not:必须不匹配,不参与算分, 非

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

GET /index_name/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "FIELD": {
              "value": "VALUE"
            }
          }
        }
      ],
      "must_not": [
        {
          "range": {
            "FIELD": {
              "gte": 10,
              "lte": 20
            }
          }
        }
      ],
      "should": [
        {
          "term": {
            "FIELD": {
              "value": "VALUE"
            }
          }
        }
      ],
      "filter": [
        {"range": {
          "FIELD": {
            "gte": 10,
            "lte": 20
          }
        }}
      ]
    }
  }
}

2.function_score:算分函数查询

可以控制文档相关性算分(query score),控制文档排名。

算分函数4部分:

        1.原始查询条件,搜索文档并根据相关性打分(query score)

        2.过滤条件,符合条件的文档才会被重新算分

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

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

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

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

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

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

                multiply:两者相乘。默认。

                replace:用function score替换query score

                其它: sum, avg, max, min

GET /hotel/_search
{
  "query": {
    "function_score": {
      "query": {            # 原始查询条件,搜索文档并根据相关性打分(query score)
        "match": {
          "FIELD": "TEXT"
        }
      },
      "functions": [
        {
          "filter": {           # 过滤条件,符合条件的文档才会被重新算分
            "term": {
              "FIELD": "VALUE"
            }
          },
          "weight": 10          # 算法函数
        }
      ],
      "boost_mode": "multiply"    # 加权模式
    }
  }
}

3.相关性算分

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

TF算法

        TF(词条频率) = 词条出现次数/文档中词条总数

TF-IDF算法(传统算法,新版本5.0后已不采用)

        IDF(逆文档频率) = Log(文档总数/包含词条的文档总数)

        score = \sum_{i}^{n}TF*IDF

BM25算法

        Score(Q,d) = \sum_{i}^{n}\log (1 + \frac{N - n + 0.5}{n + 0.5})\cdot \frac{f_{i}}{f_{i} + k_{1} \cdot (1 - b + b \cdot \frac{dl}{avgdl})}

7.搜索结果处理

1.排序

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

GET /index_name/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "FIELD": {            # 指定要排序的字段
        "order": "desc"     # 排序方式
      }
    },
    {
      "_geo_distance": {    # 地理坐标的排序
        "FIELD": {
          "lat": 40,
          "lon": -70
        },
        "order": "asc",
        "unit": "km"
      }
  ]
}

2.分页

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

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

GET /index_name/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,     # 分页开始的位置,默认从0开始
  "size": 20,    # 去多少条
  "sort": [
    {
      "FIELD": {
        "order": "desc"
      }
    }
  ]
}

深度分页问题

        ES是分布式的,所以会面临深度分页问题。例如获取from=990,size=10的数据:

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

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

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

        如果搜索页数过深,或者结果集越大,对内存和CPU的消耗也越高。因此ES设定结果集查询的上限是10000。

深度分页解决方案

1.search after:分页时需要排序,原理是从上一次的排序值开始,查询下一次数据。推荐。但是只能往后翻页,不能往前,随机。

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

3.高亮

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

原理:

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

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

GET /index_name/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT"
    }
  },
  "highlight": {
    "fields": {
      "age": {        # 指定要高亮的字段
        "pre_tags": "<em>",        # 用来标记高亮字段的前置标签
        "post_tags": "</em>",      # 用来标记高亮字段的后置标签
        "require_field_match": "false"   # 搜索字段是否必须与高亮字段一致,默认是true
      }
    }
  }
}

8.RestClient查询文档

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

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

示例:match_all查询

/* match_all查询 */
    @Test
    void testMatchAll() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().query(QueryBuilders.matchAllQuery());
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析结果
        SearchHits searchHits = response.getHits();
        // 5.查询的总条数
        long total = searchHits.getTotalHits().value;
        System.out.println("total = " + total);
        // 6.查询的结果数组
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            // 获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            System.out.println("hotelDoc = " + hotelDoc);
        }
        System.out.println(response);
    }

1.全文检索查询

1.match查询

/* match查询 */
    @Test
    void testMatch() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().query(QueryBuilders.matchQuery("key", "value"));
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析结果
        SearchHits searchHits = response.getHits();
        // 5.查询的总条数
        long total = searchHits.getTotalHits().value;
        System.out.println("total = " + total);
        // 6.查询的结果数组
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            // 获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            System.out.println("hotelDoc = " + hotelDoc);
        }
    }

2.bool查询

/* bool查询 */
    @Test
    void testBool() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        // 2.1准备booleanQuery
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        // 2.2添加term
        boolQuery.must(QueryBuilders.termQuery("key", "value"));
        // 2.3添加range
        boolQuery.filter(QueryBuilders.rangeQuery("key").lte(200));
        request.source().query(boolQuery);
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析结果
        handleResponse(response);
    }

3.排序和分页

/* 分页排序 */
    @Test
    void testPageAndSort() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().query(QueryBuilders.matchAllQuery());
        // 2.1排序
        request.source().sort("key", SortOrder.ASC);
        // 2.2分页
        request.source().from(0).size(10);
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析结果
        handleResponse(response);
    }

4.高亮

    /* 高亮 */
    @Test
    void testHighlight() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().query(QueryBuilders.matchQuery("key", "value"));
        // 2.1高亮
        request.source().highlighter(new HighlightBuilder().field("key").requireFieldMatch(false).preTags("<em>").postTags("</em>"));
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析结果
        handleResponse(response);
    }
private static void handleResponse(SearchResponse response) {
        SearchHits searchHits = response.getHits();
        // 5.查询的总条数
        long total = searchHits.getTotalHits().value;
        System.out.println("total = " + total);
        // 6.查询的结果数组
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            // 获取文档source
            String json = hit.getSourceAsString();
            // 反序列化
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
            if (!CollectionUtils.isEmpty(hit.getHighlightFields())) {
                // 获取高亮结果
                Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                // 根据字段名获取高亮结果
                HighlightField highlightField = highlightFields.get("name");
                // 获取高亮值
                String name = highlightField.getFragments()[0].string();
                // 覆盖非高亮结果
                hotelDoc.setName(name);
            }
            System.out.println("hotelDoc = " + hotelDoc);
        }
    }

9.数据聚合aggregations

可以实现对文档数据的统计,分析,运算。参与聚合的字段类型必须是keyword,数值,日期,布尔。

1.聚合分类

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

        TermAggregation:按照文档字段值分组

        Date Histogram:按照日期阶梯分组

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

        Avg:平均

        Max,Min:最值

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

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

2.DSL实现Bucket聚合

GET /index_name/_search
{
  "size": 0,      # 设置size为0,结果中不包含文档,只包含聚合结果
  "aggs": {       # 定义聚合
    "NAME": {         # 聚合名称
      "terms": {      # 聚合的类型
        "field": "",    # 参与聚合的字段
        "order": {
          "_count": "asc"
        }, 
        "size": 10      # 希望获取的聚合结果数量
      }
    }
  }
}

默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序,可通过order属性修改

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

GET /index_name/_search
{
  "query": {
    "range": {
      "FIELD": {
        "gte": 10,
        "lte": 20
      }
    }
  }, 
  "size": 0,
  "aggs": {
    "NAME": {
      "terms": {
        "field": "",
        "order": {
          "_count": "asc"
        }, 
        "size": 10
      }
    }
  }
}

聚合三要素: 聚合名称,聚合类型,聚合字段

聚合可配置属性: 

        size:指定聚合结果数量

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

        field:指定聚合字段

3.DSL实现Metrics聚合

GET /index_name/_search
{
  "size": 0,
  "aggs": {
    "NAME": {
      "terms": {
        "field": "",
        "order": {
          "NAME2.avg": "asc"     # 对聚合出的平均值进行排序
        }, 
        "size": 10
      },
      "aggs": {       #  NAME聚合的子聚合,就是分组后对每组分别计算
        "NAME2": {    # 聚合名称
          "stats": {     # 聚合类型,stats可以计算min,max,avg等
            "field": "field_name"   # 聚合字段
          }
        }
      }
    }
  }
}

4.RestAPI实现聚合

    @Test
    void testAggregation() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().size(0);
        request.source().aggregation(AggregationBuilders.terms("聚合名称").field("字段名称").size(10));
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析结果
        // 4.1解析聚合结果
        Aggregations aggregations = response.getAggregations();
        // 4.2根据聚合名称获取聚合结果
        Terms terms = aggregations.get("组合结果");
        // 4.3获取buckets
        List<? extends Terms.Bucket> buckets = terms.getBuckets();
        // 4.4遍历buckets
        for (Terms.Bucket bucket : buckets) {
            // 4.5获取key
            String key = bucket.getKeyAsString();
            System.out.println("key = " + key);
        }
    }

10.自动补全

1.安装配音分词器

下载配音分词器文件----上传至es日期插件目录(/var/lib/docker/volumes/es-plugins/_data)---重启es容器(时间很长)

测试:

POST /_analyze
{
  "text": ["测试内容"],
  "analyzer": "pinyin"
}

测试结果:

{
  "tokens" : [
    {
      "token" : "ce",
      "start_offset" : 0,
      "end_offset" : 0,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "csnr",
      "start_offset" : 0,
      "end_offset" : 0,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "shi",
      "start_offset" : 0,
      "end_offset" : 0,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "nei",
      "start_offset" : 0,
      "end_offset" : 0,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "rong",
      "start_offset" : 0,
      "end_offset" : 0,
      "type" : "word",
      "position" : 3
    }
  ]
}

2.自定义分词器

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

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

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

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

自定义方法:在插件索引库时,通过settings来配置自定义的analyzer(分词器)

PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": {       # 自定义分词器
        "my_analyzer": {     # 分词器名称
          "tokenizer": "ik_max_word",
          "filter": "py"    # filter名称,指向下方py
        }
      },
      "filter": {
        "py": { 
          "type": "pinyin",       # 分词器类型
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_analyzer"   # 使用上方的分词器
      }
    }
  }
}

拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用。有同音字问题。需要在创建索引时指定,在搜索时使用另一个分词器。

  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_analyzer",
        "search_analyzer": "ik_smart"
      }
    }
  }

3.Completion Suggester查询

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

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

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

# 自动补全的索引库
PUT index_name
{
  "mappings": {
    "properties": {
      "title": {
        "type": "completion"
      }
    }
  }
}
# 查询语法
GET /index_name/_search
{
  "suggest": {
    "YOUR_SUGGESTION": {   # 查询名称
      "text": "YOUR TEXT",   # 要查询的数据
      "completion": {
        "FIELD": "MESSAGE",     # 字段名
        "skip_duplicates": true,   # 跳过重复的
        "size": 10   # 一次查10个
      },
    }
  }
}

4.RestAPI实现自动补全

    @Test
    void testSuggest() throws IOException {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().suggest(new SuggestBuilder()
                .addSuggestion("suggestName",
                        SuggestBuilders.completionSuggestion("字段名")
                                .prefix("关键字")
                                .skipDuplicates(true)
                                .size(10)
                )
        );
        // 3.发起请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析结果
        Suggest suggest = response.getSuggest();
        // 4.1根据补全查询名称,获取补全结果
        CompletionSuggestion suggestion = suggest.getSuggestion("suggestName");
        // 4.2获取options
        List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();
        for (CompletionSuggestion.Entry.Option option : options) {
            System.out.println("option.getText() = " + option.getText().toString());
        }
        System.out.println("response = " + response);
    }

11.数据同步

1.常用数据同步方案

1.同步调用

        优点:实现简单,粗暴

        缺点:业务耦合度高

2.异步通知

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

        缺点:依赖mq的可靠性

3.监听binlog

        优点:完全解除服务间耦合

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

12.ES集群

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

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

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

1.搭建ES集群

1.编写一个docker-compose文件

通过容器来模拟节点

version: '2.2'
services:
  es01:
    image: elasticsearch:7.12.1    # 镜像
    container_name: es01           # 容器名称
    environment:
      - node.name=es01             # 节点名称,不能重复
      - cluster.name=es-docker-cluster    # 集群名称,集群名称相同则在同一个集群
      - discovery.seed_hosts=es02,es03      # 集群中其他节点的ip,容器内用名称指定
      - cluster.initial_master_nodes=es01,es02,es03    # 候选主节点
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    volumes:
      - data01:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
    networks:
      - elastic
  es02:
    image: elasticsearch:7.12.1
    container_name: es02
    environment:
      - node.name=es02
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es01,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    volumes:
      - data02:/usr/share/elasticsearch/data
    ports:
      - 9201:9200
    networks:
      - elastic
  es03:
    image: elasticsearch:7.12.1
    container_name: es03
    environment:
      - node.name=es03
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es01,es02
      - cluster.initial_master_nodes=es01,es02,es03
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    volumes:
      - data03:/usr/share/elasticsearch/data
    networks:
      - elastic
    ports:
      - 9202:9200
volumes:
  data01:
    driver: local
  data02:
    driver: local
  data03:
    driver: local

networks:
  elastic:
    driver: bridge

2.修改配置

es运行需要修改一些linux系统权限,修改`/etc/sysctl.conf`文件

vi /etc/sysctl.conf

添加下面的内容:

vm.max_map_count=262144

然后执行命令,让配置生效:

sysctl -p

3.启动

docker-compose up -d

2.ES集群监控

推荐使用cerebro来监控es集群状态,官方网址:https://github.com/lmenezes/cerebro

下载cerebro压缩包,解压,进入bin目录,双击其中的cerebro.bat文件即可启动服务。访问http://localhost:9000 即可进入管理界面。输入你的elasticsearch的任意节点的地址和端口,点击connect即可:

创建分片索引库

PUT /index_name
{
  "settings": {
    "number_of_shards": 3, // 分片数量
    "number_of_replicas": 1 // 副本数量
  },
  "mappings": {
    "properties": {
      // mapping映射定义 ...
    }
  }
}

 3.ES集群的节点角色

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

elasticsearch中的每个节点角色都有自己不同的职责,因此建议集群部署时,每个节点都有独立的角色

4.集群脑裂

默认情况下,每个节点都是master eligible节点,因此一旦master节点宕机,其他候选节点会推举一个成为主节点。当主节点与其他节点网络故障时,可能会发生脑裂问题。即两个主节点。

为了避免脑裂,需要要求选票超过(eligible节点数量 + 1) / 2才能当选为主,因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes,在es7.0以后,已经成为默认配置,会自动计算合适的eligible节点数量,一般不会发生脑裂问题。

5.分布式存储

当新增文档是,应该保存到不同分片,保证数据均衡,coordinating node通过hash算法来计算

shard = hash(_routing) % number_of_shards

_routing默认是文档id, number_of_shards是分片数量

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

6.分布式查询

elasticsearch的查询分两个阶段:

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

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

7.故障转移

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值