017.Elasticsearch搜索操作入门篇

1. 多种搜索方式

1.1 Query String Search:在请求URL中包括search的参数

# 语法
curl -X GET "ip:port/index_name/type_name/_search?q=key1=value1&key2=value2"

# 举例
# 查看全部文档
curl -X GET "node01:9200/shop/product/_search"
# 搜索商品名称中包含“Toothpaste”的商品,而且按照price降序排序
curl -X GET "node01:9200/shop/product/_search?q=name:Toothpaste&sort=price:desc"

# 扩展
GET /index_name/type_name/_search?q=key:value
GET /index_name/type_name/_search?q=+key:value
GET /index_name/type_name/_search?q=-key:value

# name的值包含"Tom"
GET /test_index/test_type/_search?q=name:Tom
# 无论那个field,只要其值包含"Tom"即可
GET /test_index/test_type/_search?q=Tom
# =+ 与 = 的效果相同
GET /test_index/test_type/_search?q=+name:Tom
# =- 的意思是:field不是指定的字符串(精确匹配,例如本例子不会过滤name=Tommy的document)
GET /test_index/test_type/_search?q=-name:Tom
  • "q=value"原理分析

假设有如下的document:

{
    "name": "Tom",
    "age": 25,
    "sex": "male",
    "country": "China"
}

在插入这条数据后,ES会自动将多个field的值,全部用字符串的方式串联起来,组成一个长字符串,作为_all field的值,同时建立索引,本例中,"_all field"="Tom 25 male China",当使用Query String Search:GET /test_index/test_type/_search?q=Tom,实际上并不会去逐一比较每个field的值,而是直接与"_all field"的分词结果做比较

  • Query String Search的适用场景

适用于临时的在命令行使用一些工具,比如curl,快速的发出请求,来检索想要的信息;但是如果查询请求很复杂,是很难去构建的,在生产环境中,几乎很少使用Query String Search

1.2 Query String Search实战

# 准备测试数据
PUT /my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "post_date": {
          "type": "text"
        }
      }
    }
  }
}

PUT /my_index/my_type/1
{
  "post_date": "2020-07-01"
}

PUT /my_index/my_type/2
{
  "post_date": "2020-07-02"
}

PUT /my_index/my_type/3
{
  "post_date": "2020-07-03"
}

# 测试分词结果
GET _analyze
{
  "text": "2020-07-01"
}

{
  "tokens" : [
    {
      "token" : "2020",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "",
      "position" : 0
    },
    {
      "token" : "07",
      "start_offset" : 5,
      "end_offset" : 7,
      "type" : "",
      "position" : 1
    },
    {
      "token" : "01",
      "start_offset" : 8,
      "end_offset" : 10,
      "type" : "",
      "position" : 2
    }
  ]
}

# 那么这3条文档建立的倒排索引如下:
---------------------
word  docId
---------------------
2020  1、2、3
07    1、2、3
01    1
02    2
03    3

# 直接与"_all field"的分词结果做比较,于是搜索出3条结果
GET /my_index/my_type/_search?q=2020

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "my_type",
        "_id" : "2",
        "_score" : 0.2876821,
        "_source" : {
          "post_date" : "2020-07-02"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "my_type",
        "_id" : "1",
        "_score" : 0.2876821,
        "_source" : {
          "post_date" : "2020-07-01"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "my_type",
        "_id" : "3",
        "_score" : 0.2876821,
        "_source" : {
          "post_date" : "2020-07-03"
        }
      }
    ]
  }
}

# 同样,将"2020-07-01"分词后,再去与文档匹配,查询出3条结果
GET /my_index/my_type/_search?q=2020-07-01
{
  "took" : 15,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.8630463,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "my_type",
        "_id" : "1",
        "_score" : 0.8630463,
        "_source" : {
          "post_date" : "2020-07-01"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "my_type",
        "_id" : "2",
        "_score" : 0.5753642,
        "_source" : {
          "post_date" : "2020-07-02"
        }
      },
      {
        "_index" : "my_index",
        "_type" : "my_type",
        "_id" : "3",
        "_score" : 0.5753642,
        "_source" : {
          "post_date" : "2020-07-03"
        }
      }
    ]
  }
}

