ElasticSearch7——数据搜索

[这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战]


搜索请求用于:

  • 在ElasticSearch 的数据流或者索引中获取数据;
  • 在响应的hits或 search results中返回匹配文档。

搜索(_search)

我们可以使用 _search API 搜索和聚合 存储在Elasticsearch 数据流或索引中的数据;查询请求API接收使用Query DSL编写的 查询语句。

测试数据

定义mapping:

PUT /book
{
  "mappings" : {
    "properties" : {
      "author" : {
        "type" : "text",
        "fields" : {
          "keyword" : {
            "type" : "keyword",
            "ignore_above" : 256
          }
        }
      },
      "info" : {
        "type" : "text",
        "fields" : {
          "keyword" : {
            "type" : "keyword",
            "ignore_above" : 256
          }
        }
      },
      "name" : {
        "type" : "text",
        "fields" : {
          "keyword" : {
            "type" : "keyword",
            "ignore_above" : 256
          }
        }
      },
      "price" : {
        "type" : "long"
      },
      "publish" : {
        "type" : "keyword"
      },
      "type" : {
        "type" : "keyword"
      }
    }
  }
}
复制代码

导入相关数据:

curl -XPOST localhost:9200/book/_bulk?pretty -H "Content-Type: application/x-ndjson" --data-binary @bookdata.json
复制代码

match

match可以用于查询指定字段:

GET /book/_search?pretty
{
  "track_total_hits": true,
  "query": {
    "match": {
      "name": "光学教程"
    }
  }
}
复制代码

查询结果如下

{
  ...
  "hits" : {
    "total" : {
      "value" : 635,
      "relation" : "eq"
    }
  },
  ...
}
复制代码

其中,track_total_hits默认为true,用于显示命中数。

match_all

match_all参数用于查询所有的数据,例如

GET /book/_search?pretty
{
  "query": {
    "match_all": {}
  }
}
复制代码

terminate_after与size

terminate_after参数用于设置搜索的最大数据,当检索到指定数量后,返回size大小的文档数。

GET /book/_search?q=name:光学教程&size=10&terminate_after=15
复制代码

分页

ElasticSearch中,支持对查询的结果集进行分页,需要设置以下两个参数:

  • from:表示跳过的数据,默认为0
  • size:表示此次请求中,结果集展示的数据量

例如:

POST /book/_search
{
  "query": {
    "match": {
      "type": "大学教材"
    }
  },
  "from": 5,
  "size": 10
}
复制代码

Elasticsearch提供了两种分页切换(下一页)方式:

  • 滚动搜索API(scroll):不推荐使用,建议使用PIT
  • search_after以及PIT

使用search_after的前提条件:

  • 多个搜索请求具有相同query和 sort值;
  • 为了避免reflash导致的页面不一致,需要设置PIT保存当前索引的状态

什么是PIT

PIT的全称是“Point in time”,被称为时间点;它是一个轻量级的视图,可以查看启动时存在的数据状态。如果启用了 Elasticsearch 安全特性,必须具有目标数据流、索引或别名的read权限。

但是时间点的设置,会阻止Elasticsearch合并段,因为这些段还在使用中。保持旧段处于活动状态意味着需要更多的磁盘空间和文件句柄。

例如:

# 添加时间点
POST /book/_pit?keep_alive=1m

# 深度分页
POST /_search
{
  "query": {
    "match": {
      "type": "大学教材"
    }
  },
  "pit":{
    "id" : "27W0AwEEYm9vaxZzVUFpSG9tN1RHcU9uWkhBUTJTQUlBABZmMllUWTlHbFN6R3ZxYnp1M1pVWVpnAAAAAAAAAQpbFi1wMGo2WDdhUkhLZlVLYUxlZlF0ZGcAARZzVUFpSG9tN1RHcU9uWkhBUTJTQUlBAAA=",
    "keep_alive": "1m"
  },
  "sort": [
    {
      "price": {
        "order": "desc"
      }
    }
  ], 
  "size": 1
}

# 设置返回的 "sort" : [ XX]
# 设置 search_after、pit
POST /_search
{
  "query": {
    "match": {
      "type": "大学教材"
    }
  },
  "pit":{
    "id" : "27W0AwEEYm9vaxZzVUFpSG9tN1RHcU9uWkhBUTJTQUlBABZmMllUWTlHbFN6R3ZxYnp1M1pVWVpnAAAAAAAAAQpbFi1wMGo2WDdhUkhLZlVLYUxlZlF0ZGcAARZzVUFpSG9tN1RHcU9uWkhBUTJTQUlBAAA=",
    "keep_alive": "1m"
  },
  "sort": [
    {
      "price": {
        "order": "desc"
      }
    }
  ], 
  "size": 1,
  "search_after" :[ 60,9]
}
复制代码

查询完成后,最好手动删除PIT,避免影响段合并

