分布式搜索 (二)

一、DSL 查询文档

1. DSL Query 的分类

Elasticsearch 提供了基于 JSON 的

DSL (Domain Specific  Language)

来定义查询

常见的查询类型包括:

查询所有:查询出所有数据,一般测

    试用

    例如:match_all

全文检索 (full text) 查询:利用分词器

   对用户输入内容分词,然后去倒排索引

   库中匹配。

   例如:match_query

              multi_match_query

精确查询:根据精确词条值查找数据,

   一般是查找 keyword、数值、日期、

   boolean 等类型字段。

   例如:ids、range 、term

地理 (geo) 查询:根据经纬度查询。

    例如:geo_distance

              geo_bounding_box

复合 (compound) 查询:复合查询可以

    将上述各种查询条件组合起来,合并查

    询条件

    例如:bool function_score   

查询 DSL 的基本语法:

GET /索引库名/_search

{

        "query":{

                "查询类型":{

                        "FIELD":"TEXT"

                }

        }

2. 全文检索查询

对用户输入内容分词,常用于搜索框搜索:

match 查询:全文检索查询的一种,会

    对用户输入内容分词,然后去倒排索引

    库检索,语法:

multi_match:与 match 查询类似,只

    不过允许同时查询多个字段,语法: 

 

 

3. 精准查询

精确查询一般是查找 keyword、数值、

日期、boolean 等类型字段,所以不会

对搜索条件分词

常见的有:

term:根据词条精确值查询

 

range:根据值的范围查询

 

 

4. 地理坐标查询

geo_bounding_box:查询 geo_point

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

 

geo_distance:查询到指定中心点小

    于某个距离值的所有文档

 

5. 复合查询

fuction score:算分函数查询,可以控制

             文档相关性算分,控制文档排名

(1) 相关性算分

利用 match 查询时,文档结果会根据与搜

索词条的关联度打分 (_score),返回结果

时按照分值降序排列

TF-IDF:在 elasticsearch5.0 之前,

              会随着词频增加而越来越大

BM25:在 elasticsearch5.0 之后,会

            随着词频增加而增大,但增

            曲线会趋于水平

(2) Function Score Query

使用 function score query,可以修改文档

的相关性算分 (query score),根据新得到

的算分排序

function score query 定义的三要素:

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

算分函数:如何计算 function score

加权方式:function score 与 query

                      score 如何运算 

(3) Boolean Query

布尔查询是一个或多个查询子句的组合

子查询的组合方式有:

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

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

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

                  类似 “

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

 

二、搜索结果处理

1. 排序

es 支持对搜索结果排序,默认是根据

相关度算分 (_score) 来排序,可以排

序字段类型有:keyword 类型、数值

类型、地理坐标类型、日期类型等

 

 

2. 分页

es 默认情况下只返回 top10 的数据,

而如果要查询更多数据就需要修改分

页参数了,通过修改 from、size

数来控制要返回的分页结果

 (2) 深度分页问题

ES 是分布式的,所以会面临深度分页问题

例如:按 price 排序后,获取 from = 990,

          size =10 的数据:

① 在每个数据分片上都排序并查询前

    1000条文档

② 将所有节点的结果聚合,在内存中

    重新排序选出前1000条文档

③ 从这1000条中,选取从990开始的

    10条文档

  

如果搜索页数过深,或者结果集 (from

+ size) 越大,对内存和 CPU 的消耗也

越高,因此 ES 设定结果集查询的上限

是10000

深度分页解决方案:

search after:分页时需要排序,原

    理是从上一次的排序值开始,查询

    下一页数据 (推荐)

scroll:原理将排序数据形成快照

    保存在内存 (不推荐使用)

总结:

from + size

优点:支持随机翻页

缺点:深度分页问题,默认查询上限

          (from + size) 是10000

场景:百度、京东、谷歌、淘宝这样

          的随机翻页搜索

after search

优点:没有查询上限 (单次查询的 size

          不超过10000)

缺点:只能向后逐页查询,不支持随机

          翻页

场景:没有随机翻页需求的搜索,例如

          手机向下滚动翻页

scroll

优点:没有查询上限 (单次查询的 size

          不超过10000)

缺点:会有额外内存消耗,并且搜索

          果是非实时的

场景:海量数据的获取和迁移,从 ES

          7.1 开始不推荐

3. 高亮

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

原理:

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

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

 搜索结果处理整体语法:

 

三、RestClient 查询文档

1. 快速入门

(1) 发起查询请求

request.source() 包含了查询、排序、

分页、高亮等所有功能

QueryBuilders 包含 match、term、

function_score、bool 等各种查询

(2) 解析响应 

 

es 返回的结果是一个 JSON 字符串,

结构包含:

hits:命中的结果
        total:总条数,其中的 value 是具

                 体的总条数值
        max_score:所有结果中得分最高

                             的文档的相关性算分
        hits:搜索结果的文档数组,其中

                的每个文档都是一个json对象
                _source:文档中的原始数据

                                也是json对象

所以解析响应结果,就是逐层解析 JSON 字

符串

@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.解析响应
    handleResponse(response);
}

