唐抉的个人博客

python环境中Elasticsearch的使用

字数统计: 9.4k阅读时长: 44 min
2023/01/13

Elasticsearch简介

Elasticsearch是目前全文搜索引擎的首选项,它可以快速地存储、搜索和分析海量数据。Elastic的底层是开源库Lucene,而Elastic是Lucene的封装,其提供了REST API的操作接口,使之可以开箱即用。维基百科、Stack Overflow、Github等网站的搜索引擎都是采用它。

Windows系统下Elasticsearch的安装和配置

安装Elasticsearch服务端

  • 官网中选择所需的合适版本下载即可。本文使用的Elasticsearch版本为5.5.3,下载链接为Elasticsearch 5.5.3 | Elastic,选择zip版本下载。
  • 下载完成后将程序解压到电脑中,打开解压目录进入到bin文件夹,双击elasticsearch.bat文件即可启动Elasticsearch服务(若启动失败,则查看环境中是否安装了jdk),窗口出现以下结果则表示服务启动成功
  • 服务启动成功后,访问localhost:9200,若看到以下显示,则代表安装成功

相关配置

  • 由于Elasticsearch可直接通过http请求访问,所以将9200端口直接暴露在网络当中是十分危险的,需要对访问进行限制。

  • 在Elasticsearch安装目录下,进入config目录,打开elasticsearch.yml配置文件。在配置文件中,修改network.host配置如下:

    1
    2
    3
    4
    # 配置网络请求中的相关参数及设置
    network.host: 0.0.0.0
    http.cors.enabled: true
    http.cors.allow-origin: "*"

    由于本人是自己测试,因此设为了允许所有来源访问且使Elasticsearch支持跨域请求,如对访问安全有要求或者是线上的服务,建议配置为具体的IP地址