DELETE /_pit
{
    "id" : "27W0AwEEYm9vaxZzVUFpSG9tN1RHcU9uWkhBUTJTQUlBABZmMllUWTlHbFN6R3ZxYnp1M1pVWVpnAAAAAAAAAQpbFi1wMGo2WDdhUkhLZlVLYUxlZlF0ZGcAARZzVUFpSG9tN1RHcU9uWkhBUTJTQUlBAAA="
}
复制代码

默认情况下,不能使用fromsize翻阅超过 10,000 次点击,该值由index.max_result_window参数决定。

查询指定字段

POST book/_search
{
  "fields": ["type","name"],
  "_source": false
}
复制代码

异步搜索(_async_search)

在实际使用中,存在需要长时间运行的查询,可能需要跨越多个远程的集群节点,因此预计不会在几毫秒内返回结果。在这种情况下,适合提交异步执行的搜索请求;还可以监视请求的进度,并在稍后阶段检索结果。

提交异步搜索

POST /book/_async_search?pretty
{
  "track_total_hits": true,
  "query": {
    "match": {
      "name": "大学英语语法"
    }
  }
}
复制代码

响应会包含正在执行的搜索的标识符,可以使用此 ID 稍后检索搜索的最终结果

查询异步进度

GET /_async_search/FmRldE8zREVEUzA2ZVpUeGs2ejJFUFEaMkZ5QTVrSTZSaVN3WlNFVmtlWHJsdzoxMDc=
复制代码

搜索结果如下:

{
  "id" : "FmRldE8zREVEUzA2ZVpUeGs2ejJFUFEaMkZ5QTVrSTZSaVN3WlNFVmtlWHJsdzoxMDc=",
  "is_partial" : true, 
  "is_running" : true,  
  "start_time_in_millis" : 1583945890986,
  "expiration_time_in_millis" : 1584377890986, 
  "response" : {
    "took" : 12144,
    "timed_out" : false,
    "num_reduce_phases" : 46, 
    "_shards" : {
      "total" : 562,
      "successful" : 188, 
      "skipped" : 0,
      "failed" : 0
    },
    "hits" : {
      "total" : {
        "value" : 456433,
        "relation" : "eq"
      },
      "max_score" : null,
      "hits" : [ ]
    },
    "aggregations" : { 
      "sale_date" :  {
        "buckets" : []
      }
    }
  }
}
复制代码
  • is_partial:执行查询时,is_partial始终设置为true。查询结束时若为false,则表示查询失败
  • is_running:表示异步查询是否正在执行
  • expiration_time_in_millis:表示异步搜索的截止时间
  • num_reduce_phases:表示查询结果集的数量
  • successful:指示有多少分片已执行查询
  • aggregations:部分聚合结果,来自已经完成查询执行的分片

检索搜索结果

GET /_async_search/status/FmRldE8zREVEUzA2ZVpUeGs2ejJFUFEaMkZ5QTVrSTZSaVN3WlNFVmtlWHJsdzoxMDc=
复制代码

删除异步搜索

可以使用删除异步搜索 API 按 ID 手动删除异步搜索:

  • 如果搜索仍在运行,搜索请求将被取消;
  • 否则,保存的搜索结果将被删除
DELETE /_async_search/FmRldE8zREVEUzA2ZVpUeGs2ejJFUFEaMkZ5QTVrSTZSaVN3WlNFVmtlWHJsdzoxMDc=
复制代码

折叠搜索结果

ES提供了参数collapse用于对指定字段对数据结果集进行折叠,不支持text类型的数据。例如:

GET /book/_search?pretty
{
  "query": {
    "match_all": {
    }
  },
  "_source": ["name","type","price"], 
  "collapse": {
    "field": "type"
  },
  "sort": [
    {
      "price": {
        "order": "desc"
      }
    }
  ], 
  "size": 1
}
复制代码

查询结果如下:

{
  "took" : 0,
  ...
    "hits" : [
      {
        "_index" : "book",
        "_type" : "_doc",
        "_id" : "379",
        "_score" : null,
        "_source" : {
          "price" : 269,
          "name" : "生物统计学和生物信息学最新进展",
          "type" : "生物科学"
        },
        "fields" : {
          "type" : [
            "生物科学"
          ]
        },
        "sort" : [
          269
        ]
      }
    ]
  }
}
复制代码

查询结果中存在多个生物科学类型的数据,将该结果集折叠后只展示一个数据。该字段需要在mapping中定义,否则也无法生效,可以查询相应的映射:

GET /book/_mapping?pretty
复制代码

过滤搜索结果集

ElasticSearch 提供了两种方法用于过滤搜索得到的结果集:

  • 带有filter子句的布尔查询
  • 后过滤器:post_filter字句

布尔查询——filter