private void handleResponse(SearchResponse response) {
    // 4.解析响应
    SearchHits searchHits = response.getHits();
    // 4.1.获取总条数
    long total = searchHits.getTotalHits().value;
    System.out.println("共搜索到" + total + "条数据");
    // 4.2.文档数组
    SearchHit[] hits = searchHits.getHits();
    // 4.3.遍历
    for (SearchHit hit : hits) {
        // 获取文档source
        String json = hit.getSourceAsString();
        // 反序列化
        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        System.out.println("hotelDoc = " + hotelDoc);
    }
}

2. match 查询

与 match_all 差别是查询条件,也就是

query 的部分

Java 代码上的差异主要是 request.source().

query() 中的参数

 

@Test
void testMatch() throws IOException {
    // 1.准备Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.准备DSL
    request.source()
        .query(QueryBuilders.matchQuery("all", "如家"));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);

}

3. 精确查询

与之前的查询相比,差异同样在查询条件

 

 

4. 复合查询--Boolean Query

 

@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("city", "杭州"));
    // 2.3.添加range
    boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));

    request.source().query(boolQuery);
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);

}

5. 排序、分页

搜索结果的排序和分页是与 query

的参数,因此同样是使用 request.

source() 来设置

 

@Test
void testPageAndSort() throws IOException {
    // 页码,每页大小
    int page = 1, size = 5;

    // 1.准备Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.准备DSL
    // 2.1.query
    request.source().query(QueryBuilders.matchAllQuery());
    // 2.2.排序 sort
    request.source().sort("price", SortOrder.ASC);
    // 2.3.分页 from、size
    request.source().from((page - 1) * size).size(5);
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);

}

6. 高亮

与之前代码差异较大:

① 查询的DSL:其中除了查询条件,

    需要添加高亮条件,同样是与 query

    同级

② 结果解析:结果除了要解析 _source

    文档数据,还要解析高亮结果

(1) 请求的 DSL 构建 

 

@Test
void testHighlight() throws IOException {
    // 1.准备Request
    SearchRequest request = new SearchRequest("hotel");
    // 2.准备DSL
    // 2.1.query
    request.source().query(QueryBuilders.matchQuery("all","如家"));
    // 2.2.高亮
    request.source().highlighter(new HighlightBuilder().
                                    field("name").requireFieldMatch(false));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);

}

 (2) 结果解析

 

