一、 全文检索基础
-
全文检索概念 将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引。 例如:字典。字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结构化的,如果字典没有音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描。然而字的某些信息可以提取出来进行结构化处理,比如读音,就比较结构化,分声母和韵母,分别只有几可以一一列举,于是将读音拿出来按一定的顺序排列,每一项读音都指向此字的详细解释的页数。我们搜索时按结构化的拼音搜到读音,然后按其指向的页数,便可找到我们的非结构化数据–即对字的解释。 这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。 虽然创建索引的过程也是非常耗时的,但是索引一旦创建就可以多次使用,全文检索主要处理的是查询,所以耗时间创建索引是值得的。 -
全文检索流程 -
相关概念
- 索引库
索引库就是存储索引的保存在磁盘上的一系列的文件。里面存储了建立好的索引信息以及文档对象。 一个索引库相当于数据库中的一张表。 - document对象
获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容。每个文档都有一个唯一的编号,就是文档id。 document对象相当于表中的一条记录。 3) field对象 如果我们把document看做是数据库中一条记录的话,field相当于是记录中的字段。field是索引库中存储数据的最小单位。field的数据类型大致可以分为数值类型和文本类型,一般需要查询的字段都是文本类型的,field的还有如下属性:
- 是否分词:是否对域的内容进行分词处理。前提是我们要对域的内容进行查询。
- 是否索引:将Field分析后的词或整个Field值进行索引,只有索引方可搜索到。比如:商品名称、商品简介分析后进行索引,订单号、身份证号不用分词但也要索引,这些将来都要作为查询条件。
- 是否存储:将Field值存储在文档中,存储在文档中的Field才可以从Document中获取 比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。
- term对象
从文档对象中拆分出来的每个单词叫做一个Term,不同的域中拆分出来的相同的单词是不同的 term。term中包含两部分一部分是文档的域名,另一部分是单词的内容。term是创建索引的关键词对象。
二、 ElasticSearch简介
-
ElasticSearch的概念 Elaticsearch,简称为es,es是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB级别的数据。es也使用Java开发并使用 Lucene作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的RESTful API来隐藏 Lucene的复杂性,从而让全文搜索变得简单。 -
ElasticSearch的使用案例
- 2013年初,GitHub抛弃了Solr,采取ElasticSearch 来做PB级的搜索。 “GitHub使用ElasticSearch搜索20TB的数据,包括13亿文件和1300亿行代码”
- 维基百科:启动以elasticsearch为基础的核心搜索架构
- SoundCloud:“SoundCloud使用ElasticSearch为1.8亿用户提供即时而精准的音乐搜索服务”
- 百度:百度目前广泛使用ElasticSearch作为文本数据分析,采集百度所有服务器上的各类指标数据及用户自定义数据,通过对各种数据进行多维分析展示,辅助定位分析实例异常或业务层面异常。目前覆盖百度内部20多个业务线(包括casio、云分析、网盟、预测、文库、直达号、钱包、风控等), 单集群最大100台机器,200个ES节点,每天导入30TB+数据
- 新浪使用ES 分析处理32亿条实时日志
- 阿里使用ES 构建挖财自己的日志采集和分析体系
-
ElasticSearch对比Solr
- Solr 利用 Zookeeper 进行分布式管理,而 Elasticsearch 自身带有分布式协调管理功能;
- Solr 支持更多格式的数据,而 Elasticsearch 仅支持json文件格式;
- Solr 官方提供的功能更多,而 Elasticsearch 本身更注重于核心功能,高级功能多由第三方插件提供;
- Solr 在传统的搜索应用中表现好于 Elasticsearch,但在处理实时搜索应用时效率明显低于 Elasticsearch
三、 ElasticSearch相关概念(术语)
1. 概述
Elasticsearch是面向文档(document oriented)的,这意味着它可以存储整个对象文档(document)。然而它不仅仅是存储,还会索引(index)每个文档的内容使之可以被搜索。在Elasticsearch中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。Elasticsearch比传统关系型数据库如下:
2. ElasticSearch核心概念
-
索引index 一个索引就是一个拥有积分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母的),并且当我们要对对应于这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。在一个集群中,可以定义任意多的索引。 -
类型type 在一个索引中,你可以定义一种或多种类型。一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。通常,会为具有一组共同字段的文档定义一个类型。比如说,我们假设你运营一个博客平台并且将你所有的数据存储到一个索引中。在这个索引中,你可以为用户数据定义一个类型,为博客数据定义另一个类型,当然,也可以为评论数据定义另一个类型。 -
字段Field 相当于是数据表的字段,对文档数据根据不同属性进行的分类标识 -
映射mapping mapping是处理数据的方式和规则方面做一些限制,如某个字段的数据类型、默认值、分析器、是否被索引等等,这些都是映射里面可以设置的,其它就是处理es里面数据的一些使用规则设置也叫做映射,按着最优规则处理数据对性能提高很大,因此才需要建立映射,并且需要思考如何建立映射才能对性能更好。 -
文档document 一个文档是一个可被索引的基础信息单元。比如,你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以JSON(Javascript Object Notation)格式来表示,而JSON是一个到处存在的互联网数据交互格式。 在一个index/type里面,你可以存储任意多的文档。注意,尽管一个文档,物理上存在于一个索引之中,文档必须被索引/赋予一个索引的type。 -
接近实时NRT Elasticsearch是一个接近实时的搜索平台。这意味着,从索引一个文档直到这个文档能够被搜索到有一个轻微的延迟(通常是1秒以内) -
集群cluster 一个集群就是由一个或多个节点组织在一起,它们共同持有整个的数据,并一起提供索引和搜索功能。一个集群由一个唯一的名字标识,这个名字默认就是“elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群 -
节点node 一个节点是集群中的一个服务器,作为集群的一部分,它存储数据,参与集群的索引和搜索功能。和集群类似,一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说挺重要的,因为在这个管理过程中,你会去确定网络中的哪些服务器对应于Elasticsearch集群中的哪些节点。 一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做“elasticsearch”的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做“elasticsearch”的集群中。 在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何 Elasticsearch节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”的集群。 -
分片和复制shards&replicas 一个索引可以存储超出单个结点硬件限制的大量数据。比如,一个具有10亿文档的索引占据1TB的磁盘空间,而任一节点都没有这样大的磁盘空间;或者单个节点处理搜索请求,响应太慢。为了解决这个问题,Elasticsearch提供了将索引划分成多份的能力,这些份就叫做分片。当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。分片很重要,主要有两方面的原因: 1)允许你水平分割/扩展你的内容容量。 2)允许你在分片(潜在地位于多个节点上)之上进行分布式的、并行的操作,进而提高性能/吞吐量。 至于一个分片怎样分布,它的文档怎样聚合回搜索请求,是完全由Elasticsearch管理的,对于作为用户的你来说,这些都是透明的。 在一个网络/云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的, Elasticsearch允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片,或者直接叫复制。 复制之所以重要,有两个主要原因: 在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的。扩展你的搜索量/吞吐量,因为搜索可以在所有的复制上并行运行。总之,每个索引可以被分成多个分片。一个索引也可以被复制0次(意思是没有复制)或多次。一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。分片和复制的数量可以在索引创建的时候指定。在索引创建之后,你可以在任何时候动态地改变复制的数量,但是你事后不能改变分片的数量。 默认情况下,Elasticsearch中的每个索引被分片5个主分片和1个复制,这意味着,如果你的集群中至少有两个节点,你的索引将会有5个主分片和另外5个复制分片(1个完全拷贝),这样的话每个索引总共就有10个分片。
四、 ElasticSearch安装
本章节将采用Docker安装,由于windows安装es非常简单,这里不再重复
1. docker镜像下载
docker pull elasticsearch:5.6.8
2. 安装es容器
docker run -di --name=c_es -p 9200:9200 -p 9300:9300 elasticsearch:5.6.8
9200端口(Web管理平台端口) 9300(服务默认端口) 浏览器输入地址访问:http://192.168.211.132:9200/
3. 开启远程连接
上面完成安装后,es并不能正常使用,elasticsearch从5版本以后默认不开启远程连接,程序直接连接会报如下错误:
我们需要修改es配置开启远程连接,代码如下: 登录容器 查看目录结构输入: dir
进入config目录
查看文件
修改elasticsearch.yml文件
vi命令无法识别,因为docker容器里面没有该命令,我们可以安装该编辑器。 设置apt下载源
docker cp ~/sources.list c_es:/etc/apt/sources.list
安装vim编辑器
安装好了后,修改elasticsearch.yml配置,如下图:
修改如下图:
同时添加下面一行代码:
重启docker
docker restart c_es
4. 系统参数配置
重启后发现重启启动失败了,这是什么原因呢?这与我们刚才修改的配置有关,因为elasticsearch在启动的时候会进行一些检查,比如最多打开的文件的个数以及虚拟内存区域数量等等,如果你放开了此配置,意味着需要打开更多的文件以及虚拟内存,所以我们还需要系统调优 修改vi /etc/security/limits.conf ,追加内容 (nofile是单个进程允许打开的最大文件个数 soft nofile 是 软限制 hard nofile是硬限制 )(追加到最后,注意是虚拟机路径,不是容器中) 修改vi /etc/sysctl.conf,追加内容 (限制一个进程可以拥有的VMA(虚拟内存区域)的数量 ) 执行下面命令 修改内核参数马上生效
重新启动虚拟机,再次启动容器,发现已经可以启动并远程访问
5. 跨域配置
修改elasticsearch/config下的配置文件:elasticsearch.yml,增加以下三句命令,并重启: 注意:不能直接访问ip+9300,配置好也不可以,继续进行后面步骤,才可以正常使用!
其中:
- http.cors.enabled: true:此步为允许elasticsearch跨域访问,默认是false。
- http.cors.allow-origin: “”:表示跨域访问允许的域名地址(表示任意)。
重启
docker restart c_es
小提示:如果想让容器开机重启,可以执行下面命令
五、 ElasticSearch的客户端操作
(一) elasticsearch-head
ElasticSearch不同于Solr自带图形化界面,我们可以通过安装ElasticSearch的head插件,完成图形化界面的效果,完成索引数据的查看。安装插件的方式有两种,在线安装和本地安装。本文档采用本地安装方式进行head插件的安装。elasticsearch-5-*以上版本安装head需要安装node和grunt
1) 下载head插件:https://github.com/mobz/elasticsearch-head 2) 将elasticsearch-head-master压缩包解压到任意目录(window下),但是要和elasticsearch的安装目录区别开 3) 下载nodejs:https://nodejs.org/en/download/ 在资料中已经提供了nodejs安装程序: 双击安装程序,步骤截图如下: 安装完毕,可以通过cmd控制台输入:node -v 查看版本号 4) 将grunt安装为全局命令,Grunt是基于Node.js的项目构建工具 进入elasticsearch-head-master目录,cmd,控制台中输入如下执行命令: 执行结果如下图: 5) 进入elasticsearch-head-master目录启动head,在命令意识符下输入命令: 6) 打开浏览器,输入 http://localhost:9100,看到如下页面: 如果不能成功连接到es服务,需要修改ElasticSearch的config目录下的配置文件: config/elasticsearch.yml,增加以下两句命令: 然后重新启动ElasticSearch服务。
(二) 使用Postman工具进行Restful接口访问
- ElasticSearch的接口语法
其中:
参数 | 解释 |
---|
VERB | 适当的 HTTP 方法 或 谓词 : GET、 POST、 PUT、 HEAD 或者 DELETE。 | PROTOCOL | http 或者 https(如果你在 Elasticsearch 前面有一个 https 代理) | HOST | Elasticsearch 集群中任意节点的主机名,或者用 localhost 代表本地机器上的节 点。 | PORT | 运行 Elasticsearch HTTP 服务的端口号,默认是 9200 。 | PATH | API 的终端路径(例如 _count 将返回集群中文档数量)。 | Path | 可能包含多个组 件,例如:_cluster/stats 和 _nodes/stats/jvm 。 | QUERY_STRING | 任意可选的查询字符串参数 (例如 ?pretty 将格式化地输出 JSON 返回值,使其更 容易阅读) | BODY | 一个 JSON 格式的请求体 (如果请求需要的话) |
- 创建索引index和映射mapping
请求url:
请求体: 解释: “article”:自定义的types “properties”:documents的内容 “id”、“title”、“content”:自定义的fields
“type”:用于指定fields的存储类型 “store”:用于设置是否存储 “index”:是否需要分词 “analyzer”:用于指定分词器 “standard”:es的标准分词器,会将将英文正确分词,将中文逐字分词。 post man 截图: elasticsearch-head查看:
-
创建索引后设置mapping 我们可以在创建索引时设置mapping信息,当然也可以先创建索引然后再设置mapping。 在上一个步骤中不设置maping信息,直接使用put方法创建一个索引,然后设置mapping信息。 请求的url:(_mapping表示设置mapping,是es的api) 请求体: 参考文档:https://www.cnblogs.com/wangzhuxing/p/9527151.html -
删除索引index 请求url: -
创建文档document 请求url: 请求体: -
修改文档document -
删除文档document -
查询文档-根据id查询 -
查询文档-querystring查询 (_search表示搜索,是es的api) 注意:将搜索内容"搜索服务器"修改为"钢索",同样也能搜索到文档,该原因会在下面讲解中得到答案 钢索 -> “钢”, “索”,搜索是分成两个词,注意Standard标准分词器,会把汉字每个字一个词存到索引库中的tilte,也是按照Standard进行的分词,所以搜索钢索能搜到这个document -
查询文档-term查询
搜索结果取决于分词器 文档创建时的mapping指定了分词器standard(会对中文进行每个字的分词,即“搜索”会变成“title:搜”和“title:索”) query_string:搜索之前对搜索的关键词分词(即搜索“title:搜”和“title:索”) term:对搜索的关键词不分词(即搜索“title:搜索”)
六、 IK分词器
1. IK分词器简介
IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。从2006年12月推出1.0版开 始,IKAnalyzer已经推出 了3个大版本。最初,它是以开源项目Lucene为应用主体的,结合词典分词和文法分析算法的中文分词组件。新版本的IKAnalyzer3.0则发展为面向Java的公用分词组件,独立于 Lucene项目,同时提供了对Lucene的默认优化实现。
IK分词器3.0的特性如下: 1)采用了特有的“正向迭代最细粒度切分算法“,具有60万字/秒的高速处理能力。 2)采用了多子处理器分析模式,支持:英文字母(IP地址、Email、URL)、数字(日期,常用中文数 量词,罗马数字,科学计数法),中文词汇(姓名、地名处理)等分词处理。 3)对中英联合支持不是很好,在这方面的处理比较麻烦.需再做一次查询,同时是支持个人词条的优化的词典存储,更小的内存占用。 4)支持用户词典扩展定义。 5)针对Lucene全文检索优化的查询分析器IKQueryParser;采用歧义分析算法优化查询关键字的搜索排列组合,能极大的提高Lucene检索的命中率。
2. IK分词器安装
-
安装ik分词器 注意:必须安装和ElasticSearch版本一样的IK分析器5.6.8,否则会报错,容器闪退 IK分词器下载地址https://github.com/medcl/elasticsearch-analysis-ik/releases 将ik分词器上传到服务器上,然后解压,并改名字为ik 将ik目录拷贝到docker容器的plugins目录下 -
IK分词器测试 访问:http://192.168.211.132:9200/_analyze?analyzer=ik_smart&pretty=true&text=我是程序员 访问:http://192.168.211.132:9200/_analyze?analyzer=ik_max_word&pretty=true&text=我是程序员 pretty=true:优化展示分词结果 text:分词内容 _analyze:es使用分词器的api analyzer:指定分词器
- ik_max_word:会将文本做最细粒度的拆分
比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。 - ik_smart:会做最粗粒度的拆分
比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。
七、 Kibana使用-掌握DSL语句
我们上面使用的是elasticsearch-head插件实现数据查找的,但是elasticsearch-head的功能比较单一,我们这里需要一个更专业的工具实现对日志的实时分析,也就是我们接下来要讲的kibana。 Kibana 是一款开源的数据分析和可视化平台,它是 Elastic Stack 成员之一,设计用于和 Elasticsearch 协作。您可以使用 Kibana 对 Elasticsearch 索引中的数据进行搜索、查看、交互操作。您可以很方便的利用图表、表格及地图对数据进行多元化的分析和呈现。 Kibana 可以使大数据通俗易懂。它很简单,基于浏览器的界面便于您快速创建和分享动态数据仪表板追踪 Elasticsearch 的实时数据变化。 搭建 Kibana 非常简单。您可以分分钟完成 Kibana 的安装并开始探索 Elasticsearch 的索引数据–没有代码、不需要额外的基础设施。
(一) Kibana下载安装
-
镜像下载 -
安装kibana容器 执行如下命令,开始安装kibana容器 ELASTICSEARCH_URL=http://192.168.220.100:9200:是指链接的ES地址 restart=always:每次服务都会重启,也就是开启启动 5601:5601:端口号 快捷键 -
访问测试 访问http://192.168.220.100:5601
(二) Kibana使用
1. 配置索引
要使用Kibana,您必须至少配置一个索引。索引用于标识Elasticsearch索引以运行搜索和分析。它们还用于配置字段。 我们修改索引名称的匹配方式即可,下面2个选项不用勾选。点击create,会展示出当前配置的索引的域信息,如下图: 域的每个标题选项分别代表如下意思:
2. 数据搜索
Discover为数据搜索部分,可以对日志信息进行搜索操作。 可以使用Discover实现数据搜索过滤和搜索条件显示以及关键词搜索,如下图:
3. DSL语句使用
-
Query DSL结构化查询介绍 Query DSL是一个Java开源框架用于构建类型安全的SQL查询语句。采用API代替传统的拼接字符串来构造查询语句。目前Querydsl支持的平台包括JPA,JDO,SQL,Java Collections,RDF,Lucene, Hibernate Search。elasticsearch提供了一整套基于JSON的DSL语言来定义查询。 -
索引操作 1、 查询所有索引 2、 删除某个索引 3、 新增索引 4、 创建映射 search_analyzer用于指定内容提供给查询时的分词器 5、 新增文档数据 我们再增加3条记录: #新增文档数据 id=2
PUT /user/userinfo/2
{
"name":"王五",
"age":35,
"city":"深圳",
"description":"王五家住在深圳!"
}
#新增文档数据 id=3
PUT /user/userinfo/3
{
"name":"张三",
"age":19,
"city":"深圳",
"description":"在深圳打工,来自湖北武汉"
}
#新增文档数据 id=4
PUT /user/userinfo/4
{
"name":"张三丰",
"age":66,
"city":"武汉",
"description":"在武汉读书,家在武汉!"
}
#新增文档数据 id=5
PUT /user/userinfo/5
{
"name":"赵子龙",
"age":77,
"city":"广州",
"description":"赵子龙来自深圳宝安,但是在广州工作!",
"address":"广东省茂名市"
}
#新增文档数据 id=6
PUT /user/userinfo/6
{
"name":"赵毅",
"age":55,
"city":"广州",
"description":"赵毅来自广州白云区,从事电子商务8年!"
}
#新增文档数据 id=7
PUT /user/userinfo/7
{
"name":"赵哈哈",
"age":57,
"city":"武汉",
"description":"武汉赵哈哈,在深圳打工已有半年了,月薪7500!"
}
6、 修改数据 a、 替换操作 更新数据可以使用之前的增加操作,这种操作会将整个数据替换掉,代码如下: b、 更新操作 我们先使用下面命令恢复数据: 使用POST更新某个列的数据 7、 删除Document -
数据查询 1、 查询所有数据 2、 根据ID查询 3、 Sort排序 4、 分页 解释: from:从下N的记录开始查询 size:每页显示条数 效果如下: -
查询模式 1、 term不分词查询 term主要用于分词精确匹配,如字符串、数值、日期等(不适合情况:1.列中除英文字符外有其它值 2. 字符串值中有冒号或中文 3.系统自带属性如_version) 2、 terms 多查询 term为不分词查询 terms 跟 term 有点类似,但 terms 允许指定多个匹配条件。 如果某个字段指定了多个值,那么文档需要一起去做匹配。 案例如下: 3、 match分词查询 match为分词查询 4、 query_string分词查询 query_string为分词查询 5、 range 范围查询 range过滤允许我们按照指定范围查找一批数据。例如我们查询年龄范围 案例如下: 6、 exists域存在查询 exists 过滤可以用于查找拥有某个域的数据 7、 bool 多条件查询 bool 可以用来合并多个条件查询结果的布尔逻辑,它包含一下操作符:
- must : 多个查询条件的完全匹配,相当于 and。
- must_not : 多个查询条件的相反匹配,相当于 not。
- should : 至少有一个查询条件匹配, 相当于 or。
这些参数可以分别继承一个过滤条件或者一个过滤条件的数组: 8、 match_all 所有查询 可以查询到所有文档,是没有查询条件下的默认语句。 9、 match 标准查询 match查询是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它。 如果你使用 match 查询一个全文本字段,它会在真正查询之前用分析器先分析match一下查询字符: 10、 prefix 查询 以什么字符开头的,可以更简单地用 prefix ,例如查询所有以张开始的用户描述 11、 multi_match 多字段查询 multi_match查询允许你做match查询的基础上同时搜索多个字段,在多个字段中同时查一个
八、 ElasticSearch编程操作
(一) 编程操作
1. 创建工程,导入坐标
pom.xml坐标
<dependencies>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>5.6.8</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
<version>5.6.8</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.9.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.24</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
2. 创建索引index
@Test
public void createIndex() throws UnknownHostException {
Settings settings = Settings.builder().put("cluster.name", "my-elasticsearch").build();
TransportClient client = new PreBuiltTransportClient(settings)
.addTransportAddress(
new InetSocketTransportAddress(InetAddress.getByName("192.168.174.128"), 9300)
);
client.admin().indices().prepareCreate("demo").get();
client.close();
}
3. 创建映射mapping
@Test
public void createMappings() throws IOException, ExecutionException, InterruptedException {
Settings settings = Settings.builder().put("cluster.name", "my-elasticsearch").build();
TransportClient client = new PreBuiltTransportClient(settings)
.addTransportAddress(
new InetSocketTransportAddress(InetAddress.getByName("192.168.174.128"), 9300)
);
XContentBuilder builder = XContentFactory.jsonBuilder()
.startObject()
.startObject("article")
.startObject("properties")
.startObject("id")
.field("type", "integer")
.field("store", "yes")
.endObject()
.startObject("title")
.field("type", "string")
.field("store", "yes")
.field("analyzer", "ik_smart")
.endObject()
.startObject("content")
.field("type", "string")
.field("store", "yes")
.field("analyzer", "ik_smart")
.endObject()
.endObject()
.endObject()
.endObject();
PutMappingRequest mapping = Requests.putMappingRequest("demo").type("article").source(builder);
client.admin().indices().putMapping(mapping).get();
client.close();
}
4. 创建映射mapping
- 建立文档(通过XContentBuilder)
@Test
public void createDocument1() throws IOException {
Settings settings = Settings.builder().put("cluster.name", "my-elasticsearch").build();
TransportClient client = new PreBuiltTransportClient(settings)
.addTransportAddress(
new InetSocketTransportAddress(InetAddress.getByName("192.168.174.128"), 9300)
);
XContentBuilder builder = XContentFactory.jsonBuilder()
.startObject()
.field("id", 1)
.field("title", "北方入秋速度明显加快,多地降温幅度最多可达10度22222")
.field("content", "阿联酋一架客机在纽约机场被隔离,10名乘客病倒")
.endObject();
client.prepareIndex()
.setIndex("demo")
.setType("article")
.setId("2")
.setSource(builder)
.get();
client.close();
}
-
建立文档(使用Jackson转换实体) 1、 创建Article实体 public class Article {
private Integer id;
private String title;
private String content;
getter/setter...
}
2、 添加jackson坐标 <dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.8.1</version>
</dependency>
3、 代码实现
@Test
public void createDocument2() throws IOException {
Settings settings = Settings.builder().put("cluster.name", "my-elasticsearch").build();
TransportClient client = new PreBuiltTransportClient(settings)
.addTransportAddress(
new InetSocketTransportAddress(InetAddress.getByName("192.168.174.128"), 9300)
);
Article article = new Article(2,"MH370坠毁在柬埔寨密林?中国一公司调十颗卫星去拍摄","警惕荒唐的死亡游戏!俄15岁少年输掉游戏后用电锯自杀");
ObjectMapper objectMapper = new ObjectMapper();
String jsonDocument = objectMapper.writeValueAsString(article);
client.prepareIndex("demo","article","3")
.setSource(jsonDocument, XContentType.JSON)
.get();
client.close();
}
5. 查询文档操作
以下查询方法区别只在于QueryBuilder的创建不同。 Ctrl+Alt+Shift+T:抽取代码块为重构方法的快捷键
1)抽取方法 1、 连接对象
```java
TransportClient client;
@Before
public void init() throws UnknownHostException {
// 1. 设置加入的集群
Settings settings = Settings.builder().put("cluster.name", "my-elasticsearch").build();
// 2. 创建Client连接对象
client = new PreBuiltTransportClient(settings)
.addTransportAddress(
new InetSocketTransportAddress(InetAddress.getByName("192.168.174.128"), 9300)
);
}
```
2、 查询方法(包含分页操作)
public void search(QueryBuilder queryBuilder) {
SearchResponse searchResponse = client.prepareSearch("demo")
.setTypes("article")
.setQuery(queryBuilder)
.setFrom(0)
.setSize(5)
.get();
SearchHits searchHits = searchResponse.getHits();
System.out.println("查询结果总记录数"+searchHits.getTotalHits());
Iterator<SearchHit> iterator = searchHits.iterator();
while(iterator.hasNext()){
SearchHit searchHit = iterator.next();
System.out.println(searchHit.getSourceAsString());
System.out.println("---文档属性---");
Map<String, Object> document = searchHit.getSource();
System.out.println("id:"+document.get("id"));
System.out.println("title:"+document.get("title"));
System.out.println("content:"+document.get("content"));
}
client.close();
}
- termQuery
@Test
public void QueryByTerm() {
QueryBuilder queryBuilder = QueryBuilders.termQuery("title", "卫星");
search(queryBuilder);
}
- QueryString
@Test
public void QueryByQueryString() {
QueryBuilder queryBuilder = QueryBuilders.queryStringQuery("卫星拍摄").defaultField("title");
search(queryBuilder);
}
- MatchQuery
@Test
public void QueryByMatchString() {
QueryBuilder queryBuilder = QueryBuilders.matchQuery("title","卫星拍摄");
search(queryBuilder);
}
- 使用文档ID查询文档
@Test
public void QueryById() {
QueryBuilder queryBuilder = QueryBuilders.idsQuery().addIds("2","3");
search(queryBuilder);
}
- 查询文档分页操作
查看上方抽取的查询方法
(二) 查询结构高亮操作
-
高亮显示的概念 京东商城搜索"笔记本" -
高亮显示的html分析 通过开发者工具查看高亮数据的html代码实现: ElasticSearch可以对查询出的内容中关键字部分进行标签和样式的设置,但是你需要告诉ElasticSearch 使用什么标签对高亮关键字进行包裹 -
高亮显示代码实现
@Test
public void highLightSearch() {
QueryBuilder queryBuilder = QueryBuilders.multiMatchQuery("卫星","title","content");
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title").field("content");
highlightBuilder.preTags("<em>");
highlightBuilder.postTags("</em>");
SearchResponse searchResponse = client.prepareSearch("demo")
.setTypes("article")
.setQuery(queryBuilder)
.highlighter(highlightBuilder)
.get();
SearchHits searchHits = searchResponse.getHits();
System.out.println("查询结果总记录数"+searchHits.getTotalHits());
Iterator<SearchHit> iterator = searchHits.iterator();
while(iterator.hasNext()){
SearchHit searchHit = iterator.next();
System.out.println(searchHit.getSourceAsString());
System.out.println("---文档属性---");
Map<String, Object> document = searchHit.getSource();
System.out.println("id:"+document.get("id"));
System.out.println("title:"+document.get("title"));
System.out.println("content:"+document.get("content"));
System.out.println("---高亮结果---");
Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
if (highlightFields.size()>0){
for (Map.Entry<String,HighlightField> entry:highlightFields.entrySet()) {
System.out.println(entry.getKey()+":"+Arrays.toString(entry.getValue().getFragments()));
}
}
}
client.close();
}
注意:如果有命中,则会返回高亮结果,前端拿到后直接用高亮结果的字符串进行替换,中的结果显示为高亮。
九、 Spring Data ElasticSearch
1. 简介
-
Spring Data Spring Data是一个员工用于简化数据库访问,并支持云服务的开源框架。其主要目标是使得对数据的访问变得方便快捷,并支持map-reduce框架和云计算数据服务。 Spring Data可以极大的简化JPA的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了CRUD外,还包括如分页、排序等一些 常用的功能。 Spring Data的官网:http://projects.spring.io/spring-data/ Spring Data常用的功能模块如下: -
Spring Data ElasticSearch Spring Data ElasticSearch 基于 spring data API 简化 elasticSearch操作,将原始操作elasticSearch的客户端API 进行封装 。Spring Data为Elasticsearch项目提供集成搜索引擎。Spring Data Elasticsearch POJO的关键功能区域为中心的模型与Elastichsearch交互文档和轻松地编写一个存储库数据访问层。 官方网站:http://projects.spring.io/spring-data-elasticsearch/
2. 入门案例
- 导入Spring Data ElasticSearch坐标
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lbl</groupId>
<artifactId>SpringDataElasticSearch</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 启动器
package com.lbl.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class,args);
}
}
- 配置文件
spring:
data:
elasticsearch:
cluster-name: my-elasticsearch
cluster-nodes: 192.168.174.128:9300
- 编写实体Article
@Document(indexName = "data-demo", type = "article")
public class Article {
@Id
@Field(type = FieldType.Integer,store = true)
private Integer id;
@Field(type = FieldType.text,store = true, analyzer = "ik_smart")
private String title;
@Field(type = FieldType.text,store = true, analyzer = "ik_smart")
private String content;
。。。
}
- 编写Dao
方法命名规则查询的基本语法findBy + 属性 + 关键词 + 连接符
关键字 | 命名规则 | 解释 | 示例 |
---|
and | findByField1AndField2 | 根据Field1和Field2获得数据 | findByTitleAndContent | or | findByField1OrField2 | 根据Field1或Field2获得数据 | findByTitleOrContent | is | findByField | 根据Field获得数据 | findByTitle | not | findByFieldNot | 根据Field获得补集数据 | findByTitleNot | between | findByFieldBetween | 获得指定范围的数据 | findByPriceBetween | lessThanEqual | findByFieldLessThan | 获得小于等于指定值的数据 | findByPriceLessThan | like | findByFieldLike | 根据根据Field获得数据(有分词,没加Like均为无分词) | findByTitleLike |
在Dao中加入以命名规则命名的接口方法,无需映射文件,即可在方法中调用。
package com.lbl.demo.dao;
import com.lbl.demo.pojo.Article;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface ArticleDao extends ElasticsearchRepository<Article,Integer> {
List<Article> findByTitle(String title);
List<Article> findByTitleOrContent(String field, String str, Pageable pageable);
}
- 创建测试类
package com.lbl.demo;
import com.lbl.demo.dao.ArticleDao;
import com.lbl.demo.pojo.Article;
import org.elasticsearch.index.query.QueryBuilders;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import java.util.Optional;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Autowired
private ArticleDao articleDao;
@Autowired
private ElasticsearchTemplate template;
@Test
public void createIndex(){
template.createIndex(Article.class);
}
@Test
public void addDocument(){
for (int i = 10; i <= 20; i++) {
Article article = new Article();
article.setId(i);
article.setTitle("女护士路遇昏迷男子跪地抢救:救人是职责更是本能"+i);
article.setContent("这是一个美丽的女护士妹妹"+i);
articleDao.save(article);
}
}
@Test
public void deleteDocumentByIdOrAll(){
articleDao.deleteById(20);
}
@Test
public void findByIdOrAll(){
Optional<Article> optional = articleDao.findById(10);
Article article = optional.get();
System.out.println(article);
Iterable<Article> articles = articleDao.findAll();
articles.forEach(a -> System.out.println(a));
}
@Test
public void findByOneField(){
List<Article> list = articleDao.findByTitle("女护士");
list.forEach(a -> System.out.println(a));
}
@Test
public void findByTitleOrContent() {
Pageable pageable = PageRequest.of(1, 5);
articleDao.findByTitleOrContent("title","女护士",pageable)
.forEach(a -> System.out.println(a));
}
@Test
public void nativeSearchQuery(){
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.queryStringQuery("女护士").defaultField("title"))
.withPageable(PageRequest.of(0, 15))
.build();
List<Article> articleList = template.queryForList(query, Article.class);
articleList.forEach(a -> System.out.println(a));
}
}
3. 聚合查询
聚合查询参考https://www.cnblogs.com/wshcn/p/8150511.html
- 实体类
要聚合的字段需要单独存储到fielddata中,所以fielddata=true
@Document(indexName = "car",type = "carInfo")
public class Car {
@Id
@Field(type = FieldType.Long,store = true)
private Long id;
@Field(type = FieldType.text,store = true, analyzer = "ik_smart")
private String name;
@Field(type = FieldType.text,store = true, analyzer = "ik_smart",fielddata = true)
private String color;
@Field(type = FieldType.text,store = true, analyzer = "ik_smart",fielddata = true)
private String brand;
@Field(type = FieldType.Double,store = true)
private Double price;
。。。
}
- Dao
package com.lxs.esdemo2.dao;
import com.lxs.esdemo2.domain.Article;
import com.lxs.esdemo2.domain.Car;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface CarDao extends ElasticsearchRepository<Car, Long> {
}
- 初始化索引库
@RunWith(SpringRunner.class)
@SpringBootTest
public class Demo2ApplicationTests {
@Autowired
private CarDao carDao;
@Autowired
private ElasticsearchTemplate template;
@Test
public void initIndex() {
template.createIndex(Car.class);
carDao.save(new Car(1l, "比亚迪A1", "红色","比亚迪", 50000d));
carDao.save(new Car(2l, "比亚迪A2", "白色", "比亚迪", 70000d));
carDao.save(new Car(3l, "比亚迪A3", "白色", "比亚迪", 80000d));
carDao.save(new Car(4l, "比亚迪A4", "红色","比亚迪", 60000d));
carDao.save(new Car(5l, "比亚迪A5", "红色","比亚迪", 90000d));
carDao.save(new Car(6l, "宝马A1", "红色", "宝马", 10000d));
carDao.save(new Car(7l, "宝马A2", "黑色", "宝马", 20000d));
carDao.save(new Car(8l, "宝马A3", "黑色", "宝马", 30000d));
carDao.save(new Car(9l, "宝马A4", "红色", "宝马", 40000d));
carDao.save(new Car(10l, "宝马A5", "红色", "宝马", 50000d));
carDao.save(new Car(11l, "奔驰A1", "红色", "奔驰", 10000d));
carDao.save(new Car(12l, "奔驰A2", "黑色", "奔驰", 20000d));
carDao.save(new Car(13l, "奔驰A3", "黑色", "奔驰", 30000d));
carDao.save(new Car(14l, "奔驰A4", "红色", "奔驰", 40000d));
carDao.save(new Car(15l, "奔驰A5", "红色", "奔驰", 50000d));
}
}
- 划分桶
按“color”划分 “aggs”为聚合关键字 “group_by_price”为自定义聚合名称
GET /car_index/car/_search
{
"query": {
"bool": {
"should": [
{
"match_all": {}
}
]
}
},
"aggs": {
"group_by_bland": {
"terms": {
"field": "color"
}
}
}
}
- 桶内度量
桶内求平均值 “avg_price”为自定义聚合名称 “avg”为求平均值的集合关键字
GET /car_index/car/_search
{
"query": {
"bool": {
"should": [
{
"match_all": {}
}
]
}
},
"aggs": {
"group_by_bland": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
4. Spring Data ElasticSearch代码实现
- 划分桶
@Test
public void querySelfAggs(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchAllQuery());
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{},null));
queryBuilder.addAggregation(AggregationBuilders.terms("group_by_color").field("color"));
AggregatedPage<Car> aggPage = (AggregatedPage<Car>)carDao.search(queryBuilder.build());
StringTerms agg = (StringTerms)aggPage.getAggregation("group_by_color");
List<StringTerms.Bucket> buckets = agg.getBuckets();
for (StringTerms.Bucket bucket : buckets) {
String color = bucket.getKeyAsString();
long docCount = bucket.getDocCount();
System.out.println("color="+color+"总数="+docCount);
}
}
- 桶内度量
即嵌套聚合
@Test
public void querySelfSubAggs(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchAllQuery());
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{},null));
queryBuilder.addAggregation(
AggregationBuilders.terms("group_by_color").field("color")
.subAggregation(AggregationBuilders.avg("avg_price").field("price"))
);
AggregatedPage<Car> aggPage = (AggregatedPage<Car>)carDao.search(queryBuilder.build());
StringTerms agg = (StringTerms)aggPage.getAggregation("group_by_color");
List<StringTerms.Bucket> buckets = agg.getBuckets();
for (StringTerms.Bucket bucket : buckets) {
String color = bucket.getKeyAsString();
long docCount = bucket.getDocCount();
InternalAvg avgPrice = (InternalAvg)bucket.getAggregations().asMap().get("avg_price");
System.out.println("color="+color+"总数="+docCount+"平均价格="+avgPrice.getValue());
}
}
|