安装Elasticsearch客户端

  • 打开cmd,使用pip命令在项目目录中安装Elasticsearch,由于本人服务端使用的版本是5.5.3,因此客户端版本也是安装.5.3版本:

    1
    2
    3
    4
    # 安装最新版本
    pip install elasticsearch
    # 安装指定版本
    pip install elasticsearch==5.5.3

  • 安装完成后,在python代码中导入elasticsearch包:

    1
    from elasticsearch import Elasticsearch

  • 接着获取elasticsearch的示例对象,不传入参数时则默认连接本地的,这里介绍三种连接方式:

    • 默认连接本地elasticsearch:

      1
      es=Elasticsearch()

    • 连接本地9200端口:

      1
      2
      es=Elasticsearch(['127.0.0.1:9200'])
      es=Elasticsearch(hosts="127.0.0.1",port=9200,timeout=3600) # timeout为超时时间

    • 连接集群,以列表的形式存放各节点的ip地址

      1
      2
      3
      4
      5
      6
      es=Elasticsearch(
      [
      {"host":"10.17.212.3","port":139},
      {"host":"192.168.233.1","port":139},
      {"host":"192.168.18.1","port":139}
      ],

  • 若需要配置忽略响应状态码,可编写代码如下:

    1
    2
    es=Elasticsearch(['127.0.0.1:9200'],ignore=400) # 忽略返回的400状态码
    es=Elasticsearch(['127.0.0.1:9200'],ignore=400) # 以列表的形式忽略多个状态码

一个简单的示例

保持Elasticsearch服务端正在运行的情况下,运行以下代码,便可以检验Elasticsearch服务端与客户端之间是否能连接:

1
2
3
4
5
6
7
8
9
from elasticsearch import Elasticsearch
es=Elasticsearch(['127.0.0.1:9200'])
print(es.index(index='py2',doc_type='doc',id=1,body={"name":"张三","age":18}))
print(es.get(index='py2',doc_type='doc',id=1))
# 第1个print为创建py2索引,并插入一条数据,第2个print查询指定文档。

# 查询结果如下:
{'_index': 'py2', '_type': 'doc', '_id': '1', '_version': 4, 'result': 'updated', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, 'created': False}
{'_index': 'py2', '_type': 'doc', '_id': '1', '_version': 4, 'found': True, '_source': {'name': '张三', 'age': 18}}

安装图形化插件

  • 点击链接下载head插件,下载后解压即可

  • 确保node.js已安装,若未安装可以在官网处下载安装

  • 打开cmd,进入到head插件的解压目录中,打开package.json文件查看使用的grunt版本,输入以下命令安装相应版本的grunt-cli

    1
    2
    PS D:\elasticsearch-head-master> npm install grunt@1.0.1
    PS D:\elasticsearch-head-master> npm install grunt@1.0.1 --save-dev

  • 继续输入以下命令下载项目所需的插件,启动图形化插件:

    1
    2
    PS D:\elasticsearch-head-master> npm install
    PS D:\elasticsearch-head-master> grunt server

    图像化插件成功启动的截图如下:

Python环境下操作Elasticsearch

基础操作

创建索引

es.indices.create():创建索引

1
2
# 创建一个索引为test的索引
es.indices.create(index='test')

删除索引

es.indices.delete():删除索引

1
2
# 删除索引为test的索引
es.indices.delete(index='test')

插入数据

es.index():插入数据

1
2
3
4
5
6
doc = {
'name': '方天',
'age': '23'
}
# 在test索引id为2的位置插入一条数据
es.index(index='test', id='2', document=doc)

删除数据

es.delete():删除指定数据

1
2
# 删除test索引id为3的数据
es.delete(index='test',id='3',doc_type='_doc')

更新数据

es.update():更新指定字段

1
2
3
4
5
6
doc = {
'name': '李邱俊',
'age': '20'
}
# 将test索引id为2的数据更新为新数据
es.update(index='test',id='2',doc=doc)

查询操作

es.get():查询数据

1
2
# 查询test索引id为1的数据
es.get(index='test',id='1')

查询函数

基础查询

1
2
# 查询test索引前10条数据,其中index为所需查询数据的索引
es.search(index='test')

过滤字段查询

1
2
# 只显示test索引的age字段信息,其中filter_path参数为添加过滤路径,显示指定字段(默认显示所有字段信息)
es.search(index='test',filter_path=['hits.hits._source.age'])

切片查询

1
2
# 查询test索引中,从序号为1的位置查询两条数据,其中from_参数为从指定索引开始查询,size为查询数据的条数
es.search(index='test', from_=1, size=2)

模糊查询(分词)

1
2
3
# 查询test索引中,age字段为20的数据
# 其中query为要查询数据的规则,match表示为模糊查询(分词),语句query={'match':{'age':20}}为检索指定的字段
es.search(index='test', query={'match':{'age':20}})

模糊查询(不分词)

1
2
# 查询test索引中,name字段为杨晨的数据,match_phrase表示为模糊查询(不分词),
es.search(index='test', query={'match_phrase':{'name':'杨晨'}})

精准单值查询

1
2
3
4
# 查询test索引中,age为20的数据,term表示为精确单值查询,terms表示为精确多值查询
es.search(index='test', query={'term':{'age':20}})
# 查询test索引中,name为杨晨的数据,查询中文,要在字段后面加上.keyword
es.search(index='test', query={'term':{'name.keyword':'杨晨'}})

多字段查询

1
2
# 查询test索引中,name和about都为小美的数据,multi_match表示为多字段查询
es.search(index='test',query={'multi_match':{'query':'小美',"fields":['name','about']}})

前缀查询

1
2
# 查询test索引中,name字段前缀为小的数据,prefix表示为前缀查询
es.search(index='test',query={'prefix':{'name.keyword':'小'}})

通配符查询

1
2
# 查询test索引中,name字段为杨*的数据,wildcard表示启用通配符查询,?代表一个字符,*代表0或多个字符,只能查询单一格式的数据
es.search(index='test',query={'wildcard':{'name.keyword':'杨?'}})

正则查询

1
2
# 查询test索引中,name字段为杨*的数据,regexp表示启用正则查询
print(es.search(index='test',query={'regexp':{'name.keyword':'杨.'}}))

多条件查询

1
2
3
# 查询test索引中,name字段为小美,id字段为1的数据
# 其中bool表示多条件查询,must表示与,should表示或,must_not表示非
print(es.search(index='test',query={'bool':{'must':{'term':{'name':'小美'},'term':{'id':'1'}}}}))

存在字段查询

1
2
# 查询test索引中,包含age字段的数据,其中exists表示存在字段查询
print(es.search(index='test',query={'exists':{'field':'age'}}))

范围查询

1
2
3
# 查询test索引中,age字段大于20小于等于23的数据
# 其中range表示范围查询,gt表示大于,gte表示大于等于,lt表示小于,lte表示小于等于
print(es.search(index='test',query={'range':{'age':{'gt':20,'lte':23}}}))

Json字段查询

1
2
3
# 查询test索引中,jsonfield1字段下json数据jsonfield2字段的数据包含'json'的数据
# 其中nested表示json数据查询,path参数为指定json数据的字段,query参数为指定的查询方式
print(es.search(index='test',query={'nested':{'path':'jsonfield1','query':{'term':{'jsonfield1.jsonfield2':'json'}}}}))

排序

1
2
# 查询test索引中的数据,按照age字段降序,其中sort表示排序查询,参数asc为升序,desc为降序
print(es.search(index='test', sort={'age.keyword':{'order':'desc'}}))

通过网络请求操作ElasticSearch

创建索引与映射字段

创建索引的语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT http://localhost:9200/索引库名
{
"mappings":{
"类型名称":{
"properties":{
"字段名":{
"type":"类型",
"index":true,
"store":true,
"analyzer":"分词器"
}
}
}
}
}

参数详情如下:

  • 类型名称:就是type的概念,类似于数据库中的不同表
  • 字段名:类似于数据库中的字段名称
  • type:类似于数据库中字段的类型,可以是text、long、short、data、object等
  • index:是否索引,默认为true。若需要根据该字段进行查询或排序,则需要将该字段index设置为true,否则设置为false
  • store:是否单独存储,默认为false,一般内容比较多的字段设置成true,可以提升查询性能
  • analyzer:分词器,如ik_smartik_max_word

示例

  • 通过postman发送PUT请求创建索引,以下代码在Elasticsearch 6.x以下能正常执行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    PUT http://localhost:9200/sku
    {
    "mappings": {
    "doc":{
    "properties":{
    "name":{
    "type":"text",
    "analyzer":"ik_smart"
    },
    "price":{
    "type":"integer"
    },
    "image":{
    "type":"text"
    },
    "createTime":{
    "type":"date"
    },
    "spuId":{
    "type":"text"
    },
    "categoryName":{
    "type":"keyword"
    },
    "brandName":{
    "type":"keyword"
    },
    "spec":{
    "type":"object"
    },
    "selNum":{
    "type":"integer"
    },
    "commentNum":{
    "type":"integer"
    }
    }
    }
    }
    }

  • 执行成功则返回的结果为:

    1
    2
    3
    4
    {
    "acknowledged": true,
    "shards_acknowledged": true
    }

查看索引字段类型

使用postman查看索引字段类型语法如下:

1
2
3
4
5
# 查看sku文档
GET http://localhost:9200/sku/_mapping

# 查看group文档
GET http://localhost:9200/group/_mapping

文档增加与修改

增加文档自动生成ID

通过POST请求,可以向一个已经存在的索引库中添加数据,语法如下:

1
2
3
4
POST http://localhost:9200/索引库名/类型名
{
"key":"value"
}

使用postman发送POST请求示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST http://localhost:9200/sku/doc
{
"name":"小米手机",
"price":200000,
"spuId":101,
"createTime":"2020-05-09",
"brandName":"小米",
"categoryName":"手机",
"saleNum":10012,
"commentNum":323,
"spec":{
"网络制式":"移动4g",
"屏幕尺寸":"4.5"
}
}

执行成功时返回信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
    "_index": "sku",
"_type": "doc",
"_id": "AYWp1L_wYrK1VsxqV5G5",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"created": true
}