private void handleResponse(SearchResponse response) {
    // 4.解析响应
    SearchHits searchHits = response.getHits();
    // 4.1.获取总条数
    long total = searchHits.getTotalHits().value;
    System.out.println("共搜索到" + total + "条数据");
    // 4.2.文档数组
    SearchHit[] hits = searchHits.getHits();
    // 4.3.遍历
    for (SearchHit hit : hits) {
        // 获取文档source
        String json = hit.getSourceAsString();
        // 反序列化
        HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
        // 获取高亮结果
        Map<String, HighlightField> highlightFields = hit.getHighlightFields();
        if (!CollectionUtils.isEmpty(highlightFields)) {
            // 根据字段名获取高亮结果
            HighlightField highlightField = highlightFields.get("name");
            if (highlightField != null) {
                // 获取高亮值
                String name = highlightField.getFragments()[0].string();
                // 覆盖非高亮结果
                hotelDoc.setName(name);
            }
        }
        System.out.println("hotelDoc = " + hotelDoc);
    }
}

四、数据聚合

1. 聚合的种类

聚合 (aggregations) 可以实现对文档

数据的统计、分析、运算,参与聚合

的字段类型必须是:keyword、数值、

日期、布尔

聚合常见的有三类:

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

     TermAggregation:按照文档字段值

                                   分组

     Date Histogram:按照日期阶梯分组,

         例如一周为一组,或者一月为一组

度量聚合 (Metric):用以计算一些值,

     Avg:求平均值         

     Max:求最大值

     Min:求最小值

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

管道聚合 (pipeline):其它聚合的结果

                                      为基础做聚合

2. DSL 实现聚合

(1) DSL 实现 Bucket 聚合

聚合必须的三要素:

① 聚合名称

② 聚合类型

③ 聚合字段

  2) 聚合结果排序

默认情况下,Bucket 聚合会统计 Bucket

内的文档数量,记为 _count,并且按照

_count 降序排序

可以修改结果排序方式:

 

   3) 限定聚合范围

默认情况下,Bucket 聚合是对索引库

所有文档做聚合,我们可以限定

聚合的文档范围,只要添加 query 条

件即可

 (2) DSL 实现 Metrics 聚合

 

3. RestAPI 实现聚合

  2) 聚合结果解析 

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

 

五、自动补全

1. 拼音分词器

要实现根据字母做补全,就必须对文档

按照拼音分词,需要安装拼音分词插件

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

2. 自定义分词器

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

character filters:在 tokenizer 之前

   对文本进行处理,例如删除字符、替

   换字符

tokenizer:将文本按照一定的规则切

   割成词条 (term),例如 keyword,就

   是不分词;还有ik_smart

tokenizer filter:将 tokenizer 输出的

   词条做进一步处理,例如大小写转换、

   同义词处理、拼音处理等

在创建索引库时,通过 settings 来配置自定

义的 analyzer(分词器):

 

 

// 自定义拼音分词器
PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": { 
        "my_analyzer": { 
          "tokenizer": "ik_max_word",
          "filter": "py"
        }
      },
      "filter": {
        "py": { 
          "type": "pinyin",
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  }
}

① 拼音分词器适合在创建倒排索引

的时候使用,但不能在搜索的时候

使用

② 因此字段在创建倒排索引时应该

用 my_analyzer 分词器,字段在搜

索时应该使用 ik_smart 分词器

 

3. 自动补全查询

es 中使用 completion suggester 查询

来实现自动补全功能,匹配以用户输

入内容开头的词条并返回

为了提高补全查询的效率,对于文档中字

段的类型有一些约束:

① 参与的字段必须是 completion 类型

② 字段的内容一般是用来补全的多个

    词条形成的数组

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

4. RestAPI 实现自动补全

@Test 
void testSuggest() throws IOEvxception {
    // 1. 准备 Request
    SearchRequest request = new SearchRequest("hotel");
    // 2. 准备 DSL
    request.source().suggest(new SuggestBuilder().addSuggestion(
                "suggestions"
            SuggestBuilders.completionSuggestion("suggestion")
                    .prefix("h")
                    .skipDuplicates(true)
                    .size(10)
    ));
    // 3. 发出请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4. 处理结果
    Suggest suggest = response.getSuggest();
    // 4.1 根据名称获取补全结果
    CompletionSuggestion suggestion = suggest.getSuggestion("hotelSuggestion");
    // 4.2 获取 options 并遍历
    for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
        //4.3 获取一个 option 中的 text,也就是补全的词条
        String text = option.getText().string();
        System.out.println(text);
    }
}

