Elasticsearch-Api-搜索
Elasticsearch 搜索 Api
Search APIs
https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html
GET /index/_search 搜索
Elasticsearch Guide [7.17] » REST APIs » Search APIs » Search API
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-search.html
通过 _search 接口可在一个或多个索引或数据流上搜索数据,可通过 q 查询串来指定查询条件,也可以通过 body 参数指定查询条件。
在指定的索引上搜索GET /<target>/_search
POST /<target>/_search
<target>
路径参数 target 可以是索引名、索引别名、数据流名,还 可以是逗号分割的多个索引/数据流,并且支持通配符 *
如果想在全部索引上搜索,可以使用 *
或 _all
,或者忽略 target 参数,例如:GET /_search
POST /_search
_search 接口的一些参数既可以放在 query string 上,也可以放在 body 中,如果两个地方同时指定了,将以 query string 中的为准
seq_no_primary_term
Query 或 Body 参数,boolean 类型,为 true 时返回 _seq_no
和 _primary_term
字段。version
Query 或 Body 参数,boolean 类型,为 true 时返回 _version
字段。
如果不带任何查询参数就会返回 index 下的所有记录。
curl --location --request GET 'http://localhost:9200/article/_doc/_search' \
--header 'Content-Type: application/json' \
--data-raw ''
返回
{
"took": 102,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "article",
"_type": "_doc",
"_id": "uv_ZjXEBrN9oq5tgVMuj",
"_score": 1.0,
"_source": {
"title": "es的使用",
"pathname": "/article/es",
"content": "新增记录的时候,也可以不指定 Id,这时要改成 POST 请求。"
}
},
{
"_index": "article",
"_type": "_doc",
"_id": "u__ojXEBrN9oq5tgGcul",
"_score": 1.0,
"_source": {
"title": "java基础",
"pathname": "/article/java-basic",
"content": "java基础介绍"
}
}
]
}
}
track_total_hits 命中个数统计
track_total_hits
query 参数,integer 或 bool 类型。此设置用于确定 Elasticsearch 是否应该精确地追踪与查询匹配的总命中数。
- 如果设为一个整数,表示精确计算命中个数的最大值,默认值 10000,查询条件命中的文档个数超过此值时不再精确计算命中个数。
- 如果设为
true
将每次都精确计算命中个数,会比较耗性能。 - 如果设为
false
将完全不计算命中个数,返回结果无hits.total
字段。
默认 _search 接口返回的 hits.total.value
值最大为 10000,搜索条件命中的文档个数超过 10000 时就不准了
默认情况下,Elasticsearch 会为最多 10,000 个命中提供精确计数。超出这个数字后,它会返回一个下限估算(例如,“10,000+”)。
命中个数大于 10000 时,hits.total.relation=gte
表示实际命中个数是大于 10000 的。
命中个数小于 10000 时,hits.total.relation=eq
value 是准确的命中数。
{
"hits":{
"total":{
"value":10000,
"relation":"gte"
}
}
}
_source 指定返回字段
_source
参数指定返回结果 hits._source
中的字段数,默认值为 true
_source
参数可放在 query string,也可以放在 body 中。
放在 query string 中时,支持以下值:
true
返回文档的全部字段false
不返回任何字段<string>
逗号分割的字段名列表,支持*
通配符
放在 body 中时可内嵌 excludes
和 excludes
字段
比如只返回文档的 _id
和 user_id
字段
{
"_source": ["_id", "user_id"],
"sort": {
"user_id": {
"order": "asc"
}
}
}
指定返回的和排除的
GET /_search
{
"_source": {
"includes": [ "obj1.*", "obj2.*" ],
"excludes": [ "*.description" ]
},
"query" : {
"term" : { "user" : "kimchy" }
}
}
The _source option
https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#source-filtering
q查询(URI搜索/lucene语法查询)
q 搜索就是 URI 搜索,通过 URI 参数 q
来指定查询相关参数。q 搜索使用 Lucene 语法,不支持完整的 ES DSL 语法,但让我们可以快速做一个查询。
q 搜索会覆盖 body 中的 query
查询参数,如果同时指定,只会使用路径中的 q 参数。
例1、从索引 tweet 里面搜索字段 user 为 kimchy 的记录
GET /tweet/_search?q=user:kimchy
例2、从索引 tweet,user 里面搜索字段 user 为 kimchy的记录
GET /tweet,user/_search?q=user:kimchy
GET /kimchy,elasticsearch/_search?q=tag:wow
例3、从所有索引里面搜索字段 tag 为 wow 的记录
GET /_all/_search?q=tag:wow
GET /_search?q=tag:wow
highlight 高亮搜索
Request Body Search
https://www.elastic.co/guide/en/elasticsearch/reference/7.6/search-request-body.html#request-body-search-highlighting
指定单个高亮字段:
GET /_search
{
"query" : {
"match": { "content": "kimchy" }
},
"highlight" : {
"fields" : {
"content" : {}
}
}
}
指定多字段高亮:
{
"query": {
"multi_match": {
"query": "产品开发",
"type": "cross_fields",
"fields": ["title", "content", "summary"]
}
},
"_source": ["_id", "title", "fileName"],
"from": 0,
"size": 10,
"highlight": {
"fields": {
"title": {},
"content": {},
"summary": {}
}
}
}
指定高亮标签和高亮片段fragment大小:
GET /_search
{
"query" : {
"match": { "content": "kimchy" }
},
"highlight" : {
"pre_tags": [
"<span style=\"color:red\">"
],
"post_tags": [
"</span>"
],
"fragment_size": 40,
"fields" : {
"content" : {}
}
}
}
ignore_unavailable=true 索引不存在时不报错
默认情况下,如果索引不存在,返回 index_not_found_exception 错误
如果希望在索引不存在时不报错,可以使用 ignore_unavailable=true 选项。这个选项会让 Elasticsearch 忽略那些在执行操作时不存在的索引。
例如:
GET /my_index/_search?ignore_unavailable=true
{
"query": { ... }
}
java 代码实现:
SearchRequest searchRequest = new SearchRequest("不存在的索引名");
// 关键配置:忽略不可用索引,并扩展通配符到开放索引
searchRequest.indicesOptions(IndicesOptions.lenientExpandOpen());
sort 排序
Elasticsearch Guide [7.17] » Search your data » Sort search results
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/sort-search-results.html
可通过 sort
字段指定查询结果排序,可指定多个字段的 顺序 asc 或 逆序 desc 排序方向。
特殊排序字段名:
- 通过
_score
指定按相关性得分排序 - 通过
_doc
指定按索引顺序排序
默认按相关性 _score 倒序
在 Elasticsearch 中,相关性得分 由一个浮点数进行表示,并在搜索结果中通过 _score
字段返回, 默认排序是 _score
降序。
指定根据 _score
排序时,默认是倒序 desc
,指定其他排序字段时,默认是升序 asc
track_scores 始终计算得分
如果排序字段中同时指定 _score
和其他字段,返回结果有 _score
值,为相关性得分。
如果排序字段中无 _score
,只有其他字段,默认不会再计算 _score
分数,返回结果中的 _score
为 null
如果设置 track_scores
为 true,会始终计算 _score
值(无 _score
时值为0)
多字段排序
例1、单字段排序,如按 user_id 升序排序
{
"sort": {
"user_id": {
"order": "asc"
}
}
}
例2、多字段排序
结果首先按第一个条件排序,仅当结果集的第一个 sort 值完全相同时才会按照第二个条件进行排序,以此类推。
{
"sort": [
{ "date": { "order": "desc" }},
{ "_score": { "order": "desc" }}
]
}
或者 先 _score 倒序,_score 相同的按 _id 升序
{
"sort": [
{ "_score": { "order": "desc" } },
{ "_id": { "order": "asc" } }
]
}
多值字段排序(mode:min/max/avg/sum)
一种情形是字段有多个值的排序,这些值并没有固有的顺序;一个多值的字段仅仅是多个值的包装,这时应该选择哪个进行排序呢?
对于数字或日期,你可以将多值字段减为单值,这可以通过使用 min, max, avg, sum, median 等排序模式 。
例如你可以按照每个 date 字段中的最早日期进行排序,通过以下方法:
{
"sort": {
"dates": {
"order": "asc",
"mode": "min"
}
}
}
值缺失时的处理(missing)
missing 参数自定对于没有排序字段的文档,如何进行排序,可设置为 _last
或 _first
或指定值,例如:
"sort" : [
{ "price" : {"missing" : "_last"} }
]
排序字段的返回值(format)
返回结果的 sort 字段中有每个字段的排序字段值。
例如指定 _score 和 _id 排序,结果中每个文档的 sort 字段是当前文档的 _score 和 _id 的值:
{
"took": 17,
"timed_out": false,
"_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 },
"hits": {
"total": { "value": 24, "relation": "eq" },
"max_score": null,
"hits": [
{
"_index": "index-1",
"_type": "_doc",
"_id": "q4a2lI8B3xTCjXouI7Gk",
"_score": 2.0,
"_source": { "id": "85cb46be02274ad8ab02b4dfd285a1e3" },
"sort": [ 2.0, "q4a2lI8B3xTCjXouI7Gk" ]
},
{
"_index": "index-2",
"_type": "_doc",
"_id": "rIa2lI8B3xTCjXouI7Gk",
"_score": 1.9851243,
"_source": { "id": "f05386cf20b34db8aa5272e8b8e4d2be" },
"sort": [ 1.9851243, "rIa2lI8B3xTCjXouI7Gk" ]
}
]
}
}
可通过 format 参数指定比如 date 等字段在返回结果 sort 字段中值的格式,例如:
"sort" : [
{ "post_date" : {"format": "strict_date_optional_time_nanos"}}
]
数值字段排序(numeric_type)
嵌套字段的排序
脚本排序(Script Sort)
es schema 中有 keyword 类型字段 data_type 和 integer 类型字段 item_index,data_type 可取值有 text, image, other,要实现如下排序逻辑:
- 如果文档的 data_type=image,排序排在最前面;
- 如果文档的 data_type 不是 image,按 item_index 升序排序
查询 DSL 如下:
GET /your_index/_search
{
"query": {
"match_all": {} // 查询条件
},
"sort": [
{
"_script": {
"type": "number",
"script": {
"source": "if (!doc.containsKey('data_type') || doc['data_type'].size() == 0) {\n return 0;\n }\n return doc['data_type'].value == 'image' ? 1 : 0;\n"
},
"order": "desc"
}
},
{
"item_index": {
"order": "asc"
}
}
]
}
实现:
一级排序,data_type 不存在时排序值是0,data_type=image 时排序值是1,desc 降序,可实现 data_type=image 的排序在最前面
二级排序,仅在第一层排序结果相同的文档间生效,对于 data_type 排序值相同的文档,按 item_index 升序排序
Java 代码实现:
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 1. 脚本排序:优先image
String scriptSource = """
if (!doc.containsKey('data_type') || doc['data_type'].size() == 0) {
return 0;
}
return doc['data_type'].value == 'image' ? 1 : 0;
""";
Script script = new Script(scriptSource);
ScriptSortBuilder scriptSort = SortBuilders.scriptSort(script, ScriptSortBuilder.ScriptSortType.NUMBER).order(SortOrder.DESC);
sourceBuilder.sort(scriptSort);
// 2. 字段排序:item_index升序
sourceBuilder.sort("item_index", SortOrder.ASC);
SearchRequest searchRequest = new SearchRequest("your_index");
searchRequest.source(sourceBuilder);
SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
分页
Elasticsearch Guide [7.17] » Search your data » Paginate search results
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/paginate-search-results.html
from/size 分页(超1万后报错)
和 SQL 使用 LIMIT 关键字返回单个 page 结果的方法相同,Elasticsearch 接受 from 和 size 参数:
from 参数,可放在路径中,也可放在 body 中,指定应该跳过的初始结果数量,默认是 0
size 参数,可放在路径中,也可放在 body 中,指定应该返回的结果数量,默认是 10
默认 from + size 的值不可超过 10000,可通过参数 index.max_result_window
增加这个限制值,但这会增加内存消耗,因为 Elasticsearch 必须为每一个可能的结果保留状态。如果需要对多于 10000 的数据进行分页,可以用 search_after 参数
如果每页展示 5 条结果,下面 3 个请求分别查询第 1 到 3 页的结果:
GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10
Result window is too large
如果 from + size 的值超过 10000,检索会报错 Result window is too large:
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [486790]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}
]
解决:
修改指定索引的 max_result_window 值:
PUT /index_name/_settings
{
"settings": {
"index.max_result_window": 2147483647
}
}
分布式系统中的深度分页问题
假设在一个有 5 个主分片的索引中搜索。 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。
现在假设我们请求第 1000 页—结果从 10001 到 10010 。所有都以相同的方式工作除了每个分片不得不产生前10010个结果以外。 然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。
可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。
所以 ES 就默认限制最多只能访问前 1w 个文档。这是通过 index.max_result_window
控制的。
es 目前支持最大的 skip 值是 index.max_result_window
,默认为 10000 。
也就是当 from + size > index.max_result_window 时,es 将返回错误
临时解决方法可以将 index.max_result_window 调高,但不是长久解决方案。
关于分库分表后的分页查询,58 架构师 沈剑 的这篇文章写的非常好
业界难题-“跨库分页”的四种方案
https://cloud.tencent.com/developer/article/1048654
scroll分页(缓存快照,无法跳页,不再推荐)
ES 不再推荐 scroll 分页,深度分页应使用 search_after
代替
为了满足深度分页的场景,es 提供了 scroll 的方式进行分页读取。
原理是缓存首次查询的结果快照,之后从每次根据游标 scroll_id 从快照中取数据。
原理上是对某次查询生成一个游标 scroll_id,后续的查询只需要根据这个游标去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。scroll_id 的生成可以理解为建立了一个临时的历史快照,在此之后的增删改查等操作不会影响到这个快照的结果。
scroll 分页过程:
1、初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照;
GET fs/_search?scroll=3m
{
"query": {"match_all": {}}
}
初始化的时候就像是普通的 search 一样
其中的 scroll=3m 代表当前查询的数据缓存 3 分钟
结果会返回一个 _scroll_id
用于之后使用
2、遍历时,从这个快照里取数据;
在遍历时候,拿到上一次遍历中的 _scroll_id
,然后带 scroll 参数,重复上一次的遍历步骤,直到结果集中返回的 hits 字段为空,表示遍历完成。
每次都要传参数 scroll,刷新搜索结果的缓存时间,另外不需要指定 index 和 type(不要把缓存的时时间设置太长,占用内存)。
请求指定的 scroll_id 时就不需要 /index/_type 等信息了。每读取一页都会重新设置 scroll_id 的生存时间
如果 srcoll_id 的生存期很长,那么每次返回的 scroll_id 都是一样的,直到该 scroll_id 过期,才会返回一个新的 scroll_id。
scroll 快照分页的问题:
1、Search context 开销不小。
2、是一个临时快照,并不是实时的分页结果。
search_after(实时分页,无法跳页,推荐)
ES 5.0 开始推出了 Search After 分页机制,可提供更实时的分页游标(live cursor),它的思想是利用上一页的分页结果来加速下一页的分页查询。
search_after
分页使用前一页中的一组排序值来检索匹配的下一页数据。
使用 search_after
分页需要多次请求的 query 和 sort 参数完全一致,如果在处理多个请求期间发生了 refresh 操作(也就是有新数据写入),则结果可能不一致。为避免这种情况发生,可以使用 PIT 时间点生成数据快照
第一次查询时指定唯一且稳定的分页方式
GET twitter/tweet/_search
{
"size": 10,
"query": {
"match" : {
"title" : "elasticsearch"
}
},
"sort": [
{"date": "asc"},
{"_id": "desc"}
]
}
这里为了避免 sort 字段相同值的导致排序不确定,这里增加了 _id 字段。
返回的结果会包含每个文档的 sort 字段的 sort value。这个就是上面所说的 分页游标(live cursor)
使用最后一个文档的 sort value 作为 search after 请求值,我们就可以这样子请求下一页结果了:
GET twitter/tweet/_search
{
"size": 10,
"query": {
"match" : {
"title" : "elasticsearch"
}
},
"search_after": [1463538857, "654323"],
"sort": [
{"date": "asc"},
{"_id": "desc"}
]
}
search_after 使用方式上跟 scroll 很像,但是相对于 scroll 它是无状态的(stateless),没有 search context 开销;而且它是每次请求都实时计算的,所以也没有一致性问题(相反,有索引变化的话,每次排序顺序会变化呢)。但是比起 from+size 方式,还是有同样的问题没法解决:就是只能顺序的翻页,不能随意跳页。
app 中的信息流翻页很适合这种方式,因为无法跳页。
问题:
1、无法跳页
2、有新的符合查询条件的数据被插入后,会查询到上一页已经返回的数据。
比如用户当前正在看第 2 页的历史数据,如果此时后台数据源新增了一条数据,那么当用户继续上推操作查看第 3 页的历史数据时,就会把第 2 页的最后一条数据获取,并且会把该条数据作为第3页的第一条数据进行展示
解决方法: 查询时参数代入 上次最后一条数据的 create_time ,下一页的数据都必须大于这个 create_time
GET /index/_count 条件计数
Count API
https://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html
使用和 _search
接口相同的查询条件,可用于查询符合条件的文档个数。
ignore_unavailable=true 索引不存在时不报错
默认情况下,如果索引不存在,返回 index_not_found_exception 错误
如果希望在索引不存在时不报错,可以使用 ignore_unavailable=true 选项。这个选项会让 Elasticsearch 忽略那些在执行操作时不存在的索引。
索引不存在时返回:
POST /index1/_count?ignore_unavailable=true
{
"count": 0,
"_shards": {
"total": 0,
"successful": 0,
"skipped": 0,
"failed": 0
}
}
PIT 时间点
Elasticsearch Guide [7.17] » REST APIs » Search APIs » Point in time API
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/point-in-time-api.html
在 Elasticsearch 中搜索数据时默认都是在最新实时数据上进行,有些情况下,比如 search_after
分页中需要将 ES 数据固定在一个数据快照上,此时可用 时间点(point in time, PIT) 解决。
POST /index/_pit 时间点
搜索请求前需要先显式地创建时间点,keep_alive
参数指定时间点的有效时间,例如:
POST /my-index-000001/_pit?keep_alive=1m
返回一个 id 字段如下:
{
"id": "64O1AwESdmVoaWNsZV9mb3VyX3doZWVsFmhMNWNLdnJ0U0NDV2tRUGtWTXdSd1EAFk9LV3JSdzEwUTVPRW1RZksyYjd2bUEAAAAAAAAAAAEWa0p4SnloRFFRTEttVDRmOUJnN0ttZwABFmhMNWNLdnJ0U0NDV2tRUGtWTXdSd1EAAA=="
}
下次请求时将 id 的值放入 pit.id
参数中传过去,例如
POST /_search
{
"size": 100,
"query": {
"match" : {
"title" : "elasticsearch"
}
},
"pit": {
"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==",
"keep_alive": "1m"
}
}
注意:
- 带有 pit 参数的 search 请求不能指定 index, routing 和 preference 参数,这些参数都从 PIT 时间点中获取。
pit.id
告诉 es 在指定的 PIT 时间点上下文中进行查询。keep_alive
参数告诉 es 本次查询需要将 PIT 时间点的有效期延长多长时间。这个参数不需要太大,只需要够本次请求执行即可。
PIT 时间点会消耗磁盘和内存资源
PIT 时间点会消耗磁盘和内存资源:
- 打开 PIT 时间点会阻止删除不需要的 Lucene Segment,因为这些 segment 还要用到,所以会占用磁盘空间。
- 打开过多的 PIT 时间点会占用堆内存,尤其在频繁删除、更新的索引上。
DELETE /_pit 删除时间点
超过 keep_alive
时间的 PIT 时间点会自动删除。
为了节省资源,不用的时间点应该及时主动关闭:
DELETE /_pit
{
"id" : "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="
}
返回:
{
"succeeded": true,
"num_freed": 3
}
“succeeded”: true 表示和此时间点相关的所有搜索上下文都已被清除,num_freed 是删除的搜索上下文的个数。
slice 搜索分片
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/point-in-time-api.html#search-slicing
页面信息
location:
protocol
: host
: hostname
: origin
: pathname
: href
: document:
referrer
: navigator:
platform
: userAgent
: