python
结果过滤
-
print(es.search(index=‘py2’, filter_path=[‘hits.total’, ‘hits.hits._source’]))
-
print(es.search(index=‘w2’, doc_type=‘doc’))
-
print(es.search(index=‘w2’, doc_type=‘doc’, filter_path=[‘hits.total’]))
filter_path参数用于减少elasticsearch返回的响应,比如仅返回hits.total和hits.hits._source内容。
除此之外,filter_path参数还支持*通配符以匹配字段名称、任何字段或者字段部分:
-
print(es.search(index=‘py2’, filter_path=[‘hits.*’]))
-
print(es.search(index=‘py2’, filter_path=[‘hits.hits._*’]))
-
print(es.search(index=‘py2’, filter_path=[‘hits.to*’])) # 仅返回响应数据的total
-
print(es.search(index=‘w2’, doc_type=‘doc’, filter_path=[‘hits.hits._*’]))
Elasticsearch(es对象)
-
es.index,向指定索引添加或更新文档,如果索引不存在,首先会创建该索引,然后再执行添加或者更新操作。
print(es.index(index='w2', doc_type='doc', id='4', body={"name":"可可", "age": 18})) # 正常 print(es.index(index='w2', doc_type='doc', id=5, body={"name":"卡卡西", "age":22})) # 正常
-
es.get,查询索引中指定文档。
print(es.get(index='w2', doc_type='doc', id=5)) # 正常
-
es.search,执行搜索查询并获取与查询匹配的搜索匹配。这个用的最多,可以跟复杂的查询条件。
- index要搜索的以逗号分隔的索引名称列表; 使用_all 或空字符串对所有索引执行操作。 doc_type
- 要搜索的以逗号分隔的文档类型列表; 留空以对所有类型执行操作。 body 使用Query DSL(QueryDomain Specific
- Language查询表达式)的搜索定义。
- _source 返回_source字段的true或false,或返回的字段列表,返回指定字段。
- _source_exclude要从返回的_source字段中排除的字段列表,返回的所有字段中,排除哪些字段。
- _source_include从_source字段中提取和返回的字段列表,跟_source差不多。
print(es.search(index='py3', doc_type='doc', body={"query": {"match":{"age": 20}}})) # 一般查询 print(es.search(index='py3', doc_type='doc', body={"query": {"match":{"age": 19}}},_source=['name', 'age'])) # 结果字段过滤 print(es.search(index='py3', doc_type='doc', body={"query": {"match":{"age": 19}}},_source_exclude =[ 'age'])) print(es.search(index='py3', doc_type='doc', body={"query": {"match":{"age": 19}}},_source_include =[ 'age']))
-
es.count,执行查询并获取该查询的匹配数。比如查询年龄是18的文档。
body = { "query": { "match": { "age": 18 } } } print(es.count(index='py2', doc_type='doc', body=body)) # {'count': 1, '_shards': {'total': 5, 'successful': 5, 'skipped': 0, 'failed': 0}} print(es.count(index='py2', doc_type='doc', body=body)['count']) # 1 print(es.count(index='w2')) # {'count': 6, '_shards': {'total': 5, 'successful': 5, 'skipped': 0, 'failed': 0}} print(es.count(index='w2', doc_type='doc')) # {'count': 6, '_shards': {'total': 5, 'successful': 5, 'skipped': 0, 'failed': 0}}
-
es.get_source,通过索引、类型和ID获取文档的来源,其实,直接返回想要的字典。
- print(es.get_source(index=‘py3’, doc_type=‘doc’, id=‘1’)) # {‘name’: ‘王五’, ‘age’: 19}
-
es.delete,删除指定的文档。比如删除文章id为4的文档,但不能删除索引,如果想要删除索引,还需要es.indices.delete来处理
print(es.delete(index='py3', doc_type='doc', id='4'))
-
s.delete_by_query,删除与查询匹配的所有文档。
- index 要搜索的以逗号分隔的索引名称列表; 使用_all 或空字符串对所有索引执行操作。 doc_type
- 要搜索的以逗号分隔的文档类型列表; 留空以对所有类型执行操作。
print(es.delete_by_query(index='py3', doc_type='doc', body={"query": {"match":{"age": 20}}}))
-
es.exists,查询elasticsearch中是否存在指定的文档,返回一个布尔值。
print(es.exists(index='py3', doc_type='doc', id='1'))
-
es.info,获取当前集群的基本信息。
-
es.ping,如果群集已启动,则返回True,否则返回False。
Indices(es.indices)
- es.indices.create,在Elasticsearch中创建索引,用的最多。比如创建一个严格模式、有4个字段、并为title字段指定ik_max_word查询粒度的mappings。并应用到py4索引中。这也是常用的创建自定义索引的方式。
body = {
"mappings": {
"doc": {
"dynamic": "strict",
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"url": {
"type": "text"
},
"action_type": {
"type": "text"
},
"content": {
"type": "text"
}
}
}
}
}
es.indices.create('py4', body=body)
-
es.indices.analyze,返回分词结果。
es.indices.analyze(body={'analyzer': "ik_max_word", "text": "皮特和茱丽当选“年度模范情侣”Brad Pitt and Angelina Jolie"})
-
es.indices.delete,在Elasticsearch中删除索引。
print(es.indices.delete(index='py4')) print(es.indices.delete(index='w3')) # {'acknowledged': True}
elasticsearch-py 7.16.0新特征
es.indices.create():
-
先创建一个配置
config= { "settings": { "index": { "number_of_shards": 1, "number_of_replicas": 0 } }, "mappings": { "dynamic": "strict", "properties": { "id": { "ignore_above": 1024, "type": "keyword" }, "body": { "type": "text", "analyzer": "english" } } } }
-
旧版参数
es.indices.create(index=index, body=config)
-
新版参数
es.indices.create(index=index, settings=config['settings'], mappings=config["mappings"])
-
作者原话
The case of “make a JSON body an API request” is likely to be applicable elsewhere too (ingest.put_pipeline, transform.put_transform, search?) A raw JSON HTTP body is a transport-layer concept compared to “this is the index’s settings” and “this is the index’s mapping” being API-layer concepts.The goal of moving away from params and body is for the client to become more than “named URLs”: being able to catch issues with your request before it’s sent to Elasticsearch (required parameters, parameter types, using API structures and enums) which means encoding Elasticsearch API information into function signatures.
I know usage of body is pervasive so I’m keeping the option to use body and params for almost all APIs in 8.x (search_template uses the params keyword) and when using these options the individual values will be picked out and expanded so they go through the same processing as if you’d passed the parameters directly:
es.create():
-
作用:在索引中增加文档,
-
基本参数:
""" index: type:str 索引 doc_type: type(由于一个index只能有1个type,可不写) document: type:dict增加的字段 id: type:str id """
由document替换了body,语义明确。
es.index():
-
作用:在索引中增加或者更新文档
-
基本参数:
""" index: type:str 索引 doc_type: type(由于一个index只能有1个type,可不写) document: type:dict增加的字段 id: type:str id """
由document替换了body,语义明确。
es.search():
-
作用:Returns results matching a query.(直接抄文档了)
-
基本参数:
""" index: type:str 索引 doc_type: type(由于一个index只能有1个type,可不写) query: type:dict增加的字段 """
由query替换了body,语义明确。
es.update():
-
作用:部分更新文档
-
基本参数:
index: type:str 索引 doc_type: type(由于一个index只能有1个type,可不写) doc: type:dict修改的字段
由doc替代了body,语义明确。
helper.bulk():
-
作用:批量增删改
-
基本参数:
""" client: # es_obj actions: # 操作 """
-
actions构造:
1.删除
{
‘_op_type’: ‘delete’,
‘_index’: ‘index-name’,
‘_id’: 42, # 这里id又可以传int类型了
}
2.更新
{
‘_op_type’: ‘update’,
‘_index’: ‘index-name’,
‘_id’: 42,
‘doc’: {‘question’: ‘The life, universe and everything.’}
}
3.增加(默认)
{
‘_index’: ‘index-name’,
‘_id’: 42,
‘_source’: {
“title”: “Hello World!”,
“body”: “…”
}
}
如果你没写_source字段,那么会将所有字段pop出去作为插入的数据!
也可以这么写:
{
“_index”: “index-name”,
“_id”:43,
“title”: “Hello World”,
“body”:“…”
}
或者
{
“_index”: “index-name”,
“title”: “Hello World”,
“body”:“…”
}
go
安装依赖库
go get github.com/olivere/elastic/v7
连接客户端
使用NewClient方法创建es客户端,在创建时,可以提供ES连接参数。
func main(){
host := "http://127.0.0.1:9200/"
client, err := elastic.NewClient(elastic.SetURL(host))
if err != nil {
// Handle error
panic(err)
}
info, code, err := client.Ping(host).Do(context.Background())
if err != nil {
panic(err)
}
fmt.Printf("Elasticsearch returned with code %d and version %s\n", code, info.Version.Number)
esversion, err := client.ElasticsearchVersion(host)
if err != nil {
panic(err)
}
fmt.Printf("Elasticsearch version %s\n", esversion)
}
elastic.SetURL(url)
用来设置ES
服务地址,如果是本地,就是127.0.0.1:9200
。支持多个地址,用逗号分隔即可。elastic.SetBasicAuth("user", "secret")
这个是基于http base auth 验证机制的账号密码。elastic.SetGzip(true)
启动gzip
压缩elastic.SetHealthcheckInterval(10*time.Second)
用来设置监控检查时间间隔elastic.SetMaxRetries(5)
设置请求失败最大重试次数,v7版本以后已被弃用elastic.SetSniff(false)
允许指定弹性是否应该定期检查集群(默认为true)elastic.SetErrorLog(log.New(os.Stderr, "ELASTIC ", log.LstdFlags))
设置错误日志输出elastic.SetInfoLog(log.New(os.Stdout, "", log.LstdFlags))
设置info日志输出
建库建表
建对应的索引以及mapping,比如:
mappingTpl = `{
"mappings":{
"properties":{
"id": { "type": "long" },
"username": { "type": "keyword" },
"nickname": { "type": "text" },
"phone": { "type": "keyword" },
"age": { "type": "long" },
"ancestral": { "type": "text" },
"identity": { "type": "text" },
"update_time": { "type": "long" },
"create_time": { "type": "long" }
}
}
}`
func (es *UserES) init() {
ctx := context.Background()
exists, err := es.client.IndexExists(es.index).Do(ctx)
if err != nil {
fmt.Printf("userEs init exist failed err is %s\n", err)
return
}
if !exists {
_, err := es.client.CreateIndex(es.index).Body(es.mapping).Do(ctx)
if err != nil {
fmt.Printf("userEs init failed err is %s\n", err)
return
}
}
}
首先判断es中是否已经存在要创建的索引,不存在,调用CreateIndex进行创建。
CURD
新增
func create() {
//使用结构体
e1 := Employee{"Jane", "Smith", 32, "I like to collect rock albums", []string{"music"}}
put1, err := client.Index().
Index("megacorp").
Type("employee").
Id("1").
BodyJson(e1).
Do(context.Background())
if err != nil {
panic(err)
}
fmt.Printf("Indexed tweet %s to index s%s, type %s\n", put1.Id, put1.Index, put1.Type)
//使用字符串
e2 := `{"first_name":"John","last_name":"Smith","age":25,"about":"I love to go rock climbing","interests":["sports","music"]}`
put2, err := client.Index().
Index("megacorp").
Type("employee").
Id("2").
BodyJson(e2).
Do(context.Background())
if err != nil {
panic(err)
}
fmt.Printf("Indexed tweet %s to index s%s, type %s\n", put2.Id, put2.Index, put2.Type)
e3 := `{"first_name":"Douglas","last_name":"Fir","age":35,"about":"I like to build cabinets","interests":["forestry"]}`
put3, err := client.Index().
Index("megacorp").
Type("employee").
Id("3").
BodyJson(e3).
Do(context.Background())
if err != nil {
panic(err)
}
fmt.Printf("Indexed tweet %s to index s%s, type %s\n", put3.Id, put3.Index, put3.Type)
}
批量增加
批量添加使用的是bulkAPI,bulkAPI允许在单个步骤中进行多次create、index,update、delete请求。如果你需要索引一个数据流比如日志事件,它可以排队和索引数百或数千批次。bulk 与其他的请求体格式稍有不同,如下所示:
{ action: { metadata }}
{ request body }
{ action: { metadata }}
{ request body }
...
这种格式类似一个有效的单行 JSON 文档 流 ,它通过换行符(\n)连接到一起。注意两个要点:
每行一定要以换行符(\n)结尾, 包括最后一行 。这些换行符被用作一个标记,可以有效分隔行。
这些行不能包含未转义的换行符,因为他们将会对解析造成干扰。这意味着这个 JSON 不 能使用 pretty 参数打印。
- action/metadata 行指定 哪一个文档 做 什么操作 。
- action 必须是以下选项之一:
- create:如果文档不存在,那么就创建它。
- index:创建一个新文档或者替换一个现有的文档。
- update:部分更新一个文档
- delete:删除一个文档
func (es *UserES) BatchAdd(ctx context.Context, user []*model.UserEs) error {
var err error
for i := 0; i < esRetryLimit; i++ {
if err = es.batchAdd(ctx, user); err != nil {
fmt.Println("batch add failed ", err)
continue
}
return err
}
return err
}
func (es *UserES) batchAdd(ctx context.Context, user []*model.UserEs) error {
req := es.client.Bulk().Index(es.index)
for _, u := range user {
u.UpdateTime = uint64(time.Now().UnixNano()) / uint64(time.Millisecond)
u.CreateTime = uint64(time.Now().UnixNano()) / uint64(time.Millisecond)
doc := elastic.NewBulkIndexRequest().Id(strconv.FormatUint(u.ID, 10)).Doc(u)
req.Add(doc)
}
if req.NumberOfActions() < 0 {
return nil
}
if _, err := req.Do(ctx); err != nil {
return err
}
return nil
}
删除
func delete() {
res, err := client.Delete().Index("megacorp").
Type("employee").
Id("1").
Do(context.Background())
if err != nil {
println(err.Error())
return
}
fmt.Printf("delete result %s\n", res.Result)
}
批量删除
func (es *UserES) BatchDel(ctx context.Context, user []*model.UserEs) error {
var err error
for i := 0; i < esRetryLimit; i++ {
if err = es.batchDel(ctx, user); err != nil {
continue
}
return err
}
return err
}
func (es *UserES) batchDel(ctx context.Context, user []*model.UserEs) error {
req := es.client.Bulk().Index(es.index)
for _, u := range user {
doc := elastic.NewBulkDeleteRequest().Id(strconv.FormatUint(u.ID, 10))
req.Add(doc)
}
if req.NumberOfActions() < 0 {
return nil
}
if _, err := req.Do(ctx); err != nil {
return err
}
return nil
}
修改
elasticsearch的修改文档是可以直接用创建去替换,如果单字段修改可以使用以下修改:
func update() {
res, err := client.Update().
Index("megacorp").
Id("2").
Doc(map[string]interface{}{"age": 88}).
Do(context.Background())
if err != nil {
println(err.Error())
}
fmt.Printf("update age %s\n", res.Result)
}
批量更新
func (es *UserES) BatchUpdate(ctx context.Context, user []*model.UserEs) error {
var err error
for i := 0; i < esRetryLimit; i++ {
if err = es.batchUpdate(ctx, user); err != nil {
continue
}
return err
}
return err
}
func (es *UserES) batchUpdate(ctx context.Context, user []*model.UserEs) error {
req := es.client.Bulk().Index(es.index)
for _, u := range user {
u.UpdateTime = uint64(time.Now().UnixNano()) / uint64(time.Millisecond)
doc := elastic.NewBulkUpdateRequest().Id(strconv.FormatUint(u.ID, 10)).Doc(u)
req.Add(doc)
}
if req.NumberOfActions() < 0 {
return nil
}
if _, err := req.Do(ctx); err != nil {
return err
}
return nil
}
根据条件更新数据
_, err = conf.ES().UpdateByQuery().
Index(indexName).
Type("_doc").
//查询条
Query(query).
//要执行的更新操作
Script(script).
Do(context.Background())
查找
func gets() {
//通过id查找
get1, err := client.Get().Index("megacorp").Type("employee").Id("2").Do(context.Background())
if err != nil {
panic(err)
}
if get1.Found {
fmt.Printf("Got document %s in version %d from index %s, type %s\n", get1.Id, get1.Version, get1.Index, get1.Type)
}
}
精确匹配单个字段 使用term查询
// 创建term查询条件,用于精确查询
termQuery := elastic.NewTermQuery("Author", "tizi")
searchResult, err := client.Search().
Index("blogs"). // 设置索引名
Query(termQuery). // 设置查询条件
Sort("Created", true). // 设置排序字段,根据Created字段升序排序,第二个参数false表示逆序
From(0). // 设置分页参数 - 起始偏移量,从第0行记录开始
Size(10). // 设置分页参数 - 每页大小
Pretty(true). // 查询结果返回可读性较好的JSON格式
Do(ctx) // 执行请求
if err != nil {
// Handle error
panic(err)
}
通过terms实现SQL的in查询
// 创建terms查询条件
termsQuery := elastic.NewTermsQuery("Author", "tizi", "tizi365")
searchResult, err := client.Search().
Index("blogs"). // 设置索引名
Query(termsQuery). // 设置查询条件
Sort("Created", true). // 设置排序字段,根据Created字段升序排序,第二个参数false表示逆序
From(0). // 设置分页参数 - 起始偏移量,从第0行记录开始
Size(10). // 设置分页参数 - 每页大小
Do(ctx) // 执行请求
匹配单个字段 某个字段使用全文搜索
// 创建match查询条件
matchQuery := elastic.NewMatchQuery("Title", "golang es教程")
searchResult, err := client.Search().
Index("blogs"). // 设置索引名
Query(matchQuery). // 设置查询条件
Sort("Created", true). // 设置排序字段,根据Created字段升序排序,第二个参数false表示逆序
From(0). // 设置分页参数 - 起始偏移量,从第0行记录开始
Size(10). // 设置分页参数 - 每页大小
Do(ctx)
范围查询
实现类似Created > '2020-07-20' and Created < '2020-07-22'的范围查询条件
// 例1 等价表达式:Created > "2020-07-20" and Created < "2020-07-29"
rangeQuery := elastic.NewRangeQuery("Created").
Gt("2020-07-20").
Lt("2020-07-29")
// 例2 等价表达式:id >= 1 and id < 10
rangeQuery := elastic.NewRangeQuery("id").
Gte(1).
Lte(10)
bool查询
bool组合查询 bool的功能类似于Sql中的and和or,将查询条件组合起来。
func (r *SearchRequest) ToFilter() *EsSearch {
var search EsSearch
if len(r.Nickname) != 0 {
search.ShouldQuery = append(search.ShouldQuery, elastic.NewMatchQuery("nickname", r.Nickname))
}
if len(r.Phone) != 0 {
search.ShouldQuery = append(search.ShouldQuery, elastic.NewTermsQuery("phone", r.Phone))
}
if len(r.Ancestral) != 0 {
search.ShouldQuery = append(search.ShouldQuery, elastic.NewMatchQuery("ancestral", r.Ancestral))
}
if len(r.Identity) != 0 {
search.ShouldQuery = append(search.ShouldQuery, elastic.NewMatchQuery("identity", r.Identity))
}
if search.Sorters == nil {
search.Sorters = append(search.Sorters, elastic.NewFieldSort("create_time").Desc())
}
search.From = (r.Num - 1) * r.Size
search.Size = r.Size
return &search
}
func (es *UserES) Search(ctx context.Context, filter *model.EsSearch) ([]*model.UserEs, error) {
boolQuery := elastic.NewBoolQuery()
boolQuery.Must(filter.MustQuery...)
boolQuery.MustNot(filter.MustNotQuery...)
boolQuery.Should(filter.ShouldQuery...)
boolQuery.Filter(filter.Filters...)
// 当should不为空时,保证至少匹配should中的一项
if len(filter.MustQuery) == 0 && len(filter.MustNotQuery) == 0 && len(filter.ShouldQuery) > 0 {
boolQuery.MinimumShouldMatch("1")
}
service := es.client.Search().Index(es.index).Query(boolQuery).SortBy(filter.Sorters...).From(filter.From).Size(filter.Size)
resp, err := service.Do(ctx)
if err != nil {
return nil, err
}
if resp.TotalHits() == 0 {
return nil, nil
}
userES := make([]*model.UserEs, 0)
for _, e := range resp.Each(reflect.TypeOf(&model.UserEs{})) {
us := e.(*model.UserEs)
userES = append(userES, us)
}
return userES, nil
}
查询之前进行了条件绑定,这个条件通过API进行设定的,根据条件绑定不同query。phone是具有唯一性的,所以可以采用精确查询,也就是使用NewTermsQuery进行绑定。Nickname、Identity、Ancestral这些都属于模糊查询,所以可以使用匹配查询,用NewMatchQuery进行绑定·。
批量查询
在一些场景中,需要通过多个ID批量查询文档。
es中提供了一个multiGet进行批量查询,multiGet批量查询的实现是跟redis的pipeline是一个道理的,缓存所有请求,然后统一进行请求,所以这里只是减少了IO的使用。
可以使用更好的方法,使用search查询,它提供了根据id查询的方法,这个方法是一次请求,完成所有的查询,更高效,推荐使用这个方法进行批量查询。
// 根据id 批量获取
func (es *UserES) MGet(ctx context.Context, IDS []uint64) ([]*model.UserEs, error) {
userES := make([]*model.UserEs, 0, len(IDS))
idStr := make([]string, 0, len(IDS))
for _, id := range IDS {
idStr = append(idStr, strconv.FormatUint(id, 10))
}
resp, err := es.client.Search(es.index).Query(
elastic.NewIdsQuery().Ids(idStr...)).Size(len(IDS)).Do(ctx)
if err != nil {
return nil, err
}
if resp.TotalHits() == 0 {
return nil, nil
}
for _, e := range resp.Each(reflect.TypeOf(&model.UserEs{})) {
us := e.(*model.UserEs)
userES = append(userES, us)
}
return userES, nil
}
嵌套查询
es提供了nested
操作来实现类似于mongodb中$eleMatch
的操作:
elastic.NewNestedQuery(
"nested_field",
query, //此处query中的字段 都需要加上nested_field前缀, 比如 nested_field.id
)
{
"bool": {
"filter": [
{
"nested": {
"path": "nested_field",
"query": {
"range": {
"nested_field.start_time": {
"from": 1581475200,
"include_lower": true,
"include_upper": true,
"to": null
}
}
}
}
},
{
"nested": {
"path": "nested_field",
"query": {
"range": {
"nested_field.end_time": {
"from": null,
"include_lower": true,
"include_upper": true,
"to": 1581481440
}
}
}
}
}
],
"must": {
"terms": {
"id": [
4181,
4175
]
}
}
}
}
实现上述查询条件的go代码如下
query := elastic.NewBoolQuery()
query.Must(elastic.NewTermsQuery("id", []int{4181, 4175}))
query.Filter(elastic.NewNestedQuery(
"nested_field",
// nested_field.start_time >= 1581475200
elastic.NewRangeQuery("nested_field.start_time").Gte(1581475200),
))
query.Filter(elastic.NewNestedQuery(
"nested_field",
// nested_field.start_time <= 1581481440
elastic.NewRangeQuery("nested_field.end_time").Lte(1581481440),
))
其他筛选
// 文档查询数量,默认为10。
client.Search().Index("index_name").Size(100).Do(context.Background())
//开始搜索的索引,默认为0。
client.Search().Index("index_name").From(100).Do(context.Background())
//排序顺序, true为降徐, false为升序
client.Search().Index("index_name").Sort("field_name", true).Do(context.Background())
// 还可以通过SortBy进行多个排序
sorts := []elastic.Sorter{
elastic.NewFieldSort("field_name01").Asc(), // 升序
elastic.NewFieldSort("field_name02").Desc(), // 降徐
}
client.Search().Index("index_name").SortBy(sorts...).Do(context.Background())
// 返回指定字段
includes:= []string{"name", "age"}
include := elastic.NewFetchSourceContext(true).Include(includes...)
client.Search().Index("index_name").FetchSourceContext(include).Do(context.Background())
//查询的总命中计数
client.Search().Index("index_name").TrackTotalHits(true).Do(context.Background())
Elasticsearch分页的三种方式:from+size、scroll、search_after
- from+size浅分页适合数据量小的场景,可以用于实时搜索场景,也可以跳页搜索。
- scroll标准方式查询不适合实时搜索查询,因为开始查询时形成一个快照,连续查询过程中,不会将新增加或修改的数据添加到查询结果中,也不支持跳页查询。区分首次查询和后续查询。
- scroll-scan 滚动扫描方式查询,在scroll标准方式基础上进行了一定的优化,特别是不需要排序的情况下,可以有效提高查询效率。区分首次查询和后续查询。
- search_after 分页的每次查询依赖于上次查询的最后一条数据,适合顺序连续查询,不支持跳页查询。区分首次查询和后续查询。
from + size
ElasticSearch默认采用的分页方式是from + size的形式,在深度分页的情况下,这种使用方式的效率是非常低的,比如我们执行如下查询:
GET /my_index/my_type/_search
{
"query": { "match_all": {}},
"from": 10,
"size": 5
}
上面的查询表示从搜索结果中取第10条开始的5条数据。
搜索一般包括两个阶段,query 和 fetch 阶段,query 阶段确定要取哪些doc,fetch 阶段取出具体的 doc。
Query阶段
- Client 发送一次搜索请求,node1 接收到请求,然后,node1 创建一个大小为 from + size 的优先级队列用来存结果,我们管 node1 叫 coordinating node。
- coordinating node将请求广播到涉及到的 shards,每个 shard 在内部执行搜索请求,然后,将结果存到内部的大小同样为 from + size 的优先级队列里,可以把优先级队列理解为一个包含 top N 结果的列表。
- 每个 shard 把暂存在自身优先级队列里的数据返回给 coordinating node,coordinating node 拿到各个 shards 返回的结果后对结果进行一次合并,产生一个全局的优先级队列,存到自身的优先级队列里。
在上面的过程中,coordinating node 拿到 (from + size) * 分片数目 条数据,然后合并并排序后选择前面的 from + size 条数据存到优先级队列,以便 fetch 阶段使用。另外,各个分片返回给 coordinating node 的数据用于选出前 from + size 条数据,所以,只需要返回唯一标记 doc 的 _id 以及用于排序的 _score 即可,这样也可以保证返回的数据量足够小。
coordinating node 计算好自己的优先级队列后,query 阶段结束,进入 fetch 阶段。
fetch阶段
query 阶段知道了要取哪些数据,但是并没有取具体的数据,这就是 fetch 阶段要做的。
- coordinating node 发送 GET 请求到相关shards。
- shard 根据 doc 的 _id 取到数据详情,然后返回给 coordinating node。
- coordinating node 返回数据给 Client。
coordinating node 的优先级队列里有 from + size 个 _doc _id,但是,在 fetch 阶段,并不需要取回所有数据,在上面的例子中,前10条数据是不需要取的,只需要取优先级队列里的第11到15条数据即可。
需要取的数据可能在不同分片,也可能在同一分片,coordinating node 使用 multi-get 来避免多次去同一分片取数据,从而提高性能。
scroll 深分页
from+size查询方式在10000-50000条数据(1000到5000页)以内的时候还是可以的,但是如果数据过多的话,就会出现深分页问题。
以上DSL语句执行后,ElasticSearch需要在各个分片上匹配排序并得到from + size
条数据,协调节点拿到这些数据再进行排序处理,然后结果集中取最后size
条数据返回。
这样的深度分页的效率非常低,因为我们只需要查询size
条数据,而ElasticSearch则需要对from + size
条数据进行排序处理后返回。
其次:ElasticSearch为了性能,限制了分页的深度,目前ElasticSearch支持的最大的查询长度是max_result_window = 10000;也就是说我们不能分页到10000条数据以上。
scroll默认方式
为了解决上面的问题,elasticsearch提出了一个scroll滚动的方式。
scroll 类似于sql中的cursor,使用scroll,每次只能获取一页的内容,然后会返回一个scroll_id。根据返回的这个scroll_id可以不断地获取下一页的内容,所以scroll并不适用于有跳页的情景。
初始搜索请求应该在查询中指定 scroll 参数,如 ?scroll=1m(1分钟),这可以告诉 Elasticsearch 需要保持搜索的上下文环境多久。
GET /my_index/my_type/_search?scroll=1m
{
"query": {
"match_all": {}
},
"size": 1,
"from": 0
}
返回结果:
{
"_scroll_id": "iT1VydTVrNjFIeURnAAAAAA...",
"took": 0,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
}
返回结果包含一个 scroll_id,可以被传递给 scroll API 来检索下一个批次的结果。
每次对 scroll API 的调用返回了结果的下一个批次结果,直到 hits 数组为空。scroll_id 则可以在请求体中传递。scroll 参数告诉 Elasticsearch 保持搜索的上下文等待另一个3m(3分钟)。返回数据的size与初次请求一致。
二次搜索:
POST /_search/scroll
{
"scroll":"3m",
"scroll_id": "iT1VydTVrNjFIeURnAAAAAA..."
}
返回结果:
{
"_scroll_id": "DnF1ZXJ5VGhlbkZldGNo...",
"took": 1,
"timed_out": false,
"terminated_early": true,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 5168,
"max_score": 1,
"hits": [
{
}
原理上来说可以把 scroll 分为初始化和遍历两步,初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照,在遍历时,从这个快照里取数据,也就是说,在初始化后对索引插入、删除、更新数据都不会影响遍历结果。因此,scroll 并不适合用来做实时搜索,而更适用于后台批处理任务,比如群发。
scroll-scan 的高效滚动
scroll API 保持了那些已经返回记录结果,所以能更加高效地返回排序的结果。但是,按照默认设定排序结果仍然需要代价。
**如果仅仅想要找到结果,不关心顺序,可以通过组合 scroll 和 scan 来关闭任何打分或者排序,以最高效的方式返回结果。**你需要做的就是将 search_type=scan 加入到查询的字符串中:
POST /my_index/my_type/_search?scroll=1m&search_type=scan
{
"query": {
"match" : {
"cityName" : "杭州"
}
}
}
设置 search_type 为 scan 可以关闭打分,让滚动更加高效。
扫描式的滚动请求和标准的滚动请求有四处不同:
- 不算分,关闭排序。结果会按照在索引中出现的顺序返回;
- 不支持聚合;
- 初始 search 请求的响应不会在 hits 数组中包含任何结果。第一批结果就会按照第一个 scroll 请求返回。
- 参数 size 控制了每个分片上而非每个请求的结果数目,所以 size 为 10 的情况下,如果命中了 5 个分片,那么每个 scroll 请求最多会返回 50 个结果。
如果你想支持打分,即使不进行排序,将 track_scores 设置为 true。
search_after 深分页
scroll 的方式,官方的建议不用于实时的请求(一般用于数据导出),因为每一个scroll_id 不仅会占用大量的资源,而且会生成历史快照,对于数据的变更不会反映到快照上。
search_after 分页的方式是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。但是需要注意,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。
为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,官方推荐使用 _uid 作为全局唯一值,其实使用业务层的 id 也可以,甚至雪花ID也行。
首次查询
POST /my_index/my_type/_search
{
"size":2,
"query": {
"match" : {
"cityName" : "杭州"
}
},
"sort": [
{"updateTime": "desc"}
]
}
查询返回结果
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 534,
"max_score": null,
"hits": [
{
"_index": "my_index",
"_type": "my_type",
"_id": "2019061010810316",
"_score": null,
"_source": {
}
第二次查询
POST /my_index/my_type/_search
{
"size":2,
"query": {
"match" : {
"cityName" : "杭州"
}
},
"search_after": [1560137241000],
"sort": [
{"updateTime": "desc"}
]
}
查询结果:
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 534,
"max_score": null,
"hits": [
{
"_index": "my_index",
"_type": "my_type",
"_id": "2019061010781031",
"_score": null,
"_source": {
}
按照第一个检索到的最后显示的“updateTime”,search_after及多个排序字段多个参数用逗号隔开,作为下一个检索search_after的参数。
当使用search_after参数时,from的值必须被设为0或者-1
go-elasticsearch
go-elasticsearch是Elasticsearch 官方提供的 Go 客户端。每个 Elasticsearch 版本会有一个对应的 go-elasticsearch 版本。官方会维护最近的两个主要版本。
go-elasticsearch 提供了 Low-level 和 Fully-typed 两套API。本文以 Fully-typed API 为例介绍 go-elasticsearch 的常用方法。
安装依赖
执行以下命令安装v8版本的 go 客户端:
go get github.com/elastic/go-elasticsearch/v8@latest
可以根据实际需求导入不同的客户端版本,也支持在一个项目中导入不同的客户端版本。
import (
elasticsearch7 "github.com/elastic/go-elasticsearch/v7"
elasticsearch8 "github.com/elastic/go-elasticsearch/v8"
)
// ...
es7, _ := elasticsearch7.NewDefaultClient()
es8, _ := elasticsearch8.NewDefaultClient()
连接 ES
指定要连接 ES 的相关配置,并创建客户端连接。
// ES 配置
cfg := elasticsearch.Config{
Addresses: []string{
"http://localhost:9200",
},
}
// 创建客户端连接
client, err := elasticsearch.NewTypedClient(cfg)
if err != nil {
fmt.Printf("elasticsearch.NewTypedClient failed, err:%v\n", err)
return
}
操作 ES
创建 index
创建一个名为 my-review-1 的 index。
// createIndex 创建索引
func createIndex(client *elasticsearch.TypedClient) {
resp, err := client.Indices.
Create("my-review-1").
Do(context.Background())
if err != nil {
fmt.Printf("create index failed, err:%v\n", err)
return
}
fmt.Printf("index:%#v\n", resp.Index)
}
索引 document
定义 document 对应的 Review 和 Tag 结构体。
// Review 评价数据
type Review struct {
ID int64 `json:"id"`
UserID int64 `json:"userID"`
Score uint8 `json:"score"`
Content string `json:"content"`
Tags []Tag `json:"tags"`
Status int `json:"status"`
PublishTime time.Time `json:"publishDate"`
}
// Tag 评价标签
type Tag struct {
Code int `json:"code"`
Title string `json:"title"`
}
创建一条 document 并添加到 my-review-1 的 index 中。
// indexDocument 索引文档
func indexDocument(client *elasticsearch.TypedClient) {
// 定义 document 结构体对象
d1 := Review{
ID: 1,
UserID: 147982601,
Score: 5,
Content: "这是一个好评!",
Tags: []Tag{
{1000, "好评"},
{1100, "物超所值"},
{9000, "有图"},
},
Status: 2,
PublishTime: time.Now(),
}
// 添加文档
resp, err := client.Index("my-review-1").
Id(strconv.FormatInt(d1.ID, 10)).
Document(d1).
Do(context.Background())
if err != nil {
fmt.Printf("indexing document failed, err:%v\n", err)
return
}
fmt.Printf("result:%#v\n", resp.Result)
}
获取 document
根据 id 获取 document。
// getDocument 获取文档
func getDocument(client *elasticsearch.TypedClient, id string) {
resp, err := client.Get("my-review-1", id).
Do(context.Background())
if err != nil {
fmt.Printf("get document by id failed, err:%v\n", err)
return
}
fmt.Printf("fileds:%s\n", resp.Source_)
}
检索 document
构建搜索查询可以使用结构化的查询条件。
// searchDocument 搜索所有文档
func searchDocument(client *elasticsearch.TypedClient) {
// 搜索文档
resp, err := client.Search().
Index("my-review-1").
Request(&search.Request{
Query: &types.Query{
MatchAll: &types.MatchAllQuery{},
},
}).
Do(context.Background())
if err != nil {
fmt.Printf("search document failed, err:%v\n", err)
return
}
fmt.Printf("total: %d\n", resp.Hits.Total.Value)
// 遍历所有结果
for _, hit := range resp.Hits.Hits {
fmt.Printf("%s\n", hit.Source_)
}
}
下面是在 my-review-1 中搜索 content 包含 “好评” 的文档。
// searchDocument2 指定条件搜索文档
func searchDocument2(client *elasticsearch.TypedClient) {
// 搜索content中包含好评的文档
resp, err := client.Search().
Index("my-review-1").
Request(&search.Request{
Query: &types.Query{
MatchPhrase: map[string]types.MatchPhraseQuery{
"content": {Query: "好评"},
},
},
}).
Do(context.Background())
if err != nil {
fmt.Printf("search document failed, err:%v\n", err)
return
}
fmt.Printf("total: %d\n", resp.Hits.Total.Value)
// 遍历所有结果
for _, hit := range resp.Hits.Hits {
fmt.Printf("%s\n", hit.Source_)
}
}
聚合
在 my-review-1 上运行一个平均值聚合,得到所有文档 score 的平均值。
// aggregationDemo 聚合
func aggregationDemo(client *elasticsearch.TypedClient) {
avgScoreAgg, err := client.Search().
Index("my-review-1").
Request(
&search.Request{
Size: some.Int(0),
Aggregations: map[string]types.Aggregations{
"avg_score": { // 将所有文档的 score 的平均值聚合为 avg_score
Avg: &types.AverageAggregation{
Field: some.String("score"),
},
},
},
},
).Do(context.Background())
if err != nil {
fmt.Printf("aggregation failed, err:%v\n", err)
return
}
fmt.Printf("avgScore:%#v\n", avgScoreAgg.Aggregations["avg_score"])
}
更新 document
使用新值更新文档。
// updateDocument 更新文档
func updateDocument(client *elasticsearch.TypedClient) {
// 修改后的结构体变量
d1 := Review{
ID: 1,
UserID: 147982601,
Score: 5,
Content: "这是一个修改后的好评!", // 有修改
Tags: []Tag{ // 有修改
{1000, "好评"},
{9000, "有图"},
},
Status: 2,
PublishTime: time.Now(),
}
resp, err := client.Update("my-review-1", "1").
Doc(d1). // 使用结构体变量更新
Do(context.Background())
if err != nil {
fmt.Printf("update document failed, err:%v\n", err)
return
}
fmt.Printf("result:%v\n", resp.Result)
}
更新可以使用结构体变量也可以使用原始JSON字符串数据。
// updateDocument2 更新文档
func updateDocument2(client *elasticsearch.TypedClient) {
// 修改后的JSON字符串
str := `{
"id":1,
"userID":147982601,
"score":5,
"content":"这是一个二次修改后的好评!",
"tags":[
{
"code":1000,
"title":"好评"
},
{
"code":9000,
"title":"有图"
}
],
"status":2,
"publishDate":"2023-12-10T15:27:18.219385+08:00"
}`
// 直接使用JSON字符串更新
resp, err := client.Update("my-review-1", "1").
Request(&update.Request{
Doc: json.RawMessage(str),
}).
Do(context.Background())
if err != nil {
fmt.Printf("update document failed, err:%v\n", err)
return
}
fmt.Printf("result:%v\n", resp.Result)
}
删除 document
根据文档 id 删除文档。
// deleteDocument 删除 document
func deleteDocument(client *elasticsearch.TypedClient) {
resp, err := client.Delete("my-review-1", "1").
Do(context.Background())
if err != nil {
fmt.Printf("delete document failed, err:%v\n", err)
return
}
fmt.Printf("result:%v\n", resp.Result)
}
删除 index
删除指定的 index。
// deleteIndex 删除 index
func deleteIndex(client *elasticsearch.TypedClient) {
resp, err := client.Indices.
Delete("my-review-1").
Do(context.Background())
if err != nil {
fmt.Printf("delete document failed, err:%v\n", err)
return
}
fmt.Printf("Acknowledged:%v\n", resp.Acknowledged)
}
FQA
修改go的时间类型time.Time序列化
go的json对Time类型的序列化结果是2020-07-16T14:49:50.3269159+08:00这种类型,对于非这类型的数据反序列化到结构体就会报错。
type Generalzy struct {
Name string `json:"name"`
CreateTime time.Time `json:"create_time"`
}
var info = `{"name":"generalzy","create_time":"2023-12-07T17:29:13.772000"}`
func main() {
me := new(Generalzy)
if err := json.Unmarshal([]byte(info), me); err != nil {
log.Fatalln(err)
}
fmt.Println(me)
}
parsing time "2023-12-07T17:29:13.772000" as "2006-01-02T15:04:05Z07:00": cannot parse "" as "Z07:00"
方法一:"继承"time.Time,重写UnmarshalJSON方法
type Datetime struct {
time.Time
}
func (t *Datetime) UnmarshalJSON(data []byte) error {
// 去除时间左右的双引号
str := string(data[1 : len(data)-1])
var err error
t.Time, err = time.Parse("2006-01-02T15:04:05.000000", str)
if err != nil {
return err
}
return nil
}
type Generalzy struct {
Name string `json:"name"`
CreateTime Datetime `json:"create_time"`
}
var info = `{"name":"generalzy","create_time":"2023-12-07T17:29:13.772000"}`
func main() {
me := new(Generalzy)
if err := json.Unmarshal([]byte(info), me); err != nil {
log.Fatalln(err)
}
fmt.Println(me)
}
这样 Datetime结构体是可以调用time.Time的所有方法的,所以看到输出的是&{generalzy 2023-12-07 17:29:13.772 +0000 UTC},因为Time有String方法。
缺点是Time不再是time.Time类,使用ORM框架时无法映射数据库的日期类了,会报错unsupported Scan, storing driver.Value type time.Time into type *main.Datetime。
方法二:自定义time.Time类型
type DateTime time.Time
func (t *DateTime) UnmarshalJSON(data []byte) (err error) {
num, err := strconv.Atoi(string(data))
if err != nil {
return err
}
*t = Time(time.Unix(int64(num), 0))
return
}
由于此方法是创建了一个全新的类型(但和time.Time可以互相转换),所以一些方法,如String()需要自己实现一遍,
这个方法还有一个很大的优点就是不影响现有框架例如ORM框架在映射数据库日期类时对日期类的解析。
方法三:自定义每个结构体的MarshalJSON和UnmarshalJSON方法
针对某些特种结构体可以自己实现结构体的json接口
,json序列化或反序列化会自动调用这些方法。
package main_test
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"reflect"
"strconv"
"strings"
"testing"
"time"
)
type VssUser struct {
Id int64 `json:"id"`
Name string `json:"name"`
CreateTime time.Time `json:"createTime"`
UpdateTime time.Time `json:"updateTime"`
}
// MarshalJSON 序列化方法
func (s *VssUser) MarshalJSON() ([]byte, error) {
log.Println("自定义json序列化")
buffer := bytes.NewBufferString("{")
reType := reflect.TypeOf(*s)
reValue := reflect.ValueOf(*s)
count := reType.NumField() - 1
for i := 0; i < reType.NumField(); i++ {
jsonKey := getJsonKey(reType.Field(i))
jsonValue, err := getJsonValue(reValue.Field(i))
if err != nil {
return nil, err
}
buffer.WriteString(fmt.Sprintf("\"%v\":%v", jsonKey, string(jsonValue)))
if i < count {
buffer.WriteString(",")
}
}
buffer.WriteString("}")
return buffer.Bytes(), nil
}
// getJsonKey 获取json的key,不考虑忽略默认值的事,不管omitempty标签
func getJsonKey(field reflect.StructField) string {
jsonTag := field.Tag.Get("json")
if len(jsonTag) == 0 {
return field.Name
} else {
return strings.Split(jsonTag, ",")[0]
}
}
func getJsonValue(value reflect.Value) ([]byte, error) {
// 指针需要使用Elem取值
if value.Kind() == reflect.Ptr {
return jsonValue(value.Elem())
} else {
return jsonValue(value)
}
}
func jsonValue(value reflect.Value) ([]byte, error) {
// time.Time类型特殊处理,改为时间戳
if value.Type().String() == "time.Time" {
method := value.MethodByName("Unix")
in := make([]reflect.Value, 0)
rtn := method.Call(in)
return ([]byte)(strconv.FormatInt(rtn[0].Int(), 10)), nil
} else {
return json.Marshal(value.Interface())
}
}
func (s *VssUser) UnmarshalJSON(data []byte) error {
log.Println("自定义json反序列化")
// 先全部用接口接收
commonArr := make(map[string]interface{})
err := json.Unmarshal(data, &commonArr)
if err != nil {
return err
}
reValue := reflect.ValueOf(s)
reType := reflect.TypeOf(*s)
for i:=0; i<reType.NumField(); i++ {
jsonKey := getJsonKey(reType.Field(i))
// 每种数据类型都要针对性处理,暂时就只写int64、string、Time了
switch reType.Field(i).Type.String() {
case "time.Time":
// 接口对象通过.(a)就转换成a类型,只有接口对象
jsonValue := commonArr[jsonKey].(float64)
time := time.Unix(int64(jsonValue), 0)
reValue.Elem().Field(i).Set(reflect.ValueOf(time))
case "int64":
jsonValue := commonArr[jsonKey].(float64)
reValue.Elem().Field(i).Set(reflect.ValueOf(int64(jsonValue)))
case "string":
jsonValue := commonArr[jsonKey].(string)
reValue.Elem().Field(i).Set(reflect.ValueOf(jsonValue))
default:
return errors.New("value error")
}
}
return nil
}
func TestJson2(t *testing.T) {
vssUser := &VssUser{
Id: 0,
Name: "testUser",
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
res, err := json.Marshal(vssUser)
if err != nil {
log.Fatal(err)
}
log.Println(string(res))
dateTime2 := &VssUser{}
json.Unmarshal(res, &dateTime2)
log.Printf("%v", *dateTime2)
}
理解深分页问题
假设在一个有5个主分片的索引中搜索。当请求结果的第一 页(结果1到10)时,每个分片产生自己最顶端10个结果然后返回它们给请求节点(requesting node),它再排序这所有的50个结果以选出顶端的10个结果。
现在假设我们请求第1000页——结果10001到10010。工作方式都相同,不同的是每个分片都必须产生顶端的 10010个结果。然后请求节点排序这50050个结果并丢弃50040个!
可以看到在分布式系统中,排序结果的花费随着分页的深入而成倍增长。这也是为什么网络搜索引擎中任何语句不能返回多于1000个结果的原因。如微博前端页面返回的页数只有50页,用工具是可以拿到50页以后的数据的。
何让查询的TotalHits()显示查询条件查询出来的总数,而不是es的最大值10000
在 Elasticsearch 中,默认情况下,查询结果的 TotalHits()
方法返回的是匹配到的文档总数,但是有一个限制,默认情况下,如果匹配的文档总数超过了 index.max_result_window
的值,Elasticsearch 将返回一个截断的结果,而不是返回实际匹配到的所有文档。
默认情况下,index.max_result_window
的值是 10,000。因此,如果匹配到的文档总数超过这个值,TotalHits()
将返回 index.max_result_window
。
如果您希望查询结果的 TotalHits()
显示实际匹配到的文档总数,而不是受限于 index.max_result_window
,可以使用 SearchSourceContext
中的 TrackTotalHits
参数。设置 TrackTotalHits
为 true
将使 Elasticsearch 在计算匹配总数时忽略 index.max_result_window
。
以下是一个示例代码:
searchSource := elastic.NewSearchSource()
searchSource.Query(yourQuery)
searchResult, err := client.Search().
Index("your_index").
SearchSource(searchSource).
TrackTotalHits(true). // 设置 TrackTotalHits 为 true
Do(context.Background())
if err != nil {
log.Fatal(err)
}
totalHits := searchResult.TotalHits()
fmt.Printf("Total Hits: %d\n", totalHits)
确保将 TrackTotalHits
设置为 true
,这样 TotalHits()
就会显示实际匹配到的文档总数,而不受 index.max_result_window
的限制。
如何提高index.max_result_window的限制
index.max_result_window
是 Elasticsearch 中一个用于限制查询结果返回文档总数的参数。它的默认值为 10,000,这意味着如果查询匹配的文档总数超过 10,000,Elasticsearch 将返回部分结果而不是所有匹配到的文档。
提高 index.max_result_window
的限制通常不是一个好的做法,因为它可能导致资源消耗增加,对性能和内存的压力加大。更适合的方法是通过分页来处理大量的匹配文档。
如果你仍然希望提高这个限制,可以通过更新索引的设置来实现。下面是一个示例,展示如何通过 Elasticsearch 的 REST API 来更新索引的设置:
PUT /your_index/_settings
{
"index.max_result_window": 20000
}
这个请求将 your_index
索引的 index.max_result_window
设置为 20,000。请注意,这个操作可能需要索引重新刷新后才会生效。
在使用这个设置时,请谨慎考虑性能和资源的问题。如果你的查询涉及大量匹配文档,使用分页查询(例如通过 size
和 from
参数)来处理结果可能是更好的选择。
打印bool查询语句
es的查询语句一般比较复杂,很容易就构建成了一棵树:
rootBoolQuery := elastic.NewBoolQuery()
childBoolQuery := elastic.NewBoolQuery()
childBoolQuery.Must(
...
).Should(
...
)
rootBoolQuery.Must(
childBoolQuery
)
想要打印出查询语句可以使用如下:
// 创建新的bool查询
query := elastic.NewBoolQuery()
query.Must(elastic.NewTermQuery("name", "wunder"))
source, _ := query.Source()
fmt.Println(source)
// 结果:map[bool:map[must:map[term:map[name:wunder]]]]