# 指定field后,就不会进行分词了,而是精确匹配,于是搜索1条数据
# 注意这里需要给完全匹配的字符串加双引号,否则还是查询到3条数据
GET /my_index/my_type/_search?q=post_date:"2020-07-01"

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.8630463,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "my_type",
        "_id" : "1",
        "_score" : 0.8630463,
        "_source" : {
          "post_date" : "2020-07-01"
        }
      }
    ]
  }
}

# GET /my_index/my_type/_search?q=post_date:"2020"
# GET /my_index/my_type/_search?q=post_date:2020
# 以上两条搜索的结果都是3条数据,原理待更新

1.3 Query DSL(Domain Specified Language):查询领域专用语言

  • 适用场景:更加适合生产环境的使用,可以构建复杂的查询

  • 基本语法

    GET /index_name/type_name/_search?key=value
    {
      "key1": "value1"
    }
    # 说明:
    # 1.HTTP协议,一般不允许get请求带上request body,但是因为get更加适合描述查询数据的操作,因此还是这么用了
    # 2.很多浏览器或服务器都支持GET+request body模式,如果不支持,也可以用POST /_search,例如
    GET /index1/type1/_search?from=0&size=2
    POST /index1/type1/_search
    {
      "from": 0,
      "size": 2
    }
    # 3.查询条件的json中,可以进行复杂的json嵌套,来实现复杂的搜索
    
  • 入门案例

    # 查询某索引的全部文档
    GET /myindex/my_type/_search
    {
      "query": {
          "match_all": {}
          }
    }
    
  • timeout参数

    GET /myindex/my_type/_search?timeout=1s
    {
      "query": {
          "match_all": {}
          }
    }
    

    查询的时候可以指定一个timeout参数,默认没有值,这个timeout比较特殊,是说,最多查询指定的时间,比较index总共有1万条数据,全部查询出来需要1min,那么用户体验肯定是不好的,设置timeout为1s,就是说,在1s秒内,能查到多少就给我返回多少,当然了能全部返回是最好的,常用的单位有毫秒(ms)、秒(s)、分钟(m)

1.4 查询结果元数据分析

{
  "took": 6,
  "timed_out": false,
  "_shards": {
    "total": 6,
    "successful": 6,
    "failed": 0
  },
  "hits": {
    "total": 10,
    "max_score": 1,
    "hits": [
      {
        "_index": ".kibana",
        "_type": "config",
        "_id": "5.2.0",
        "_score": 1,
        "_source": {
          "buildNum": 14695
        }
      }
    ]
  }
}
  • took:整个搜索请求花费了多少毫秒

  • hits.total:本次搜索,返回了几条结果

  • hits.max_score:本次搜索的所有结果中,最大的相关度分数是多少,这个分数越大,排名越靠前,说明这条文档越符合搜索条件

  • hits.hits:默认查询前10条数据,按照"_score"降序排序

  • shards.total:此文档在几个shard上存在(包括主分片和其副本)

  • shards.successful:在这些shard上请求这条数据,可以响应的shard的个数

  • shards.failed:在这些shard上请求这条数据失败的shard个数

  • hits.hits._index:文档所属index

  • hits.hits._type:文档所属type

  • hits.hits._id:文档id

  • hits.hits._source:文档内容

2. 词条查询(term)和全文检索(full text)

  • 词条查询:词条查询不会分析查询条件,只有当词条和查询字符串串完全匹配时,才匹配搜
    索。
  • 全文查询:ElasticSearch引擎会先分析查询字符串,将其拆分成多个单词,只要已分析的字
    段中包含词条的任意一个,或全部包含,就匹配查询条件,返回该文档;如果不包含任意一
    个分词,表示没有任何文档匹配查询条件
  • 查询结果与使用termmatch与数据本身的类型息息相关

