');}

ElasticSearch入门学习笔记

Author Avatar
Rico 9月 22, 2017

本文为慕课网ElasticSearch学习笔记,步骤均为亲手尝试实践

ElasticSearch简介

基于Apache Lucene构建的开源搜索引擎

采用Java编写,提供简单易用的RESTFul API

轻松的横向扩展,支持PB级的结构化或非结构化数据处理

可应用场景

  • 海量数据分析引擎
  • 站内搜索引擎
  • 数据仓库(利用ES强大的分布式存储能力)

一线公司实际应用场景

  • 英国卫报–实时分析公众对文章的回应
  • 维基百科,Github–站内实时搜索
  • 百度–实时日志监控平台

ES版本历史和选择

版本历史 1.x–>2.x–>5.x
版本选择:
es5.x支持lucene6,性能有大幅提升,磁盘空间相交之前也少了一半,查询性能提升了25%左右
2.x不被重视,也不会继续开发了
我们这里选择5.x

单实例安装

官网 https://www.elastic.co/

下载好之后

tar -zxvf elasticsearch-5.6.1.tar.gz
cd elasticsearch-5.6.1

可以看到如下文件结构

bin是存放es启动脚本的
config是存放es配置文件的目录
lib是它依赖的第三方库的目录
modules是它的模块目录
plugins是它的第三方插件的目录

使用命令./bin/elasticsearch来启动ES

started表示启动成功,地址是localhost:9200

访问http://localhost:9200
显示如下

es单实例安装完成

插件安装

实用Head插件安装

es返回的是json格式的,不是很友好,head插件提供了友好的web界面,同时还可以实现基本信息的查看,rest请求的模拟,以及数据的基本检索

head插件地址 https://github.com/mobz/elasticsearch-head

head要求node大于等于6.0

我们使用8.2.1版本的node

下载完成之后使用npm install --registry=https://registry.npm.taobao.org安装

使用命令npm run start 运行这个插件 然后访问http://localhost:9100

es和head插件是2个不同的程序,而且端口也不一样,他们2个之间互相访问存在跨域问题,所以这里改2个配置

vim config/elasticsearch.yml

在最下面加入2行

  1. http.cors.enabled: true #注意:后面有空格
  2. http.cors.allow-origin: "*" #注意:后面有空格

然后用后台方式启动es
./bin/elasticsearch -d

然后再启动head插件,访问localhost:9100

就可以看到head连接es成功了,而且健康值是绿色

健康值还有黄色和红色
黄色代表健康值不是很好,但是可以正常使用;red表示集群的情况很差了

后台启动的es如何关闭呢? 答:使用 kill -9

分布式安装

我们会建一个集群,3个节点,1个master ,2个slave

编辑conf/elasticsearch.yml

加入如下配置

  1. cluster.name: rico #指定集群的名字
  2. node.name: master #给master起一个名字
  3. node.master: true #告诉它它就是master
  4. network.host: 127.0.0.1 #绑定的IP ,端口不用改,默认就是9200

IP绑定为127.0.0.1之后就不能用localhost访问了,必须用127.0.0.1了

然后启动es和head
访问如下 ,可以看到集群名字和master已经变成rico和master了

我们新建2个目录,es_slave1es_slave2,然后把es的程序拷贝进去,然后解压

然后修改slave的es的配置

加入如下配置

  1. luster.name: rico #这个必须与master保证一致
  2. node.name: slave1 #给这个节点起一个名字
  3. network.host: 127.0.0.1 #绑定IP
  4. http.port: 8200 ## 不能和master的端口冲突 默认不配就是9200和master重了
  5. discovery.zen.ping.unicast.hosts: ["127.0.0.1"] #这个配置的作用是找到master,如果不加这个配置的话,这个slave会游离于集群之外,找不到集群的master的

slave可以不加

  1. http.cors.enabled: true
  2. http.cors.allow-origin: "*"

先后启动slave1和slave2,然后我们访问head插件地址就可以看到下图

我们可以看到一个master(五角星),2个slave(圆点)

从这里可以看到,我们的es扩容是非常容易的

基础概念

每个集群都有一个唯一的名字,默认叫elasticsearch,所有节点都是通过集群名字来加入集群的, 每一个节点还有它自己的名字

节点能够存储数据,参与集群索引数据,搜索数据的独立服务

通常我们定义有相同字段的文档作为一个类型
文档是es里面最小的存储单位
索引在es中是通过名字来识别的,而且必须是英文字母小写且不含中划线,我们都是通过这个名字来对索引进行增删查改的操作

索引相当于数据库里的database ,类型相当于table,文档相当于sql里的一行记录

