当前位置 : 首页 » 文章分类 :  开发  »  Elasticsearch-Api-搜索

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 中时可内嵌 excludesexcludes 字段

比如只返回文档的 _iduser_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语法查询)

https://www.elastic.co/guide/en/elasticsearch/reference/7.17/search-search.html#search-api-query-params-q

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分页(缓存快照,无法跳页,不再推荐)

https://www.elastic.co/guide/en/elasticsearch/reference/7.17/paginate-search-results.html#scroll-search-results

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、是一个临时快照,并不是实时的分页结果。

https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-scroll


search_after(实时分页,无法跳页,推荐)

https://www.elastic.co/guide/en/elasticsearch/reference/7.17/paginate-search-results.html#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


上一篇 Elasticsearch-Api-索引操作

下一篇 Elasticsearch-Mapping 映射

阅读
评论
5.4k
阅读预计23分钟
创建日期 2025-04-15
修改日期 2025-04-15
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论