新增文档指定ID

若想要在新增文档时指定ID,可以通过PUT命令,其语法如下:

1
2
3
4
PUT http://localhost:9200/索引库名/类型/ID值
{
...
}

使用postman发送PUT请求示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name":"小米电视",
"price":100000,
"spuId":10110,
"createTime":"2020-05-09",
"brandName":"小米",
"categoryName":"电视",
"saleNum":10012,
"commentNum":323,
"spec":{
"网络制式":"移动4g",
"屏幕尺寸":"39"
}
}

执行成功时返回信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"_index": "sku",
"_type": "doc",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"created": true
}

通过GET请求可以查询新增的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
GET http://localhost:9200/sku/_search
# 返回的结果为
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1.0,
"hits": [
{
"_index": "sku",
"_type": "doc",
"_id": "AYWp1L_wYrK1VsxqV5G5",
"_score": 1.0,
"_source": {
"name": "小米手机",
"price": 200000,
"spuId": 101,
"createTime": "2020-05-09",
"brandName": "小米",
"categoryName": "手机",
"saleNum": 10012,
"commentNum": 323,
"spec": {
"网络制式": "移动4g",
"屏幕尺寸": "4.5"
}
}
},
{
"_index": "sku",
"_type": "doc",
"_id": "1",
"_score": 1.0,
"_source": {
"name": "小米电视",
"price": 100000,
"spuId": 10110,
"createTime": "2020-05-09",
"brandName": "小米",
"categoryName": "电视",
"saleNum": 10012,
"commentNum": 323,
"spec": {
"网络制式": "移动4g",
"屏幕尺寸": "39"
}
}
}
]
}
}

修改索引文档

可以继续通过PUT/索引库名/类型/ID值的方式来更改刚才插入的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT http://localhost:9200/sku/doc/1
{
"name":"华为电视",
"price":100000,
"spuId":10110,
"createTime":"2020-05-09",
"brandName":"华为",
"categoryName":"电视",
"saleNum":10012,
"commentNum":323,
"spec":{
"网络制式":"移动4g",
"屏幕尺寸":"39"
}
}

通过ID删除索引文档

1
DELETE http://localhost:9200/sku/doc/1

索引查询

基本语法如下:

1
2
3
4
5
6
7
8
GET http://localhost:9200/索引库名/_search
{
"query":{
"查询类型":{
"查询条件":"查询条件值"
}
}
}

这里的query代表一个查询对象,里面可以有不同的查询属性:

  • 查询类型:如match_all、match、term、range等
  • 查询条件:查询条件会根据类型的不同,写法也有差异