六、数据同步

数据同步:es 中的数据来自于 mysql

                  数据库,因此 mysql 数据

                  发生改变时,es 也必须跟

                  着改变

跨微服务的数据同步问题解决方案

同步调用

    优点:实现简单,粗暴

    缺点:业务耦合度高

异步通知

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

    缺点:依赖 mq 的可靠性

监听 binlog

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

    缺点:开启 binlog 增加数据库负担、

              实现复杂度高

七、集群

单机的 es 做数据存储,必然面临两个问

题,海量数据存储问题和单点故障问题

解决方案:

海量数据存储问题:将索引库从逻辑上

    拆分为N个分片(shard),存储到多个节

    点

单点故障问题:将分片数据在不同节点

    备份(replica)

1. 搭建 ES 集群

  (1) 创建 es 集群

   1) 编写一个 docker-compose 文件,内容如下:

version: '2.2'
services:
  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
    container_name: es01
    environment:
      - node.name=es01
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es02,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data01:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
    networks:
      - elastic
  es02:
    image: docker.elastic.co/elasticsearch/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
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data02:/usr/share/elasticsearch/data
    networks:
      - elastic
  es03:
    image: docker.elastic.co/elasticsearch/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
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data03:/usr/share/elasticsearch/data
    networks:
      - elastic

volumes:
  data01:
    driver: local
  data02:
    driver: local
  data03:
    driver: local

networks:
  elastic:
    driver: bridge

  2) es 运行需要修改一些 linux 系统权限,

      修改 /etc/sysctl.conf 文件

① 添加 vm.max_map_count=262144

② 执行命令 sysctl -p

  3) 通过 docker-compose 文件启动集群

docker-compose up -d

(2) 集群状态监控

使用 cerebro 来监控 es 集群状态

GitHub - lmenezes/cerebro

(3) 创建索引库

方法一:利用 kibana 的 DevTools 创建索引库

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

方法二:利用 cerebro 创建索引库

 

4) 查看分片效果

回到首页,即可查看索引库分片效果:

  

2. 集群脑裂问题

(1) ES 集群的节点角色

(2) ES 集群的分布式查询

es 中的每个节点角色都有自己不同

的职责,因此建议集群部署时,每

个节点都有独立的角色 

各节点的作用: 

aster eligible:参与集群选主;主节点可

    以管理集群状态、管理分片信息、处理创

    建和删除索引库的请求

data:数据的 CRUD

coordinator:路由请求到其它节点;合并

                         查询到的结果,返回给用户 

 (3) ES 集群的脑裂

默认情况下,每个节点都是 master eligible

节点,因此一旦 master 节点宕机,其它候

选节点会选举一个成为主节点,当主节点

与其他节点网络故障时,可能发生脑裂问题

为了避免脑裂,需要要求选票超过 (eligible

节点数量 + 1) / 2 才能当选为主,因此

eligible 节点数量最好是奇数,对应配置项

discovery.zen.minimum_master_nodes

在 es7.0 以后,已经成为默认配置,因此一

般不会发生脑裂问题

3. 集群分布式存储

新增文档时,应该保存到不同分片,保证

数据均衡

coordinating node 通过 hash 算法计算文档应该

存储到哪个分片:

shard = hash(_routing) % number_of_shards

① _routing 默认是文档的 id

② 算法与分片数量有关,因此索引库一旦

    创建,分片数量不能修改

 

4. 集群分布式查询

es 查询分成两个阶段:

scatter phase分散阶段,coordinating

    node 会把请求分发到每一个分片

gather phase聚集阶段,coordinating

    node 汇总 data node 的搜索结果,并处

    理为最终结果集返回给用户

5. 集群故障转移

集群的 master 节点会监控集群中的节点状

态,如果发现有节点宕机,会立即将宕机节

点的分片数据迁移到其它节点,确保数据安

全,这个叫做故障转移

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值