举个例子:
一个信息查询系统,用ES来做存储,里面的数据可以分为各种各样的索引,例如图书索引,家具索引,汽车索引。 图书索引又可以分为各种类型,例如科普类,计算机类,小说类等,具体到每一本书就是文档

为何要分片?

假设一个索引的数据量很大,就会造成硬盘的压力很大,同时,搜索速度也会出现瓶颈,这时就可以将索引分成多个分片,分摊压力,分片的同时还允许用户进行水平的拆分,和分布式的操作,可以提高搜索和其他操作的效率

备份有哪些好处?
当一个主分片失败,或者出现问题时,备份的分片就可以代替工作,从而提高了es的可用性,备份的分片还可以执行搜索操作,以分摊搜索的压力,es默认在创建索引时会创建5个分片,1份备份,这个数量是可以修改的,分片的数量只能在创建索引时指定,而不能再后期进行修改,而备份可以修改

索引创建

RESTFul API

创建索引

非结构化创建

我们使用head插件来新建索引

粗方框的是主分片,西细方框的是备份分片

如何知道一个索引是结构化还是非结构化的?

点击查看索引信息,mappings如果是空的就是非结构化的

结构化创建

如何建立结构化的索引呢?
点击复合索引
输入book索引,加一个类型novel,小说,接着加一个关键词_mappings给它加一个映射
内容写json结构体,也就是它的映射结构,

  1. {
  2. "novel": {//novel是类型前缀,
  3. //然后在这里定义它的结构
  4. "properties": {//关键词properties,在这里定义它的每一个字段
  5. "title": {//自定义的 标题
  6. "type": "text"//给title定义类型 type:text 表示是文本
  7. }
  8. }
  9. }
  10. }

结果是”acknowledged”:true表示成功


这时我们看到mappings里面有内容了,有一个novel小说,它里面是有结构映射的

接下来我们使用工具POSTMan ,它是一个http模拟器


新的索引名 people

  1. {
  2. "settings":{//关键词settings,指定索引的配置
  3. "number_of_shards":3,//指定索引的分片数
  4. "number_of_replicas":1//指定备份数
  5. },
  6. "mappings":{//索引映射定义
  7. "man":{//加一个类型 man
  8. "properties":{//属性
  9. "name":{//名字
  10. "type":"text"//类型是文本类型
  11. },
  12. "county":{//国家
  13. "type":"keyword"//国家是不可切分的,是一个关键词
  14. },
  15. "age":{//年龄
  16. "type":"integer"//类型是int
  17. },
  18. "date":{//出生日期
  19. "type":"date",//格式是date
  20. "format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"//多种类型定义 年月日 小时分秒和年月日和时间戳
  21. }
  22. }
  23. },
  24. "woman":{
  25. }
  26. }
  27. }

去head插件里看一下 ,可以看到有people了,点击索引信息,可以看到格式映射是完全按照我们自己定义的

数据插入

  • 指定文档id插入
  • 自动产生文档id插入

文档id是唯一索引值,指向文档数据

采用put方式,people是索引,man是类型,1是我们给他指定的一个id,也就是文档id
返回结果和我们的信息也是对应的,index是people,type是man,id是1

我们回head插件看到docs是1了,也就是我们刚插入的数据成功了

我们浏览数据

id是我们自己指定的

接下来我们看如何让es在我们插入文档的时候自动产生id

采用post方式,url不要写id,这样es就会产生id

修改

  • 直接修改文档
  • 脚本修改文档

直接修改文档

127.0.0.1:9200/people/man/1/_update

方式是post,1表示我们要修改的文档的id,_update是关键词,表示我们要对文档进行修改

  1. {
  2. "doc":{//关键词,要修改的文本就写在doc里面
  3. "name":"HogwartsRico"
  4. }
  5. }

我们查看head插件,可以看到数据已经修改了

脚本修改

  1. {
  2. "script":{//script关键字 es以脚本的方式去运行修改
  3. "lang":"painless",//指定脚本语言,es支持很多的脚本语言, painless是内置的脚本语言,还支持js,python,groovy等脚本语言,这里我们选择es内置的painless
  4. "inline":"ctx._source.age+=10"//inline指定脚本内容,ctx代表es上下文,_source代表es当前的文档
  5. }
  6. }

可以看到结果修改了

我们还可以将参数放到外面

删除

删除文档

这里我们删除id为1的文档
使用delete方式

删除索引

使用head插件删除

这里有个确认操作,因为删除操作是很危险的,当我们删除索引时,索引和所有数据都会被删除

使用rest删除

删除操作非常危险,谨慎操作

查询