查询所有数据(match_all)

代码示例如下,其中query代表查询对象,match_all代表查询所有:

1
2
3
4
5
6
7
8
9
GET http://localhost:9200/sku/_search
{
"query": {
"match_all": {}
}
}

# 也可以简写成
GET http://localhost:9200/sku/_search

匹配查询(match)

查询名称包含手机的记录代码示例:

1
2
3
4
5
6
7
8
GET http://localhost:9200/sku/_search
{
"query": {
"match": {
"name": "手机"
}
}
}

默认情况下,在查询时会先搜索关键字进行分词,对分词后的字符串进行查询,只要是包含这些字符串的都是要被查询出来的,多个词之间是or的关系。如查询“小米电视”,查询结果会有“小米手机”、“小米电视”、“三星电视”等包含了“小米”和“电视”的词汇。

由于查询结果的匹配分值_score是不一样的,分值高的排在前面。

若是想要精确查询,想要只查询包含“小米电视”的记录,语句该这样写:

1
2
3
4
5
6
7
8
9
10
11
GET http://localhost:9200/sku/_search
{
"query": {
"match": {
"name": {
"query": "小米电视",
"operator": "and"
}
}
}
}

控制精度

若用户给定5个查询词项,想查找只包含其中4个的文档时,将operator操作符参数设置成and只会将此文档排除。此时match查询的最小匹配参数minimum_should_match便起到重要作用了,利用该参数便可以指定必须匹配的词项数用来表示一个文档是否相关。可以将这一参数设置为某个具体数字,最常用的是无法控制用户搜索时输入的单词数量时,将其设置为一个百分数。

1
2
3
4
5
6
7
8
9
10
11
GET http://localhost:9200/sku/_search
{
"query": {
"match": {
"title": {
"query": "quick brown dog",
"minimum_should_match": "75%"
}
}
}
}

多字段查询(multi_match)

multi_matchmatch类似,不同的是它可以在多个字段中查询:

1
2
3
4
5
6
7
8
9
GET http://localhost:9200/sku/_search
{
"query": {
"multi_match": {
"query": "小米",
"fields": ["name","brandName","categoryName"]
}
}
}

查询字段的模糊匹配

字段名称可以用模糊匹配的方式给出:任何与模糊模式正则匹配的字段都会被包括在搜索条件中。如可以使用以下方式同时匹配book_title、chapter_title和section_title(书名、章名、节名)这三个字段:

1
2
3
4
5
6
{
"multi_match": {
"query": "Quick brown fox",
"fields": "*_title"
}
}

提升单个字段的权重

可以使用^字符语法为单个字段提升权重,在字段名称的末尾添加^boost,其中boost是一个浮点数:

1
2
3
4
5
6
{
"multi_match": {
"query": "Quick brown fox",
"fields": [ "*_title", "chapter_title^2" ]
}
}

此时chapter_title字段的boost值为2,而其他两个字段的默认boost值为1。

词条查询(term)

term查询被用于精确匹配,这些精确值可能是数字、时间、布尔或者是那些未分词的字符串。

1
2
3
4
5
6
7
8
GET http://localhost:9200/sku/_search
{
"query": {
"term": {
"price": 200000
}
}
}

多词条查询(terms)

terms查询和term查询一样,区别是它允许指定多个值进行匹配。如果这个词段包含了指定中的任何一个值,那么这个文档便视为满足条件(类似于SQL中的in):

1
2
3
4
5
6
7
8
GET http://localhost:9200/sku/_search
{
"query": {
"terms": {
"price": [200000,100000]
}
}
}

布尔组合(bool)

bool把各种其他查询通过must(与)、must_not(非)、should(或)的方式进行组合。

如查询名称包含手机的,且品牌为小米的记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET http://localhost:9200/sku/_search
{
"query": {
"bool": {
"must": [
{"match": {
"name": "手机"
}},
{
"term": {
"brandName": {
"value": "小米"
}
}
}
]
}
}
}

如查询名称包含手机的,或品牌为小米的记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET http://localhost:9200/sku/_search
{
"query": {
"bool": {
"should": [
{"match": {
"name": "手机"
}},
{
"term": {
"brandName": {
"value": "小米"
}
}
}
]
}
}
}

过滤查询

过滤是针对搜索结果进行过滤,过滤器主要判断的是文档是否匹配,不去计算和判断文档的匹配度得分,因此过滤器性能比查询要高,且方便缓存。因此推荐尽量使用过滤器去实现查询或者过滤器和查询共同使用。

过滤品牌为小米的记录如下:

1
2
3
4
5
6
7
8
9
10
11
12
GET http://localhost:9200/sku/_search
{
"query": {
"bool": {
"filter": [
{"match":{
"brandName":"小米"
}}
]
}
}
}

