elasticsearch 学习总结
elasticsearch
Elasticsearch是用Java语言开发的基于Lucene的搜索服务器,它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。
1、es index 字段类型
1.1、字符串
- text :于全文索引,搜索时会自动使用分词器进行分词再匹配
- keyword : 不分词,搜索时需要匹配完整的值
1.2、数值型
- 整型: byte,short,integer,long
- 浮点型: float, half_float, scaled_float,double
1.3、日期型
- date
虽然字段类型为date,但json没有date类型。有三种表示的形式:
- 将日期时间格式化后的字符串,如 "202101-01" 或者 "2021/01/01 10:10:10"。
- long 型的整数,意义是 milliseconds-since-the-epoch,是自 1970-01-01 00:00:00 UTC 以来经过的毫秒数。
- int 型的整数,意义是 seconds-since-the-epoch, 是指自 1970-01-01 00:00:00 UTC 以来经过的秒数。
它是可以指定format为的,例如:
"create_time": {
"type": "date",
"format": "yyy-MM-dd HH:mm:ss",
}
format 默认"strict_date_optional_time||epoch_millis"。
- strict_date_optional_time:只要是 ISO datetime parser 可以正常解析的都是 strict_date_optional_time,如链接。
- epoch_millis:UNIX 纪元时间,以毫秒为单位。
这种情况下可以解析下面三种日期格式:
- "2021-05-12"
- "2021-05-12T12:10:30Z"
- 1121231241568
不过我用得不多,我只试过用"yyy-MM-dd HH:mm:ss"的时候,搜索的时候,搜索条件如果是不匹配的格式,会直接报错,不记得用默认格式有没有报错了,好像也有些问题,所以后来我直接用了keyword。
1.4、其他
- ip:IPV4的地址.
- boolean:布尔值如true/false。5.4以前貌似可以接受0/1这些数字和字母。但5.4之后只接受true/false以及字符串的"true"/
"false"。- binary:二进制, 会把值当做经过 base64 编码的字符串,默认不存储,且不可搜索。值不能嵌入换行符\n。
其他还有些对象啊、数组啊、范围啊、地址位置之类的,参考以下链接吧:
1、https://segmentfault.com/a/1190000022722763
2、https://www.cnblogs.com/chy18883701161/p/12723658.html
3、官网:https://www.elastic.co/guide/en/elasticsearch/reference/7.6/mapping-types.html#mapping-types
2、es 创建index
假设我们要一张index存文件的元数据,示例如下:
body = {
"mappings": {
"_doc": { # doc_type
"properties": { # 元数据的field及field type
"create_time": {
"type": "keyword",
},
"file_size": {
"type": "keyword",
},
"file_name": {
"type": "keyword",
},
}
},
}
}
es.indices.create(index=index_name, body=body)
首先,index和doc_type可以理解为一般数据库的数据库和数据表,但也不是那么一样。一个index一般我们只有一个doc_type,所以网上很多示例一般常用_doc作为表名,它是什么其实都无所谓,真正重要的是index。这点其实跟SQLite有点像,一般一个.sqlite也是只有一个main表。
说明:
1、做搜索其实有的时候不建议存格式是"yyy-MM-dd
HH:mm:ss"的date类型,搜索的时候除非整个格式完全匹配才能搜索,否则不仅搜不出来,还会报错。对于需要在所有类型搜索的话,keyword就够了。
2、indices是针对index级别的增删改查的类,比如容易混淆的是,有一个es.index()
,这个其实主要是类似insert的操作。
3、增加字段类型es.indices.put_mapping
,说是说创建或更新数据doc字段结构,但其实一般只能新增doc_type的field,而且因为涉及到元数据的修改,只能是master节点来执行。
4、关于数字类型,可以看到file_size是文件大小,其实原本应该是数字类型,但是搜索时传入其他字符串类型文字会报错,有两种解决办法:一种就是改为字符串类型,一种就是在查询的时候传入lenient
参数为true,就会忽略这个错。
3、es.bulk批量写入数据
在python中,es批量插入数据,有两种方式,一种是es的bulk,一种是helpers.bulk,之前搜了很久,全都是后者。
但一来是为了封装,二来也是方便,他们有一个共同点,就是都要自己指定id,之前原本我搜到一些文档说es自带的可以不用,但是我测试的时候没有成功,必须要有,我是用python的uuid库生成的,然后去掉-分隔符。
helper.bulk传入的body格式:
[{
"_index": "index_name", # 数据库名称
"_type": "doc_type", # 数据表名
"_id":"1233", # ID
"_source":{ # 要写入的数据
"field1": value1, # 字段及值
}
}]
es.bulk传入的body格式:
[{
'index': { # 数据库信息
'_id': id, # 必传
'_index': 'index_name', #_index和_type都不传时,需要在bulk函数中指定参数
'_type': 'doc_type'
}
},
{
"field1": value1, # 对应_source部分,是要写入的数据,不需要id
},
…… # 重复上面的步骤,一个index,一个data
]
没错,你没看错,一条数据,占了两个dict,而且多条数据也是根据顺序来确定哪个数据是哪个表的,一开始我其实并不太相信怎么是这样的,也觉得很不方便。但是后来发现index里可以省略_index和_type,也就是说,当这一整个都是写入某一张表的时候, 'index'里有id就可以,例如:
>>> body = [{
'index': { '_id': id}
},
{
"field1": value1,
},
]
# refresh使文档在索引操作后立即可供搜索
>>> es.bulk(index=index_name, doc_type=doc_type, body=body, refresh=True)
4、@query_param 装饰器
研究到这个装饰器,是因为我想要封装es类,不封装其实也可满足需要,只是想每次调用都在初始化时先连接上es。创建、删除index、插入数据什么的其实不太需要额外的param,但search的时候就很需要。
一开始,我觉得很好奇,为什么源码里,每个方法最后只有一个params,但我们调用的时候,却可以无限传参,而且不是以
**kwargs
的方式,而且如果我在封装时照着它的格式来写却无法实现后面的参数被传入。后来我就发现每个方法都有一个@query_param装饰器,且这个装饰器里传入的参数,正好是前面我刚刚说的,不是以**kwargs方式传入,也没有指定参数,却可以被接收的参数。
比如,我们用search方法做例子,分页需要用到size:
@query_param(
……
"size"
……
)
def search(self, index=None, doc_type=None, body=None, params=None):
if "from_" in params:
params["from"] = params.pop("from_")
……
然后看@query_param的源码:
def query_params(*es_query_params):
"""
Decorator that pops all accepted parameters from method's kwargs and puts
them in the params argument.
"""
def _wrapper(func):
@wraps(func)
def _wrapped(*args, **kwargs):
params = {}
if "params" in kwargs:
params = kwargs.pop("params").copy()
for p in es_query_params + GLOBAL_PARAMS:
if p in kwargs:
v = kwargs.pop(p)
if v is not None:
params[p] = _escape(v)
# don't treat ignore and request_timeout as other params to avoid escaping
for p in ("ignore", "request_timeout"):
if p in kwargs:
params[p] = kwargs.pop(p)
return func(*args, params=params, **kwargs)
return _wrapped
return _wrapper
其实就是将装饰器里传入的参数加入params里,并过滤掉重复的。很明显,它就是我们可以追加参数的原因了。
所以我们封装的时候,也要加这个装饰器,也可以选择性的加自己需要的:
from elasticsearch import Elasticsearch
from elasticsearch.client.utils import query_params
class ESUtil(object):
def __init__(self, hosts=None, port=None):
"""
有很多种方式可以连接
:param hosts:eg: [{"host": "ip1", "port": 9200}] or ["ip1:9200"] or ["ip1"] or 'ip1'
:param port:当hosts为字符串时,可以通过port指定port
"""
self.hosts = hosts
self.port = port
self.es_client = Elasticsearch(hosts=self.hosts, port=self.port)
# 这里示范假设我们只需要三种参数
@query_params(
"from_",
"size",
"sort",
)
def by_search(self, index_name=None, doc_type=None, body=None, params=None):
"""根据条件查询数据"""
return self.es_client.search(index=index_name, doc_type=doc_type, body=body, params=params)
那我们连接使用的时候,假设我们要实现分页查询:
es.search_data(
index_name="index_name", # index名,可以理解为数据库
doc_type="_doc", # doc,可以理解为数据表,但是一个index应该只允许有一个doc,所以叫什么无所谓,但是我们一般默认取_doc
body=body, # 查询条件
params={"from": page_num * page_size}, # from是指从第几条数据开始展示,page_num是指获取第几页
size=page_size # 一页展示多少条数据
)
page_num
和 page_size
是自己传的参数值:
page_size
:一页展示多少条数据page_num
:获取第几页
from是指从第几条数据开始展示,比如总共200条数据,设置size为100,即一页展示100条,如果from为0,那第一页的数据就是0-99,那第二页就是100-200,所以一般page_num获取第几页,那from就是page_num * page_size。
根据search方法的源码和 @query_params的参数可知,from
可以写作from_
,它会将它自动转化成from
。也可以不写在params里,像size一样加在后面,但这种方式要写from_
。
注意事项:
1、size的大小不能超过index.max_result_window这个参数的设置,默认为10,000。如果大于这个数,需要在create
index时指定参数"max_result_window"。2、from + size这种方式在深度分页时会有性能问题,数据越大,往后搜索性能越低,尽量控制查询数据在10000-50000条数据。但是目前我觉得在需要分页这件事上,似乎没有更好的办法,scroll和search_after虽然适合深分页,但是他们都需要根据上一次获取的数据的scroll_id或最后一条数据来获取下一页的数据,他们不适合跳到指定第几页。
3、scroll 更适合数据导出或后台,不适合用于实时的请求,因为每一个 scroll_id
不仅会占用大量的资源,而且会生成历史快照,对于数据的变更不会反映到快照上。search_after倒是可以实时同步查询中的增删改查啥的,它需要使用一个唯一值的字段作为排序字段,比如_id。
5、ES HTTP 查询数据
Elasticsearch 提供强大且全面的 REST API 集合,这些 API 可用来执行各种任务,例如检查集群的运行状况、针对索引执行
CRUD(创建、读取、更新、删除)和搜索操作,以及执行诸如筛选和聚合等高级搜索操作。因此,我们通过HTTP请求,使用curl,或者在浏览器就可以进行查询。浏览器主要是GET请求,能做一些简单的查询,如果想要写数据或者携带一些其他参数,可能还是要借助curl,postman,或者直接用python这样使用封装好的方法。
5.1 查看当前所有index
http://ip:9200/_cat/indices?v
5.2 search查询
# pretty表格式化,不然就是缩在一起,index是你的index名,不需要大括号,默认一页十条数据
http://ip:9200/{index}/_doc/_search?pretty
# 需要翻页,from表从第几条开始查询,size表每页多少条。from默认为0,size默认为10
http://ip:9200/{index}/_doc/_search?pretty&from=0&size=100
# 根据条件查询,关键词q,查询file_type值为docx的值,精确匹配,相当于term,=后是字段类型,可以是*号,:是条件
http:///ip:9200/{index}/_doc/_search?q=file_type:docx&pretty
# 所有结果按照_id升序排序
http:///ip:9200/{index}/_doc/_search?q=*&sort=_id:asc&pretty
5.3 source查询
# 根据_id查询某条记录,如果_source没有指定某个字段则返回这条记录所有信息,如果指定则只返回这条信息
# 这种source的方法只能根据id查询
http://ip:9200/{index}/_doc/{_id}?_source{=file_type}&pretty
# 查询id为154fd92abd41496d8025d0417fb0cc6d的file_type字段值
>>>http://ip:9200/{index}/_doc/154fd92abd41496d8025d0417fb0cc6d?_source=file_type&pretty
{
"_index" : "es-metadata",
"_type" : "_doc",
"_id" : "154fd92abd41496d8025d0417fb0cc6d",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"file_type" : "docx"
}
}
# 查询id为154fd92abd41496d8025d0417fb0cc6d的所有信息
>>>http://ip:9200/{index}/_doc/154fd92abd41496d8025d0417fb0cc6d?_source&pretty
{
"_index" : "es-meta",
"_type" : "_doc",
"_id" : "154fd92abd41496d8025d0417fb0cc6d",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"create_time" : "2021-05-19 11:30:22",
"file_size" : "21940919",
"file_type" : "docx",
"file_path" : "/tmp/a/test1.docx",
"file_name" : "test1.docx"
}
}
6、ES Python 查询
es中的查询请求有两种方式,一种是简易版的查询,另外一种是使用JSON完整的请求体,叫做结构化查询(DSL)。其实推荐后者,可读性,修改的灵活性都很好,有点像ORM那样。
6.1简单查询
网上有非常多的示例,这里大概归类一些常用的关键词及链接,不多做演示。
- term/terms:精确/完全匹配,大概相当于select * from table where xx='' or yy='',前者只有一个条件;
- match: 模糊匹配,是一种 bool 类型的查询。
- wildcard:通配符查询,就是利用*号匹配,是我最喜欢用的一种。
- prefix:前缀查询.
- range:范围查询,类似between …and…
term和match的区别:
当查询的词只有一个时没有区别。当有多个词时,受分词影响较大。如果开启了分词,则无法匹配到原来的词,但是没开启则能匹配。如"a
b",分词了就会变成有两个词"a"和"b",那"a b"这个词就无法匹配了。而match,前面说过,match是一种 bool类型的查询。也就是说,默认其实是有一个隐藏的操作符or的,当有多个词的时候,匹配到任意一个就算匹配到了。match还有其他几种方法,由于我主要用的通配符,没太涉及,这里不详细说明。
参考链接:
1、https://www.jb51.net/article/156935.htm
2、https://www.cnblogs.com/yjf512/p/4897294.html
3、https://segmentfault.com/a/1190000017110948
6.2 复合查询
三个关键词:must,should,must_not。这三个可以相互穿插着使用。
- must:文档必须完全匹配条件
- should:至少满足一个条件,这个文档就符合
- must_not:文档必须不匹配条件
这个理解起来其实很好理解,但是写起来嘛就长的,所以才推荐DSL。主要需要注意的事情是,如果是基于某个条件又有复合查询的,需要嵌套在条件里面,而不是并排。
6.3 ES Python DSL查询
这个需要另外一个第三方库,拿官方示例说明,Search需要一个ES实例,一个index名,由于一般只允许有一个doc,所以也不需要指定。其他的查询方法的第一个参数是查询的关键词,第二个是字段名和字段值。
from elasticsearch import Elasticsearch
from elasticsearch_dsl import Search
client = Elasticsearch()
s = Search(using=client, index="index_name") \
.filter("term", category="search") \
.query("match", title="python") \
.exclude("match", description="beta")
参考链接
1、curl:https://blog.csdn.net/ty4315/article/details/52264198
2、https://www.yht7.com/news/108484
3、https://www.bbsmax.com/A/VGzlyNaNJb/
4、bulk:https://blog.csdn.net/Areigninhell/article/details/85095825
5、date:https://www.jianshu.com/p/a44f6523912b
6、DSL:https://elasticsearch-dsl.readthedocs.io/en/latest/
本文来自博客园,作者:苏酒酒,转载请注明原文链接:https://www.cnblogs.com/sujiujiu/p/15370005.html