2.1 准备测试数据

# text:用于全文检索,该类型的字段将通过分词器进行分词
# keyword:不分词,只能搜索该字段完整的值
PUT /shop
{
  "mappings": {
    "product": {
      "properties": {
        "name": {
          "type": "keyword"
        },
        "desc": {
          "type": "text"
        }
      }
    }
  }
}

PUT shop/product/1
{
    "name": "Yunnanbaiyao Toothpaste",
    "desc": "Yunnanbaiyao Toothpaste"
}

PUT /shop/product/2
{
    "name": "Darlie Toothpaste",
    "desc": "Darlie Toothpaste"
}

PUT /shop/product/3
{
    "name": "ZhongHua Toothpaste",
    "desc": "ZhongHua Toothpaste"
}

2.2 term测试

# 词条查询

# 没有结果
POST /shop/product/_search
{
    "query": {
        "term": {
            "name": "Toothpaste"
        }
    }
}

# 有结果(1条)
POST /shop/product/_search
{
    "query": {
        "term": {
            "name": "Darlie Toothpaste"
        }
    }
}

# 没有结果
POST /shop/product/_search
{
    "query": {
        "term": {
            "desc": "Toothpaste"
        }
    }
}

# 没有结果
# 这个没有结果,是因为"Darlie Toothpaste"分词后分成了"darlie"和"toothpaste"
# 所有完全匹配"Darlie Toothpaste"是无法匹配到的
POST /shop/product/_search
{
    "query": {
        "term": {
            "desc": "Darlie Toothpaste"
        }
    }
}


# 有3条结果
POST /shop/product/_search
{
    "query": {
        "term": {
            "desc": "toothpaste"
        }
    }
}

2.3 terms测试

# 没有数据
POST /shop/product/_search
{
    "query": {
        "terms": {
            "name": ["Darlie", "Toothpaste"]
        }
    }
}

# 两条结果
POST /shop/product/_search
{
    "query": {
        "terms": {
            "name": ["Darlie Toothpaste", "Yunnanbaiyao Toothpaste"]
        }
    }
}

# 一条结果
POST /shop/product/_search
{
    "query": {
        "terms": {
            "name": ["Darlie", "Yunnanbaiyao Toothpaste"]
        }
    }
}

# 没有结果
POST /shop/product/_search
{
    "query": {
        "terms": {
            "desc": ["Darlie Toothpaste", "Yunnanbaiyao Toothpaste"]
        }
    }
}

# 1个结果
POST /shop/product/_search
{
    "query": {
        "terms": {
            "desc": ["darlie", "Yunnanbaiyao Toothpaste"]
        }
    }
}

2.4 match测试

# 有结果(1条)
GET /shop/product/_search
{
    "query": {
        "match": {
            "name": "Darlie Toothpaste"
        }
    }
}

# 没有结果
GET /shop/product/_search
{
    "query": {
        "match": {
            "name": "Toothpaste"
        }
    }
}

# 有结果(3条)
GET /shop/product/_search
{
    "query": {
        "match": {
            "desc": "Toothpaste"
        }
    }
}

# 有结果(3条)
GET /shop/product/_search
{
    "query": {
        "match": {
            "desc": "Darlie Toothpaste"
        }
    }
}

2.5 multi_match测试

PUT /shop/product/5
{
    "name": "Apple Toothpaste",
    "desc": "Apple Darlie"
}

PUT /shop/product/6
{
    "name": "Orage Darlie",
    "desc": "Orage"
}

# 3条结果
POST /shop/product/_search
{
    "query": {
        "multi_match": {
            "query": "Darlie",
            "fields": ["name", "desc"]
        }
    }
}

# 4条结果
POST /shop/product/_search
{
    "query": {
        "multi_match": {
            "query": "Orage Darlie",
            "fields": ["name", "desc"]
        }
    }
}