查询结果如下(注意_score为0):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"took": 3,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0.0,
"hits": [
{
"_index": "sku",
"_type": "doc",
"_id": "AYWp1L_wYrK1VsxqV5G5",
"_score": 0.0,
"_source": {
"name": "小米手机",
"price": 200000,
"spuId": 101,
"createTime": "2020-05-09",
"brandName": "小米",
"categoryName": "手机",
"saleNum": 10012,
"commentNum": 323,
"spec": {
"网络制式": "移动4g",
"屏幕尺寸": "4.5"
}
}
}
]
}
}

分组查询

按分组名称聚合查询,统计每个分组的数量,类似于SQL中的group by,示例如下:

1
2
3
4
5
6
7
8
9
10
11
GET http://localhost:9200/sku/_search
{
"size": 0,
"aggs": {
"sku_category": {
"terms": {
"field": "categoryName"
}
}
}
}

将size设为0时,不会将数据查询出来,目的是让查询更快。

除此之外还可以查询多个分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET http://localhost:9200/sku/_search
{
"size": 0,
"aggs": {
"sku_category": {
"terms": {
"field": "categoryName"
}
},
"sku_brand": {
"terms": {
"field": "brandName"
}
}
}
}

范围

Elasticsearch的范围查询可以用来查询处于某个范围内的文档,range查询可同时提供包含(inclusive)和不包含(exclusive)这两种范围表达式,范围的选项如下:

  • gt:大于
  • lt:小于
  • gte:大于或等于
  • lte:小于或等于

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET http://localhost:9200/sku/_search
{
"query" : {
"constant_score" : {
"filter" : {
"range" : {
"price" : {
"gte" : 20,
"lt" : 40
}
}
}
}
}
}

若想要范围是无界,如[20,+∞),只须省略其中一边(gte)的限制即可。

日期范围

range查询同样可以应用在日期字段上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET http://localhost:9200/sku/_search
{
"query" : {
"constant_score" : {
"filter" : {
"range" : {
"timestamp" : {
"gt" : "2014-01-01 00:00:00",
"lt" : "2014-01-07 00:00:00"
}
}
}
}
}
}

当使用它处理日期字段时,range查询支持对日期计算进行操作,如查找时间戳在过去一小时内的所有文档:

1
2
3
4
5
"range" : {
"timestamp" : {
"gt" : "now-1h"
}
}

日期计算还可以被应用到某个具体的时间,并非只能是一个像now这样的占位符。只要在某个日期后加上一个双管符号(||)并紧跟一个日期数学表达式就能做到,如设置早于2014年1月1日的日期都额外加1个月:

1
2
3
4
5
6
"range" : {
"timestamp" : {
"gt" : "2014-01-01 00:00:00",
"lt" : "2014-01-01 00:00:00||+1M"
}
}

存在查询(exists)

处理非Null值

设置各条测试数据的tags值:

1
2
3
4
5
6
7
8
9
10
11
POST http://localhost:9200/my_index/posts/_bulk
{ "index": { "_id": "1" }}
{ "tags" : ["search"] }
{ "index": { "_id": "2" }}
{ "tags" : ["search", "open_source"] }
{ "index": { "_id": "3" }}
{ "other_field" : "some data" }
{ "index": { "_id": "4" }}
{ "tags" : null }
{ "index": { "_id": "5" }}
{ "tags" : ["search", null] }

tags值顺序为:

1、tags 字段有 1 个值。 2、tags 字段有 2 个值。 3、tags 字段缺失。 4、tags 字段被置为 null 。 5、tags 字段有 1 个值和 1 个 null 。

现在要找到那些被设置过标签字段的文档,并不关心标签的具体内容,只要标签存在即可,使用exists查询如下:

1
2
3
4
5
6
7
8
9
10
GET http://localhost:9200/my_index/posts/_search
{
"query" : {
"constant_score" : {
"filter" : {
"exists" : { "field" : "tags" }
}
}
}
}

查询返回了3个文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 3,
"max_score": 1.0,
"hits": [
{
"_index": "my_index",
"_type": "posts",
"_id": "5",
"_score": 1.0,
"_source": {
"tags": [
"search",
null
]
}
},
{
"_index": "my_index",
"_type": "posts",
"_id": "2",
"_score": 1.0,
"_source": {
"tags": [
"search",
"open_source"
]
}
},
{
"_index": "my_index",
"_type": "posts",
"_id": "1",
"_score": 1.0,
"_source": {
"tags": [
"search"
]
}
}
]
}
}

尽管文档5的tags存在null值,但它仍会被命中返回。该文档之所以存在,是因为标签有实际值( search )可以被索引,所以 null 对过滤不会产生任何影响。

因此,只要tags字段存在项(term)的文档都会命中并作为结果返回。

处理Null值