布尔查询的filter子句会在过滤器上下文中执行,会忽略评分并考虑缓存子句。

POST /book/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "publish": "高等教育出版社"
          }
        }
      ]
    }
  },
  "aggs": {
    "all_publish": {
      "terms": {
        "field": "publish",
        "size": 10
      }
    }
  },
  "size": 1,
  "_source": ["name","type","price"]
}

复制代码

post_filter字句

通过query字句可以查询数据,通过aggs字句可以获取聚合数据。而post_filter字句可以在查询后再对数据进行过滤,但是不会影响过聚合结果。

image.png

例如:

POST /book/_search
{
  "query": {
    "match": {
      "type": "大学教材"
    }
  },
  "aggs": {
    "all_publish": {
      "terms": {
        "field": "publish",
        "size": 10
      }
    }
  },
  "post_filter": {
    "term": {
      "type": "测试"
    }
  }
}
复制代码

查询结果如下,hits中并不包含字句,但是aggregations中已经包含了数据聚合的结果。

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "all_publish" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "高等教育出版社",
          "doc_count" : 211
        },
        {
          "key" : "科学出版社",
          "doc_count" : 5
        }
      ]
    }
  }
}
复制代码

当需要对搜索结果和聚合结果做不同的过滤时,才需要使用 post_filterpost_filter 的特性是在 查询后 执行,过滤后会影响缓存等性能优化,所以只应当与聚合一起使用。

近实时搜索

需要先了解两个概念:

  • 实时搜索:数据插入数据库后立即就可以搜索
  • 近实时(Near Real Time,简称NRT)搜索:意味着文档在被索引后几乎立即可用于搜索。

ElasticSearch借助Lucene 中的段实现了近实时搜索,那什么是段(Segment)呢?

段(Segment)

ElasticSearch索引由几个分片及副本组成,每个分片都是一个Lucene索引

Lucene通过倒排索引处理数据,这个索引是不可变的。当添加新的文档时,如果每次都创建新的倒排索引,那么会消耗大量的资源。所以,在Lucene 中索引被分成更小的块,是一个小型的倒排索引,称为段(Segment)

search.png

reflash、合并与提交

当添加新的文档时,在打开、提交、关闭writer后,会创建一个新的段。向索引中添加新文档时,它们会被添加到下一个段中,以前的段永远不会被修改

Segment加入内存时已经可以被访问了,但是还没有被持久化到磁盘中,因此会在异常时丢失。

默认情况下,ElasticSearch 每隔1秒会用Buffer中的document新建一个 Segment,这个操作叫做刷新(refresh)。因为有一秒的间隔时间,所以ElasticSearch的搜索是近实时的。

如果每秒创建一个,那么Segment的数量必然是爆炸性增长,会在很短的时间内消耗掉所有的句柄。

在ElasticSearch中,有一个线程专门用于合并多个小Segment;在合并的同时,还会排除掉修改或删除的老版本的Segment。最后,会修改Commit Point,这时候数据才被持久化。

image.png

translog

对 Lucene 的更改后,只会在 Lucene 提交期间持久保存到磁盘,这是一项相对昂贵的操作,因此无法在每次索引或删除操作后执行。在两次提交之间,虽然数据可以被查询到;但也只是存在于内存中,随时有可能因为异常导致数据丢失。在每一次对 Elasticsearch 进行操作时,会将数据记录到事务日志(translog)中,默认是每5秒记录一次。

流程如下:

  • 文档被索引后,会添加到内存缓冲区,同时会追加到translog当中
  • reflash后,段列表会被写入到提交点中,缓存会清空但事务日志不会
  • 步骤二重复N次后,事务日志会变得很大;这时索引会被flush,会有以下几个变化
    • 段被全量提交,并清空了内存缓冲区
    • 清空事务日志

异常恢复:

  • 当ElasticSearch启动后,它会从磁盘中读取最后一个提交点去恢复最新的段,然后会重新加载tranwslog中记录的未提交的变更。

_flush API

执行一个提交并且截断 translog 的行为在 Elasticsearch 被称作一次 flush。分片每30分钟被自动刷新(flush),或者在 translog 太大的时候也会刷新。例如:

POST /book/_flush
复制代码

相关设置

  • index.translog.sync_interval:设置日志写入间隔,默认是5S
  • index.translog.durability
    • request(默认),所有的请求都会被记录到trans log 并持久化到磁盘
    • async,异步方式;默认间隔5秒后记录一次,可能丢失数据
  • index.translog.flush_threshold_size:translog的最大阈值,如果达到这个界限那么会自动触发flush,默认是512mb

关于近实时搜索,也可以阅读十张图带大家看懂 ES 原理,虽然不是最新的,但逻辑很清晰。

待续。。。

参考文档

十张图带大家看懂 ES 原理
near-real-time

猜你喜欢

转载自juejin.im/post/7036389568068714526