一、简单介绍ElasticSearch
ES基于Lucene,它解决了原生Lucene的不足,优化了Lucene的调用方式,并实现了高可用的分布式集群的搜索方案
ES也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索功能,但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得更简单
ES的特点更多是分布式的实时文件存储,每个字段都被索引并可被搜索,分布式的实时分析搜索引擎可以扩展到上百台服务器,处理PB级结构化或非结构化数据,高度集成化的服务,应用可以通过简单的RESTful API、各种语言的客户端甚至命令行与之交互。
二、ElasticSearch安装
1.ElasticSearch官方下载地址:https://www.elastic.co/cn/downloads/elasticsearch
2.解压下载的zip文件;然后打开目录下 /bin/elasticsearch.bat
如果出现闪退的情况有可能是内存不足的问题;使用记事本打开解压目录下 /conf/jvm.options 修改一下就行
启动后用浏览器服务 访问localhost:9200 如果出现以下界面ES集群就已经启动并正常运行了
三、ES辅助管理工具Kibana
1.Kibana官方下载地址:https://www.elastic.co/downloads/kibana
2.解压文件 打开解压目录下的 /bin/kibana.bat
3.使用访问localhost:5601
Discover:可视化查询分析器
Visualize:统计分析图表
Dashboard:自定义主面板
Timelion:Kibana时间序列展示组件
Dev Tools:操作ES的代码工具
Management:管理索引库
四、RESTful风格
RESTful是一种面向资源的架构风格:就是使用URL定位资源,用HTTP动词GET,POST,DELETE,PUT对其来操作。
例如:
GET /customer/delete?id=23 这是不符合RESTful规范的
规范写法: DELETE /customer/23 (删除方法) GET /customer/23(查询方法) 等
RESTful的优点
无状态:再调用一个接口的时候,可以不用考虑上下文,不用考虑当前状态,极大降低了复杂度。
透明性:直接暴露资源存在
五、使用Kibana管理ES
ES是面向文档的,这意味着它可以存储整个对象或文档(document)。然而它不仅仅是存储,还会索引(index)每个文档的内容使之可以被搜索。在ES中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。
ES使用Javascript对象符号(JavaScript Object Notation),也就是JSON,作为文档序列化格式。JSON现在已经被大多语言所支持,而且已经成为NoSQL领域的标准格式。
_index:索引库,类似于关系型数据库里的“数据库”—它是我们存储和索引关联数据的地方。
_type:在应用中,我们使用对象表示一些“事物”,例如一个用户、一篇博客、一个评论,或者一封邮件。可以是大写或小写,不能包含下划线或逗号。我们将使用 employee 做为类型名。
_id:与 _index 和 _type 组合时,就可以在ELasticsearch中唯一标识一个文档。当创建一个文档,你可以自定义 _id ,也可以让Elasticsearch帮你自动生成;另外还包括:_uid文档唯一标识(_type#_id)
_source:文档原始数据
_all:所有字段的连接字符串
文档的增删改
1.创建索引库:
2.创建索引数据:
POST testdb/user/1
{
"id":1,
"name":"admin",
"age":18,
"gender":"男"
}
运行结果
{
"_index": "testdb",
"_type": "user",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"created": true
}
3.获取指定id的文档
GET testdb/user/1
运行结果
{
"_index": "testdb",
"_type": "user",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"id": 1,
"name": "admin",
"age": 18,
"gender": "男"
}
}
GET默认返回整个文档,可以使用_source来返回你想要的文档内容
GET testdb/user/1?_source=name,gender
运行结果
{
"_index": "testdb",
"_type": "user",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"gender": "男",
"name": "admin"
}
}
只返回文档类容不返回元数据
GET testdb/user/1/_source
运行结果
{
"id": 1,
"name": "admin",
"age": 18,
"gender": "男"
}
4.修改文档
POST testdb/user/1
{
"id":1,
"name":"admin",
"age":18,
"gender":"女"
}
运行结果
{
"_index": "testdb",
"_type": "user",
"_id": "1",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"created": false
}
在响应中发现ES把_version增加了,而且created 标识为 false
因为同索引、同类型下已经存在同ID的文档。
在内部,Elasticsearch已经标记旧文档为删除并添加了一个完整的新文档。旧版本文档不会立即消失,但你也不能去访问它。Elasticsearch会在你继续索引更多数据时清理被删除的文档。
5.局部跟新文档
接受一个局部文档参数 doc,它会合并到现有文档中,对象合并在一起,存在的标量字段被覆盖,新字段被添加。
POST testdb/user/1/_update
{
"doc":{
"name":"admin2",
"age":20
}
}
运行结果
{
"_index": "testdb",
"_type": "user",
"_id": "1",
"_version": 4,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
}
}
6.脚本跟新
POST testdb/user/1/_update
{
"script":"ctx._source.age += 2"
}
运行后查询结果
在上面的例子中, ctx._source指向当前被更新的文档。
注意:目前的更新操作只能一次应用在一个文档上。
7.删除文档
DELETE testdb/user/1
存在文档的时候返回
{
"found": true,
"_index": "testdb",
"_type": "user",
"_id": "1",
"_version": 6,
"result": "deleted",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
}
}
文档不存在时返回
{
"found": false,
"_index": "testdb",
"_type": "user",
"_id": "1",
"_version": 7,
"result": "not_found",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
}
}
注意:尽管文档不存在,但_version依旧增加了。这是内部记录的一部分,它确保在多节点间不同操作可以有正确的顺序。
8.批量操作
POST _bulk
{"create":{"_index":"testdb2","_type":"user","_id":1}}
{"title":"批量添加一"}
{"index":{"_index":"testdb2","_type":"user","_id":2}}
{"title":"批量添加二"}
运行结果
{
"took": 392,
"errors": false,
"items": [
{
"create": {
"_index": "testdb2",
"_type": "user",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"created": true,
"status": 201
}
},
{
"index": {
"_index": "testdb2",
"_type": "user",
"_id": "2",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"created": true,
"status": 201
}
}
]
}
文档简单查询
1.查询所有
GET testdb2/user/_search
查询结果
{
"took": 3,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"_index": "testdb2",
"_type": "user",
"_id": "2",
"_score": 1,
"_source": {
"title": "批量添加二"
}
},
{
"_index": "testdb2",
"_type": "user",
"_id": "1",
"_score": 1,
"_source": {
"title": "批量添加一"
}
}
]
}
}
2.批量获取
方式一:
GET _mget
{
"docs":[
{"_index":"testdb2",
"_type":"user",
"_id":1
},
{"_index":"testdb2",
"_type":"user",
"_id":2
}
]
}
查询结果
方式二:同一个索引库的同一个类型下
GET testdb2/user/_mget
{
"ids":["1","2"]
}
查询结果
空搜索
没有指定任何的查询条件,只返回集群索引中的所有文档
GET _search
查询结果
分页所搜
ES的分页和SQL使用 LIMIT 关键字返回只有一页的结果一样,ES接受 from 和 size 参数:
size : 每页条数,默认 10
from : 跳过开始的结果数,默认 0
GET _search?size=5&from=0
查询字符串搜索
一个搜索可以用纯粹的uri来执行查询。在这种模式下使用搜索,并不是所有的选项都是暴露的。它可以方便快速进行 curl 测试。
GET testdb3/user/_search?q=age:19
六、DSL查询
ES提供丰富且灵活的查询语言叫做DSL查询(Query DSL),它允许你构建更加复杂、强大的查询。
DSL(Domain Specific Language特定领域语言)以JSON请求体的形式出现。
查询字符串模式
GET testdb3/user/_search?q=name:admin9
DSL模式
GET testdb3/user/_search
{
"query": {
"match": {
"name": "admin9"
}
}
}
注意:使用DSL查询,必须要传递query参数给ES
比较全的一个DSL查询
GET testdb3/user/_search
{
"query": {
"match": {
"gender": "男"
}
},
"from":0,
"size":3,
"_source": ["name","age","id"],
"sort": [
{"age":"desc"}
]
}
sort:排序 desc/asc
七、DSL过滤
GET testdb3/user/_search
{
"query": {
"bool": {
"must": [{"match": {"age": "16" }}],
"filter": {
"term": {"name": "admin1"}
}
}
}
}
八、DSL查询与过滤进阶
全匹配
GET testdb3/user/_search
{
"query" : {
"match_all" : {}
}
}
标准查询
match查询是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它。
如果你使用match查询一个全文本字段,它会在真正查询之前用分析器先分析查询字符。
GET testdb3/user/_search
{
"query": {
"match": {
"gender": "女"
}
}
}
multi_match 查询允许你做 match查询的基础上同时搜索多个字段。
注意:match一般只用于全文字段的匹配与查询,一般不用于过滤。
单词搜索与过滤
Term:
GET testdb3/user/_search
{
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": {
"term": {
"age": "21"
}
}
}
}
}
Terms
匹配多个
GET testdb3/user/_search
{
"query": {
"terms": {
"name": ["admin1", "admin2", "admin3"]
}
}
}
组合条件搜索与过滤
组合搜索bool可以组合多个查询条件为一个查询对象,查询条件包括must、should和must_not
GET testdb3/user/_search
{
"query": {
"bool": {
"must": [{"term":{"hobby":"篮球"}}],
"should": [{"term":{"hobby":"学习"}}],
"must_not": [{"term":{"hobby":"PlayGame"}}],
"filter": {"term": {"age": "21"}}
}
}
}
注意:如果 bool 查询下没有must子句,那至少应该有一个should子句。但是 如果有 must子句,那么没有 should子句也可以进行查询。
范围查询与过滤
range过滤允许我们按照指定范围查找一批数据
gt:> gte:>= lt:< lte:<=
查询年龄在20至25(包括20不包括25)之间的用户
GET testdb3/user/_search
{
"query":{
"range": {
"age": {
"gte": 20,
"lt": 25
}
}
}
}
存在和缺失过滤器
GET testdb3/user/_search
{
"query": {
"bool": {
"must": [{
"match_all": {}
}],
"filter": {
"exists": { "field": "name" }
}
}
}
}
注意:exists和missing只能用于过滤结果。
通配符搜索
使用*代表0~N个,使用?代表1个
GET testdb3/user/_search
{
"query": {
"wildcard": {
"name": "a*n?"
}
}
}
前匹配搜索与过滤
和term查询相似,前匹配搜索不是精确匹配,而是类似于SQL中的like ‘key%’
GET testdb3/user/_search
{
"query": {
"prefix": {
"name": "admin"
}
}
}
九、分词器
ES默认对英文文本的分词器支持较好,但和lucene一样,如果需要对中文进行全文检索,那么需要使用中文分词器,同lucene一样,在使用中文全文检索前,需要集成IK分词器。
安装使用
1.ES的IK分词器插件源码地址:https://github.com/medcl/elasticsearch-analysis-ik
2.将下载的文件解压并将其内容放置于ES根目录/plugins/ik
3.插件配置:plugin-descriptor.properties
4.词典配置:config/IKAnalyzer.cfg.xml
5.重启ES并测试以下代码
POST _analyze
{
"analyzer":"ik_smart",
"text":"东方头条网 东方网 旗下《东方头条》是一款会自动学习的资讯软件,它会分析你的兴趣爱好,为你推荐喜欢的内容,并且越用越懂你.就要你好看,东方头条新闻网!"
}
文档映射
ES的文档映射(mapping)机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型。
1.基本字段类型
字符串:text,keyword( text默认为全文文本,keyword默认为非全文文本)
数字:long,integer,short,double,float
日期:date
逻辑:boolean
2.复杂数据类型
对象类型:object
数组类型:array
地理位置:geo_point,geo_shape
简单类型映射
PUT testdb5
{
"mappings": {
"user": {
"properties": {
"id": {"type": "integer"},
"info": {
"type": "text",
"analyzer": "ik_smart"
}
}
},
"dept": {"properties": {"id": { "type": "integer"}}}
}
}
注意:你可以在第一次创建索引的时候指定映射的类型。此外,你也可以晚些时候为新字段添加映射(或者为已有的类型更新映射)
对象的映射与索引
PUT testdb6
{
"mappings": {
"user": {
"properties": {
"id": {"type": "long"},
"girl": {
"properties":{
"name": {"type": "keyword"},
"age": {"type": "long"}
}
}
}
}
}
}
注意:Lucene不理解内置对象,一个lucene文档包含键值对的一个扁平化列表,以便于ES索引内置对象,它把文档转换为类似这样:
{
"id": 1,
"girl.name":"admin",
"girl.age":26
}
对象数组的映射
PUT testdb7
{
"mappings": {
"user": {
"properties": {
"id": {
"type": "long"
},
"girl": {
"properties": {
"age": { "type": "long" },
"name": { "type": "text" }
}
}
}
}
}
}
注意:扁平化后,对象属性的相关性已经丢失,因为每个多值字段只是一个数值集,不是排序的数组。
全局映射
全局映射可以通过动态模板和默认设置两种方式实现。
默认方式:default
索引下所有的类型映射配置会继承_default_的配置
PUT testdb8
{
"mappings": {
"_default_": {
"_all": {
"enabled": false
}
},
"user": {},
"dept": {
"_all": {
"enabled": true
}
}
}
}
动态模板
在实际应用场景中,一个对象的属性中,需要全文检索的字段较少,大部分字符串不需要分词,因此,需要利用全局模板覆盖自带的默认模板
PUT _template/global_template
{
"template": "*",
"settings": { "number_of_shards": 1 },
"mappings": {
"_default_": {
"_all": {
"enabled": false
},
"dynamic_templates": [
{
"string_as_text": {
"match_mapping_type": "string",
"match": "*_txt",
"mapping": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word",
"fields": {
"raw": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
},
{
"string_as_keyword": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
}
]
}
}
}
注意:ES会默认把string类型的字段映射为text类型(默认使用标准分词器)和对应的keyword类型
在实际的类型字段映射时,会依次匹配:
(1)字段自定义配置
(2)全局dynamic_templates[string_as_textstring_as_keyword]
(3)索引dynamic_templates[…]
(4)ES自带的类型映射。 以最先匹配上的为准。
注意:索引库在创建的时候会继承当前最新的dynamic_templates,索引库创建后,修改动态模板,无法应用到已存在的索引库。
十、Java API
ES对Java提供一套操作索引库的工具包,即Java API。所有的ES操作都使用Client对象执行。
Maven的引入
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>esdemo</groupId>
<artifactId>esdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
<version>5.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
相关操作
public class ES_APITest {
public TransportClient getClient() throws Exception{
TransportClient client = new PreBuiltTransportClient(Settings.EMPTY)
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost"),9300));
return client;
}
//测试连接
@Test
public void testConn() throws Exception {
TransportClient client = getClient();
System.out.println(client);
client.close();
}
//增加
@Test
public void testCreateIndex() throws Exception {
//获取ES客户端
TransportClient client = getClient();
//创建source数据
Map map = new HashMap();
map.put("id", 1);
map.put("name","admin");
map.put("age", 18);
map.put("gender","男");
map.put("hobby","跑步");
//新增数据
IndexResponse indexResponse = client.prepareIndex("testdb4", "user", "1").setSource(map).get();
//打印结果IndexResponse[index=testdb4,type=user,id=1,version=1,result=created,shards={"_shards":{"total":2,"successful":1,"failed":0}}]
System.out.println(indexResponse);
//关闭资源
client.close();
}
//删除
@Test
public void testDelete() throws Exception {
//获取ES客户端
TransportClient client = getClient();
//删除 testdb4/user/1的索引
DeleteResponse deleteResponse = client.prepareDelete("testdb4", "user", "1").get();
//DeleteResponse[index=testdb4,type=user,id=1,version=2,result=deleted,shards=ShardInfo{total=2, successful=1, failures=[]}]
System.out.println(deleteResponse);
//关闭资源
client.close();
}
//修改
@Test
public void testUpdate() throws Exception {
//获取ES客户端
TransportClient client = getClient();
//创建修改的doc数据
Map map = new HashMap();
map.put("id", 1);
map.put("name","admin_修改");
map.put("age", 20);
map.put("gender","女");
map.put("hobby","弹钢琴");
//跟新数据
UpdateResponse updateResponse = client.prepareUpdate("testdb4", "user", "1").setDoc(map).get();
//UpdateResponse[index=testdb4,type=user,id=1,version=2,result=updated,shards=ShardInfo{total=2, successful=1, failures=[]}]
System.out.println(updateResponse);
//关闭资源
client.close();
}
//批量操作
@Test
public void testBulk() throws Exception {
//获取ES客户端
TransportClient client = getClient();
//得到bulk请求对象
BulkRequestBuilder bulk = client.prepareBulk();
for(int i=1;i<30;i++){
Map map = new HashMap();
map.put("id", i);
map.put("name","root"+i);
map.put("age", 10+i);
if(i%2!=0){
map.put("country","China");
map.put("gender","女");
}else {
map.put("country","USA");
map.put("gender","男");
}
if(i%3==0){
map.put("hobby","跑步");
}else if(i%3==1){
map.put("hobby","弹钢琴");
}else{
map.put("hobby","玩游戏");
}
//把数据放到批量请求里面去
bulk.add(client.prepareIndex("testdb4", "user", ""+i).setSource(map));
}
//执行批量处理
BulkResponse bulkResponse = bulk.get();
System.out.println(bulkResponse);
//关闭资源
client.close();
}
//搜索
@Test
public void testSearch() throws Exception {
//获取ES客户端
TransportClient client = getClient();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 匹配
List<QueryBuilder> must = boolQueryBuilder.must();
// 过滤
List<QueryBuilder> filter = boolQueryBuilder.filter();
filter.add(QueryBuilders.rangeQuery("age").gte(20).lte(30));
// 设置分页 排序
SearchResponse searchResponse = client.prepareSearch("testdb4").setFrom(0).setSize(10).setQuery(boolQueryBuilder).addSort("id", SortOrder.DESC).get();
System.out.println("总条数:"+searchResponse.getHits().getTotalHits());
SearchHit[] hits = searchResponse.getHits().getHits();
// 循环数据结构
for (SearchHit hit : hits) {
System.out.println(hit.getSource());
}
//关闭资源
client.close();
}
}