我们先造几个模拟数据

  1. {
  2. "settings":{"number_of_shards": 3,"number_of_replicas": 1},
  3. "mappings":{
  4. "novel":{
  5. "properties":{
  6. "word_count":{"type":"integer"},
  7. "author":{"type":"keyword"},
  8. "title":{"type":"text"},
  9. "publish_date":{"type":"date","format":"yyyy-MM-dd HH:mm:ss || yyyy-MM-dd || epoch_millis"}
  10. }
  11. }
  12. }
  13. }
  1. {
  2. "word_count":50000,
  3. "title":"菜谱",
  4. "author":"abc",
  5. "publish_date":"2017-09-21"
  6. }

等等

简单查询

查询id唯一的文档

127.0.0.1:9200/book/novel/1

条件查询

  1. {
  2. "query":{//es所有查询都是以query为关键词的
  3. "match_all":{}//查询所有的数据
  4. }
  5. }

took:2表示这个查询花2毫秒
hits代表响应结果
hits的total是8,表示总共有8条数据,hits默认返回10条数据
如果size不指定,那么默认返回10条
_shards:描述了查询分片的信息,查询了多少个分片、成功的分片数量、失败的分片数量等
_score是文档的分数信息,与排名相关度有关,参考各大搜索引擎的搜索结果,就容易理解

如何指定返回的数据大小和从哪里返回呢?

  1. {
  2. "query":{
  3. "match_all":{}
  4. },
  5. "from":1,//返回第二条数据(从0开始数)
  6. "size":1
  7. }

文档的顺序是按照_score默认倒排的
如何将查询变成关键词查询呢?

这里的返回结果是以_score默认倒排的,如何自定义自己的排序呢?

  1. {
  2. "query":{
  3. "match":{"title":"java"}
  4. },
  5. "sort":[//指定需要排序的字段
  6. {
  7. "publish_date":{//publish_date作为排序字段
  8. "order":"desc"//默认是升序的,我们改成倒序
  9. }
  10. }
  11. ]
  12. }

返回结果

我们发现score这个字段变成null了,是因为我们指定了排序规则,

下面的高级查询会做更多关于查询的介绍

聚合查询

单个分组聚合

  1. {
  2. "aggs":{//aggs是聚合查询的关键词
  3. "group_by_word_count":{//给聚合条件起一个名字,这个名字是自定义的,可以随便起
  4. "terms":{//关键词
  5. "field":"word_count"//指定聚合的字段
  6. }
  7. }
  8. }
  9. }

结果会将所有数据返回并在结尾返回聚合后的数据

多个分组聚合

  1. {
  2. "aggs":{
  3. "group_by_word_count":{
  4. "terms":{
  5. "field":"word_count"
  6. }
  7. },
  8. "group_by_publish_date":{
  9. "terms":{
  10. "field":"publish_date"
  11. }
  12. }
  13. }
  14. }

es的其他功能函数

  1. {
  2. "aggs":{
  3. "grades_word_count":{
  4. "stats":{//stats表示进行下计算
  5. "field":"word_count"
  6. }
  7. }
  8. }
  9. }

结果

count总共有12个数 min最小值 avg 平均值 sum总和

也直接支持这里面的函数

  1. {
  2. "aggs":{
  3. "grades_word_count":{
  4. "min":{
  5. "field":"word_count"
  6. }
  7. }
  8. }
  9. }

SpringBook集成ES

ESConfig.java

  1. @Configuration
  2. public class ESConfig {
  3. @Bean
  4. public TransportClient client() throws UnknownHostException {
  5. InetSocketTransportAddress node=new InetSocketTransportAddress(
  6. InetAddress.getByName("localhost"),//address
  7. 9300//tcp端口
  8. );
  9. //设置自定义配置
  10. Settings settings = Settings.builder().put("cluster.name","rico").build();//集群名
  11. TransportClient client=new PreBuiltTransportClient(settings);//这里需要传入es的配置
  12. client.addTransportAddress(node);
  13. return client;
  14. }
  15. }

增加和查询

Controller.java

  1. @Autowired
  2. private TransportClient client;
  3. //获取图书的功能
  4. @GetMapping("/get/book/novel")
  5. @ResponseBody
  6. public ResponseEntity getBookNovel(@RequestParam(name="id",defaultValue = "") String id){
  7. if(id.isEmpty()){
  8. return new ResponseEntity(HttpStatus.NOT_FOUND);
  9. }
  10. GetResponse result= this.client.prepareGet("book","novel",id).get();//索引,类型 prepareGet获取索引
  11. if(result.isExists()){
  12. return new ResponseEntity(result.getSource(), HttpStatus.OK);
  13. }else{
  14. return new ResponseEntity(HttpStatus.NOT_FOUND);
  15. }
  16. //http://localhost:8080/get/book/novel?id=2
  17. }
  18. //增加图书的功能
  19. @PostMapping("add/book/novel")
  20. @ResponseBody
  21. public ResponseEntity add(@RequestParam(name="title") String title,
  22. @RequestParam(name="author") String author,
  23. @RequestParam(name="word_count") int wordCount,
  24. @RequestParam(name="publish_date") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date publish_date){
  25. XContentBuilder content= null;
  26. try {
  27. content = XContentFactory.jsonBuilder().startObject().field("title",title).field("author",author).field("word_count",wordCount).field("publish_date",publish_date.getTime()).endObject();
  28. } catch (IOException e) {
  29. e.printStackTrace();
  30. return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
  31. }
  32. IndexResponse result= this.client.prepareIndex("book","novel").setSource(content).get();//prepareIndex准备构建索引 book是索引名 nove是type
  33. return new ResponseEntity(result.getId(),HttpStatus.OK);
  34. }


