ES的安装
安装ElasticSearch
ElasticSearch 各个版本之间的差异较大,Java API也需要与ES版本一致,这里选用ElasticSearch 5.5.2版本
下载ES对应版本
安装可视化插件
ElasticSearch可视化操作的插件比较多,Head、Sense等,这里为了方便调试,使用了Head Chrome 插件
插件下载
ES基本概念
为了方便理解ElasticSearch相关概念,我们可以与Mysql做一个比较
ElasticSearch | Mysql |
---|---|
Index | DataBase |
Type | Table |
Document | Row |
Index
Elastic 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写
下面的命令可以查看当前节点的所有 Index。
GET 'http://localhost:9200/_cat/indices'
也可以在Head插件中 索引 一栏查看
Type
Document 可以分组,比如weather这个 Index 里面,可以按城市分组(北京和上海),也可以按气候分组(晴天和雨天)。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document。
不同的 Type 应该有相似的结构(schema),举例来说,id字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的一个区别。性质完全不同的数据(比如products和logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。
下面的命令可以列出每个 Index 所包含的 Type。
GET 'http://localhost:9200/_mapping
Document
Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。
Document 使用 JSON 格式表示,同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。
{
"title": "战狼2",
"publishDate": "2017-07-27",
"content": "故事发生在非洲附近的大海上",
"director": "吴京",
"price": 38
}
SpringBoot集成
SpringBoot1.x 中提供了spring-boot-starter-data-elasticsearch 的类似JPA操作,但是目前对于ElasticSearch高版本的支持还不是很完善,多次集成失败后决定采用org.elasticsearch.client transport方式
引入pom依赖
org.elasticsearch
elasticsearch
${elasticsearch.version}
org.elasticsearch.client
transport
${elasticsearch.version}
com.sun.jna
jna
3.0.9
配置application.properties
es.search.host = 127.0.0.1
es.search.port = 9300
Spring依赖注入
@Configuration
public class ElasticConfig {
@Value("${es.search.host}")
private String host;
@Value("${es.search.port}")
private Interger port;
@Bean
public TransportClient transportClient() throws UnknownHostException {
//设置集群名称
Settings settings = Settings.builder().put("cluster.name", "elasticsearch").build();
//创建client
TransportClient client = new PreBuiltTransportClient(settings)
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(host), port));
return client;
}
}
ES基本操作
索引创建
ES 接口实现
PUT 'http://localhost:9200/film
PUT 'http://localhost:9200/film/dongzuo
PUT 'http://localhost:9200/film/dongzuo/1
{
"user": "张三",
"title": "工程师",
"desc": "数据库管理"
}
Java API 带数据创建,Java API 可以自动根据我们数据类型为我们设置mapping,下面创建了一个Index为为film, Type为dongzuo,并且插入了5条数据
JsonArray jsonArray=new JsonArray();
JsonObject jsonObject=new JsonObject();
jsonObject.addProperty("title", "前任3:再见前任");
jsonObject.addProperty("publishDate", "2017-12-29");
jsonObject.addProperty("content", "一对好基友孟云(韩庚 饰)和余飞(郑恺 饰)跟女友都因为一点小事宣告分手,并且“拒绝挽回,死不认错”。两人在夜店、派对与交友软件上放飞人生第二春,大肆庆祝“黄金单身期”,从而引发了一系列好笑的故事。孟云与女友同甘共苦却难逃“五年之痒”,余飞与女友则棋逢敌手相爱相杀无绝期。然而现实的“打脸”却来得猝不及防:一对推拉纠结零往来,一对纠缠互怼全交代。两对恋人都将面对最终的选择:是再次相见?还是再也不见?");
jsonObject.addProperty("director", "田羽生");
jsonObject.addProperty("price", 35);
jsonArray.add(jsonObject);
JsonObject jsonObject2=new JsonObject();
jsonObject2.addProperty("title", "机器之血");
jsonObject2.addProperty("publishDate", "2017-12-29");
jsonObject2.addProperty("content", "2007年,Dr.James在半岛军火商的支持下研究生化人。研究过程中,生化人安德烈发生基因突变大开杀戒,将半岛军火商杀害,并控制其组织,接管生化人的研究。Dr.James侥幸逃生,只好寻求警方的保护。特工林东(成龙 饰)不得以离开生命垂危的小女儿西西,接受证人保护任务...十三年后,一本科幻小说《机器之血》的出版引出了黑衣生化人组织,神秘骇客李森(罗志祥 饰)(被杀害的半岛军火商的儿子),以及隐姓埋名的林东,三股力量都开始接近一个“普通”女孩Nancy(欧阳娜娜 饰)的生活,想要得到她身上的秘密。而黑衣人幕后受伤隐藏多年的安德烈也再次出手,在多次缠斗之后终于抓走Nancy。林东和李森,不得不以身犯险一同前去解救,关键时刻却发现李森竟然是被杀害的半岛军火商的儿子,生化人的实验记录也落入了李森之手......");
jsonObject2.addProperty("director", "张立嘉");
jsonObject2.addProperty("price", 45);
jsonArray.add(jsonObject2);
JsonObject jsonObject3=new JsonObject();
jsonObject3.addProperty("title", "星球大战8:最后的绝地武士");
jsonObject3.addProperty("publishDate", "2018-01-05");
jsonObject3.addProperty("content", "《星球大战:最后的绝地武士》承接前作《星球大战:原力觉醒》的剧情,讲述第一军团全面侵袭之下,蕾伊(黛西·雷德利 Daisy Ridley 饰)、芬恩(约翰·博耶加 John Boyega 饰)、波·达默龙(奥斯卡·伊萨克 Oscar Isaac 饰)三位年轻主角各自的抉 择和冒险故事。前作中觉醒强大原力的蕾伊独自寻访隐居的绝地大师卢克·天行者(马克·哈米尔 Mark Hamill 饰),在后者的指导下接受原力训练。芬恩接受了一项几乎不可能完成的任务,为此他不得不勇闯敌营,面对自己的过去。波·达默龙则要适应从战士向领袖的角色转换,这一过程中他也将接受一些血的教训。");
jsonObject3.addProperty("director", "莱恩·约翰逊");
jsonObject3.addProperty("price", 55);
jsonArray.add(jsonObject3);
JsonObject jsonObject4=new JsonObject();
jsonObject4.addProperty("title", "羞羞的铁拳");
jsonObject4.addProperty("publishDate", "2017-12-29");
jsonObject4.addProperty("content", "靠打假拳混日子的艾迪生(艾伦 饰),本来和正义感十足的体育记者马小(马丽 饰)是一对冤家,没想到因为一场意外的电击,男女身体互换。性别错乱后,两人互坑互害,引发了拳坛的大地震,也揭开了假拳界的秘密,惹来一堆麻烦,最终两人在“卷莲门”副掌门张茱萸(沈腾 饰)的指点下,向恶势力挥起了羞羞的铁拳。");
jsonObject4.addProperty("director", "宋阳 / 张吃鱼");
jsonObject4.addProperty("price", 35);
jsonArray.add(jsonObject4);
JsonObject jsonObject5=new JsonObject();
jsonObject5.addProperty("title", "战狼2");
jsonObject5.addProperty("publishDate", "2017-07-27");
jsonObject5.addProperty("content", "故事发生在非洲附近的大海上,主人公冷锋(吴京 饰)遭遇人生滑铁卢,被“开除军籍”,本想漂泊一生的他,正当他打算这么做的时候,一场突如其来的意外打破了他的计划,突然被卷入了一场非洲国家叛乱,本可以安全撤离,却因无法忘记曾经为军人的使命,孤身犯险冲回沦陷区,带领身陷屠杀中的同胞和难民,展开生死逃亡。随着斗争的持续,体内的狼性逐渐复苏,最终孤身闯入战乱区域,为同胞而战斗。");
jsonObject5.addProperty("director", "吴京");
jsonObject5.addProperty("price", 38);
jsonArray.add(jsonObject5);
for(int i=0;i
查看记录
ES 接口
获取Id为1的记录,如果Id不正确,就查不到数据,found字段就是false
Get 'http://localhost:9200/film/dongzuo/1
返回结果
{
"_index": "film",
"_type": "manhua",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"user": "张三",
"title": "工程师",
"desc": "数据库管理"
}
}
Java API 接口
根据id 获取文档
GetResponse response=client.prepareGet("film", "dongzuo", "1").get();
System.out.println(response.getSourceAsString());
删除记录
ES 接口
删除Id为1的记录
DELETE 'http://localhost:9200/film/dongzuo/1
返回结果
{
"found": true,
"_index": "film",
"_type": "manhua",
"_id": "1",
"_version": 2,
"result": "deleted",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
}
}
Java API
根据Id 删除文档
DeleteResponse response=client.prepareDelete("film", "dongzuo", "1").get();
更新记录
更新Id为1的记录
POST 'http://localhost:9200/film/dongzuo/1
{
"user": "李四",
"title": "工程师",
"desc": "数据库管理"
}
返回结果
{
"found": true,
"_index": "film",
"_type": "manhua",
"_id": "1",
"_version": 2,
"result": "deleted",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
}
}
Java API
根据Id修改文档
JsonObject jsonObject=new JsonObject();
jsonObject.addProperty("user", "李四");
jsonObject.addProperty("title", "工程师");
jsonObject.addProperty("desc", "数据库管理");
UpdateResponse response=client.prepareUpdate("film", "dongzuo", "1").setDoc(jsonObject.toString(), XContentType.JSON).get();
数据查询
条件查询
ES 接口
POST 'http://localhost:9200/accounts/person/_search'
{
"query" : { "match" : { "title" : "机器" }}
}
Java API
/**
*查询title字段保护 “战”,content字段包含 “星球”,两个条件同时成立
*
*/
@Test
public void searchMulti1(){
SearchRequestBuilder srb = client.prepareSearch("film").setTypes("dongzuo");
MatchPhraseQueryBuilder query1 = QueryBuilders.matchPhraseQuery("title", "战");
MatchPhraseQueryBuilder query2 = QueryBuilders.matchPhraseQuery("content", "星球");
SearchResponse sr = srb.setQuery(
QueryBuilders.boolQuery()
.must(query1)
.must(query2))
.execute().actionGet();
SearchHits hits = sr.getHits();
for (SearchHit hit : hits)
{
System.out.println(hit.getSourceAsString());
}
}
/**
* 查询title字段保护 “战”,content字段不包含 “武士”,两个条件同时成立
*/
@Test
public void searchMulti2(){
SearchRequestBuilder srb = client.prepareSearch("film").setTypes("dongzuo");
MatchPhraseQueryBuilder query1 = QueryBuilders.matchPhraseQuery("title", "战");
MatchPhraseQueryBuilder query2 = QueryBuilders.matchPhraseQuery("content", "武士");
SearchResponse sr = srb.setQuery(
QueryBuilders.boolQuery()
.must(query1)
.mustNot(query2))
.execute().actionGet();
SearchHits hits = sr.getHits();
for (SearchHit hit : hits)
{
System.out.println(hit.getSourceAsString());
}
}
/**
* 查询title字段包含 “战”或者 content字段包含 “军火”的记录
*/
@Test
public void searchMultiShould(){
SearchRequestBuilder srb = client.prepareSearch("film").setTypes("dongzuo");
MatchPhraseQueryBuilder query1 = QueryBuilders.matchPhraseQuery("title", "战");
MatchPhraseQueryBuilder query2 = QueryBuilders.matchPhraseQuery("content", "军火");
SearchResponse sr = srb.setQuery(
QueryBuilders.boolQuery()
.should(query1)
.should(query2))
.execute().actionGet();
SearchHits hits = sr.getHits();
for (SearchHit hit : hits)
{
System.out.println(hit.getSourceAsString());
}
}
/**
* 查询发布日期大于等于2017-12-19的记录
*/
@Test
public void searchRange()
{
SearchRequestBuilder srb = client.prepareSearch("film").setTypes("dongzuo");
RangeQueryBuilder query1 = QueryBuilders.rangeQuery("publishDate").gte("2017-12-19");
SearchResponse sr = srb.setQuery(
QueryBuilders.boolQuery()
.must(query1))
.execute().actionGet();
SearchHits hits = sr.getHits();
for (SearchHit hit : hits)
{
System.out.println(hit.getSourceAsString());
}
}
需要注意的是Java API中的查询是支持多个索引Index,多个Type同时查询
分页
ElasticSearch一次查询默认返回的是10条数据,我们可以通过size、from这两个参数指定进行实现分页。size表示取多少个记录,from表示偏移量
ES 接口
POST http://localhost:9200/film/dongzuo/
{
"query": {
"match": {
"content": "故事"
}
},
"from": 1,
"size": 5
}
Java API
@Test
public void searchByPage()
{
SearchRequestBuilder srb = client.prepareSearch("film").setTypes("dongzuo");
MatchAllQueryBuilder mqb = QueryBuilders.matchAllQuery();
SearchResponse sr = srb.setQuery(mqb).setFrom(1).setSize(5).execute().actionGet();
SearchHits hits = sr.getHits();
for (SearchHit hit :hits)
{
System.out.println(hit.getSourceAsString());
}
}
排序
ES排序可以通过指定sort字段进行排序 ES接口
POST http://localhost:9200/film/dongzuo/_search
{
"query": {
"match": {
"content": "故事"
}
},
"from": 1,
"size": 5,
"sort": [
{
"publishDate": {
"order": "desc"
}
}
]
}
Java API
@Test
public void searchByPageAndSort()
{
SearchRequestBuilder srb = client.prepareSearch("film").setTypes("dongzuo");
MatchAllQueryBuilder mqb = QueryBuilders.matchAllQuery();
//按照publishDate 进行降序排序
SearchResponse sr = srb.setQuery(mqb).setFrom(1).setSize(20).addSort("publishDate", SortOrder.DESC).execute().actionGet();
SearchHits hits = sr.getHits();
for (SearchHit hit :hits)
{
System.out.println(hit.getSourceAsString());
}
}
分组统计
ElasticSearch的Java API操作中我们可以通过Aggregation 进行多个字段分组统计,如下面的代码,按照publishDate字段 对数据分组,然后调用sum对每一个分组内的price字段进行合计
@Test
public void groupByDate()
{
SearchRequestBuilder srb = client.prepareSearch("film").setTypes("dongzuo");
TermsAggregationBuilder teamAgg = AggregationBuilders.terms("count").field("publishDate");
SumAggregationBuilder sumAgg = AggregationBuilders.sum("sumPrice").field("price");
teamAgg.subAggregation(sumAgg);
srb.addAggregation(teamAgg);
SearchResponse sr = srb.execute().actionGet();
Terms termsGroup = sr.getAggregations().get("count");
List extends Terms.Bucket> buckets = termsGroup.getBuckets();
for (Terms.Bucket b : buckets)
{
Aggregations aggregations = b.getAggregations();
InternalSum sumPrices = aggregations.get("sumPrice");
double value = sumPrices.getValue();
System.out.println(value);
}
}
权重查询
ElasticSearch中多字段搜索时,为了更精准的匹配到我们的数据,我们可以给相关字段加上查询权重,boost. 这些词的文档获得更高的相关度评分 _score ,也就是说,它们会出现在结果集的更上面
@Test
public void weightSearchTest()
{
SearchRequestBuilder srb = client.prepareSearch("company").setTypes("person");
MatchPhraseQueryBuilder mpq1 = QueryBuilders.matchPhraseQuery("title", "工程师");
MatchPhraseQueryBuilder mpq2 = QueryBuilders.matchPhraseQuery("desc", "工程师").boost(0.1f);
srb.setQuery(QueryBuilders.boolQuery().should(mpq1).should(mpq2)).setSize(3);
SearchResponse sr = srb.execute().actionGet();
SearchHits hits = sr.getHits();
hits.forEach(searchHitFields ->
{
System.out.println(searchHitFields.getSourceAsString());
});
}
地理位置计算
ElasticSearch给我们提供了方便的地理位置信息计算API,我们可以对地理位置进行距离计算,远近排序
@Test
public void geoDiastanceTest() {
SearchRequestBuilder srb = client.prepareSearch("hotel").setTypes("address");
//查询距离39.929986, 116.395645 1000km范围内的数据
GeoDistanceQueryBuilder gdr = QueryBuilders.geoDistanceQuery("location")
.point(new GeoPoint(39.929986, 116.395645))
.distance(1000, DistanceUnit.KILOMETERS);
srb.setQuery(gdr);
//按照距离远近进行排序
GeoDistanceSortBuilder sort = new GeoDistanceSortBuilder("location",new GeoPoint(39.929986, 116.395645));
sort.unit(DistanceUnit.KILOMETERS);//距离单位公里
sort.order(SortOrder.ASC);
sort.point(39.929986, 116.395645);//注意纬度在前,经度在后
srb.addSort(sort);
SearchResponse searchResponse = srb.execute().actionGet();
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHists = hits.getHits();
System.out.println("北京附近的城市(" + hits.getTotalHits() + "个):");
for (SearchHit hit : searchHists) {
String city = (String) hit.getSource().get("name");
String title = (String) hit.getSource().get("desc");
// 获取距离值,并保留两位小数点
BigDecimal geoDis = new BigDecimal((Double) hit.getSortValues()[0]);
Map hitMap = hit.getSource();
hitMap.put("geoDistance", geoDis.setScale(2, BigDecimal.ROUND_HALF_DOWN));
System.out.println(city + "距离北京" + hit.getSource().get("geoDistance") + DistanceUnit.KILOMETERS.toString() + "---" + title);
}
}
ik分词插件
安装
elasticsearch-analysis-ik 在ik的github上,我们可以找到对应ealstic版本的ik插件,进行下载安装
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v5.5.2/elasticsearch-analysis-ik-5.5.2.zip
分词查询
创建指定analysis的索引mapping,ik中常见的analyzer主要有ik_max_word、ik_smart 以中华人民共和国为例 ik_smart 切分为中华人民共和国 ik_max_word 切分为 中华人民共和国、中华人民、人民。。。
PUT localhost:9200/accounts
{
"mappings": {
"person": {
"properties": {
"user": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
},
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
},
"desc": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
}
}
}
}
}
Java API
@Test
public void searchPart() {
SearchRequestBuilder srb = client.prepareSearch("company").setTypes("person");
MatchQueryBuilder mqb = QueryBuilders.matchQuery("title", "资料worker");
SearchResponse sr = srb.setQuery(mqb.analyzer(ANALYZER))
.setFetchSource(new String[]{"desc"}, null)
.execute()
.actionGet();
SearchHits hits = sr.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
@Test
public void searchMultiPart()
{
SearchRequestBuilder srb = client.prepareSearch("company").setTypes("person");
MultiMatchQueryBuilder mqb = QueryBuilders.multiMatchQuery("java资料", "title", "desc");
SearchResponse sr = srb.setQuery(mqb.analyzer(ANALYZER))
.setFetchSource(new String[]{"desc","user","title"}, null)
.execute()
.actionGet();
SearchHits hits = sr.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
同义词查询
配置同义词是为了能够检索一个词的时候相关词也能够检索到。关联词和同义词可以合二为一配置在这个文件里。 新建同义词文件:在Elasticsearch的confg目录下新建文件夹analysis并在其下创建文件synonyms.txt
向synonyms.txt中添加一下内容,注意文档编码格式为utf-8
中国,中华人民共和国,china,中华
创建index:自定义分词器和过滤器并引用IK分词器
PUT http://localhost:9200/paper
{
"index": {
"analysis": {
"analyzer": {
"by_smart": {
"type": "custom",
"tokenizer": "ik_smart",
"filter": [
"by_tfr",
"by_sfr"
],
"char_filter": [
"by_cfr"
]
},
"by_max_word": {
"type": "custom",
"tokenizer": "ik_max_word",
"filter": [
"by_tfr",
"by_sfr"
],
"char_filter": [
"by_cfr"
]
}
},
"filter": {
"by_tfr": {
"type": "stop",
"stopwords": [
" "
]
},
"by_sfr": {
"type": "synonym",
"synonyms_path": "analysis/synonyms.txt"
}
},
"char_filter": {
"by_cfr": {
"type": "mapping",
"mappings": [
"| => |"
]
}
}
}
}
}
创建mapping
POST http://localhost:9200/paper/country
{
"mappings": {
"country": {
"dynamic": true,
"properties": {
"name": {
"type": "text",
"analyzer": "ik_synonym"
},
"area": {
"type": "text",
"analyzer": "ik_synonym",
"fielddata": true
},
"number": {
"type": "long"
},
"category_id": {
"type": "long"
}
}
}
}
}
分别插入中华、中华、中国。
PUT http://localhost:9200/paper/country
{
"name": "中华",
"area": "亚洲",
"number": 1,
"category_id": 1
}
搜索中华,将得到所有数据
POST http://localhost:9200/paper/country/_search
{
"query":
{
"match" :
{
"name" : "中华"
}
}
}
返回结果
{
"took": 6,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 0.51623213,
"hits": [{
"_index": "paper",
"_type": "country",
"_id": "AWap1Ztwn7iNxjanvOmr",
"_score": 0.51623213,
"_source": {
"name": "中华",
"area": "亚洲",
"number": 1,
"category_id": 1
}
}, {
"_index": "paper",
"_type": "country",
"_id": "AWap1XUrn7iNxjanvOmq",
"_score": 0.25811607,
"_source": {
"name": "中国",
"area": "亚洲",
"number": 1,
"category_id": 1
}
}]
}
}
权重排序
在日常的业务需求中,我们通常存在一种情况,需要通过某个字段计算得出的值进行排序,再ElasticSearch的Java API中,提供了ScriptSortBuilder,让我们可以通过编写脚本代码来进行逻辑计算,并按照计算结果进行排序。
@Test
public void searchOrderByCal()
{
SearchRequestBuilder srb = client.prepareSearch("movie").setTypes("grade");
MatchAllQueryBuilder mqb = QueryBuilders.matchAllQuery();
//isStar =true 得分1 isPlayed =true得分1
ScriptSortBuilder ssb = SortBuilders.scriptSort(new Script("doc['isStar'].value?doc['isPlayed'].value?2:1 : doc['isPlayed'].value?1:0"), ScriptSortBuilder.ScriptSortType.NUMBER).order(SortOrder.DESC);
SearchResponse sr = srb.setQuery(mqb).addSort(ssb).execute().actionGet();
sr.getHits().forEach(searchHitFields -> {
System.out.println(searchHitFields.getSourceAsString());
});
}