2.6 match_phrase测试

# 有结果(1条)
POST /shop/product/_search
{
    "query": {
        "match_phrase": {
            "desc": "Darlie Toothpaste"
        }
    }
}

# 有结果(3条)
POST /shop/product/_search
{
    "query": {
        "match_phrase": {
            "desc": "Toothpaste"
        }
    }
}

# 没有结果
POST /shop/product/_search
{
    "query": {
        "match_phrase": {
            "name": "Toothpaste"
        }
    }
}

# 有结果(1条)
POST /shop/product/_search
{
    "query": {
        "match_phrase": {
            "name": "Darlie Toothpaste"
        }
    }
}

2.7 match_all测试

# 查询全部数据
POST /shop/product/_search
{
    "query": {
        "match_all": {}
    }
}

GET /shop/product/_search
{
    "query": {
        "match_all": {
            "desc": "Darlie Toothpaste"
        }
    }
}

# 结果
{
    "error":{
        "root_cause":[
            {
                "type":"parsing_exception",
                "reason":"[5:13] [match_all] unknown field [desc], parser not found","line":5,
                "col":13
            }
        ],
        "type":"parsing_exception",
        "reason":"[5:13] [match_all] unknown field [desc], parser not found",
        "line":5,
        "col":13,
        "caused_by":{
            "type":"x_content_parse_exception",
            "reason":"[5:13] [match_all] unknown field [desc], parser not found"}
        },
        "status":400
    }
}

# 分页
GET /shop/product/_search
{
    "query": {
        "match_all": {}
    },
    "from": 0,
    "size": 10
}

2.8 match_phrase_prefix测试

PUT /shop/product/7
{
    "name": "Darlie Pro Toothpaste",
    "desc": "Darlie Pro Toothpaste"
}

# 有结果(1条)
POST /shop/product/_search
{
    "query": {
        "match_phrase_prefix": {
            "name": "Darlie Toothpaste"
        }
    }
}

# 没有结果
POST /shop/product/_search
{
    "query": {
        "match_phrase_prefix": {
            "name": "Darlie"
        }
    }
}

# 有结果(2条)
POST /shop/product/_search
{
    "query": {
        "match_phrase_prefix": {
            "desc": "Darlie"
        }
    }
}

# 有结果(1条)
POST /shop/product/_search
{
    "query": {
        "match_phrase_prefix": {
            "desc": "Darlie Pro"
        }
    }
}

# 有结果(1条)
POST /shop/product/_search
{
    "query": {
        "match_phrase_prefix": {
            "desc": "Darlie Toothpaste"
        }
    }
}

2.9 总结

keyword text
term 完全匹配才返回 完全匹配分词后的单词才返回
terms 传入多个字符串,返回那些可以完全匹配的结果 每个传入的单词,在分词后的所有单词中进行匹配,完全匹配才返回
match_all 查询全部数据,不能传入任何参数 查询全部数据,不能传入任何参数
match 完全匹配才返回 对输入字符串进行分词,指定的字段文本分词后的词语中包含任意一个输入字符串的分词词语,就算匹配,就可以作为结果返回
multi_match 指定的多个字段都完全匹配才返回 对输入字符串进行分词,指定的字段文本分词后的词语中包含任意一个输入字符串的分词词语,就算匹配,就可以作为结果返回
match_phrase 完全匹配才返回 输入字符串不分词,指定的字段文本分词后的词语中包含完整的输入字符串,才可以算匹配,才能作为结果返回
match_phrase_prefix 完全匹配才返回 输入一个单词,例如"hello",只要指定的字段文本分词后的词语中有一个词语是以"hello"作为前缀,就算匹配,输入一个短语例如"hello world tom",那么先匹配分词的后的词语中包含"hello world"的文档,然后在这些文档中过滤,只要这些文档的词语中包含以"tom"开头的词语,就算匹配

3. 范围查询

GET /company/employee/_search
{
  "query": {
    "range": {
      "age": {
        "gte": 31
      }
    }
  }
}