exists查询字段为空的或没有这个字段时的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
GET http://localhost:9200/my_index/posts/_search
{
"query": {
"bool": {
"must_not": {
"exists": {
"field": "tags"
}
}
}
}
}

此时的查询结果便只有3和4两个文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1.0,
"hits": [
{
"_index": "my_index",
"_type": "posts",
"_id": "4",
"_score": 1.0,
"_source": {
"tags": null
}
},
{
"_index": "my_index",
"_type": "posts",
"_id": "3",
"_score": 1.0,
"_source": {
"other_field": "some data"
}
}
]
}
}

最佳字段(dis_max)

假设有个网站允许用户搜索博客的内容,以下面两篇博客内容文档为例:

1
2
3
4
5
6
7
8
9
10
11
PUT http://localhost:9200/my_index/my_type/1
{
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}

PUT http://localhost:9200/my_index/my_type/2
{
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}

用户输入词组 “Brown fox” 然后点击搜索按钮。事先,并不知道用户的搜索项是会在 title 还是在 body 字段中被找到,但是,用户很有可能是想搜索相关的词组。用肉眼判断,文档 2 的匹配度更高,因为它同时包括要查找的两个词。运行以下 bool 查询查看实际结果:

1
2
3
4
5
6
7
8
9
10
11
GET http://localhost:9200/my_index/my_type/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}

查询结果发现是文档 1 的评分更高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"took": 3,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 0.56977004,
"hits": [
{
"_index": "my_index",
"_type": "my_type",
"_id": "2",
"_score": 0.56977004,
"_source": {
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}
},
{
"_index": "my_index",
"_type": "my_type",
"_id": "1",
"_score": 0.53565365,
"_source": {
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}
}
]
}
}

bool的评分如下:

1、它会执行 should 语句中的两个查询。 2、加和两个查询的评分。 3、乘以匹配语句的总数。 4、除以所有语句总数(这里为:2)。

由于文档 1 的两个字段都包含 brown 这个词,所以两个 match 语句都能成功匹配并且有一个评分。文档 2 的 body 字段同时包含 brown 和 fox 这两个词,但 title 字段没有包含任何词。这样, body 查询结果中的高分,加上 title 查询中的 0 分,然后乘以二分之一,就得到比文档 1 更低的整体评分。

在这个例子中title 和 body 字段是相互竞争的关系,因此需要找到单个最佳匹配的字段。

若不是简单将每个字段的评分结果加在一起,而是将最佳匹配字段的评分作为查询的整体评分,则结果中同时包含 brown 和 fox 的单个字段比反复出现相同词语的多个不同字段有更高的相关度。

dis_max 查询

不使用 bool 查询时,可以使用dis_max即分离最大化查询,其指的是: 将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回 :

1
2
3
4
5
6
7
8
9
10
11
GET http://localhost:9200/my_index/my_type/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}

得到的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"took": 3,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 0.56977004,
"hits": [
{
"_index": "my_index",
"_type": "my_type",
"_id": "2",
"_score": 0.56977004,
"_source": {
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}
},
{
"_index": "my_index",
"_type": "my_type",
"_id": "1",
"_score": 0.2824934,
"_source": {
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}
}
]
}
}

最佳字段调优

当用户搜索 “quick pets” 时会发生什么呢?在前面的例子中,两个文档都包含词 quick ,但是只有文档 2 包含词 pets ,两个文档中都不具有同时包含 两个词 的 相同字段 。而一个简单的 dis_max 查询会采用单个最佳匹配字段,而忽略其他的匹配:

1
2
3
4
5
6
7
8
9
10
11
GET http://localhost:9200/my_index/my_type/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
]
}
}
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 0.28488502,
"hits": [
{
"_index": "my_index",
"_type": "my_type",
"_id": "2",
"_score": 0.28488502,
"_source": {
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}
},
{
"_index": "my_index",
"_type": "my_type",
"_id": "1",
"_score": 0.25316024,
"_source": {
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}
}
]
}
}

从结果中可以发现,两个评分是完全相同的。理想中同时匹配 title 和 body 字段的文档比只与一个字段匹配的文档的相关度理应更高,但事实并非如此,这是因为dis_max 查询只会简单地使用单个最佳匹配语句的评分_score作为整体评分。

tie_breaker参数

可以通过指定 tie_breaker 这个参数将其他匹配语句的评分也考虑其中:

1
2
3
4
5
6
7
8
9
10
11
12
GET http://localhost:9200/my_index/my_type/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
],
"tie_breaker": 0.3
}
}
}

结果:文档 2 的相关度比文档 1 略高。

tie_breaker 参数提供了一种 dis_max 和 bool 之间的折中选择,它的评分方式如下:

  • 获得最佳匹配语句的评分 _score 。
  • 将其他匹配语句的评分结果与 tie_breaker 相乘。
  • 对以上评分求和并规范化。