删除

  1. @DeleteMapping("delete/book/novel")
  2. @ResponseBody
  3. public ResponseEntity delete(@RequestParam(name="id") String id){
  4. DeleteResponse result=this.client.prepareDelete("book","novel",id).get();
  5. return new ResponseEntity(result,HttpStatus.OK);
  6. }

更新

  1. @PutMapping("update/book/novel")
  2. @ResponseBody
  3. public ResponseEntity update(
  4. @RequestParam(name="id") String id,
  5. @RequestParam(name="title",required = false) String title,
  6. @RequestParam(name="author",required = false) String author,
  7. @RequestParam(name="word_count",required = false) Integer word_count,
  8. @RequestParam(name="publish_date",required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date publish_date
  9. ){
  10. UpdateRequest update=new UpdateRequest("book","novel",id);
  11. XContentBuilder content=null;
  12. try {
  13. content=XContentFactory.jsonBuilder().startObject();
  14. if(title!=null){
  15. content.field("title",title);
  16. }
  17. if(author!=null){
  18. content.field("author",author);
  19. }
  20. if(word_count!=null){
  21. content.field("word_count",word_count);
  22. }
  23. if(publish_date!=null){
  24. content.field("publish_date",publish_date.getTime());
  25. }
  26. content.endObject();//必须以endObject结尾
  27. update.doc(content);
  28. try {
  29. UpdateResponse result=this.client.update(update).get();
  30. return new ResponseEntity(result,HttpStatus.OK);
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
  34. } catch (ExecutionException e) {
  35. e.printStackTrace();
  36. return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
  37. }
  38. } catch (IOException e) {
  39. e.printStackTrace();
  40. return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
  41. }
  42. }
  43. //一般情况下,我们如果要在表单中上传文件,一般会将form的enctype参数设置为multipart/form-data。这种方式只支持POST的请求方式。
  44. //put的话就用 x-www-form-urlencoded

符合查询接口

  1. @PostMapping("query/book/novel")
  2. @ResponseBody
  3. public ResponseEntity query(
  4. @RequestParam(name="author",required = false) String author,
  5. @RequestParam(name="title",required = false) String title,
  6. @RequestParam(name="gt_word",defaultValue = "0") Integer gt_word,//大于多少字
  7. @RequestParam(name="lt_word",required = false) Integer lt_word//小于多少
  8. ){
  9. BoolQueryBuilder boolQuery= QueryBuilders.boolQuery();
  10. if(author!=null){
  11. boolQuery.must(QueryBuilders.matchQuery("author",author));
  12. }
  13. if(title!=null){
  14. boolQuery.must(QueryBuilders.matchQuery("title",title));
  15. }
  16. RangeQueryBuilder rangeQueryBuilder=QueryBuilders.rangeQuery("word_count").from(gt_word);
  17. if(lt_word!=null&&lt_word>0){
  18. rangeQueryBuilder.to(lt_word);
  19. }
  20. boolQuery.filter(rangeQueryBuilder);
  21. SearchRequestBuilder builder=this.client.prepareSearch("book")
  22. .setTypes("novel")
  23. .setSearchType(SearchType.DEFAULT.DFS_QUERY_THEN_FETCH)
  24. .setQuery(boolQuery)
  25. .setFrom(0).setSize(10);//当你的数据量足够多的时候,可以用QUERY_THEN_FETCH
  26. System.out.println(builder);//直接打印请求体
  27. SearchResponse response=builder.get();
  28. List<Map<String,Object>> result=new ArrayList<Map<String,Object>>();
  29. for(SearchHit hit:response.getHits()){
  30. result.add(hit.getSource());
  31. }
  32. return new ResponseEntity(result,HttpStatus.OK);
  33. }


This blog is under a CC BY-NC-SA 3.0 Unported License
本文链接:http://hogwartsrico.github.io/2017/09/22/Introduction-of-ElasticSearch/