4. query上下文和filter上下文

  • query上下文:在搜索中,计算每个满足条件的document的相关度,进行评分,即给document的"_score"赋值,并进行倒序排序,然后返回结果,使用query上下文查询的结果无法缓存

  • filter上下文:在搜索中,只是过滤出符合条件的document,不计算相关度,使用filter上下文查询的结果将被缓存,以提高整体的查询效率,缓存不需要太多的内存,它只缓存哪些文档与此filter条件相匹配

  • 说明:这里说的query上下文和filter上下文是描述的两种现象,而非使用"query"就是query上下文,使用"filter"就是filter上下文,例如:

    # 既有query上下文,也有filter上下文
    GET /_search
    {
      "query": { 
        "bool": { 
          "must": [
            {"match": {"title": "Search"}},
            {"match": {"content": "Elasticsearch" }}
          ],
          "filter": [ 
            {"term": {"status": "published" }},
            {"range": {"publish_date": {"gte": "2015-01-01" }}}
          ]
        }
      }
    }
    
    # 这个查询,虽然DSL语句中有"query"这个单词,但是它本质上只有一个filter上下文
    GET /_search
    {
        "query" : {
            "bool" : {
                "filter" : {
                    "term" : {
                        "author_id" : 1
                    }
                }
            }
        }
    }
    
    # 这个查询也是一个filter上下文查询,并且"constant_score"规定将符合条件的document的score都设置为"bootst"指定的值
    # 如果没有boost,则查询出的document的score都为1.0
    GET /_search
    {
        "query": {
            "constant_score" : {
                "filter" : {
                    "term" : { "user" : "kimchy"}
                },
                "boost" : 1.2
            }
        }
    }
    

一般来说,如果是进行搜索,需要将最匹配搜索条件的数据先返回,那么用query上下文;如果只是要根据条件筛选出一部分数据,不关注其排序,那么用filter,如果希望越符合搜索条件的document排名越靠前,就把这些搜索条件要放在query上下文中,如果只是想查询到数据并不关注其排名,就放到filter上下文中。

5. 多条件组合查询

# 语法
GET /index_name/type_name/_search
{
    "query": {
        "bool": {
            "must": [],
            "should": [],
            ...
        }
    }
}
    
# 示例
POST _search
{
  "query": {
    "bool" : {
      "must" : {
        "term" : { "user" : "kimchy" }
      },
      "filter": {
        "term" : { "tag" : "tech" }
      },
      "must_not" : {
        "range" : {
          "age" : { "gte" : 10, "lte" : 20 }
        }
      },
      "should" : [
        { "term" : { "tag" : "wow" } },
        { "term" : { "tag" : "elasticsearch" } }
      ],
      "minimum_should_match" : 1
    }
  }
}

# bool中可以放置的内容
must,must_not,should,filter
  • must:属于query上下文,单个或多个条件必须都满足,参与评分
  • filter:属于filter上下文,根据条件进行过滤,不参与评分
  • should:如果查询条件组合同时存在should和query上下文,那么should中的一个或者多个条件都可以不满足,如果查询条件组合中只有should或者只有should和filter上下文,那么should中的一个或者多个条件必须满足一个,should的条件参与评分
  • must_not:属于filter上下文,单个或者多个条件必须都不满足,不参与评分,使用must_not查询出的document的分数都为0
  • minimum_should_match:使用这个参数来指定should的具体行为
    • 正数,例如3,那么should的多个条件中必须满足3个条件
    • 负数,例如-2,代表可以有2个条件不满足,其他都应该满足
    • 百分比正数:代表should条件总数的百分比个条件应该满足,例如总共10个条件,百分比为30%,那么至少3个条件应该满足,需满足条件的个数向下取整
    • 百分比负数:代表占此比例的条件可以不满足,其余的均需要满足,计算结果向下取整
    • 百分比和数字组合:3<90%,如果条件个数<=3,那么必须全部满足,否则,满足90%(向下取整)即可
    • 多个组合(空格隔开):2<-25% 9<-3,如果条件个数<=2,则必须都满足,如果条件个数为[3,9],则需要25%的条件满足,否则,只能有3个条件不满足,其余都需要满足
  • must、must_not、should、filter之间是&的关系,即这些条件都应该满足

