视频教程:【狂神说Java】ElasticSearch7.6.x最新完整教程通俗易懂
视频地址:https://www.bilibili.com/video/BV17a4y1x7zq
拒绝白嫖,感谢狂神分享的视频教程
ElasticSearch 概述
ElasticSearch,简称 es,是一个开源的高扩展的分布式全文搜索引擎,它可以近乎实时的存储、检索数据,本身扩展性很好,可以扩展到上百台服务器,处理PB级别的数据。es 也使用 Java 开发并使用 Lucene 作为其核心来实现所有索引和搜索功能,但是他的目的是通过简单的 RestFul API 来隐藏 Lucene 的复杂性,从而让全文搜索更简单。
据国际权威的数据库产品测评机构 DB Engines 的统计,在2016年1月,ElasticSearch 已经超过 Solr 等,成为排名第一的搜索引擎类应用。
谁在使用:
-
*
-
The Guardian(国外新闻网站)
-
Stack Overflow(国外程序异常讨论论坛)
-
GitHub(开源代码管理)
-
电商网站
-
日志数据分析,logstash 采集日志,es 进行复杂的数据分析,ELK 技术(elasticsearch + logstash + kibana)
… …
ES 与 solr
ElasticSearch 简介
Elasticsearch是一个实时分布式搜索和分析引擎。它让你以前所未有的速度处理大数据成为可能。
它用于全文搜索、结构化搜索、分析以及将这三者混合使用:
*使用Elasticsearch提供全文搜索并高亮关键字,以及输入实时搜索(search-asyou-type)和搜索纠错(did-you-mean)等搜索
建议功能。
英国卫报使用Elasticsearch结合用户日志和社交网络数据提供给他们的编辑以实时的反馈,以便及时了解公众对新发表的文章的回
应。
*结合全文搜索与地理位置查询,以及more-like-this功能来找到相关的问题和答案。
Github使用Elasticsearch检索1300亿行的代码。
但是Elasticsearch不仅用于大型企业,它还让像DataDog以及Klout这样的创业公司将最初的想法变成可扩展的解决方案。Elasticsearch可以在你的笔记本上运行,也可以在数以百计的服务器上处理PB级别的数据。Elasticsearch是一个基于Apache Lucene™的开源搜索引擎。无论在开源还是专有领域, Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。
但是, Lucene只是一个库。想要使用它,你必须使用Java来作为开发语言并将其直接集成到你的应用中,更糟糕的是, Lucene非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。
Elasticsearch也使用Java开发并使用Lucene作为其核心来实现所有索引|和搜索的功能,但是它的目的是通过简单的RESTful API来
隐藏Lucene的复杂性,从而让全文搜索变得简单。
Solr 简介
Solr是Apache’下的一-个*开源项目,采用Java开发,它是基于Lucene的全文搜索服务器。Solr提供 了比Lucene更为丰富的查询
语言,同时实现了可配置、可扩展,并对索引、搜索性能进行了优化。
Solr可以独立运行, 运行在Jetty、Tomcat等这些Servlet容器中 , Solr索引的实现方法很简单,用POST方法向Solr 服务器发送一
个描述Field及其内容的XM[文档, Solr根据xml文档添加、删除、更新索引。Solr 搜索只需要发送HTTP GET请求,然后对Solr
返回Xm|、json等格式的查询结果进行解析,组织页面布局。Solr不提供构建UI的功能 , Solr提供了-个管理界面,通过管理界面可
以查询Solr的配置和运行情况。
solr是基于lucene开发企业级搜索服务器,实际上就是封装了lucene。
Solr是一个独立的企业级搜索应用服务器,它对外提供类似于Web-service的API接口。用户可以通过http请求,向搜索引|擎服务器
提交-定格式的文件,生成索引;也可以通过提出查找请求,并得到返回结果。
Lucene 简介
Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是-一个完整的全文
检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。
Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立
起完整的全文检索引擎。Lucene是一 套用于全文检索和搜 寻的开源程式库,由Apache软件基金会支持和提供。Lucene提供了一个
简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具。 就其本身而言,
Lucene是当前以及最近几年最受欢迎的免费Java信息检索程序库。人们经常提到信息检索程序库,虽然与搜索引擎有关,但不应该
将信息检索程序库与搜索引擎相混淆。
Lucene是一个全文检索引擎的架构。那什么是全文搜索引擎?
全文搜索引擎是名副其实的搜索引擎,国外具代表性的有Google、FastlAllTheWeb、 AltaVista、 Inktomi、 Teoma、 WiseNut等
国内著名的有百度( Baidu)。它们都是通过从互联网上提取的各个网站的信息(以网页文字为主)而建立的数据库中,检索与用
户查询条件匹配的相关记录,然后按一定的排列顺序将结果返回给用户,因此他们是真正的搜索弓|擎。
从搜索结果来源的角度,全文搜索引擎又可细分为两种,-种是拥有自己的检索程序( Indexer) , 俗称"蜘蛛”( Spider )程序
或机器人”( Robot )程序,并自建网页数据库,搜索结果直接从自身的数据库中调用,如上面提到的7家引擎;另-种则是租用其
他弓|擎的数据库,并按自定的格式排列搜索结果,如Lycos引擎。
ElasticSearch 和 Solr 比较
-
当单纯地对已有数据进行搜索时,Solr 更快
-
当实时建立索引时,Solr 会产生 I/O 阻塞,查询性能较差,ElasticSearch 具有明显优势
-
随着数据量的增加,Solr 的搜索效率会变得更低,而 ElasticSearch 却没有明显的变化
-
将搜索基础设施由 Solr 转向 ElasticSearch 后,将提高将近 50× 的搜索性能
ElasticSearch vs Solr总结
- es基本是开箱即用(解压就可以用! ) , 非常简单。Solr安装略微复杂
- Solr 利用Zookeeper进行分布式管理,而Elasticsearch自身带有分布式协调管理功能。
- Solr 支持更多格式的数据,比如JSON、XML、 CSV ,而Elasticsearch仅支持json文件格式。
- Solr 官方提供的功能更多,而Elasticsearch本身更注重于核心功能,高级功能多有第三方插件提供,例如图形化界面需要kibana友好支撑。
- Solr 查询快,但更新索引时慢(即插入删除慢) , 用于电商等查询多的应用;
- ES建立索引快(即查询慢) ,即实时性查询快,用于facebook新浪等搜索。
- Solr是传统搜索应用的有力解决方案,但Elasticsearch 更适用于新兴的实时搜索应用。
- Solr比较成熟,有一个更大,更成熟的用户、开发和贡献者社区,而Elasticsearch相对开发维护者较少,更新太快,学习使用
成本较高。
ElasticSearch 安装
-
环境要求:JDK 1.8 以上,Nodejs
-
下载:官网 https://www.elastic.co/
-
Windows安装包:elasticsearch-7.15.2-windows-x86_64.zip,解压即用
-
目录结构:
+ bin 启动文件 + config 配置文件 + log4j2.properties 日志配置文件 + jvm.options 虚拟机配置文件,建议把内存调小, + elasticsearch.yml elasticsearch配置文件,默认9200端口,会有跨域问题 + lib 相关jar包 + logs 日志 + modules 模块 + plugins 插件
-
启动,双击 /elasticsearch-7.15.2/bin/elasticsearch.bat
访问 127.0.0.1:9200,得到页面
{ "name" : "CRATER-PC", "cluster_name" : "elasticsearch", // 集群名称,一个服务也是集群 "cluster_uuid" : "8AWbPdNgRymiY6VXtuiAuA", "version" : { "number" : "7.15.2", "build_flavor" : "default", "build_type" : "zip", "build_hash" : "93d5a7f6192e8a1a12e154a2b81bf6fa7309da0c", "build_date" : "2021-11-04T14:04:42.515624022Z", "build_snapshot" : false, "lucene_version" : "8.9.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }
ElasticSearch-head 安装
-
下载地址:https://github.com/mobz/elasticsearch-head
-
解压后,是一个前端 webpackage 项目
-
cd elasticsearch-head
-
npm install
-
npm run start
-
open
http://localhost:9100/
进入页面后,产生了跨域问题(跨ip,跨端口),因为在 9100端口 连接 9200端口,解决:
-
停止 ElasticSearch,修改 elasticsearch.yml,添加以下内容。
http.cors.enabled: true http.cors.allow-origin: "*"
再次重启 ElasticSearch,head 连接器成功。
可以随便新建一个索引,当前可以理解为数据库
关于符合查询,head 作为一个数据展示工具,不建议在这里面写命令。以后在 Kibana 里查询
Kibana 安装
了解 ELK
ELK是Elasticsearch、Logstash、 Kibana三大开源框架首字母大写简称。市面上也被成为Elastic Stack(Elastic 技术栈)。
其中Elasticsearch是一 个基于Lucene、分布式、通过Restful方式进行交互的近实时搜索平台框架。像类似百度、谷歌这种大数据全文搜索引擎的场景都可以使用Elasticsearch作为底层支持框架,可见Elasticsearch提供的搜索能力确实强大,市面上很多时候我们简称Elasticsearch为es。
Logstash是ELK的*数据流引擎,用于从不同目标(文件/数据存储/MQ )收集的不同格式数据,经过过滤后支持输出到不同目的
地(文件/MQ/redis/elasticsearch/kafka等)。
Kibana可以将elasticsearch的数据通过友好的页面展示出来,提供实时分析的功能。
市面上很多开发只要提到ELK能够-致说出它是一 个日志分析架构技术栈总称,但实际上ELK不仅仅适用于日志分析,它还可以支持
其它任何数据分析和收集的场景,日志分析和收集只是更具有代表性。并非唯一性。
安装 Kibana
Kibana是一个针对 Elasticsearch 的开源分析及可视化平台,用来搜索、查看交互存储在 Elasticsearch 索引中的数据。使用Kibana,可以通过各种图表进行高级数据分析及展示。Kibana让海量数据更容易理解。它操作简单,基于浏览器的用户界面可以快速创建仪表板( dashboard )实时显示 Elasticsearch 查询动态。设置Kibana非常简单。无需编码或者额外的基础架构,几分钟内就可以完成Kibana安装并启动 Elasticsearch 索引监测。
官网: https://www.elastic.co/cn/kibana
-
下载 kibana-7.15.2-windows-x86_64.zip。注意:Kibana 版本必须和 ES 版本一致。
-
解压,是一个标准的工程
-
启动:双击 /bin/kibana.bat
-
访问 localhost:5601
-
在左侧菜单栏找到
Dev Tools
使用 Kibana 上使用开发工具 -
汉化(国际化),在
/x-pack/plugins/translations/translations
下有一个zh-CN.json
文件,用于翻译。编辑
config/kibana.yml
文件,做如下修改:#i18n.locale: "en" i18n.locale: "zh-CN"
重启 Kibana,语言变成了中文
ElasticSearch 核心概念
ElasticSearch 是面向文档的。关系型数据库和 ElasticSearch 的对比
Relational DB | ElasticSearch |
---|---|
数据库(database) | 索引(indices) |
表(tables) | type |
行(rows) | documents |
字段(columns) | fields |
ElasticSearch(集群)中可以包含多个索引(数据库),每个索引中可以包含多个类型(表),每个类型下又包含多个文档(行),每个文档中又包含多个字段(列)。
物理设计:
ElasticSearch 在后台把每个 索引划分成多个分片 ,每片分片可以在集群中的不同服务器间迁移
逻辑设计:
一个索引类型中,包含多个文档,比如说文档1,文档2。当索引一篇文档时 ,可以通过这样的一各顺序找到它:索引 => 类型 => 文档ID
,通过这个组合我们就能索引到某个具体的文档。注意:D不必是整数,实际上它是个字符串。
文档
之前说elasticsearch是面向文档的,那么就意味着索引和搜索数据的最小单位是文档,Elasticsearch中,文档有几个重要属性:
- 自我包含, 一篇文档同时包含字段和对应的值,也就是同时包含 key : value
- 可以是层次型的,一个文档中包含自文档,复杂的逻辑实体就是这么来的。就是一 个json对象,fastjson进行自动转换。
- 灵活的结构,文档不依赖预先定义的模式,我们知道关系型数据库中,要提前定义字段才能使用,在elasticsearch中,对于字段是非常灵活的,有时候,我们可以忽略该字段,或者动态的添加一个新的字段。
- 尽管我们可以随意的新增或者忽略某个字段,但是,每个字段的类型非常重要,比如一个年龄字段类型,可以是字符串也可以是整型。因为elasticsearch会保存字段和类型之间的映射及其他的设置。这种映射具体到每个映射的每种类型,这也是为什么在Elasticsearch中,类型有时候也称为映射类型。
类型
类型是文档的逻辑容器,就像关系型数据库一样,表格是行的容器。类型中对于字段的定义称为映射,比如name映射为字符串类型。
我们说文档是无模式的,它们不需要拥有映射中所定义的所有字段,比如新增一个字段,那么Elasticsearch是怎么做的呢?Elasticsearch 会自动的将新字段加入映射,但是这个字段的不确定它是什么类型,Elasticsearch就开始猜,如果这个值是18,那么
Elasticsearch 会认为它是整形。但是 Elasticsearch 也可能猜不对,所以最安全的方式就是提前定义好所需要的映射,这点跟关系型数据库殊途同归了,先定义好字段,然后再使用,别整什么幺蛾子。
索引
就是数据库。
索引是映射类型的容器,ElasticSearch 中的索引是一个非常大的文档集合。索引存储了映射类型的字段和其他设置,然后它们被存储到了各个分片上。
物理设计:节点和分片如何工作
一个集群至少有一个节点,而一个节点就是一个 ElasticSearch 进程,节点可以有多个索引默认。如果创建索引,那么索引将会有 5 个分片(primary shard,有称主分片)构成的,每一个主分片都会有一个副本(replica shard,又称复制分片)
上图是一个有 3 个节点的集群,可以看到主分片和对应的复制分片都不会在同一个节点内,这样有利于某一个节点挂了,数据也不至于丢失。实际上,一个分片是一个 Lucene 索引,一个包含 倒排索引 的文件目录,倒排索引使得 ElasticSearch 在不扫描全部文档的情况下,就能得到那些文档包含特定的关键字。
倒排索引
ElasticSearch 使用的是一种被成为倒排索引的结构,其采用 Lucene 倒排索引作为底层。这种结构适用于快速的全文搜索,一个索引由文档中所有不重复的列表构成,对于每一个词,都有一个包含它的文档列表。
-
例如,现在有两个文档,每个文档包含如下内容:
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 √ × To × √ every √ √ forever √ √ day √ √ study × √ good √ √ to √ × up √ √ -
现在,尝试检索 to forever,只需要查看每个词条的文档
term doc_1 doc_2 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 中,索引这个词被频繁使用,这就是术语的使用。在 Elasticsearch 中,索引被分为多个分片,每份分片是一个Lucene的索引。所以一个 Elasticsearch 索引是由多 个Lucene索引组成的。如无特指,说起索弓|都是指elasticsearch的索引。
接下来的一切操作都在 kibana 中 Dev Tools 下的 Console 里完成。
IK 分词器插件
分词:即把一段字符划分成一个个的关键字,在搜索的时候会把字符信息进行分词,会把数据库中或者索引库中的数据进行分词,然后进行一个匹配操作,默认的中文分词是将每个字看成一个词,比如“我叫陨石坑”会被分为"我"、"叫”、“陨”、“石”、“坑”,这显然是不符合要求的,所以需要安装中文分词器 IK 来解决这个问题。
IK提供了两个分词算法: ik _smart 和 ik_max_word,其中 ik_smart 为最少切分, ik_max _word 为最细粒度划分。
安装 IK 分词器
-
下载: https://github.com/medcl/elasticsearch-analysis-ik/releases
注意:版本需要对应
elasticsearch-analysis-ik-7.15.2.zip
-
进入
elasticsearch-7.15.2/plugins
目录下,新建一个ik
目录,解压 -
重启 ElasticSearch 并观察,可以看到插件被加载了
也可以使用elasticsearch-plugin
命令查看插件 -
使用 Kibana 测试
ik_smart 最少切分:
ik_max_word 最细粒度划分,穷尽词库的所有可能: -
测试中发现,如下测试案例,使用哪种分词算法,都不能拆出想要的 “混元形意”、“耗子尾汁”,对于这种词,需要添加到自定义的字典中。
-
自定义字典,在 ik 分词器插件的 config 目录下,新建一个字典文件
crater.dic
,内容如下混元形意 耗子尾汁
修改同目录下的
IKAnalyzer.cfg.xml
文件,配置上自定义的字典<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>IK Analyzer 扩展配置</comment> <!--用户可以在这里配置自己的扩展字典 --> <entry key="ext_dict">crater.dic</entry> <!--用户可以在这里配置自己的扩展停止词字典--> <entry key="ext_stopwords"></entry> <!--用户可以在这里配置远程扩展字典 --> <!-- <entry key="remote_ext_dict">words_location</entry> --> <!--用户可以在这里配置远程扩展停止词字典--> <!-- <entry key="remote_ext_stopwords">words_location</entry> --> </properties>
重启后观察日志,加载了自定义的字典
-
再次测试,自定义的词都被拆分出来了
Rest 风格说明
一种软件架构风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务端交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。
基本命令说明
method | url 地址 | 描述 |
---|---|---|
PUT | 127.0.0.1:9200/索引名称/类型名称/文档ID | 创建文档(指定文档ID) |
POST | 127.0.0.1:9200/索引名称/类型名称 | 创建文档(随机文档ID) |
POST | 127.0.0.1:9200/索引名称/类型名称/文档ID/_update | 修改文档 |
DELETE | 127.0.0.1:9200/索引名称/类型名称/文档ID | 删除文档 |
GET | 127.0.0.1:9200/索引名称/类型名称/文档ID | 通过文档ID查询文档 |
POST | 127.0.0.1:9200/索引名称/类型名称/_search | 查询所有数据 |
索引的基本操作
新增索引
-
创建一个索引
# 其中类型名以后的版本就去掉了 PUT /索引名/类型名/文档ID {请求体} # 返回值包括 "_index": 当前索引名 "_type" 当前类型 "_version" 当前版本(1表示还没有修改过)
在 elasticsearch-head 中的概览和索引页面,也可以看到新建的索引
在数据索引页面可以看到数据的详情信息,完成了自动增加索引。这也是初期可以把 ES 当做数据库来学习的原因。
-
字段的数据类型
-
字符串类型
text、keyword
-
数值型
long、Integer、short、byte、double、float、half float、scaled float
-
日期类型
date
-
te布尔类型
boolean
-
二进制类型
binary
-
等等…
-
-
指定字段的类型
类似于建库(建立索引和字段对应类型),也可看做规则的建立
-
获取 test2 索引的信息
-
默认的数据类型
_doc
默认类型(default type),type 在未来的版本中会逐渐弃用,可以不指定,也可以显式的指定如果文档字段没有指定类型,那么 ElasticSearch 会默认配置字段类型。
扩展: 通过
GET _cat
命令查看 ElasticSearch 的信息其他命令:
GET _cat/indices GET _cat/aliases GET _cat/allocation GET _cat/count GET _cat/fielddata GET _cat/health GET _cat/indices GET _cat/master GET _cat/nodeattrs GET _cat/nodes GET _cat/pending_tasks GET _cat/plugins GET _cat/recovery GET _cat/repositories GET _cat/segments GET _cat/shards GET _cat/snapshots GET _cat/tasks GET _cat/templates GET _cat/thread_pool
修改索引
修改有两种实现方案
-
方案一:PUT,改一下请求体中需要修改的数据,再次新增,这样就覆盖了旧的数据,实现修改。
这种方案的弊端就是,一旦请求体遗漏了字段,就会造成数据丢失。
-
方案二:POST
删除索引
-
命令
# 删除索引 DELETE /test1
# 删除文档 DELETE /test3/_doc/1
文档的基本操作
简单查询
-
首先添加一些数据
PUT /crater/user/1 { "name": "陨石坑", "age": 24, "desc": "一顿操作猛如虎,一看工资两千五", "tags": ["技术宅","宝藏男孩","直男"] } PUT /crater/user/2 { "name": "张三", "age": 35, "desc": "法外狂徒", "tags": ["罗老师","刑法"] } PUT /crater/user/3 { "name": "李四", "age": 30, "desc": "不可描述", "tags": ["淑女","跳舞"] }
-
查询数据
# 命令 GET /crater/user/1
修改文档
-
更新数据
方式一:
# 命令 PUT /crater/user/3 { "name": "李四123", "age": 30, "desc": "不可描述", "tags": ["淑女","跳舞"] }
方式二:推荐使用
# 命令 POST /crater/user/3/_update { "doc": { "name": "李四456" } }
简单条件查询
-
GET 查询
# 命令 GET /crater/user/_search?q=name:陨石
复杂条件查询
条件、投影、排序、分页
排序、分页、高亮、模糊查询、精准查询
-
一般情况,不使用
GET /crater/user/_search?q=name:陨石
这种形式,而是使用参数实体# 再新增一个文档,供测试 PUT /crater/user/4 { "name": "陨石坑主", "age": 18, "desc": "操作猛如虎,工资两千五", "tags": ["技术宅","宝藏男孩","直男"] }
# 命令 GET /crater/user/_search { "query": { "match": { "name": "陨石" } } }
-
也可以过滤查询的属性,类似于数据库的投影
# 命令 GET /crater/user/_search { "query": { "match": { "name": "陨石" } }, "_source": ["name","desc"] }
-
可以再加上排序,如根据年龄倒序排序
# 命令 GET /crater/user/_search { "query": { "match": { "name": "陨石" } }, "_source": ["name","age"], "sort": [ { "age": { "order": "desc" } } ] }
-
可以再加上分页查询
# 命令 GET /crater/user/_search { "query": { "match": { "name": "陨石" } }, "_source": ["name","age"], "sort": [ { "age": { "order": "desc" } } ], "from": 0, # 起始页码,从0开始 "size": 1 # 每页条数 }
布尔值查询
-
must(and),多条件查询,所有条件都有符合
# 命令 GET /crater/user/_search { "query": { "bool": { "must": [ { "match": { "name": "陨石" } }, { "match": { "age": "24" } } ] } } }
-
should(or)满足一个条件即可
# 命令 GET /crater/user/_search { "query": { "bool": { "should": [ { "match": { "name": "陨石" } }, { "match": { "age": "24" } } ] } } }
-
must_not(not)
# 命令 GET /crater/user/_search { "query": { "bool": { "must_not": [ { "match": { "name": "陨石" } }, { "match": { "age": "24" } } ] } } }
-
查询的同时,再使用 filter 过滤
# 命令 GET /crater/user/_search { "query": { "bool": { "must": [ { "match": { "name": "陨石" } } ], "filter": { "range": { "age": { "gte": 20 } } } } } }
匹配多个条件
-
多个条件使用空格隔开,只要满足一个即可以被查出,后续可以使用得分再进行判断筛选
# 命令 GET /crater/user/_search { "query": { "match": { "tags": "男 宅" } } }
精确查询
term 查询是直接通过倒排索引指定的词条进行精确查找
关于分词
- term:直接精确查询
- match:会使用分词器解析(先分析文档,然后通过分析的文档进行查询)
两个类型 text 和 keyword
-
text 会被分词器解析,建立索引前会将这些文本进行分词,转化为词的组合,建立索引
-
keyword 类型字段只能用本身来进行检索
-
搭建测试
PUT testdb { "mappings": { "properties": { "name": { "type": "text" }, "desc": { "type": "keyword" } } } } PUT testdb/_doc/1 { "name": "陨石坑 name", "desc": "陨石坑 desc1" } PUT testdb/_doc/2 { "name": "陨石坑 name", "desc": "陨石坑 desc2" }
-
两种类型对于分词器的区别
# 被解析 GET _analyze { "analyzer": "standard", "text": "陨石坑 name" }
# 不被解析 GET _analyze { "analyzer": "keyword", "text": "陨石坑 desc" }
-
测试
# match 去匹配 {"name":"陨石坑"},可以查询到两条数据 GET /testdb/_doc/_search { "query": { "match": { "name": "陨石坑" } } } # match 去匹配 {"desc":"陨石坑"},查询不到数据, # keyword 类型字段不会被分词器解析,只能用本身来进行检索,如{"desc":"陨石坑 desc1"} GET /testdb/_doc/_search { "query": { "match": { "desc": "陨石坑" } } } # 用 term 去匹配 {"name":"陨石坑"}、{"desc":"陨石坑"},都查询不到数据,因为需要全值精确匹配 # 如果期望查询到数据: # 对于 name,需要匹配被解析出的 "陨","石","坑","name" # 对于 desc,需要匹配 "陨石坑 desc1"、"陨石坑 desc2" GET /testdb/_doc/_search { "query": { "term": { "name": "陨石坑" } } } GET /testdb/_doc/_search { "query": { "term": { "desc": "陨石坑" } } } # 用 match 匹配 {"name":"陨坑"} 也可以查询到两条数据,
-
总结:
match(模糊)和 term(精确)指的是查询时匹配程度
例如 “陨石坑 name” 是一个text,在创建时就被分词成为 “陨”,“石”,“坑”,“name”,所以不能用 term 查询 “陨石坑 name”,但是可以使用 term 查询 “陨”,“石”,“坑”,“name”,因为对比分词的索引能够精确匹配这四个词
而 “陨石坑 desc1” 是 keyword 类型的,在创建的时候就不分词,只有一个索引,所以无论使用 match 还是 term,都需要全值
match 和 term 负责查询条件的数据是否分词,text 和 keyword 负责存储的数据是否分词
多个值匹配精确查询
-
添加测试数据
PUT testdb/_doc/3 { "t1": "22", "t2": "2021-12-20" } PUT testdb/_doc/4 { "t1": "33", "t2": "2021-12-17" }
-
用 term 精确查询多个值
GET testdb/_search { "query": { "bool": { "should": [ { "term": { "t1": "22" } }, { "term": { "t1": "33" } } ] } } }
高亮查询
-
查询时,使用 “highlight”,指定高亮的字段
可以看到,查询结果里,把查询条件里想要查询的词加上了标签
# 命令 GET crater/user/_search { "query": { "match": { "name": "陨石" } }, "highlight": { "fields": { "name": {} } } }
-
em
标签是默认标签,也可以指定自定义标签# 命令 GET crater/user/_search { "query": { "match": { "name": "陨石" } }, "highlight": { "pre_tags": "<p class='key' style='color:red'>", "post_tags": "</p>", "fields": { "name": {} } } }
可以看到,词被自定义标签包裹起来了
集成 SpringBoot
客户端文档:https://www.elastic.co/guide/en/elasticsearch/client/index.html
推荐使用 Java REST Client
:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html
选择 Java High Level REST Client
高级客户端,根据文档操作:
-
引入原生依赖
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.15.2</version> </dependency>
-
初始化
绑定集群中的节点,一个节点也是一个集群
RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost("localhost", 9200, "http"), new HttpHost("localhost", 9201, "http")));
使用完毕需要关闭
client.close();
测试
-
搭建项目环境,最重要的依赖
-
自定义ES依赖,保证和本地一致
<properties> <java.version>1.8</java.version> <!-- 自定义ES版本依赖,保证和本地一致 --> <elasticsearch.version>7.15.2</elasticsearch.version> </properties>
-
注入 RestHighLevelClient (客户端)
package com.crater.config; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ElasticSearchClientConfig { @Bean public RestHighLevelClient restHighLevelClient() { RestHighLevelClient client = new RestHighLevelClient( RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")) ); return client; } }
索引 API 测试
-
测试索引的创建
@SpringBootTest class CraterEsApiApplicationTests { @Autowired @Qualifier("restHighLevelClient") private RestHighLevelClient client; /** * 测试索引的创建 */ @Test void testCreateIndex() throws IOException { // 1.创建索引请求 CreateIndexRequest request = new CreateIndexRequest("crater_index"); // 2.执行创建请求 IndicesClient indices = client.indices(); CreateIndexResponse createIndexResponse = indices.create(request, RequestOptions.DEFAULT); System.out.println(createIndexResponse); } }
# 打印内容 org.elasticsearch.client.indices.CreateIndexResponse@38e8e841
-
测试获取索引
/** * 测试获取索引 */ @Test void testExistIndex() throws IOException { GetIndexRequest request = new GetIndexRequest("crater_index"); boolean exists = client.indices().exists(request, RequestOptions.DEFAULT); System.out.println(exists); }
-
测试删除索引
/** * 测试删除索引 */ @Test void testDeleteIndex() throws IOException { DeleteIndexRequest request = new DeleteIndexRequest("crater_index"); AcknowledgedResponse delete = client.indices().delete(request, RequestOptions.DEFAULT); System.out.println(delete.isAcknowledged()); }
文档 API 测试
-
测试添加文档
/** * 测试添加文档 */ @Test void testAddDocument() throws IOException { // 创建对象 User user = new User("陨石坑", 17); // 创建请求 IndexRequest request = new IndexRequest("crater_index"); // 规则:GET /crater_index/_doc/1 request.id("1"); request.timeout(TimeValue.timeValueSeconds(1)); // 数据放入请求 JSON request.source(JSON.toJSONString(user), XContentType.JSON); // 客户端发送请求,获取响应结果 IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT); System.out.println(indexResponse.status()); System.out.println(indexResponse.toString()); }
# 返回值 CREATED IndexResponse[index=crater_index,type=_doc,id=1,version=1,result=created,seqNo=0,primaryTerm=1,shards={"total":2,"successful":1,"failed":0}]
-
获取文档,先判断是否存在
/** * 获取文档,先判断是否存在:GET crater_index/_doc/1 */ @Test void testIsExist() throws IOException { GetRequest getRequest = new GetRequest("crater_index", "1"); // 不获取返回的 _source 的上下文 getRequest.fetchSourceContext(new FetchSourceContext(false)); // 不排序 getRequest.storedFields("_none_"); boolean exists = client.exists(getRequest, RequestOptions.DEFAULT); System.out.println(exists); }
获取文档
/** * 获取文档 */ @Test void testGetDocument() throws IOException { GetRequest getRequest = new GetRequest("crater_index", "1"); GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT); System.out.println(getResponse.getSourceAsString()); System.out.println(getResponse); }
# 返回值 {"age":17,"name":"陨石坑"} {"_index":"crater_index","_type":"_doc","_id":"1","_version":1,"_seq_no":0,"_primary_term":1,"found":true,"_source":{"age":17,"name":"陨石坑"}}
-
修改文档信息
/** * 更新文档信息 */ @Test void testUpdateRequest() throws IOException { UpdateRequest request = new UpdateRequest("crater_index", "1"); request.timeout(TimeValue.timeValueSeconds(1)); User user = new User("陨石坑YYDS", 3); request.doc(JSON.toJSONString(user), XContentType.JSON); UpdateResponse updateResponse = client.update(request, RequestOptions.DEFAULT); System.out.println(updateResponse.status()); }
-
批量插入
/** * 批量插入 */ @Test void testBulkRequest() throws IOException { BulkRequest bulkRequest = new BulkRequest(); bulkRequest.timeout("10s"); List<User> users = new ArrayList<>(); users.add(new User("陨石坑01", 14)); users.add(new User("陨石坑02", 14)); users.add(new User("陨石坑03", 14)); users.add(new User("陨石坑04", 14)); users.add(new User("陨石坑05", 14)); // 批处理请求 for (int i = 0; i < users.size(); i++) { bulkRequest.add( // 批量更新、批量删除,在这里修改不同Request即可 new IndexRequest("crater_index") .id((i + 1) + "") .source(JSON.toJSONString(users.get(i)), XContentType.JSON) ); } BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT); System.out.println(bulkResponse.status()); }
实战 - 京东搜索
环境搭建
-
按照测试环境搭建项目环境
-
yml 配置
server.port=9090 # 关闭 thymeleaf 缓存 spring.thymeleaf.cache=false
-
将静态文件复制到目录下
文件获取:关注
狂神说
,恢复 ElasticSearch,知识无价,支持正版 -
新建 controller
@Controller public class IndexController { @RequestMapping({"/","index"}) public String index() { return "index"; } }
-
启动项目,访问 http://127.0.0.1:9090/
爬虫
-
在京东商城搜索一个词条,可以看到访问的 url
https://search.jd.com/Search?keyword=java
-
引入 jsoup 包,用来解析网页的数据
<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.13.1</version> </dependency>
-
在 utils 包下新建
HtmlParseUtil
类测试,可以获取数据
@Component public class HtmlParseUtil { public static void main(String[] args) throws IOException { List<Content> javas = new HtmlParseUtil().parseJD("MacBok"); for (Content java : javas) { System.out.println(java); } } public List<Content> parseJD(String keyWord) throws IOException{ String url = "https://search.jd.com/Search?keyword=" + keyWord; // 解析网页,Jsoup返回的Document就是浏览器Document对象 Document document = Jsoup.parse(new URL(url), 30000); // 操作Document Element element = document.getElementById("J_goodsList"); // 获取 li 标签 Elements elements = element.getElementsByTag("li"); // 获取元素中的内容 List<Content> goodList = new ArrayList<>(); for (Element el : elements) { // 图片多的网站,为了响应速度,采取懒加载,JD真正的的url保存在data-lazy-img String img = el.getElementsByTag("img").eq(0).attr("data-lazy-img"); String price = el.getElementsByClass("p-price").eq(0).text(); String title = el.getElementsByClass("p-name").eq(0).text(); goodList.add(new Content(title, img, price)); } return goodList; } }
编写业务
-
编写 Service 层
@Service public class ContentService { @Autowired private RestHighLevelClient restHighLevelClient; @Autowired private HtmlParseUtil htmlParseUtil; public Boolean parseContent(String keywords) throws IOException { List<Content> contents = 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(); } }
-
编写 Controller 层
@RestController public class ContentController { @Autowired private ContentService contentService; @GetMapping("/parse/{keywords}") public Boolean parse(@PathVariable("keywords") String keywords) throws IOException { return contentService.parseContent(keywords); } }
-
调用这个接口,可以把数据加入 ES,前提新建索引
jd_goods
-
获取这些数据,实现搜索功能
Service 层:
/** * 爬取的数据插入 ES */ 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 sourceBuilder = new SearchSourceBuilder(); // 分页 sourceBuilder.from(pageNo); sourceBuilder.size(pageSize); // 精准匹配 TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", keyword); sourceBuilder.query(termQueryBuilder); sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS)); // 执行搜索 searchRequest.source(sourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 解析结果 List<Map<String, Object>> list = new ArrayList<>(); for (SearchHit documentFields : searchResponse.getHits().getHits()) { list.add(documentFields.getSourceAsMap()); } return list; }
Controller 层
@GetMapping("/search/{keywords}/{pageNo}/{pageSize}") public List<Map<String, Object>> search(@PathVariable("keywords") String keywords, @PathVariable("pageNo") int pageNo, @PathVariable("pageSize") int pageSize) throws IOException { return contentService.searchPage(keywords, pageNo, pageSize); }
访问这个接口测试一下
前端显示
采用 Vue,但没有使用脚手架
-
将
vue.min.js
axios.min.js
复制到/static/js/
主要代码,详细代码见 gitee
<!-- 商品详情 --> <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> {{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>
<!-- 前端使用 Vue --> <script th:src="@{/js/axios.min.js}"></script> <script th:src="@{/js/vue.min.js}"></script> <script> new Vue({ el: '#app', // 绑定最外层div data: { keyword: "", // 搜索关键字 results: [] // 搜索结果 }, methods: { searchKey() { var keyword = this.keyword; // 对接后端接口 axios.get('search/'+keyword+'/1/10').then(res => { this.results = res.data; }) } } }) </script>
-
测试
高亮显示
-
在 ContentService 添加高亮构造器
// 高亮 HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.requireFieldMatch(false); // 是否多个匹配的都高亮 highlightBuilder.field("title"); highlightBuilder.preTags("<span style='color:red'>"); highlightBuilder.postTags("</span>"); sourceBuilder.highlighter(highlightBuilder);
-
解析高亮字段,替换原来的内容
for (SearchHit hit : searchResponse.getHits().getHits()) { // 解析高亮的字段 Map<String, HighlightField> highlightFields = hit.getHighlightFields(); HighlightField title = highlightFields.get("title"); Map<String, Object> sourceAsMap = hit.getSourceAsMap(); // 查询的原始结果 // 将原来的字段,换为高亮的字段 if (title != null) { Text[] fragments = title.fragments(); StringBuilder newTitle = new StringBuilder(); for (Text text : fragments) { newTitle.append(text); } sourceAsMap.put("title", newTitle.toString()); // 替换 } list.add(sourceAsMap); }
-
页面标题,把值解析成为 html
<!--标题--> <p class="productTitle"> <a v-html="result.title"> </a> </p>
-
测试,关键词被高亮了
完结撒花!!!