本文参考:

https://blog.csdn.net/qq_26803795/article/details/106423578(csdn)
https://blog.csdn.net/lisen01070107/article/details/108288037(bilibili狂神说视频笔记)

ElasticSearch在实际生产里通常和LogStash,Kibana,FileBeat一起构成Elastic Stack来使用,它是这些组件里面最核心的一个。因此学好ElasticSearch的必要性不言而喻,但是由于ElasticSearch官方更新太过频繁且文档陈旧,同时在Linux下安装配置的过程较繁杂,不利于入门使用。
ElasticSearch简称为ES。

ElasticSearch简介

ES的概念及使用场景

ElasticSearch是一个分布式,高性能、高可用、可伸缩、RESTful 风格的搜索和数据分析引擎。通常作为Elastic Stack(ELK)的核心来使用,Elastic Stack大致是如下这样组成的:
ELK:收集清洗数据(Logstash) ==> 搜索、存储(ElasticSearch) ==> 展示(Kibana)
ElasticSearch详解 - 图1

ES是一个近实时(NRT)的搜索引擎,一般从添加数据到能被搜索到只有很少的延迟(大约是1s),而查询数据是实时的。一般我们可以把ES配合logstash,kibana来做日志分析系统,或者是搜索方面的系统功能,比如在网上商城系统里实现搜索商品的功能也会用到ES。
**
疑问:搜索商品的时候为啥要用ES呢?用sql的like进行模糊查询,它不香吗?
我们假设一个场景:我们要买苹果吃,咱们想买天水特产的花牛苹果,然后在搜索框输入天水花牛苹果,这时候咱们希望搜索到所有的售卖天水花牛苹果的商家,但是如果咱们技术上根据这个天水花牛苹果使用sql的like模糊查询,是不能匹配到诸如天水特产花牛苹果,天水正宗,果园直送精品花牛苹果这类的不连续的店铺的。所以sql的like进行模糊查询来搜索商品还真不香!

ES的应用

  • Elasticsearch是一个实时分布式搜索和分析引擎。 它让你以前所未有的速度处理大数据成为可能。
  • 它用于 全文搜索、结构化搜索、分析以及将这三者混合使用:
  • 维基百科使用Elasticsearch提供全文搜索并高亮关键字, 以及输入实时搜索(search-asyou-type)和搜索纠错(did-you-mean)等搜索建议功能。
  • 英国卫报使用Elasticsearch结合用户日志和社交网络数据提供给他们的编辑以实时的反馈,以便及时了解公众对新发表的文章的回应。
  • StackOverflow结合全文搜索与地理位置查询,以及more-like-this功能来找到相关的问题和答案。
  • Github使用Elasticsearch检索1300亿行的代码。
  • 但是Elasticsearch不仅用于大型企业,它还让像DataDog以及Klout这样的创业公司将最初的想法变成可扩展的解决方案。
  • Elasticsearch可以在你的笔记本上运行,也可以在数以百计的服务器上处理PB级别的数据。
  • Elasticsearch是一个基于Apache Lucene™的开源搜索引擎。无论在开源还是专有领域, Lucene可被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。
  • 但是, Lucene只是一个库。 想要使用它,你必须使用Java来作为开发语言并将其直接集成到你的应用中,更糟糕的是, Lucene非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。
  • Elasticsearch也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。

    ES的特性、优缺点(与Solar对比)

  1. 当单纯的对已有数据进行搜索时,Solr更快
  2. 当实时建立索引时,Solr会产生io阻塞,查询性能较差,ElasticSearch具有明显的优势
  3. 随着数据量的增加,Solr的搜索效率会变得更低,而ElasticSearch却没有明显的变化
  4. 转变我们的搜索基础设施后从Solr ElasticSearch,我们看见一个即时~ 50x提高搜索性能!

总结
1、es基本是开箱即用(解压就可以用) ,非常简单。Solr安装略微复杂一丢丢!
2、Solr 利用Zookeeper进行分布式管理,而Elasticsearch 自身带有分布式协调管理功能 。
3、Solr 支持更多格式的数据,比如JSON、XML、 CSV , 而Elasticsearch仅支持json文件格式。
4、Solr 官方提供的功能更多,而Elasticsearch本身更注重于核心功能,高级功能多有第三方插件提供,例如图形化界面需要kibana友好支撑
5、Solr 已有查询快,但更新索引时慢(即插入删除慢) ,用于电商等查询多的应用;
ES建立索引快(即查询慢) ,即实时性查询快,用于facebook新浪等搜索。
Solr是传统搜索应用的有力解决方案,但Elasticsearch更适用于新兴的实时搜索应用。
6、Solr比较成熟,有一个更大,更成熟的用户、开发和贡献者社区,而Elasticsearch相对开发维护者较少,更新太快,学习使用成本较高。

基本概念

ES和mysql相关的基本概念的对比表格,先看一下:

ES MySql
字段(colunms)
文档 (doc) 一行数据
类型(type) 已废弃
索引(index) 数据库

ES里的数据其实就是指索引下的类型里面的JSON格式的数据。ES只支持JSON格式。

文档(Document)

我们知道Java是面向对象的,而Elasticsearch是面向文档的,也就是说文档是所有可搜索数据的最小单元。ES的文档就像MySql中的一条记录,只是ES的文档会被序列化成json格式,保存在Elasticsearch中;

  • 这个json对象是由字段组成,字段就相当于Mysql的列,每个字段都有自己的类型(字符串、数值、布尔、二进制、日期范围类型);
  • 当我们创建文档时,如果不指定字段的类型,Elasticsearch会帮我们自动匹配类型;
  • 每个文档都有一个ID,类似MySql的主键,咱们可以自己指定,也可以让Elasticsearch自动生成;
  • 文档的json格式支持数组/嵌套,在一个索引(数据库)或类型(表)里面,你可以存储任意多的文档。

