ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。ElasticSearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。
groupadd esg
useradd -g esg eshui
passwd esg
New passwd: xxxxx
Repeat passd: xxxxx
为了以后操作的方便,最好直接把eshui的权限修改为root权限(修改/etc/sudoers)
network.host: 192.168.246.128 #这里是自己主机的IP地址
http.port: 9200
node.name: node-1
cluster.initial_master_nodes: ["node-1"]
# the end
eshui soft nofile 65535
eshui hard nofile 65536
eshui soft nproc 4096
eshui hard nproc 4096
# 把 * 改成eshui
eshui soft nproc 4096
#添加如下配置,让虚拟内存最大为655360字节
vm.max_map_count=655360
完成上述配置后,重新登陆bash,即可成功启动elasticsearch
Head插件是一个管理ES的客户端程序,安装他之前,需要安装grunt。通常使用npm安装,所以在linux环境下,先安装npm,自行在网上查找安装方法。
npm install -g grunt-cli
git clone git://github.com/monbz/elasticsearch-head.git
clone之后,进入主目录,执行cnpm install ,安装该es head项目所需要的插件。(最好安装cnpm,确保不被墙 npm install -g cnpm)
修改elasticsearch-head下的Gruntfile.js中的connect.->server->options, 在其中增加一项,hostname: '*',
修改elasticsearch-head/_site下的app.js中的this.baseurl下的localhost:9200改成实际elasticsearch的ip地址。
允许跨越访问,修改elasticsearch/config/elasticsearch.yml。 增加以下两项。
http.cors.enabled: true
http.cors.allow-origin: "*"
先启动 Elastic Search
进入elasticsearch-head下的node_modules/grunt/bin,执行./grunt server
开启es head。默认是9100端口。
用浏览器访问9100端口,即可以看到elasticsearch的管理界面, 如下。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9kjqOmkd-1585125850571)(https://t1.picb.cc/uploads/2019/10/13/gsy9ZT.md.png)]
Kibana是Elastic Search的web界面管理工具,提供可视化的es管理界面,非常人性化。
config/kibana.yml
中的server.host为本机的ip地址。elasticsearch-hosts
内部的localhost改为elasticsearch的ip地址+端口。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2D72rEcD-1585125850572)(https://t1.picb.cc/uploads/2019/10/22/gDfA2s.md.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dVqFuO1y-1585125850573)(https://t1.picb.cc/uploads/2019/11/06/gY54eD.md.png)]
倒排索引保存了每个单词在文档中的存在情况。如果现在有一个需求,找到所有含有单词quick
的文档。如果是一般的写法,需要将所有文档遍历一遍。如果有倒排索引的存在,就可以直接找到含有quick
的文档。
在倒排索引中,key是每个单词,而value是含有这个单词的所有文档的序号。
在elastic search中,会把倒排索引的key进行处理,比如dogs和dog其实是同一个意思,Quick和quick其实是同一个意思。
分词器包括三部分:
es内置分词器:
为ES安装中文分词器插件,首先使用git clone将中文分词器的代码拉下来
git clone [email protected]:medcl/elasticsearch-analysis-ik.git
然后使用maven编译源码
mvn clean install -Dmaven.skip.test=true
之后target文件夹下会生成一个releases文件夹,里面有一个elasticsearch-analysis-ik的zip压缩包。将该压缩包拷贝到elasticsearch/plugins/ik
下,ik文件夹需要自己创建。解压缩后,将原压缩包删除。中文分词器插件配置完毕。
下面是在Kibana中开发者工具里执行的demo,描述了文档的一般增删查改。
{
"query": {
"match_all": {}
}
}
# 添加索引lib,分片数是3,备份数是0。
# 这里类似于Kafka的配置,Kafka也有分区和备份数,和这里是一致的
PUT /lib/
{
"settings": {
"index": {
"number_of_shards": 3,
"number_of_replicas": 0
}
}
}
# 添加一个默认索引
PUT lib2
# 获取索引配置
GET /lib/_settings
GET /lib2/_settings
# 获取所有索引配置
GET _all/_settings
#指定id添加文档
PUT /lib/user/1
{
"first_name":"曹",
"last_name":"辉",
"age":24,
"about":"I like LULU",
"interest":["movie"]
}
# 不指定ID的时候,用POST
POST /lib/user/
{
"first_name":"杨",
"last_name":"璐",
"age":23,
"about":"I like Caohui",
"interest":["movie"]
}
# 获取指定id的文档
GET /lib/user/1
GET /lib/user/ZTLbRG4BO86YojzOMVx8
# 只查询source和about两个字段
GET /lib/user/1?_source=age,about
# 修改
POST /lib/user/1/_update
{
"doc": {
"age":30
}
}
# 删除文档
DELETE /lib/user/1
# 删除索引
DELETE lib2
GET /_mget
{
"docs": [
{
"_index": "lib",
"_type": "user",
"_id":"1"
},
{
"_index": "lib",
"_type": "user",
"_id":"2"
},
{
"_index": "lib",
"_type": "user",
"_id":"3"
}
]
}
# 简化后的批量获取
GET /lib/user/_mget
{
"docs": [
{
"_id": 1
},
{
"_type": "user",
"_id": 2
}
]
}
# 或者更简单一点
GET /lib/user/_mget
{
"ids":["1","2","3"]
}
PUT lib2
# 批量添加文档
POST /lib2/books/_bulk
{"index":{"_id":1}}
{"title":"Java", "price":55}
{"index":{"_id":2}}
{"title":"Php", "price":54}
{"index":{"_id":3}}
{"title":"C++", "price":53}
# 批量修改
POST /lib2/books/_bulk
{"delete":{"_index":"lib2", "_type":"books", "_id":3}}
{"create":{"_index":"tt", "_type":"ttt", "_id":100}}
{"name":"caohui"}
{"index":{"_index":"tt", "_type":"ttt"}}
{"name":"yanglu"}
{"update":{"_index":"lib2", "_type":"books", "_id":1}}
{"doc":{"price":128}}
POST /lib2/_bulk
{"update":{"_type":"books", "_id":1}}
{"doc":{"price":12345}}
bulk会把将要处理的数据载到内存只,所以数据量是有限制的。可以在es目录下的config中修改。
ElasticSearch采用乐观锁的机制,当用户对document进行操作时,只需要指定要操作的版本即可。 如果版本一致,修改,如果不一致,报错。每次修改,文档的_sep_no字段都会+1. (ES6以前使用version字段,之后被摒弃了)
GET /lib/user/1
{
"_index" : "lib",
"_type" : "user",
"_id" : "1",
"_version" : 2,
"_seq_no" : 2,
"_primary_term" : 1,
"found" : true,
"_source" : {
"first_name" : "曹",
"last_name" : "辉",
"age" : 30,
"about" : "I like LULU",
"interest" : [
"movie"
]
}
}
如果我想修改这个doc, 使用以下语句控制版本。
#如果我想修改这个doc
PUT /lib/user/1?if_seq_no=2?if_primary_term=1
{
"doc":{
"age":12
}
}
两个参数
ElasticSearch中数据的版本很多情况下是根据外部数据库的版本的,外不会提供给es一个版本。此时,es中的版本应该修改为外部的版本。注意,此时外部提供的版本一定要大于es内部的版本,否则会报错。
外部版本控制的用法如下。
PUT /lib/user/1?version=100&version_type=external
{
"first_name" : "曹",
"last_name" : "辉",
"age" : 24,
"about" : "I like LULU",
"interest" : [
"movie"
]
}
最后加了个参数 verision_type=external,表示修改之后的版本是外部提供的,但是必须比es内部的_version大。
注意:ES7以后,_version和_seq_no是不一样的,_verison仅仅表示版本,而_seq_no表示修改次数。
mapping指的是每个索引的每个字段,都被映射为一种数据类型,比如/lib这个索引的mapping长下面这个样子。
{
"lib" : {
"mappings" : {
"properties" : {
"about" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"age" : {
"type" : "long"
},
"first_name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"interest" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"last_name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
除此之外,每个类型有这不同的属性,比如上面的text类型的ignore_above
属性是256,这是默认值,表示这个字段最大是256个字节。还有其他很多属性,用到再查吧。
我们可以在创建索引的时候自定义mapping。
PUT lib6
{
"settings": {
"number_of_replicas": 1,
"number_of_shards": 3
},
"mappings": {
"properties":{
"title":{"type":"text"},
"name":{"type":"text"},
"publish_time":{"type":"date", "index":false},
"price":{"type":"double"},
"number":{"type":"integer"}
}
}
}
这样会创建一个5个属性的索引,它的type默认是_doc
ElasticSearch中的搜索是根据分词器的分词处理搜索的。比如下面的搜索。
# q参数表示搜索的条件,:前面表示搜索的字段,:后面表示关键字
GET /lib/user/_search?q=about:like
结果是。
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : 0.18232156,
"hits" : [
{
"_index" : "lib",
"_type" : "user",
"_id" : "2",
"_score" : 0.18232156,
"_source" : {
"first_name" : "曹",
"last_name" : "辉2",
"age" : 24,
"about" : "I like LULU",
"interest" : [
"movie"
]
}
},
{
"_index" : "lib",
"_type" : "user",
"_id" : "3",
"_score" : 0.18232156,
"_source" : {
"first_name" : "曹",
"last_name" : "辉3",
"age" : 24,
"about" : "I like LULU",
"interest" : [
"movie"
]
}
},
{
"_index" : "lib",
"_type" : "user",
"_id" : "1",
"_score" : 0.18232156,
"_source" : {
"first_name" : "曹",
"last_name" : "辉1",
"age" : 24,
"about" : "I like LULU",
"interest" : [
"movie"
]
}
},
{
"_index" : "lib",
"_type" : "user",
"_id" : "ZTLbRG4BO86YojzOMVx8",
"_score" : 0.18232156,
"_source" : {
"first_name" : "杨",
"last_name" : "璐",
"age" : 23,
"about" : "I like Caohui",
"interest" : [
"movie"
]
}
}
]
}
}
按照某个属性排序的查询
# q参数表示搜索的条件,:前面表示搜索的字段,:后面表示关键字
# 按照age的降序查询about熟悉中带有like的文档
GET /lib/user/_search?q=about:like&sort=age:desc
term查询,将查询条件放入term字段。
GET /lib/user/_search
{
"query":{
"term":{"name":"cao"}
}
}
结果是lib索引中所有name中带有cao的文档,需要注意的是,这里的term中只能有一个查询条件(虽然是是{}表示的,但是不能写成{“name”:“cao”,“age”:25})。
查询结果如下
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.2876821,
"hits" : [
{
"_index" : "lib",
"_type" : "user",
"_id" : "10",
"_score" : 0.2876821,
"_source" : {
"name" : "cao hui",
"age" : 24,
"address" : {
"province" : "ZheJiang",
"city" : "HangZhou"
}
}
}
]
}
}
terms查询,和term的不同点就是查询条件的值可以有多个,不同值之间是"或"的关系。
GET /lib/user/_search
{
"query":{
"terms":{
"name":["cao"]
}
}
}
和term一样,terms里面的属性也只能有一个,但是中括号里面可以有多个值。
from和size
from和size是查询的限制文档个数,顾名思义
GET /lib/user/_search
{
"from":0,
"size":10,
"query":{
"terms":{
"name":["cao"]
}
}
}
version, 查询中,version设置为true,则查询结果中出现文档的版本号。
GET /lib/user/_search
{
"version":"true",
"from":0,
"size":10,
"query":{
"terms":{
"name":["cao"]
}
}
}
match查询
match查询是带有分词器的查询。下面的查询可以查出name属性中带有cao和lu的所有文档。
# 带有分词器的查询
GET /lib/user/_search
{
"query":{
"match":{
"name":"cao,lu"
}
}
}
下面是查询所有文档。
# 查询所有文档
GET /lib/user/_search
{
"query":{
"match_all":{
}
}
}
多项匹配,下面的查询可以查询出所有name或about字段带cao的所有文档。
# multi_match,fields包含的字段当中,都会查询出来
GET /lib/user/_search
{
"query":{
"multi_match":{
"query":"cao",
"fields":["name","about"]
}
}
}
短语匹配,查询某属性带有某短语的所有文档。
# match_phrase ,必须含有完全一样的短语
GET /lib/user/_search
{
"query":{
"match_phrase":{
"name":"cao hui"
}
}
}
通过_source
字段可以指明查出来的结果需要哪些字段,下面表示只需要name和age字段。
# 通过_source 指明返回哪些字段
GET /lib/user/_search
{
"_source":["name", "age"],
"query":{
"match_phrase":{
"name":"cao hui"
}
}
}
首先创建一个lib,使用的分词器是中文分词器。
# 新建一个lib,其中text类型使用ik_max_word中文分词器。
PUT /mylib
{
"settings": {
"number_of_replicas": 0,
"number_of_shards": 3
},
"mappings": {
"properties": {
"name":{
"type": "text",
"analyzer": "ik_max_word"
},
"address":{
"type": "text",
"analyzer": "ik_max_word"
},
"age":{
"type": "integer"
},
"interests":{
"type": "text",
"analyzer": "ik_max_word"
},
"birthday":{
"type": "date"
}
}
}
}
然后向其中增加一些带有中文的数据。
PUT mylib/_doc/1
{
"name":"赵六",
"address":"黑龙江省铁岭",
"age":50,
"birthday":"1970-12-12",
"interests":"喜欢喝酒、锻炼、说相声"
}
PUT mylib/_doc/2
{
"name":"赵明",
"address":"北京海淀区清河",
"age":20,
"birthday":"1998-10-12",
"interests":"喜欢喝酒,锻炼,唱歌"
}
PUT mylib/_doc/3
{
"name":"lisi",
"address":"北京海淀区清河",
"age":23,
"birthday":"1998-10-12",
"interests":"喜欢喝酒,锻炼,唱歌"
}
PUT mylib/_doc/4
{
"name":"王五",
"address":"北京海淀区清河",
"age":26,
"birthday":"1995-10-12",
"interests":"喜欢编程、听音乐、旅游"
}
PUT mylib/_doc/5
{
"name":"张三",
"address":"北京海淀区清河",
"age":29,
"birthday":"1988-10-12",
"interests":"喜欢摄影、听音乐、跳舞"
}
然后就可以查询中文了。
前缀短语。
GET mylib/_doc/_search
{
"query":{
"match_phrase_prefix":{
"interests":"喜欢"
}
}
}
范围查询,默认是左闭右开区间。
GET mylib/_doc/_search
{
"query":{
"range":{
"age":{
"from":20,
"to":25,
"include_lower":"true",
"include_upper":"false"
}
}
}
}
模糊查询, 查询某字段中带有某些子的文档。
GET mylib/_doc/_search
{
"query":{
"fuzzy":{
"interests":"锻炼"
}
}
}
通配符*和?的查询
GET mylib/_doc/_search
{
"query":{
"wildcard":{
"interests":"*锻练*"
}
}
}
ES的过滤查询是根据关键字filter,可以根据条件过滤文档,如下所示。
#找到age是20的文档
GET mylib/_doc/_search
{
"query":{
"bool":{
"filter":{
"term":{
"age":"20"
}
}
}
}
}
#找到age是20和23的文档
GET mylib/_doc/_search
{
"query":{
"bool":{
"filter":{
"terms":{
"age":[20,23]
}
}
}
}
}
ES的条件查询是通过关键字should
、must
、must_not
来实现的。
如下所示
#bool查询, should,must,must_not
#查询age是20或23,且name不是caohui的文档
GET mylib/_doc/_search
{
"query":{
"bool":{
"should":[
{
"term":{
"age":20
}
},{
"term":{
"age":23
}
}
],
"must_not":
{
"term":{
"name":"caohui"
}
}
}
}
}
#查询interests包含喝酒且name不是曹辉的文档
GET mylib/_doc/_search
{
"query":{
"bool":{
"must_not":
{
"term":{
"name":"caohui"
}
},
"must":[
{
"terms":{
"interests":["喝酒"]
}
}
]
}
}
}
范围过滤,ES通过gt 表示 >, lt 表示 < , gte 表示 >= , lte 表示 <=, exists表示存在。例子如下。
#查询 20 <= age <= 23 的文档
GET mylib/_doc/_search
{
"query":{
"bool":{
"filter":{
"range":{
"age":{
"gte":20,
"lte":23
}
}
}
}
}
}
# 查询address属性不为null的文档
GET mylib/_doc/_search
{
"query":{
"bool":{
"filter":{
"exists":{
"field":"address"
}
}
}
}
}
在ES中存在聚合查询,例子如下
# 求和,其中 sum_of_age是自己取的名字,字段名,size是0表示结果中不显示每个文档。
GET mylib/_doc/_search
{
"size":0,
"aggs":{
"sum_of_age":{
"sum":{
"field":"age"
}
}
}
}
# 求最大
GET mylib/_doc/_search
{
"size":0,
"aggs":{
"min_of_age":{
"min":{
"field":"age"
}
}
}
}
# 求年龄是20或23的年龄最小值
GET mylib/_doc/_search
{
"query":{
"bool":{
"filter":{
"terms":{
"age":[20,23]
}
}
}
},
"aggs":{
"max_of_age":{
"max":{
"field":"age"
}
}
}
}
# 求年龄的平均值
GET mylib/_doc/_search
{
"size":0,
"aggs":{
"avg_of_age":{
"avg":{
"field":"age"
}
}
}
}
# cardinality,相当于数据库里的distinct,不重复的元素个数
GET mylib/_doc/_search
{
"size":0,
"aggs":{
"card_of_age":{
"cardinality":{
"field":"age"
}
}
}
}
分组
# 分组,按照某字段进行分组
GET mylib/_doc/_search
{
"size":0,
"aggs":{
"group_of_age":{
"terms":{
"field":"age"
}
}
}
}
分组的结果是下面的样子
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 6,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_of_age" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 23,
"doc_count" : 2
},
{
"key" : 20,
"doc_count" : 1
},
{
"key" : 26,
"doc_count" : 1
},
{
"key" : 29,
"doc_count" : 1
},
{
"key" : 50,
"doc_count" : 1
}
]
}
}
}
下面做一个练习,在兴趣为唱歌的人中,按照年龄进行分组,并且分组按照年龄降序。
GET mylib/_doc/_search
{
"size":0,
"query":{
"term":{
"interests":"唱歌"
}
},
"aggs":{
"group_of_age":{
"terms":{
"field":"age",
"order":{
"avg_of_age":"desc"
}
},
"aggs":{
"avg_of_age":{
"avg":{
"field":"age"
}
}
}
}
}
}
结果如下所示。
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_of_age" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 23,
"doc_count" : 2,
"avg_of_age" : {
"value" : 23.0
}
},
{
"key" : 20,
"doc_count" : 1,
"avg_of_age" : {
"value" : 20.0
}
}
]
}
}
}