tie_breaker可以是 0 到 1 之间的浮点数,其中0代表使用dis_max最佳匹配语句的普通逻辑,1表示所有匹配语句同等重要。最佳的精确值需要根据数据与查询调试得出,但是合理值应该与零接近(处于0.1-0.4之间),这样就不会颠覆dis_max最佳匹配性质的根本。

IK分词器

分词是全文搜索引擎必用的技术,由于Elasticsearch原生的分词器不支持中文,因此需要安装一个中文分词器,这里用的分词器是IK分词器。

安装

访问IK分词器下载与Elasticsearch对应版本的中文分词器,这里下载的版本为5.5.3,具体地址为Release v5.5.3 · medcl/elasticsearch-analysis-ik (github.com)。将解压后的文件夹放入Elasticsearch根目录里的E:\elasticsearch-5.5.3\plugins\ik下,重启Elasticsearch后便可使用

配置

IK分词器提供了两个分词算法ik_smartik_max_word。其中ik_smart为智能最少切分,ik_max_word为最细粒度划分,

  • ik_max_word:会将文本做最细粒度划分,如「我是程序员」会被拆分为「我、是、程序员、程序、员」
  • ik_smart:会将文本做最少切分,如「我是程序员」会被拆分为「我、是、程序员」

中文分词

  • 不使用ik分词器时的效果:

    • 通过postman发送GET请求查询分词:

    1
    2
    3
    4
    GET http://localhost:9200/_analyze
    {
    "text":"我是程序员"
    }

    postman操作如图所示:

    得到的结果如下,可以发现Elasticsearch的默认分词器无法识别中文:我、程序、程序员这样的词汇,而是简单地将每个字拆完分为一个词:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    {
    "tokens": [
    {
    "token": "我",
    "start_offset": 0,
    "end_offset": 1,
    "type": "<IDEOGRAPHIC>",
    "position": 0
    },
    {
    "token": "是",
    "start_offset": 1,
    "end_offset": 2,
    "type": "<IDEOGRAPHIC>",
    "position": 1
    },
    {
    "token": "程",
    "start_offset": 2,
    "end_offset": 3,
    "type": "<IDEOGRAPHIC>",
    "position": 2
    },
    {
    "token": "序",
    "start_offset": 3,
    "end_offset": 4,
    "type": "<IDEOGRAPHIC>",
    "position": 3
    },
    {
    "token": "员",
    "start_offset": 4,
    "end_offset": 5,
    "type": "<IDEOGRAPHIC>",
    "position": 4
    }
    ]
    }

  • 使用ik分词器后的效果:

    • 通过postman发送GET请求查询分词将文本做最少切分:

      1
      2
      3
      4
      5
      GET http://localhost:9200/_analyze
      {
      "analyzer":"ik_smart",
      "text":"我是程序员"
      }

      得到的结果如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      {
      "tokens": [
      {
      "token": "我",
      "start_offset": 0,
      "end_offset": 1,
      "type": "CN_CHAR",
      "position": 0
      },
      {
      "token": "是",
      "start_offset": 1,
      "end_offset": 2,
      "type": "CN_CHAR",
      "position": 1
      },
      {
      "token": "程序员",
      "start_offset": 2,
      "end_offset": 5,
      "type": "CN_WORD",
      "position": 2
      }
      ]
      }

    • 通过postman发送GET请求查询分词将文本做最细粒度划分:

      1
      2
      3
      4
      5
      GET http://localhost:9200/_analyze
      {
      "analyzer":"ik_max_word",
      "text":"我是程序员"
      }

      得到的结果如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      {
      "tokens": [
      {
      "token": "我",
      "start_offset": 0,
      "end_offset": 1,
      "type": "CN_CHAR",
      "position": 0
      },
      {
      "token": "是",
      "start_offset": 1,
      "end_offset": 2,
      "type": "CN_CHAR",
      "position": 1
      },
      {
      "token": "程序员",
      "start_offset": 2,
      "end_offset": 5,
      "type": "CN_WORD",
      "position": 2
      },
      {
      "token": "程序",
      "start_offset": 2,
      "end_offset": 4,
      "type": "CN_WORD",
      "position": 3
      },
      {
      "token": "员",
      "start_offset": 4,
      "end_offset": 5,
      "type": "CN_CHAR",
      "position": 4
      }
      ]
      }

自定义词库

随着各种新词的不断出现,分词器并不认识一些网络热词。若要补充新词到ik的词库中,则需要进入目录E:\elasticsearch-5.5.3\plugins\ik\config中创建custom.dic文件,写入新词。同时打开IKAnalyzer.cfg.xml文件,将添加custom.dic配置后,重启Elasticsearch。

配置文件参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">custom.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