注意:虽然在实际存储上,文档存在于某个索引里,但是文档必须被赋予一个索引下的类型才可以。

类型(Type)

类型就相当于MySql里的表,我们知道MySql里一个库下可以有很多表,最原始的时候ES也是这样,一个索引下可以有很多类型,但是从6.0版本开始,type已经被逐渐废弃,但是这时候一个索引仍然可以设置多个类型,一直到7.0版本开始,一个索引就只能创建一个类型了(_doc)。这一点,大家要注意,网上很多资料都是旧版本的,没有对这点进行说明。

索引(Index)

  • 索引就相当于MySql里的数据库,它是具有某种相似特性的文档集合。反过来说不同特性的文档一般都放在不同的索引里;
  • 索引的名称必须全部是小写;
  • 在单个集群中,可以定义任意多个索引;
  • 索引具有mapping和setting的概念,mapping用来定义文档字段的类型,setting用来定义不同数据的分布。

索引是映射类型的容器, elasticsearch中的索引是一个非常大的文档集合。索|存储了映射类型的字段和其他设置。然后它们被存储到了各个分片上了。我们来研究下分片是如何工作的。
物理设计: 节点和分片如何工作?
一个集群至少有一 个节点,而一个节点就是一-个elasricsearch进程 ,节点可以有多个索引默认的,如果你创建索引,那么索引将会有个5个分片( primary shard ,又称主分片)构成的,每一个主分片会有-一个副本( replica shard ,又称复制分片)
ElasticSearch详解 - 图2
上图是一个有3个节点的集群, 可以看到主分片和对应的复制分片都不会在同-个节点内,这样有利于某个节点挂掉了,数据也不至于丢失。实际上, 一个分片是- -个Lucene索引, -一个包含倒排索引的文件目录,倒排索引的结构使得elasticsearch在不扫描全部文档的情况下,就能告诉你哪些文档包含特定的关键字。不过,等等,倒排索引是什么鬼?(详情见倒排索引

倒排索引

这种结构适用于快速的全文搜索,一个索引由文档中所有不重复的列表构成,对于每一个词,都有一个包含它的文档列表
例如,现在有两个文档,每个文档包含如下内容:
Study every day, good good up to forever # 文档1包含的内容
To forever, study every day,good good up # 文档2包含的内容
为创建倒排索引,我们首先要将每个文档拆分成独立的词(或称为词条或者tokens) ,然后创建一一个包含所有不重 复的词条的排序列表,然后列出每个词条出现在哪个文档:

term doc_1 doc_2
Study x
To x x
every
forever
day
study x
good
every
to x
up

现在,我们试图搜索 to forever,只需要查看包含每个词条的文档。
两个文档都匹配, 但是第一个文档比第二个匹配程度更高。如果没有别的条件,现在,这两个包含关键字的文档都将返回。

再来看一个示例, 比如我们通过博客标签来搜索博客文章。那么倒排索引列表就是这样的一个结构:

博客文章(原始数据) 博客文章(原始数据) 索引列表(倒排索引) 索引列表(倒排索引)
博客文章ID 标签 标签 博客文章ID
1 python python 1,2,3
2 python linux 3,4
3 linux,python
4 linux

如果要搜索含有python标签的文章, 那相对于查找所有原始数据而言,查找倒排索引后的数据将会快的多。只需要查看标签这一栏,然后获取相关的文章ID即可。完全过滤掉无关的所有数据,提高效率!
一个elasticsearch索引是由多 个Lucene索引组成的。别问为什么,谁让elasticsearch使用Lucene作为底层呢

节点(node)

  • 一个节点就是一个ES实例,其实本质上就是一个java进程;
  • 节点的名称可以通过配置文件配置,或者在启动的时候使用-E node.name=ropledata指定,默认是随机分配的。建议咱们自己指定,因为节点名称对于管理目的很重要,咱们可以通过节点名称确定网络中的哪些服务器对应于ES集群中的哪些节点;
  • ES的节点类型主要分为如下几种:

Master Eligible节点:每个节点启动后,默认就是Master Eligible节点,可以通过设置node.master: false 来禁止。Master Eligible可以参加选主流程,并成为Master节点(当第一个节点启动后,它会将自己选为Master节点);注意:每个节点都保存了集群的状态,只有Master节点才能修改集群的状态信息。
Data节点:可以保存数据的节点。主要负责保存分片数据,利于数据扩展。
Coordinating 节点:负责接收客户端请求,将请求发送到合适的节点,最终把结果汇集到一起。
注意:每个节点默认都起到了Coordinating node的职责。一般在开发环境中一个节点可以承担多个角色,但是在生产环境中,还是设置单一的角色比较好,因为有助于提高性能。

分片(shard)

elasticsearch在后台把每个索引划分成多个分片。每个分片可以在集群中的不同服务器间迁移。一个人就是一个集群! ,即启动的ElasticSearch服务,默认就是一个集群,且默认集群名为elasticsearch。了解分布式或者学过mysql分库分表的应该对分片的概念比较熟悉,ES里面的索引可能存储大量数据,这些数据可能会超出单个节点的硬件限制。
为了解决这个问题,ES提供了将索引细分为多个碎片的功能,这就是分片。这里咱们可以简单去理解,在创建索引时,只需要咱们定义所需的碎片数量就可以了,其实每个分片都可以看作是一个完全功能性和独立的索引,可以托管在集群中的任何节点上。
疑问:分片有什么好处和注意事项呢?

  1. 通过分片技术,咱们可以水平拆分数据量,同时它还支持跨碎片(可能在多个节点上)分布和并行操作,从而提高性能/吞吐量;
  2. ES可以完全自动管理分片的分配和文档的聚合来完成搜索请求,并且对用户完全透明;
  3. 主分片数在索引创建时指定,后续只能通过Reindex修改,但是较麻烦,一般不进行修改。

    副本分片(replica shard)

    熟悉分布式的朋友应该对副本对概念不陌生,为了实现高可用、遇到问题时实现分片的故障转移机制,ElasticSearch允许将索引分片的一个或多个复制成所谓的副本分片。
    疑问:副本分片有什么作用和注意事项呢?
  • 当分片或者节点发生故障时提供高可用性。因此,需要注意的是,副本分片永远不会分配到复制它的原始或主分片所在的节点上;
  • 可以提高扩展搜索量和吞吐量,因为ES允许在所有副本上并行执行搜索;
  • 默认情况下,ES中的每个索引都分配5个主分片,并为每个主分片分配1个副本分片。主分片在创建索引时指定,不能修改,副本分片可以修改。

    极速安装配置

    咱们如果想很爽的使用ES,需要安装3个东西:ES、Kibana、ElasticSearch Head。通过Kibana可以对ES进行便捷的可视化操作,通过ElasticSearch Head可以查看ES的状态及数据,可以理解为ES的图形化界面。
    咱们做开发的应该把时间花在刀刃上,而不是花费大量时间去安装配置。用docker启动前辈们已经配置好的ES环境就可以了!

    用Docker安装ES+Kibana

  1. 搜索docker镜像库里可用的ES镜像

docker search elasticsearch
image.png
可以看到,stars排名第一的是官方的ES镜像,第二是大牛已经融合了ES7.7和Kibana7.7的镜像,那咱们就用第二个了。

  1. 把这个镜像从镜像库拉下来

docker pull nshou/elasticsearch-kibana
image.png

  1. 最后咱们把镜像启动为容器就可以了,端口映射保持不变,咱们给这个容器命名为eskibana,到这里ES和Kibana就安装配置完成了!节省大把时间放到开发上来

docker run -d -p 9200:9200 -p 9300:9300 -p 5601:5601 --name eskibana nshou/elasticsearch-kibana
9200 是ES节点与外部通讯使用的端口。它是http协议的RESTful接口(各种CRUD操作都是走的该端口,如查询:http://localhost:9200/user/_search)
9300是ES节点之间通讯使用的端口。它是tcp通讯端口,集群间和TCPclient都走的它。(java程序中使用ES时,在配置文件中要配置该端口)
5601是kibana节点与外部通讯使用的接口。

安装ElasticSearch Head(浏览器版)

  1. 咱们还需要安装ElasticSearch Head,它相当于是ES的图形化界面,这个更简单,它是一个浏览器的扩展程序,直接在chrome浏览器扩展程序里下载安装即可:

    1. 打开chrome浏览器,在扩展程序chrome应用商店那里,搜索elasticsearch:image.png

    2. 选择ElasticSearch Head,点击添加至Chrome,进行扩展程序的安装即可:

image.png
到这里咱们的ES、Kibana、ElasticSearch Head都已经安装完成了,下面咱们验证一下,看是否安装成功!
验证ES:http://宿主机IP:9200/ (如本地:http://127.0.0.1:9200/
image.png
验证Kibana:http://宿主机IP:5601/
image.png
image.png
image.png
image.png

验证ES Head: 这个更简单,只需要点击之前咱们安装的那个扩展程序图标就可以了。点击信息(info),还可以看到集群或者索引的信息,很方便,大家没事可以玩一玩,熟悉一下:
image.png

安装IK分词器(elasticsearch插件)

IK分词器:中文分词器

分词:即把一段中文或者别的划分成一个个的关键字,我们在搜索时候会把自己的信息进行分词,会把数据库中或者索引库中的数据进行分词,然后进行一一个匹配操作,默认的中文分词是将每个字看成一个词(不使用用IK分词器的情况下),比如“我爱狂神”会被分为”我”,”爱”,”狂”,”神” ,这显然是不符合要求的,所以我们需要安装中文分词器ik来解决这个问题。
IK提供了两个分词算法: (ik_smart和ik_max_word )

  • ik_smart为最少切分
  • ik_max_word为最细粒度划分!

下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases
安装参考:https://blog.csdn.net/liboyang71/article/details/78553634(Linux 安装Elasticsearch和配置ik分词器步骤)
【ik_smart】测试:

  1. GET _analyze
  2. {
  3. "analyzer": "ik_smart",
  4. "text": "我是社会主义接班人"
  5. }
  6. //输出
  7. {
  8. "tokens" : [
  9. {
  10. "token" : "我",
  11. "start_offset" : 0,
  12. "end_offset" : 1,
  13. "type" : "CN_CHAR",
  14. "position" : 0
  15. },
  16. {
  17. "token" : "是",
  18. "start_offset" : 1,
  19. "end_offset" : 2,
  20. "type" : "CN_CHAR",
  21. "position" : 1
  22. },
  23. {
  24. "token" : "社会主义",
  25. "start_offset" : 2,
  26. "end_offset" : 6,
  27. "type" : "CN_WORD",
  28. "position" : 2
  29. },
  30. {
  31. "token" : "接班人",
  32. "start_offset" : 6,
  33. "end_offset" : 9,
  34. "type" : "CN_WORD",
  35. "position" : 3
  36. }
  37. ]
  38. }

【ik_max_word】测试:

  1. GET _analyze
  2. {
  3. "analyzer": "ik_max_word",
  4. "text": "我是社会主义接班人"
  5. }
  6. //输出
  7. {
  8. "tokens" : [
  9. {
  10. "token" : "我",
  11. "start_offset" : 0,
  12. "end_offset" : 1,
  13. "type" : "CN_CHAR",
  14. "position" : 0
  15. },
  16. {
  17. "token" : "是",
  18. "start_offset" : 1,
  19. "end_offset" : 2,
  20. "type" : "CN_CHAR",
  21. "position" : 1
  22. },
  23. {
  24. "token" : "社会主义",
  25. "start_offset" : 2,
  26. "end_offset" : 6,
  27. "type" : "CN_WORD",
  28. "position" : 2
  29. },
  30. {
  31. "token" : "社会",
  32. "start_offset" : 2,
  33. "end_offset" : 4,
  34. "type" : "CN_WORD",
  35. "position" : 3
  36. },
  37. {
  38. "token" : "主义",
  39. "start_offset" : 4,
  40. "end_offset" : 6,
  41. "type" : "CN_WORD",
  42. "position" : 4
  43. },
  44. {
  45. "token" : "接班人",
  46. "start_offset" : 6,
  47. "end_offset" : 9,
  48. "type" : "CN_WORD",
  49. "position" : 5
  50. },
  51. {
  52. "token" : "接班",
  53. "start_offset" : 6,
  54. "end_offset" : 8,
  55. "type" : "CN_WORD",
  56. "position" : 6
  57. },
  58. {
  59. "token" : "人",
  60. "start_offset" : 8,
  61. "end_offset" : 9,
  62. "type" : "CN_CHAR",
  63. "position" : 7
  64. }
  65. ]
  66. }

Rest风格说明

它是一种软件架构风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁更有层次更易于实现缓存等机制。

1.基本Rest命令说明:

method url地址 描述
PUT(创建,修改) 宿主机ip:9200/索引名称/类型名称/文档id 创建文档(指定文档id)
POST(创建) 宿主机ip:9200/索引名称/类型名称 创建文档(随机文档id)
POST(修改) 宿主机ip:9200/索引名称/类型名称/文档id/_update 修改文档
DELETE(删除) 宿主机ip:localhost:9200/索引名称/类型名称/文档id 删除文档
GET(查询) 宿主机ip:localhost:9200/索引名称/类型名称/文档id 查询文档通过文档ID
POST(查询) 宿主机ip:localhost:9200/索引名称/类型名称/文档id/_search 查询所有数据

2、关于索引的基本操作

1.创建一个索引

PUT /索引名/类型名(高版本都不写了,都是_doc)/文档id
{请求体}
image.png

完成了自动添加了索引!数据也成功的添加了。
ElasticSearch详解 - 图14
那么name这个字段用不用指定类型呢?
ElasticSearch详解 - 图15
指定字段的类型properties 就比如sql创表。获得这个规则!可以通过GET请求获得具体的信息
ElasticSearch详解 - 图16
如果自己不设置文档字段类型,那么es会自动给默认类型
ElasticSearch详解 - 图17

2.cat命令

*获取健康值
ElasticSearch详解 - 图18
image.png
还有很多 可以自动展示 都试试
*修改索引
1.修改我们可以还是用原来的PUT的命令,根据id来修改
ElasticSearch详解 - 图20
但是如果没有填写的字段 会重置为空了 ,相当于java接口传对象修改,如果只是传id的某些字段,那其他没传的值都为空了。
2.还有一种update方法 这种不设置某些值 数据不会丢失

  1. //局部更新【推荐】。不会将不修改的值清除,不会丢失数据。且(内容不变的情况)仅第一次执行version会变化,之后无论执行多少次都不会变化
  2. POST /test3/_doc/1/_update
  3. {
  4. "doc":{
  5. "name":"帅"
  6. }
  7. }
  8. //全局更新。下面两种都会将不修改的值清空,会丢失数据。且(无论内容是否变化)执行一次version都会+1
  9. POST /test3/_doc/1
  10. {
  11. "name":"帅"
  12. }
  13. POST /test3/_doc/1
  14. {
  15. "doc":{
  16. "name":"帅"
  17. }
  18. }
  19. ***
  20. 全局更新本质上是替换操作,即使内容一样也会去替换;
  21. 局部更新本质上是更新操作,只有遇到新的东西才更新,没有新的修改就不更新;
  22. 局部更新比全局更新的性能好,因此推荐使用局部更新。

image.png
image.png
发现上面这种方式修改,age和birthday属性都没被影响!
image.png
image.png
发现上面这种方式修改,age和birthday属性都没了!


*
删除索引**
通过DELETE命令实现删除,根据你的请求来判断是删除索引还是删除文档记录
ElasticSearch详解 - 图25

3.关于文档的基本操作

普通查询

最简单的搜索是GET。搜索功能search
image.png
这边name是text 所以做了分词的查询。 如果是keyword就不会分词搜索了

复杂操作搜索select(排序,分页,高亮,模糊查询,精准查询)**
ElasticSearch详解 - 图27

结果过滤,就是只展示列表中某些字段:"_source":["name","age","xxx"]

  1. //测试只能一个字段查询
  2. GET fred/user/_search
  3. {
  4. "query": {
  5. "match": {
  6. "name": "fred"
  7. }
  8. },
  9. "_source":["name","age"]
  10. }

ElasticSearch详解 - 图28
包含:"_source":{"includes":["name","age","xxx"]}
不包含:"_source":{"excludes":["name","age","xxx"]}
ElasticSearch详解 - 图29
排序: "sort":{ "age":{ "order":"asc" } }
ElasticSearch详解 - 图30
分页: "from": currentPage, "size": pageSize
ElasticSearch详解 - 图31

多条件查询

布尔值查询
must(and)’&&’,所有的条件都要符合
ElasticSearch详解 - 图32
should(or‘ || ‘ 或者的 ,跟数据库一样
ElasticSearch详解 - 图33
must_not(not) ‘!’
ElasticSearch详解 - 图34
条件区间

  • gt大于
  • gte大于等于
  • lt小于
  • lte小于等于

ElasticSearch详解 - 图35
匹配多个条件(数组)
ElasticSearch详解 - 图36
match没用倒排索引 这边改正一下
精确查找
term查询是直接通过倒排索引指定的词条进程精确查找的(term查询, 不进行分词操作)

  1. POST /lisen/user/_search
  2. {
  3. "query": {
  4. "term": {
  5. "name": "李"
  6. }
  7. }
  8. }

关于分词

  • term,直接查询精确的
  • match,会使用分词器解析!(先分析文档,然后通过分析的文档进行查询)

ElasticSearch详解 - 图37
standard默认的是被分词了
ElasticSearch详解 - 图38keyword没有被分词
精确查询多个值
ElasticSearch详解 - 图39

高亮

ElasticSearch详解 - 图40
还能自定义高亮的样式
ElasticSearch详解 - 图41

springboot集成ES

1.引入依赖包

创建一个springboot的项目 同时勾选上springboot-web的包以及Nosql的elasticsearch的包(注意ES依赖版本)。
如果没有就手动引入

  1. <!--es客户端-->
  2. <dependency>
  3. <groupId>org.elasticsearch.client</groupId>
  4. <artifactId>elasticsearch-rest-high-level-client</artifactId>
  5. <version>7.6.2</version>
  6. </dependency>
  7. <!--springbootelasticsearch服务-->
  8. <dependency>
  9. <groupId>org.springframework.boot</groupId>
  10. <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
  11. </dependency>

注意下spring-boot的parent包内的依赖的es的版本是不是你对应的版本

  1. <!--这边配置下自己对应的版本-->
  2. <properties>
  3. <java.version>1.8</java.version>
  4. <elasticsearch.version>7.6.2</elasticsearch.version>
  5. </properties>

2.注入RestHighLevelClient 客户端

  1. @Configuration
  2. public class ElasticSearchClientConfig {
  3. @Bean
  4. public RestHighLevelClient restHighLevelClient(){
  5. RestHighLevelClient client = new RestHighLevelClient(
  6. RestClient.builder(new HttpHost("192.168.72.20",9200,"http"))
  7. );
  8. return client;
  9. }
  10. }

3.索引的增、删、是否存在

  1. //测试索引的创建
  2. @Test
  3. void testCreateIndex() throws IOException {
  4. //1.创建索引的请求
  5. CreateIndexRequest request = new CreateIndexRequest("lisen_index");
  6. //2客户端执行请求,请求后获得响应
  7. CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
  8. System.out.println(response);
  9. }
  10. //测试索引是否存在
  11. @Test
  12. void testExistIndex() throws IOException {
  13. //1.创建索引的请求
  14. GetIndexRequest request = new GetIndexRequest("lisen_index");
  15. //2客户端执行请求,请求后获得响应
  16. boolean exist = client.indices().exists(request, RequestOptions.DEFAULT);
  17. System.out.println("测试索引是否存在-----"+exist);
  18. }
  19. //删除索引
  20. @Test
  21. void testDeleteIndex() throws IOException {
  22. DeleteIndexRequest request = new DeleteIndexRequest("lisen_index");
  23. AcknowledgedResponse delete = client.indices().delete(request,RequestOptions.DEFAULT);
  24. System.out.println("删除索引--------"+delete.isAcknowledged());
  25. }

4.文档的操作

  1. //测试添加文档
  2. @Test
  3. void testAddDocument() throws IOException {
  4. User user = new User("lisen",27);
  5. IndexRequest request = new IndexRequest("lisen_index");
  6. request.id("1");
  7. //设置超时时间
  8. request.timeout("1s");
  9. //将数据放到json字符串
  10. request.source(JSON.toJSONString(user), XContentType.JSON);
  11. //发送请求
  12. IndexResponse response = client.index(request,RequestOptions.DEFAULT);
  13. System.out.println("添加文档-------"+response.toString());
  14. System.out.println("添加文档-------"+response.status());
  15. // 结果
  16. // 添加文档-------IndexResponse[index=lisen_index,type=_doc,id=1,version=1,result=created,seqNo=0,primaryTerm=1,shards={"total":2,"successful":1,"failed":0}]
  17. // 添加文档-------CREATED
  18. }
  19. //测试文档是否存在
  20. @Test
  21. void testExistDocument() throws IOException {
  22. //测试文档的 没有index
  23. GetRequest request= new GetRequest("lisen_index","1");
  24. //没有indices()了
  25. boolean exist = client.exists(request, RequestOptions.DEFAULT);
  26. System.out.println("测试文档是否存在-----"+exist);
  27. }
  28. //测试获取文档
  29. @Test
  30. void testGetDocument() throws IOException {
  31. GetRequest request= new GetRequest("lisen_index","1");
  32. GetResponse response = client.get(request, RequestOptions.DEFAULT);
  33. System.out.println("测试获取文档-----"+response.getSourceAsString());
  34. System.out.println("测试获取文档-----"+response);
  35. // 结果
  36. // 测试获取文档-----{"age":27,"name":"lisen"}
  37. // 测试获取文档-----{"_index":"lisen_index","_type":"_doc","_id":"1","_version":1,"_seq_no":0,"_primary_term":1,"found":true,"_source":{"age":27,"name":"lisen"}}
  38. }
  39. //测试修改文档
  40. @Test
  41. void testUpdateDocument() throws IOException {
  42. User user = new User("李逍遥", 55);
  43. //修改是id为1的
  44. UpdateRequest request= new UpdateRequest("lisen_index","1");
  45. request.timeout("1s");
  46. request.doc(JSON.toJSONString(user),XContentType.JSON);
  47. UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
  48. System.out.println("测试修改文档-----"+response);
  49. System.out.println("测试修改文档-----"+response.status());
  50. // 结果
  51. // 测试修改文档-----UpdateResponse[index=lisen_index,type=_doc,id=1,version=2,seqNo=1,primaryTerm=1,result=updated,shards=ShardInfo{total=2, successful=1, failures=[]}]
  52. // 测试修改文档-----OK
  53. // 被删除的
  54. // 测试获取文档-----null
  55. // 测试获取文档-----{"_index":"lisen_index","_type":"_doc","_id":"1","found":false}
  56. }
  57. //测试删除文档
  58. @Test
  59. void testDeleteDocument() throws IOException {
  60. DeleteRequest request= new DeleteRequest("lisen_index","1");
  61. request.timeout("1s");
  62. DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
  63. System.out.println("测试删除文档------"+response.status());
  64. }
  65. //测试批量添加文档
  66. @Test
  67. void testBulkAddDocument() throws IOException {
  68. ArrayList<User> userlist=new ArrayList<User>();
  69. userlist.add(new User("cyx1",5));
  70. userlist.add(new User("cyx2",6));
  71. userlist.add(new User("cyx3",40));
  72. userlist.add(new User("cyx4",25));
  73. userlist.add(new User("cyx5",15));
  74. userlist.add(new User("cyx6",35));
  75. //批量操作的Request
  76. BulkRequest request = new BulkRequest();
  77. request.timeout("1s");
  78. //批量处理请求
  79. for (int i = 0; i < userlist.size(); i++) {
  80. request.add(
  81. new IndexRequest("lisen_index")
  82. .id(""+(i+1))
  83. .source(JSON.toJSONString(userlist.get(i)),XContentType.JSON)
  84. );
  85. }
  86. BulkResponse response = client.bulk(request, RequestOptions.DEFAULT);
  87. //response.hasFailures()是否是失败的
  88. System.out.println("测试批量添加文档-----"+response.hasFailures());
  89. // 结果:false为成功 true为失败
  90. // 测试批量添加文档-----false
  91. }
  92. //测试查询文档
  93. @Test
  94. void testSearchDocument() throws IOException {
  95. SearchRequest request = new SearchRequest("lisen_index");
  96. //构建搜索条件
  97. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  98. //设置了高亮
  99. sourceBuilder.highlighter();
  100. //term name为cyx1的
  101. TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "cyx1");
  102. sourceBuilder.query(termQueryBuilder);
  103. sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
  104. request.source(sourceBuilder);
  105. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  106. System.out.println("测试查询文档-----"+JSON.toJSONString(response.getHits()));
  107. System.out.println("=====================");
  108. for (SearchHit documentFields : response.getHits().getHits()) {
  109. System.out.println("测试查询文档--遍历参数--"+documentFields.getSourceAsMap());
  110. }
  111. // 测试查询文档-----{"fragment":true,"hits":[{"fields":{},"fragment":false,"highlightFields":{},"id":"1","matchedQueries":[],"primaryTerm":0,"rawSortValues":[],"score":1.8413742,"seqNo":-2,"sortValues":[],"sourceAsMap":{"name":"cyx1","age":5},"sourceAsString":"{\"age\":5,\"name\":\"cyx1\"}","sourceRef":{"fragment":true},"type":"_doc","version":-1}],"maxScore":1.8413742,"totalHits":{"relation":"EQUAL_TO","value":1}}
  112. // =====================
  113. // 测试查询文档--遍历参数--{name=cyx1, age=5}
  114. }

ElasticSearch实战(实现京东的搜索效果, 高亮)

1.项目准备

配置文件

  1. # 更改端口,防止冲突
  2. server.port=9999
  3. # 关闭thymeleaf缓存
  4. spring.thymeleaf.cache=false

导入前端后测试页面
Config

  1. @Configuration
  2. public class ElasticSearchClientConfig {
  3. //elk
  4. @Bean
  5. public RestHighLevelClient restHighLevelClient(){
  6. RestHighLevelClient client = new RestHighLevelClient(
  7. RestClient.builder(
  8. new HttpHost("192.168.72.20", 9200, "http")
  9. )
  10. );
  11. return client;
  12. }
  13. }

2.爬虫

京东网站:http://search.jd.com/search?keyword=java
爬虫依赖

  1. <dependency>
  2. <groupId>org.jsoup</groupId>
  3. <artifactId>jsoup</artifactId>
  4. <version>1.10.2</version>
  5. </dependency>

爬取数据(获取请求返回的页面信息,筛选出可用的)
创建HtmlParseUtil

  1. public class HtmlParseUtil {
  2. public static void main(String[] args) throws IOException {
  3. //获取请求
  4. // 使用前需要联网
  5. // 请求url
  6. String url="https://search.jd.com/Search?keyword=java";
  7. // 1.解析网页(jsoup 解析返回的对象是浏览器Document对象)
  8. Document document = Jsoup.parse(new URL(url), 30000);
  9. // 使用document可以使用在js对document的所有操作
  10. // 2.获取元素(通过id)
  11. Element j_goodsList = document.getElementById("J_goodsList");
  12. //System.out.println("j_goodsList = " + j_goodsList);
  13. // 3.获取J_goodsList ul 每一个 li
  14. Elements lis = j_goodsList.getElementsByTag("li");
  15. // 4.获取li下的 img、price、name
  16. for (Element li : lis) {
  17. // 关于图片特别多的网站,所有图片都是延时加载的!
  18. // source-data-lazy-img
  19. // String img = li.getElementsByTag("img").eq(0).attr("src");// 获取li下 第一张图片
  20. String img = li.getElementsByTag("img").eq(0).attr("data-lazy-img");// 获取li下 第一张图片
  21. String name = li.getElementsByClass("p-name").eq(0).text();
  22. String price = li.getElementsByClass("p-price").eq(0).text();
  23. System.out.println("=======================");
  24. System.out.println("img : " + img);
  25. System.out.println("name : " + name);
  26. System.out.println("price : " + price);
  27. }
  28. new HtmlParseUtil().parseJD("python").forEach(System.out::println);
  29. //注意中文不能,需要转义
  30. }
  31. }
  1. // 打印标签内容
  2. Elements lis = j_goodsList.getElementsByTag("li");
  3. System.out.println(lis);

打印所有li标签,发现img标签中并没有属性src的设置,只是data-lazy-ing设置图片加载的地址(原因:一般图片特别多的网站,为确保性能,所有的图片都是通过延迟加载的。)
ElasticSearch详解 - 图42

HtmlParseUtil中图片获取属性将src更改为 data-lazy-img
service调用

  1. @Service
  2. public class ContentService {
  3. @Autowired
  4. private RestHighLevelClient restHighLevelClient;
  5. /*// 不能直接使用 @Autowired 需要Spring容器
  6. public static void main(String[] args) throws IOException {
  7. new ContentService().parseContent("java");
  8. }*/
  9. // 1、解析数据放入es索引中
  10. public Boolean parseContent(String keywords) throws IOException {
  11. List<Content> contents=new HtmlParseUtil().parseJD(keywords);
  12. //把查询的数据放入es中
  13. BulkRequest bulkRequest=new BulkRequest();
  14. bulkRequest.timeout("2m");
  15. for (int i=0;i<contents.size();i++){
  16. bulkRequest.add(new IndexRequest("jd_goods")
  17. .source(JSON.toJSONString(contents.get(i)), XContentType.JSON));
  18. }
  19. BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
  20. return !bulk.hasFailures();
  21. }
  22. // 2、获取这些数据实现搜索功能(未实现高亮)
  23. public List<Map<String,Object>> searchPage(String keyword,int pageNo,int pageSize) throws IOException {
  24. if (pageNo<=1){
  25. pageNo=1;
  26. }
  27. //条件搜索
  28. SearchRequest searchRequest = new SearchRequest("jd_goods");
  29. SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
  30. //分页
  31. searchSourceBuilder.from(pageNo);
  32. searchSourceBuilder.size(pageSize);
  33. //精准匹配关键字
  34. TermQueryBuilder termQueryBuilder= QueryBuilders.termQuery("title",keyword);
  35. searchSourceBuilder.query(termQueryBuilder);
  36. searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
  37. //执行搜索
  38. searchRequest.source(searchSourceBuilder);
  39. SearchResponse searchResponse=restHighLevelClient.search(searchRequest,RequestOptions.DEFAULT);
  40. //解析结果
  41. ArrayList<Map<String,Object>> list=new ArrayList<>();
  42. for (SearchHit documentFields:searchResponse.getHits()){
  43. list.add(documentFields.getSourceAsMap());
  44. }
  45. return list;
  46. }
  47. //3、获取这些数据实现搜索高亮功能
  48. public List<Map<String,Object>> searchPageHighlightBuilder(String keyword,int pageNo,int pageSize) throws IOException {
  49. if (pageNo<=1){
  50. pageNo=1;
  51. }
  52. //条件搜索
  53. SearchRequest searchRequest = new SearchRequest("jd_goods");
  54. SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
  55. //分页
  56. searchSourceBuilder.from(pageNo);
  57. searchSourceBuilder.size(pageSize);
  58. //精准匹配关键字
  59. TermQueryBuilder termQueryBuilder= QueryBuilders.termQuery("title",keyword);
  60. searchSourceBuilder.query(termQueryBuilder);
  61. searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
  62. //高亮
  63. HighlightBuilder highlightBuilder=new HighlightBuilder();
  64. highlightBuilder.field("title");
  65. highlightBuilder.requireFieldMatch(false);// 多个高亮显示!
  66. highlightBuilder.preTags("<span style='color:red'>");
  67. highlightBuilder.postTags("</span>");
  68. searchSourceBuilder.highlighter(highlightBuilder);
  69. //执行搜索
  70. searchRequest.source(searchSourceBuilder);
  71. SearchResponse searchResponse=restHighLevelClient.search(searchRequest,RequestOptions.DEFAULT);
  72. //解析结果
  73. ArrayList<Map<String,Object>> list=new ArrayList<>();
  74. for (SearchHit hit:searchResponse.getHits()){
  75. Map<String, HighlightField> highlightFieldMap=hit.getHighlightFields();
  76. HighlightField title=highlightFieldMap.get("title");
  77. Map<String,Object> sourceAsMap=hit.getSourceAsMap(); //原来的结果
  78. //解析高亮的字段,将原来的字段换为我们高亮的字段即可!
  79. if (title!=null){
  80. Text[] fragments=title.fragments();
  81. String n_title="";
  82. for (Text text : fragments) {
  83. n_title+=text;
  84. }
  85. sourceAsMap.put("title",n_title); //高亮字段替换掉原来的内容即可!
  86. }
  87. list.add(sourceAsMap);
  88. }
  89. return list;
  90. }
  91. }

controller层

  1. @GetMapping("/parse/{keyword}")
  2. public Boolean parse(@PathVariable("keyword") String keyword) throws IOException {
  3. System.out.println("keyword = " + keyword);
  4. return contentService.parseContent(keyword);
  5. }
  6. @GetMapping("/search/{keyword}/{pageNo}/{pageSize}")
  7. public List<Map<String, Object>> parse(@PathVariable("keyword") String keyword,
  8. @PathVariable("pageNo") int pageNo,
  9. @PathVariable("pageSize") int pageSize) throws IOException {
  10. //if (pageNo==0)
  11. //System.out.println(contentService.searchPage(keyword,pageNo,pageSize));
  12. return contentService.searchPage(keyword,pageNo,pageSize);
  13. }

3.前端(使用Vue)

npm install vue
npm install axios
引入js

  1. <script th:src="@{/js/vue.min.js}"></script>
  2. <script th:src="@{/js/axios.min.js}"></script>

index.html

  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="utf-8"/>
  5. <title>狂神说Java-ES仿京东实战</title>
  6. <link rel="stylesheet" th:href="@{/css/style.css}"/>
  7. </head>
  8. <body class="pg">
  9. <div class="page" id="app">
  10. <div id="mallPage" class=" mallist tmall- page-not-market ">
  11. <!-- 头部搜索 -->
  12. <div id="header" class=" header-list-app">
  13. <div class="headerLayout">
  14. <div class="headerCon ">
  15. <!-- Logo-->
  16. <h1 id="mallLogo">
  17. <img th:src="@{/images/jdlogo.png}" alt="">
  18. </h1>
  19. <div class="header-extra">
  20. <!--搜索-->
  21. <div id="mallSearch" class="mall-search">
  22. <form name="searchTop" class="mallSearch-form clearfix">
  23. <fieldset>
  24. <legend>天猫搜索</legend>
  25. <div class="mallSearch-input clearfix">
  26. <div class="s-combobox" id="s-combobox-685">
  27. <div class="s-combobox-input-wrap">
  28. <input v-model="keyword" type="text" autocomplete="off" value="dd" id="mq"
  29. class="s-combobox-input" aria-haspopup="true">
  30. </div>
  31. </div>
  32. <button type="submit" @click.prevent="searchKey" id="searchbtn">搜索</button>
  33. </div>
  34. </fieldset>
  35. </form>
  36. <ul class="relKeyTop">
  37. <li><a>狂神说Java</a></li>
  38. <li><a>狂神说前端</a></li>
  39. <li><a>狂神说Linux</a></li>
  40. <li><a>狂神说大数据</a></li>
  41. <li><a>狂神聊理财</a></li>
  42. </ul>
  43. </div>
  44. </div>
  45. </div>
  46. </div>
  47. </div>
  48. <!-- 商品详情页面 -->
  49. <div id="content">
  50. <div class="main">
  51. <!-- 品牌分类 -->
  52. <form class="navAttrsForm">
  53. <div class="attrs j_NavAttrs" style="display:block">
  54. <div class="brandAttr j_nav_brand">
  55. <div class="j_Brand attr">
  56. <div class="attrKey">
  57. 品牌
  58. </div>
  59. <div class="attrValues">
  60. <ul class="av-collapse row-2">
  61. <li><a href="#"> 狂神说 </a></li>
  62. <li><a href="#"> Java </a></li>
  63. </ul>
  64. </div>
  65. </div>
  66. </div>
  67. </div>
  68. </form>
  69. <!-- 排序规则 -->
  70. <div class="filter clearfix">
  71. <a class="fSort fSort-cur">综合<i class="f-ico-arrow-d"></i></a>
  72. <a class="fSort">人气<i class="f-ico-arrow-d"></i></a>
  73. <a class="fSort">新品<i class="f-ico-arrow-d"></i></a>
  74. <a class="fSort">销量<i class="f-ico-arrow-d"></i></a>
  75. <a class="fSort">价格<i class="f-ico-triangle-mt"></i><i class="f-ico-triangle-mb"></i></a>
  76. </div>
  77. <!-- 商品详情 -->
  78. <div class="view grid-nosku">
  79. <div class="product" v-for="result in results">
  80. <div class="product-iWrap">
  81. <!--商品封面-->
  82. <div class="productImg-wrap">
  83. <a class="productImg">
  84. <img :src="result.img">
  85. </a>
  86. </div>
  87. <!--价格-->
  88. <p class="productPrice">
  89. <em><b>¥</b>{{result.price}}</em>
  90. </p>
  91. <!--标题-->
  92. <p class="productTitle">
  93. <a v-html="result.title"></a>
  94. </p>
  95. <!-- 店铺名 -->
  96. <div class="productShop">
  97. <span>店铺: 狂神说Java </span>
  98. </div>
  99. <!-- 成交信息 -->
  100. <p class="productStatus">
  101. <span>月成交<em>999笔</em></span>
  102. <span>评价 <a>3</a></span>
  103. </p>
  104. </div>
  105. </div>
  106. </div>
  107. </div>
  108. </div>
  109. </div>
  110. </div>
  111. <script th:src="@{/js/axios.min.js}"></script>
  112. <script th:src="@{/js/vue.min.js}"></script>
  113. <script>
  114. new Vue({
  115. el:"#app",
  116. data:{
  117. keyword:'', //搜素的关键字
  118. results:[] //搜素的结果
  119. },
  120. methods:{
  121. searchKey(){
  122. let keyword = this.keyword;
  123. console.log(keyword);
  124. axios.get('search/'+keyword+'/0/10').then(response=>{
  125. console.log(response.data);
  126. this.results=response.data; //绑定数据
  127. })
  128. }
  129. }
  130. });
  131. </script>
  132. </body>
  133. </html>