6. 自定义排序规则

默认情况下,是按照"_score"降序排序的,使用"sort"参数来自定义排序规则

GET /company/employee/_search
{
  "query": {
    "range": {
      "age": {
        "gte": 30
      }
    }
  },
  "sort": [
    {
      "join_date": {
        "order": "desc"
      }
    }
  ]
}

GET /company/employee/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "age": {
            "gte": 30
          }
        }
      }
    }
  },
  "sort": [
    {
      "join_date": {
        "order": "asc"
      }
    }
  ]
}

7. DSL校验

构建好一个复杂的查询之后,可以先校验一下语句,通过返回的异常信息来修改语句,校验语法如下:

GET /test_index/test_type/_validate/query?explain
{
  "query": {
    "math": { # 这里故意把match写为了math
      "test_field": "test"
    }
  }
}

{
  "valid": false,
  "error": "org.elasticsearch.common.ParsingException: no [query] registered for [math]"
}

GET /test_index/test_type/_validate/query?explain
{
  "query": {
    "match": {
      "test_field": "test"
    }
  }
}

{
  "valid": true,
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "explanations": [
    {
      "index": "test_index",
      "valid": true,
      "explanation": "+test_field:test #(#_type:test_type)"
    }
  ]
}

8. 搜索参数总结

  • preference:决定了哪些shard会被用来执行搜索操作,有以下值可选:
    • _primary:只在主分片中查询,在6.1版本已经废弃
    • _primary_first:优先在主分片中查询,在6.1版本已经废弃
    • _only_local:只在本地查询,查询结果可能不全
    • _local:优先在本地的分片中查询
    • _prefer_nodes:abc,xyz:在指定的node上查询,abc和xyz是node的id,查询顺序不确定
    • _only_nodes:abc*,x*yz,...:在指定的node上查询,这里可以使用正则表达式
    • _shards:2,3:在指定shard上查询
GET /my_index/my_type/_search?preference=_primary
{
  "query": {
    "match_all": {}
  }
}

GET /my_index/my_type/_search?preference=_primary_first
{
  "query": {
    "match_all": {}
  }
}

GET /my_index/my_type/_search?preference=_only_local
{
  "query": {
    "match_all": {}
  }
}

GET /my_index/my_type/_search?preference=_local
{
  "query": {
    "match_all": {}
  }
}

GET /my_index/my_type/_search?preference=_shards:1,2,3
{
  "query": {
    "match_all": {}
  }
}

GET /my_index/my_type/_search?preference=_prefer_nodes:bfkgPqlhSbWH8O6fL1R47Q
{
  "query": {
    "match_all": {}
  }
}

Bouncing Result问题:

两个Document排序,Field值相同,但是请求到不同的shard上,排序结果可能不同,每次请求轮询到不同的shard上,每次页面上看到的搜索结果的排序可能都不一样,这就是Bouncing Result问题,解决问题就是利用preference设置每次请求都到同一个shard上。

解决方案就是将preference设置为一个字符串,比如说user_id,让每个user每次搜索的时候,都使用同一个replica shard去执行,就不会看到bouncing results了

  • timeout:限定在一定时间内,将获取到的部分数据直接返回,避免查询耗时过长

  • routing:使用routing的值来获取document所在的shard编号,默认为document的id,在一些场景下可以使用routing=user_id(或者其他业务数据),这样的话可以让同一个user对应的数据到一个shard上去

  • search_type:默认值为"query_then_fetch",使用"dfs_query_then_fetch"可以提升相关度排序的精准度

你可能感兴趣的:(017.Elasticsearch搜索操作入门篇)