在进行以上配置后,分词器在看到文件中的新词时,便会知道这是一个词汇。

  • 未设置自定义词库时的效果如下:

    • 通过postman发送GET请求查询分词:

      1
      2
      3
      4
      5
      GET http://localhost:9200/_analyze
      {
      "analyzer":"ik_smart",
      "text":"阿里嘎多美羊羊桑"
      }

      得到的结果如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      {
      "tokens": [
      {
      "token": "阿里",
      "start_offset": 0,
      "end_offset": 2,
      "type": "CN_WORD",
      "position": 0
      },
      {
      "token": "嘎",
      "start_offset": 2,
      "end_offset": 3,
      "type": "CN_CHAR",
      "position": 1
      },
      {
      "token": "多美",
      "start_offset": 3,
      "end_offset": 5,
      "type": "CN_WORD",
      "position": 2
      },
      {
      "token": "羊",
      "start_offset": 5,
      "end_offset": 6,
      "type": "CN_CHAR",
      "position": 3
      },
      {
      "token": "羊",
      "start_offset": 6,
      "end_offset": 7,
      "type": "CN_CHAR",
      "position": 4
      },
      {
      "token": "桑",
      "start_offset": 7,
      "end_offset": 8,
      "type": "CN_CHAR",
      "position": 5
      }
      ]
      }

  • 设置自定义词库后的效果如下:

    • 通过postman发送GET请求查询分词:

      1
      2
      3
      4
      5
      GET http://localhost:9200/_analyze
      {
      "analyzer":"ik_smart",
      "text":"阿里嘎多美羊羊桑"
      }

      得到的结果如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      {
      "tokens": [
      {
      "token": "阿里嘎多",
      "start_offset": 0,
      "end_offset": 4,
      "type": "CN_WORD",
      "position": 0
      },
      {
      "token": "美羊羊",
      "start_offset": 4,
      "end_offset": 7,
      "type": "CN_WORD",
      "position": 1
      },
      {
      "token": "桑",
      "start_offset": 7,
      "end_offset": 8,
      "type": "CN_CHAR",
      "position": 2
      }
      ]
      }

CATALOG
  1. 1. Elasticsearch简介
  2. 2. Windows系统下Elasticsearch的安装和配置
    1. 2.1. 安装Elasticsearch服务端
      1. 2.1.1. 相关配置
    2. 2.2. 安装Elasticsearch客户端
      1. 2.2.0.1. 一个简单的示例
  3. 2.3. 安装图形化插件
  • 3. Python环境下操作Elasticsearch
    1. 3.1. 基础操作
      1. 3.1.1. 创建索引
      2. 3.1.2. 删除索引
      3. 3.1.3. 插入数据
      4. 3.1.4. 删除数据
      5. 3.1.5. 更新数据
      6. 3.1.6. 查询操作
    2. 3.2. 查询函数
      1. 3.2.1. 基础查询
      2. 3.2.2. 过滤字段查询
      3. 3.2.3. 切片查询
      4. 3.2.4. 模糊查询(分词)
      5. 3.2.5. 模糊查询(不分词)
      6. 3.2.6. 精准单值查询
      7. 3.2.7. 多字段查询
      8. 3.2.8. 前缀查询
      9. 3.2.9. 通配符查询
      10. 3.2.10. 正则查询
      11. 3.2.11. 多条件查询
      12. 3.2.12. 存在字段查询
      13. 3.2.13. 范围查询
      14. 3.2.14. Json字段查询
      15. 3.2.15. 排序
  • 4. 通过网络请求操作ElasticSearch
    1. 4.1. 创建索引与映射字段
      1. 4.1.1. 查看索引字段类型
    2. 4.2. 文档增加与修改
      1. 4.2.1. 增加文档自动生成ID
      2. 4.2.2. 新增文档指定ID
      3. 4.2.3. 修改索引文档
      4. 4.2.4. 通过ID删除索引文档
    3. 4.3. 索引查询
      1. 4.3.1. 查询所有数据(match_all)
      2. 4.3.2. 匹配查询(match)
      3. 4.3.3. 控制精度
      4. 4.3.4. 多字段查询(multi_match)
        1. 4.3.4.1. 查询字段的模糊匹配
        2. 4.3.4.2. 提升单个字段的权重
      5. 4.3.5. 词条查询(term)
      6. 4.3.6. 多词条查询(terms)
      7. 4.3.7. 布尔组合(bool)
      8. 4.3.8. 过滤查询
      9. 4.3.9. 分组查询
      10. 4.3.10. 范围
        1. 4.3.10.1. 日期范围
      11. 4.3.11. 存在查询(exists)
        1. 4.3.11.1. 处理非Null值
        2. 4.3.11.2. 处理Null值
      12. 4.3.12. 最佳字段(dis_max)
        1. 4.3.12.1. dis_max 查询
      13. 4.3.13. 最佳字段调优
        1. 4.3.13.1. tie_breaker参数
  • 5. IK分词器
    1. 5.1. 安装
    2. 5.2. 配置
    3. 5.3. 中文分词
    4. 5.4. 自定义词库