在学习Elasticsearch的过程中想找一些可以系统的描述es操作的文章,但是官网没有中文页面,ES中文指南的排版和翻译又很突兀和不协调,因此决定自己看一遍官方的maunal总结一下,由于没时间把所有章节全部翻一遍,所以写一篇学习笔记以便完成初步的学习。
概念总览:
在描述ES的基本操作之前,首先来介绍几个概念:
Relational DB -> Databases -> Tables -> Rows -> Columns Elasticsearch -> Indices -> Types -> Documents -> Fields
以上是早期的官方文档贴出的一个概念介绍图,其含义不用多说,其实ES更适合与MongoDB类比:
MongoDB -> DBs -> Collections -> Documents -> Fields Elasticsearch -> Indices -> Types -> Documents -> Fields
ES里的Index可以看做一个库,Documents相当于表的行,而Types相当于表。
但是Types的概念将会被逐渐弱化并可能在未来版本中删除,而在Elasticsearch 6中,一个index下已经只能包含一个type了,因此可以将index理解为一个表,types意如其名仅用于展示一个document所属的分类,实际上在本文对ES进行操作时由于index和type的一对一关系,许多时候查询document已经只需要指定index而无需再指定type了。
本文使用Elasticsearch 6.5.4和Kibana 6.5.4下的环境进行演示。
一、Kibana命令行操作
使用Kibana操作ES是当前最简单的一种方式,且提供命令补全、index名称补全等便捷的功能。同时console界面的小扳手点进去还有和官方手册里一样的“copy as CURL”选择,将选中的命令copy之后粘贴到linux中就会转换为curl命令的格式,对于想要了解curl直接操作ES的同学是很有帮助的。
我个人并不建议直接使用curl操作ES,因为很多时候需要自己设置header,麻烦且低效。
Elasticsearch官方操作手册地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
这里参考官网的reference手册对内置API进行详细梳理,由于官方手册的介绍方式不适用于我这种新手,我只能打乱顺序学习,本部分的介绍基本遵循学习传统数据库的流程,主要分为以下7个部分:
Note:本文所有命令都是在Kibana console操作的,关于Kibana的安装配置和使用,参考《Kibana安装配置》一文。
1.数据结构搭建
结构的搭建主要包含index的创建和删除、查询等等,types无需创建。
#创建名为test的index,两种写法等同,名字不能包含特殊字符,只能小写,不能以-, _, +开头,不能超过255字节。 PUT test PUT /test --PUT /test的本质是PUT http://ip:9200/test,kibana做了优化因此写不写之前的/无所谓 #当然你还可以直接插入一条数据,index会自动被创建 PUT leo/dramas/1 { "name":"权力的游戏" } #查看创建好的index的详细信息 GET leo #删除index DELETE leo #查询当前所有的index,这里调用了_cat的API GET _cat/indices
上图为我测试创建的多个indices,每列的列名分别是:
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size --其中pri表示number of shards,rep表示number of replicas,新建的index health为yellow的原因是我只有一台服务器因此未能创建replica。
在使用GET <index_name>查看index详细信息时可以看到,每个index下都有一个名为mapping的属性,这个属性用于描述当前type下的大致field有哪些,当然也别忘了在6.5.4版本里一个index下只有一种type了。
2.增
即向ES插入数据:
#插入单条数据,用PUT或POST都可以 PUT test/books/1 {"name":"《阿Q正传》","price":100} PUT test/books/2 {"name":"《钢铁是怎样炼成的》","price":200} PUT test/books/3 {"name":"《西游记》","price":300}
插入多条数据,目前只能用_bulk API来实现,index表示新插入数据,create同理,在python的index()方法中op_type=create表示如果index不存在那么直接创建index并插入数据,而op_type=index表示向已存在的index中插数据,此外还可以一起bulk delete、update等操作。
PUT test/books/_bulk { "index":{"_id":4}} {"name":"《围城》","price":101} { "index":{"_id":5}} {"name":"《格林童话》","price":108} } #如果你不想设置主键_id,那么可以直接置空,系统会创建默认主键,写法如下: PUT test/books/_bulk { "index":{}} {"name":"《围城》","price":101} { "index":{}} {"name":"《格林童话》","price":108}
注意插入数据时如果指定的_id已经存在,那么新插入的数据会直接替换原ID的数据。
查看下插入的数据:
GET test/books/_search {"query":{"match_all":{}}} GET test/books/_search {"query":{"match":{"_id":1}}} GET test/_search {"query": {"range": {"price": {"lte":1000} } } }
index下也有_search API因此这里你也可以省略books直接查询整个index所有types下的记录,实际上在6版本中由于types概念的弱化(一个index只能有一种type)许多查询都可以直接不写type名了。
这里的query和range以及lte都是DSL关键字,其实query只相当于模糊查询或全文搜索。关于查询,更系统的DSL(domain specific language)关键字及示例会在第5部分“查”补充。
3.删
记录的删除通常由2个API,直接DELETE和POST _delete_by_query完成,示例如下:
#DELETE只能根据ID进行删除,本例中删除的是系统自定义的ID因此比较奇怪。 DELETE test/books/_mbEdGgBH8b_BYBmOW-C #_delete_by_query API允许你删除符合query条件的记录,其query body与上边的查询过滤的query body规则一样。 POST test/_delete_by_query {"query": {"range": {"price": {"lte":1000} } } } #其实删除、修改和查询还涉及到多版本控制的概念,这个概念在传统数据库中已经很熟悉了,就是为了保证数据一致性的。 #关于版本控制的内容会在第6部分“版本控制”补充。
4.改
记录更新也是2个API,_update和_update_by_query,前者根据ID进行更新,后者可以更新指定的query结果。此外你还可以不使用这两个API直接像新插入数据那样更新数据,只是此时你的body部分必须包含所有的fields了,否则操作完毕后你会发现document只剩下你所更新的那几个fields,其他的全没了。
至于为什么删除使用DELETE命令,而更新只能用_update的API,只是因为ES是RESTFUL风格的,http的指令有DELETE但并没有UPDATE关键字。
更新涉及到版本控制以便维护数据一致性,其实分为两个操作:get和reindex,大致步骤是:首先取到相应的document,然后执行更新script,最后返回执行的结果。至于具体的多版本控制机制将在第6部分解释。
更新涉及的DSL语言也与其他操作很不一样:
#_update API,表示将id为5的document的price改为100 POST test/books/5/_update {"script": {"source":"ctx._source.price=params.price", "lang":"painless", "params":{ "price":100 } }
这里的script,source,lang,params都是DSL关键字,lang=painless表示使用painless脚本语言来编写script来完成。
ctx我暂理解为当前事务,ctx._source表示当前定位的document,params表示本次更新用到的数据,source则表示更新操作,通俗来讲就是用params的数据+source的操作一起完成更新。
#如果只是简单的增加新field和删除field那么格式就比较简单: POST test/books/5/_update { "script":"ctx._source.booktype='少儿童话'" } POST test/books/5/_update { "script":"ctx._source.remove('少儿童话')" } #此外ctx._source或ctx._source.<field_name>还有很多其他的方法和属性,这里贴一个官网的示例来作出引申,更多的示例慢慢实践吧。 POST test/_doc/1/_update { "script" : { "source": "if (ctx._source.tags.contains(params.tag)) { ctx.op = 'delete' } else { ctx.op = 'none' }", "lang": "painless", "params" : { "tag" : "green" } } }
这个示例的含义就是:对于id=1的document,如果tags包含green字符,那么删掉这个document,否则不操作。至于contains是模糊匹配还是精确匹配,有兴趣的可以花几十秒做个测试。
5.查
前4个部分的示例中已经有许多查询的示例了,这里在之前的基础上介绍一些比较复杂的查询,首先来了解一个DSL的概念:
DSL:Domain Specific Language,ES提供一种基于JSON的查询语言,这种查询语言包含两种子句模式:
1.Leaf query clauses
2.Compound query clauses --常用的就是bool组合查询
好吧,其实这里介绍这两个概念对理解复杂查询毫无作用,我只是照搬下官方手册,防止某天顿悟时找不到概念,接下来再看两个DSL的概念:
Query一般来说包含两各部分:query context 或 filter context:
举例来说:
GET /_search { "query": { "bool": { "must": [ { "match": { "title": "Search" }}, { "match": { "content": "Elasticsearch" }} ], "filter": [ { "term": { "status": "published" }}, { "range": { "publish_date": { "gte": "2015-01-01" }}} ] } } }
这个例子的query就包含了所有2种context,并使用了bool组合查询,可以看到bool是最外围的关键字,must与filter并行。
bool组合查询的子关键字主要包含must,must_not,should,分别对应AND、NOT、OR三种逻辑运算,此外还有一个filter子关键字。
--filter与must:match的区别:
参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
1.must:match会为匹配到的每个记录打分,称作scoring,表示匹配程度,查询的结果按打分进行排序。
2.filter与must:match基本一致,唯一的区别是其结果不参与打分,相当于一个再过滤。
到这里DSL的4个概念就介绍完了,是的全部介绍完了。官网总共也就这几行,更多关于关键字的具体应用需要到特定的页面且也通常都是一个简单的示例完事,因此只能靠日常实践了。
介绍完DSL那么回到实际应用中来,用于查询的API一般也是2种:直接通过GET index/doc_type/doc_id获取,以及_search API
#GET获取比较简单,只要有id就可以了,没id请使用_search API GET test/books/1 #_search API是查询使用的核心API,包含诸如聚合、排序、集群查询、explain API等等等等,这里只贴个官方链接和一个示例算啦,重在实践掌握。 https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html POST /twitter/_search?routing=kimchy { "query": { "bool" : { "must" : { "query_string" : { "query" : "some query string here" } }, "filter" : { "term" : { "user" : "kimchy" } } } } } #这里的?routing=kimchy是指在集群中查询时可以指定名为kimchy的shard。
6.版本控制
Versionning,在官网中暂未找到独立的说明页面,只找到2篇古老的博客,分别是2011年和2013年的,地址如下:
https://www.elastic.co/blog/versioning
https://www.elastic.co/blog/elasticsearch-versioning-support
第一篇:
内容显示versioning是由elasticsearch在0.15版本引入的新特性”乐观并发控制“引申出来的,只介绍了每个document都会有个由系统控制自增的_version属性,并未对版本控制机制作出细节解释。
不过既然是乐观并发控制我们可以参考传统RDBMS数据库中的乐观锁来理解,即数据库服务器会自动进行document快照存储以便实现事务一致性,接下来看下第二篇博客(实际上看完第二篇博客,里边也确实介绍了乐观锁定)。
第二篇:
以一个经典的丢失更新示例来描述下乐观并发控制的必要性:
#首先造一条数据 PUT bank_account/shanghai/1 { "name":"leo", "deposit":100 } GET bank_account/shanghai/1 #如下为插入的数据,可以看到_version属性值为1 { "_index" : "bank_account", "_type" : "shanghai", "_id" : "1", "_version" : 1, "found" : true, "_source" : { "name" : "leo", "deposit" : 100 } } #如果这时候两个商户同时要从我账户里扣1块钱,结果就是两家同时取到我账户余额为100,各扣了一元并把99的余额写入ES,这显然是错的。因此ES推出了versioning特性。
对于index中的每条记录都会有一个_version的属性,其取值范围为:[1,2^63),插入数据时默认的_version都是1,每次对这个document进行修改或删除操作都会使其+1,这个过程是由ES自己控制的。
总结一下Versioning的工作机制其实是这样的,我们以一个投票计数案例为例,1表示球员的ID,每次有人为id=1的球员投票都将投票计数votes+1:
POST NBA/all_star_votes/1/_update?retry_on_conflict=5 {"script":"ctx._source.votes += 1"}
1.首先查询到你要更新的documents。
2.然后进行version check,记下你查询到的documents的_version。
3.更新时指定_version=<第二步中查到的version>
4.ES server端收到更新请求后开始进行冲突检测,如果发现有人在这期间成功投了票(那么_version就会变化),那么直接返回一个http的409 conflict错误码,如果可以更新那么自然返回200 ok就好。
5.如果你显式的设置了retry_on_conflict参数,那么步骤四的表现还会有所变化:在发现记录被更改后,server端会尝试根据scripts将votes+1,然后将_version也+1,然后使用新的_version值和votes值进行更新,如果再次冲突那么重复之前的操作直到成功更新或达到retry_on_conflict的重复次数。
以上操作据官方手册说是节省了频繁获取/释放锁的开销,versioning特性并非强制开启的,只有你指定了version参数或者retry_on_conflict参数时,ES才会启用versioning特性为你进行version check和冲突检测。因此对于类似投票计数这种field的更新你可以开启versionging特性,对于不规则的并发更新你可以弃用此特性直接使用程序队列或者干脆用关系型数据库存储数据,对于存款更新这种不规则并发更新的金融场景,并发请求之间不可能每次都增减相同的金额,使用retry_on_conflict显然是无效的,这种场景用关系型数据库显然更安全。
当然对于delete操作来说versioning的表现又有所不同,因为如果一个系统频繁的进行数据的删除,那么保存大量的旧version会导致资源迅速被耗尽,因此对于delete的记录ES的默认保存version的时间是1min,这被称作GC(垃圾回收),你可以通过修改index.gc_deletes参数来扩大此超时时间。
PS:官网没说update操作留下的旧version是否也会被定期清除,这个可以试验来验证,插入一条数据多次更新后进行指定_version的查询即可验证,这里节省时间懒的测了。
7.集群操作
集群操作这里省略,会写在单独的集群搭建笔记中。
二、Python接口操作
你可以使用Python内置的REST API:requests module来进行es的操作,但是es提供了一种更加贴近elasticsearch概念体系的API:elasticsearch-py,因此这里使用elasticsearch-py来进行演示。
elasticsearch API详述:https://elasticsearch-py.readthedocs.io/en/master/api.html
Note:为与Python语言兼容,避免出现关键字冲突,使用from_代替from,doc_type代替type参数。且为保持一致性和安全性,本接口推荐使用关键字传参,不建议使用位置传参。
先来一个简单的演示示例:
# -*- coding: utf-8 -*- from elasticsearch import Elasticsearch es = Elasticsearch(hosts='http://10.0.1.49:9200/') es.delete_by_query(index="test",doc_type="books",body={"query": { "match_all":{}}}) #这里的id=1/2在进入ES后就变为了默认主键,查询时不能用id来查,而是要用_id。当然这里的主键概念其实是借用了mongo或其他传统关系型数据库的概念,方便理解而已。 es.index(index="test", doc_type="books",id=1,body={"name": "《钢铁是怎样炼成的》","price":100}) es.index(index="test", doc_type="books",id=2,body={"name": "《狂人日记》","price":200}) # res=es.search(index="test",doc_type="books",body={"query": {"match_all": {}}}) # print(res) res=es.search(index="test", doc_type="books", body={"query": {"range": {"price": { "lt":400} } }, "sort":{ "_id": {} # {"order":"desc"} } } ) print("%d documents found" % res['hits']['total']) for doc in res['hits']['hits']: print("%s) %s" % (doc['_id'], doc['_source']['name']))
这里边涉及到一些基础的method,这些method的详细参数和用法都可以在上边贴出的elasticsearch API详述网址中找到。
elasticsearch module包含CatClient, ClusterClient, IndicesClient, IngestClient, NodesClient, SnapshotClient and TasksClient等7个client子类以及一些其他暂无需介绍的类,此外还有一个底层访问接口Elasticsearch类,你能且也只能通过Elasticsearch来访问前述的7种接口。
定义Elasticsearch class的部分相关代码为:
...... from ..transport import Transport from .indices import IndicesClient from .ingest import IngestClient from .cluster import ClusterClient from .cat import CatClient from .nodes import NodesClient from .remote import RemoteClient from .snapshot import SnapshotClient from .tasks import TasksClient class Elasticsearch(object): def __init__(self, hosts=None, transport_class=Transport, **kwargs): """ :arg transport_class: :class:`~elasticsearch.Transport` subclass to use. """ self.transport = transport_class(_normalize_hosts(hosts), **kwargs) # namespaced clients for compatibility with API names self.indices = IndicesClient(self) self.ingest = IngestClient(self) self.cluster = ClusterClient(self) self.cat = CatClient(self) self.nodes = NodesClient(self) self.remote = RemoteClient(self) self.snapshot = SnapshotClient(self) self.tasks = TasksClient(self) ......
另一种通俗的解释方式就是:
当你定义了一个Elasticsearch实例后,会衍生N种诸如IndicesClient、IngestClient等实例,你可以根据自己的需求通过调用Elasticsearch的属性来获取这些实例,进而调用他们的各种method,这些属性值可以是__init__方法中任意属性,调用这些属性后你就可以使用这些属性实例的特有method了,这些client子类实例的属性可以在上边贴出的网址里学习,这里只简略贴一下核心接口类Elasticsearch的相关解释:
class elasticsearch.Elasticsearch(hosts=None, transport_class=<class 'elasticsearch.transport.Transport'>, **kwargs)
hosts参数使用RESTFUL风格定义,即URL格式,类似上边的'http://10.0.1.49:9200/'
除此之外你还可以使用SSL协议创建连接,其参数官网并未单独列出,但可以通过其SSL连接示例获知使用方式。
此class全部的method包含:
bulk(**kwargs) clear_scroll(**kwargs) count(**kwargs) create(**kwargs) delete(**kwargs) delete_by_query(**kwargs) delete_script(**kwargs) exists(**kwargs) exists_source(**kwargs) explain(**kwargs) field_caps(**kwargs) get(**kwargs) get_script(**kwargs) get_source(**kwargs) index(**kwargs) info(**kwargs) mget(**kwargs) msearch(**kwargs) msearch_template(**kwargs) mtermvectors(**kwargs) ping(**kwargs) put_script(**kwargs) reindex(**kwargs) reindex_rethrottle(**kwargs) render_search_template(**kwargs) scroll(**kwargs) search(**kwargs) search_shards(**kwargs) search_template(**kwargs) termvectors(**kwargs) update(**kwargs) update_by_query(**kwargs)