本文参考:
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)
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对比)
- 当单纯的对已有数据进行搜索时,Solr更快
- 当实时建立索引时,Solr会产生io阻塞,查询性能较差,ElasticSearch具有明显的优势
- 随着数据量的增加,Solr的搜索效率会变得更低,而ElasticSearch却没有明显的变化
- 转变我们的搜索基础设施后从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 ,又称复制分片)
上图是一个有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提供了将索引细分为多个碎片的功能,这就是分片。这里咱们可以简单去理解,在创建索引时,只需要咱们定义所需的碎片数量就可以了,其实每个分片都可以看作是一个完全功能性和独立的索引,可以托管在集群中的任何节点上。
疑问:分片有什么好处和注意事项呢?
- 通过分片技术,咱们可以水平拆分数据量,同时它还支持跨碎片(可能在多个节点上)分布和并行操作,从而提高性能/吞吐量;
- ES可以完全自动管理分片的分配和文档的聚合来完成搜索请求,并且对用户完全透明;
- 主分片数在索引创建时指定,后续只能通过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
- 搜索docker镜像库里可用的ES镜像
docker search elasticsearch
可以看到,stars排名第一的是官方的ES镜像,第二是大牛已经融合了ES7.7和Kibana7.7的镜像,那咱们就用第二个了。
- 把这个镜像从镜像库拉下来
docker pull nshou/elasticsearch-kibana
- 最后咱们把镜像启动为容器就可以了,端口映射保持不变,咱们给这个容器命名为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(浏览器版)
咱们还需要安装ElasticSearch Head,它相当于是ES的图形化界面,这个更简单,它是一个浏览器的扩展程序,直接在chrome浏览器扩展程序里下载安装即可:
打开chrome浏览器,在扩展程序chrome应用商店那里,搜索elasticsearch:
选择ElasticSearch Head,点击
添加至Chrome
,进行扩展程序的安装即可:
到这里咱们的ES、Kibana、ElasticSearch Head都已经安装完成了,下面咱们验证一下,看是否安装成功!
验证ES:http://宿主机IP:9200/ (如本地:http://127.0.0.1:9200/)
验证Kibana:http://宿主机IP:5601/
验证ES Head: 这个更简单,只需要点击之前咱们安装的那个扩展程序图标就可以了。点击信息(info),还可以看到集群或者索引的信息,很方便,大家没事可以玩一玩,熟悉一下:
安装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】测试:
GET _analyze
{
"analyzer": "ik_smart",
"text": "我是社会主义接班人"
}
//输出
{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "社会主义",
"start_offset" : 2,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "接班人",
"start_offset" : 6,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 3
}
]
}
【ik_max_word】测试:
GET _analyze
{
"analyzer": "ik_max_word",
"text": "我是社会主义接班人"
}
//输出
{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "社会主义",
"start_offset" : 2,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "社会",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "主义",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "接班人",
"start_offset" : 6,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "接班",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "人",
"start_offset" : 8,
"end_offset" : 9,
"type" : "CN_CHAR",
"position" : 7
}
]
}
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
{请求体}
完成了自动添加了索引!数据也成功的添加了。
那么name这个字段用不用指定类型呢?
指定字段的类型properties 就比如sql创表。获得这个规则!可以通过GET请求获得具体的信息
如果自己不设置文档字段类型,那么es会自动给默认类型
2.cat命令
*获取健康值
还有很多 可以自动展示 都试试
*修改索引
1.修改我们可以还是用原来的PUT的命令,根据id来修改
但是如果没有填写的字段 会重置为空了 ,相当于java接口传对象修改,如果只是传id的某些字段,那其他没传的值都为空了。
2.还有一种update方法 这种不设置某些值 数据不会丢失
//局部更新【推荐】。不会将不修改的值清除,不会丢失数据。且(内容不变的情况)仅第一次执行version会变化,之后无论执行多少次都不会变化
POST /test3/_doc/1/_update
{
"doc":{
"name":"帅"
}
}
//全局更新。下面两种都会将不修改的值清空,会丢失数据。且(无论内容是否变化)执行一次version都会+1
POST /test3/_doc/1
{
"name":"帅"
}
POST /test3/_doc/1
{
"doc":{
"name":"帅"
}
}
***
全局更新本质上是替换操作,即使内容一样也会去替换;
局部更新本质上是更新操作,只有遇到新的东西才更新,没有新的修改就不更新;
局部更新比全局更新的性能好,因此推荐使用局部更新。
发现上面这种方式修改,age和birthday属性都没被影响!
发现上面这种方式修改,age和birthday属性都没了!
*删除索引**
通过DELETE命令实现删除,根据你的请求来判断是删除索引还是删除文档记录
3.关于文档的基本操作
普通查询
最简单的搜索是GET。搜索功能search
这边name是text 所以做了分词的查询。 如果是keyword就不会分词搜索了
复杂操作搜索select(排序,分页,高亮,模糊查询,精准查询)**
结果过滤,就是只展示列表中某些字段:"_source":["name","age","xxx"]
//测试只能一个字段查询
GET fred/user/_search
{
"query": {
"match": {
"name": "fred"
}
},
"_source":["name","age"]
}
包含:"_source":{"includes":["name","age","xxx"]}
不包含:"_source":{"excludes":["name","age","xxx"]}
排序: "sort":{ "age":{ "order":"asc" } }
分页: "from": currentPage, "size": pageSize
多条件查询
布尔值查询
must(and)’&&’,所有的条件都要符合
should(or)‘ || ‘ 或者的 ,跟数据库一样
must_not(not) ‘!’
条件区间
- gt大于
- gte大于等于
- lt小于
- lte小于等于
匹配多个条件(数组)
match没用倒排索引 这边改正一下
精确查找
term查询是直接通过倒排索引指定的词条进程精确查找的(term查询, 不进行分词操作)
POST /lisen/user/_search
{
"query": {
"term": {
"name": "李"
}
}
}
关于分词
- term,直接查询精确的
- match,会使用分词器解析!(先分析文档,然后通过分析的文档进行查询)
standard默认的是被分词了
keyword没有被分词
精确查询多个值
高亮
springboot集成ES
1.引入依赖包
创建一个springboot的项目 同时勾选上springboot-web
的包以及Nosql的elasticsearch
的包(注意ES依赖版本)。
如果没有就手动引入
<!--es客户端-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.6.2</version>
</dependency>
<!--springboot的elasticsearch服务-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
注意下spring-boot的parent包内的依赖的es的版本是不是你对应的版本
<!--这边配置下自己对应的版本-->
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.6.2</elasticsearch.version>
</properties>
2.注入RestHighLevelClient 客户端
@Configuration
public class ElasticSearchClientConfig {
@Bean
public RestHighLevelClient restHighLevelClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.72.20",9200,"http"))
);
return client;
}
}
3.索引的增、删、是否存在
//测试索引的创建
@Test
void testCreateIndex() throws IOException {
//1.创建索引的请求
CreateIndexRequest request = new CreateIndexRequest("lisen_index");
//2客户端执行请求,请求后获得响应
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
System.out.println(response);
}
//测试索引是否存在
@Test
void testExistIndex() throws IOException {
//1.创建索引的请求
GetIndexRequest request = new GetIndexRequest("lisen_index");
//2客户端执行请求,请求后获得响应
boolean exist = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println("测试索引是否存在-----"+exist);
}
//删除索引
@Test
void testDeleteIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("lisen_index");
AcknowledgedResponse delete = client.indices().delete(request,RequestOptions.DEFAULT);
System.out.println("删除索引--------"+delete.isAcknowledged());
}
4.文档的操作
//测试添加文档
@Test
void testAddDocument() throws IOException {
User user = new User("lisen",27);
IndexRequest request = new IndexRequest("lisen_index");
request.id("1");
//设置超时时间
request.timeout("1s");
//将数据放到json字符串
request.source(JSON.toJSONString(user), XContentType.JSON);
//发送请求
IndexResponse response = client.index(request,RequestOptions.DEFAULT);
System.out.println("添加文档-------"+response.toString());
System.out.println("添加文档-------"+response.status());
// 结果
// 添加文档-------IndexResponse[index=lisen_index,type=_doc,id=1,version=1,result=created,seqNo=0,primaryTerm=1,shards={"total":2,"successful":1,"failed":0}]
// 添加文档-------CREATED
}
//测试文档是否存在
@Test
void testExistDocument() throws IOException {
//测试文档的 没有index
GetRequest request= new GetRequest("lisen_index","1");
//没有indices()了
boolean exist = client.exists(request, RequestOptions.DEFAULT);
System.out.println("测试文档是否存在-----"+exist);
}
//测试获取文档
@Test
void testGetDocument() throws IOException {
GetRequest request= new GetRequest("lisen_index","1");
GetResponse response = client.get(request, RequestOptions.DEFAULT);
System.out.println("测试获取文档-----"+response.getSourceAsString());
System.out.println("测试获取文档-----"+response);
// 结果
// 测试获取文档-----{"age":27,"name":"lisen"}
// 测试获取文档-----{"_index":"lisen_index","_type":"_doc","_id":"1","_version":1,"_seq_no":0,"_primary_term":1,"found":true,"_source":{"age":27,"name":"lisen"}}
}
//测试修改文档
@Test
void testUpdateDocument() throws IOException {
User user = new User("李逍遥", 55);
//修改是id为1的
UpdateRequest request= new UpdateRequest("lisen_index","1");
request.timeout("1s");
request.doc(JSON.toJSONString(user),XContentType.JSON);
UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
System.out.println("测试修改文档-----"+response);
System.out.println("测试修改文档-----"+response.status());
// 结果
// 测试修改文档-----UpdateResponse[index=lisen_index,type=_doc,id=1,version=2,seqNo=1,primaryTerm=1,result=updated,shards=ShardInfo{total=2, successful=1, failures=[]}]
// 测试修改文档-----OK
// 被删除的
// 测试获取文档-----null
// 测试获取文档-----{"_index":"lisen_index","_type":"_doc","_id":"1","found":false}
}
//测试删除文档
@Test
void testDeleteDocument() throws IOException {
DeleteRequest request= new DeleteRequest("lisen_index","1");
request.timeout("1s");
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
System.out.println("测试删除文档------"+response.status());
}
//测试批量添加文档
@Test
void testBulkAddDocument() throws IOException {
ArrayList<User> userlist=new ArrayList<User>();
userlist.add(new User("cyx1",5));
userlist.add(new User("cyx2",6));
userlist.add(new User("cyx3",40));
userlist.add(new User("cyx4",25));
userlist.add(new User("cyx5",15));
userlist.add(new User("cyx6",35));
//批量操作的Request
BulkRequest request = new BulkRequest();
request.timeout("1s");
//批量处理请求
for (int i = 0; i < userlist.size(); i++) {
request.add(
new IndexRequest("lisen_index")
.id(""+(i+1))
.source(JSON.toJSONString(userlist.get(i)),XContentType.JSON)
);
}
BulkResponse response = client.bulk(request, RequestOptions.DEFAULT);
//response.hasFailures()是否是失败的
System.out.println("测试批量添加文档-----"+response.hasFailures());
// 结果:false为成功 true为失败
// 测试批量添加文档-----false
}
//测试查询文档
@Test
void testSearchDocument() throws IOException {
SearchRequest request = new SearchRequest("lisen_index");
//构建搜索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//设置了高亮
sourceBuilder.highlighter();
//term name为cyx1的
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "cyx1");
sourceBuilder.query(termQueryBuilder);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println("测试查询文档-----"+JSON.toJSONString(response.getHits()));
System.out.println("=====================");
for (SearchHit documentFields : response.getHits().getHits()) {
System.out.println("测试查询文档--遍历参数--"+documentFields.getSourceAsMap());
}
// 测试查询文档-----{"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}}
// =====================
// 测试查询文档--遍历参数--{name=cyx1, age=5}
}
ElasticSearch实战(实现京东的搜索效果, 高亮)
1.项目准备
配置文件
# 更改端口,防止冲突
server.port=9999
# 关闭thymeleaf缓存
spring.thymeleaf.cache=false
导入前端后测试页面
Config
@Configuration
public class ElasticSearchClientConfig {
//elk
@Bean
public RestHighLevelClient restHighLevelClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.72.20", 9200, "http")
)
);
return client;
}
}
2.爬虫
京东网站:http://search.jd.com/search?keyword=java
爬虫依赖
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.2</version>
</dependency>
爬取数据(获取请求返回的页面信息,筛选出可用的)
创建HtmlParseUtil
public class HtmlParseUtil {
public static void main(String[] args) throws IOException {
//获取请求
// 使用前需要联网
// 请求url
String url="https://search.jd.com/Search?keyword=java";
// 1.解析网页(jsoup 解析返回的对象是浏览器Document对象)
Document document = Jsoup.parse(new URL(url), 30000);
// 使用document可以使用在js对document的所有操作
// 2.获取元素(通过id)
Element j_goodsList = document.getElementById("J_goodsList");
//System.out.println("j_goodsList = " + j_goodsList);
// 3.获取J_goodsList ul 每一个 li
Elements lis = j_goodsList.getElementsByTag("li");
// 4.获取li下的 img、price、name
for (Element li : lis) {
// 关于图片特别多的网站,所有图片都是延时加载的!
// source-data-lazy-img
// String img = li.getElementsByTag("img").eq(0).attr("src");// 获取li下 第一张图片
String img = li.getElementsByTag("img").eq(0).attr("data-lazy-img");// 获取li下 第一张图片
String name = li.getElementsByClass("p-name").eq(0).text();
String price = li.getElementsByClass("p-price").eq(0).text();
System.out.println("=======================");
System.out.println("img : " + img);
System.out.println("name : " + name);
System.out.println("price : " + price);
}
new HtmlParseUtil().parseJD("python").forEach(System.out::println);
//注意中文不能,需要转义
}
}
// 打印标签内容
Elements lis = j_goodsList.getElementsByTag("li");
System.out.println(lis);
打印所有li标签,发现img标签中并没有属性src的设置,只是data-lazy-ing设置图片加载的地址(原因:一般图片特别多的网站,为确保性能,所有的图片都是通过延迟加载的。)
HtmlParseUtil中图片获取属性将src
更改为 data-lazy-img
service调用
@Service
public class ContentService {
@Autowired
private RestHighLevelClient restHighLevelClient;
/*// 不能直接使用 @Autowired 需要Spring容器
public static void main(String[] args) throws IOException {
new ContentService().parseContent("java");
}*/
// 1、解析数据放入es索引中
public Boolean parseContent(String keywords) throws IOException {
List<Content> contents=new HtmlParseUtil().parseJD(keywords);
//把查询的数据放入es中
BulkRequest bulkRequest=new BulkRequest();
bulkRequest.timeout("2m");
for (int i=0;i<contents.size();i++){
bulkRequest.add(new IndexRequest("jd_goods")
.source(JSON.toJSONString(contents.get(i)), XContentType.JSON));
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
return !bulk.hasFailures();
}
// 2、获取这些数据实现搜索功能(未实现高亮)
public List<Map<String,Object>> searchPage(String keyword,int pageNo,int pageSize) throws IOException {
if (pageNo<=1){
pageNo=1;
}
//条件搜索
SearchRequest searchRequest = new SearchRequest("jd_goods");
SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
//分页
searchSourceBuilder.from(pageNo);
searchSourceBuilder.size(pageSize);
//精准匹配关键字
TermQueryBuilder termQueryBuilder= QueryBuilders.termQuery("title",keyword);
searchSourceBuilder.query(termQueryBuilder);
searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
//执行搜索
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse=restHighLevelClient.search(searchRequest,RequestOptions.DEFAULT);
//解析结果
ArrayList<Map<String,Object>> list=new ArrayList<>();
for (SearchHit documentFields:searchResponse.getHits()){
list.add(documentFields.getSourceAsMap());
}
return list;
}
//3、获取这些数据实现搜索高亮功能
public List<Map<String,Object>> searchPageHighlightBuilder(String keyword,int pageNo,int pageSize) throws IOException {
if (pageNo<=1){
pageNo=1;
}
//条件搜索
SearchRequest searchRequest = new SearchRequest("jd_goods");
SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
//分页
searchSourceBuilder.from(pageNo);
searchSourceBuilder.size(pageSize);
//精准匹配关键字
TermQueryBuilder termQueryBuilder= QueryBuilders.termQuery("title",keyword);
searchSourceBuilder.query(termQueryBuilder);
searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
//高亮
HighlightBuilder highlightBuilder=new HighlightBuilder();
highlightBuilder.field("title");
highlightBuilder.requireFieldMatch(false);// 多个高亮显示!
highlightBuilder.preTags("<span style='color:red'>");
highlightBuilder.postTags("</span>");
searchSourceBuilder.highlighter(highlightBuilder);
//执行搜索
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse=restHighLevelClient.search(searchRequest,RequestOptions.DEFAULT);
//解析结果
ArrayList<Map<String,Object>> list=new ArrayList<>();
for (SearchHit hit:searchResponse.getHits()){
Map<String, HighlightField> highlightFieldMap=hit.getHighlightFields();
HighlightField title=highlightFieldMap.get("title");
Map<String,Object> sourceAsMap=hit.getSourceAsMap(); //原来的结果
//解析高亮的字段,将原来的字段换为我们高亮的字段即可!
if (title!=null){
Text[] fragments=title.fragments();
String n_title="";
for (Text text : fragments) {
n_title+=text;
}
sourceAsMap.put("title",n_title); //高亮字段替换掉原来的内容即可!
}
list.add(sourceAsMap);
}
return list;
}
}
controller层
@GetMapping("/parse/{keyword}")
public Boolean parse(@PathVariable("keyword") String keyword) throws IOException {
System.out.println("keyword = " + keyword);
return contentService.parseContent(keyword);
}
@GetMapping("/search/{keyword}/{pageNo}/{pageSize}")
public List<Map<String, Object>> parse(@PathVariable("keyword") String keyword,
@PathVariable("pageNo") int pageNo,
@PathVariable("pageSize") int pageSize) throws IOException {
//if (pageNo==0)
//System.out.println(contentService.searchPage(keyword,pageNo,pageSize));
return contentService.searchPage(keyword,pageNo,pageSize);
}
3.前端(使用Vue)
npm install vue
npm install axios
引入js
<script th:src="@{/js/vue.min.js}"></script>
<script th:src="@{/js/axios.min.js}"></script>
index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>狂神说Java-ES仿京东实战</title>
<link rel="stylesheet" th:href="@{/css/style.css}"/>
</head>
<body class="pg">
<div class="page" id="app">
<div id="mallPage" class=" mallist tmall- page-not-market ">
<!-- 头部搜索 -->
<div id="header" class=" header-list-app">
<div class="headerLayout">
<div class="headerCon ">
<!-- Logo-->
<h1 id="mallLogo">
<img th:src="@{/images/jdlogo.png}" alt="">
</h1>
<div class="header-extra">
<!--搜索-->
<div id="mallSearch" class="mall-search">
<form name="searchTop" class="mallSearch-form clearfix">
<fieldset>
<legend>天猫搜索</legend>
<div class="mallSearch-input clearfix">
<div class="s-combobox" id="s-combobox-685">
<div class="s-combobox-input-wrap">
<input v-model="keyword" type="text" autocomplete="off" value="dd" id="mq"
class="s-combobox-input" aria-haspopup="true">
</div>
</div>
<button type="submit" @click.prevent="searchKey" id="searchbtn">搜索</button>
</div>
</fieldset>
</form>
<ul class="relKeyTop">
<li><a>狂神说Java</a></li>
<li><a>狂神说前端</a></li>
<li><a>狂神说Linux</a></li>
<li><a>狂神说大数据</a></li>
<li><a>狂神聊理财</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- 商品详情页面 -->
<div id="content">
<div class="main">
<!-- 品牌分类 -->
<form class="navAttrsForm">
<div class="attrs j_NavAttrs" style="display:block">
<div class="brandAttr j_nav_brand">
<div class="j_Brand attr">
<div class="attrKey">
品牌
</div>
<div class="attrValues">
<ul class="av-collapse row-2">
<li><a href="#"> 狂神说 </a></li>
<li><a href="#"> Java </a></li>
</ul>
</div>
</div>
</div>
</div>
</form>
<!-- 排序规则 -->
<div class="filter clearfix">
<a class="fSort fSort-cur">综合<i class="f-ico-arrow-d"></i></a>
<a class="fSort">人气<i class="f-ico-arrow-d"></i></a>
<a class="fSort">新品<i class="f-ico-arrow-d"></i></a>
<a class="fSort">销量<i class="f-ico-arrow-d"></i></a>
<a class="fSort">价格<i class="f-ico-triangle-mt"></i><i class="f-ico-triangle-mb"></i></a>
</div>
<!-- 商品详情 -->
<div class="view grid-nosku">
<div class="product" v-for="result in results">
<div class="product-iWrap">
<!--商品封面-->
<div class="productImg-wrap">
<a class="productImg">
<img :src="result.img">
</a>
</div>
<!--价格-->
<p class="productPrice">
<em><b>¥</b>{{result.price}}</em>
</p>
<!--标题-->
<p class="productTitle">
<a v-html="result.title"></a>
</p>
<!-- 店铺名 -->
<div class="productShop">
<span>店铺: 狂神说Java </span>
</div>
<!-- 成交信息 -->
<p class="productStatus">
<span>月成交<em>999笔</em></span>
<span>评价 <a>3</a></span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script th:src="@{/js/axios.min.js}"></script>
<script th:src="@{/js/vue.min.js}"></script>
<script>
new Vue({
el:"#app",
data:{
keyword:'', //搜素的关键字
results:[] //搜素的结果
},
methods:{
searchKey(){
let keyword = this.keyword;
console.log(keyword);
axios.get('search/'+keyword+'/0/10').then(response=>{
console.log(response.data);
this.results=response.data; //绑定数据
})
}
}
});
</script>
</body>
</html>