目录
- 前言
- 1 什么是全文检索
- 2 Lucene 实现全文检索的流程说明
- 3 Lucene实战
- 4.Elastic search介绍和安装
- 5.使用kibana对索引库操作
- 6.使用kibana对类型及映射操作
- # 6.1.创建字段映射
- 7.使用kibana对文档操作
- 8.查询
- 9. 聚合aggregations
- 10.Elasticsearch集群
- 11.Elasticsearch客户端
- 12.Spring Data Elasticsearch
前言
文章内容输出来源:拉勾教育JAVA就业训练营
1 什么是全文检索
1.1 数据分类
我们生活中的数据总体分为两种:结构化数据和非结构化数据。
- 结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
- 非结构化数据:指不定长或无固定格式的数据,如邮件,word 文档等磁盘上的文件
1.2 结构化数据搜索
常见的结构化数据也就是数据库中的数据。
在数据库中搜索很容易实现,通常都是使用 sql语句进行查询,而且能很快的得到查询结果。
1.3 非结构化数据查询方法
( 1 ) 顺序扫描法(Serial Scanning)
用户搜索----->文件
所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。如利用 windows 的搜索也可以搜索文件内容 ,只是相当的慢。
( 2 ) 全文检索(Full-text Search)
- 用户通过查询索引库---->生成索引----->文档
- 全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方法。这个过程类似于通过字典的目录查字的过程。
- 将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引。
- 例如:字典。字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结构化的,如果字典没有音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描。然而字的某些信息可以提取出来进行结构化处理,比如读音,就比较结构化,分声母和韵母,分别只有几种可以一一列举,于是将
读音拿出来按一定的顺序排列,每一项读音都指向此字的详细解释的页数。我们搜索时按结构化的拼音搜到读音,然后按其指向的页数,便可找到我们的非结构化数据——也即对字的解释。 - 这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-Text Search) 。虽然创建索引的过程也是非常耗时的,但是索引一旦创建就可以多次使用,全文检索主要处理的是查询,所以耗时间创建索引是值得的。
- 建立索引
- 检索索引
1.4 如何实现全文检索
- 可以使用 Lucene 实现全文检索。Lucene 是 apache 下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene 的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能。
Lucene适用场景:
- 在应用中为数据库中的数据提供全文检索实现。
- 开发独立的搜索引擎服务、系统
Lucene的特性: - 1). 稳定、索引性能高
- 每小时能够索引150GB以上的数据
- 对内存的要求小,只需要1MB的堆内存
- 增量索引和批量索引一样快
- 索引的大小约为索引文本大小的20%~30%
- 2).高效、准确、高性能的搜索算法
-良好的搜索排序- 强大的查询方式支持:短语查询、通配符查询、临近查询、范围查询等
- 支持字段搜索(如标题、作者、内容)
- 可根据任意字段排序
- 支持多个索引查询结果合并
- 支持更新操作和查询操作同时进行
- 支持高亮、join、分组结果功能
- 速度快
- 可扩展排序模块,内置包含向量空间模型、BM25模型可选
- 可配置存储引擎
- 3).跨平台
- 纯java编写
- 作为Apache开源许可下的开源项目,你可以在商业或开源项目中使用
- Lucene有多种语言实现版(如C,C++、Python等),不仅仅是JAVA
Lucene架构:
1.5 全文检索的应用场景
对于数据量大、数据结构不固定的数据可采用全文检索方式搜索,
- 单机软件的搜索:word、markdown
- 站内搜索:京东、淘宝、拉勾,索引源是数据库
- 搜索引擎:百度、Google,索引源是爬虫程序抓取的数据
2 Lucene 实现全文检索的流程说明
2.1 索引和搜索流程图
1、绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:
- 确定原始内容即要搜索的内容–>采集文档–>创建文档–>分析文档–>索引文档
2、红色表示搜索过程,从索引库中搜索内容,搜索过程包括:
- 用户通过搜索界面–>创建查询–>执行搜索,从索引库搜索–>渲染搜索结果
2.2 创建索引
核心概念:
Document:
- 用户提供的源是一条条记录,它们可以是文本文件、字符串或者数据库表的一条记录等等。一条记录经过索引之后,就是以一个Document的形式存储在索引文件中的。用户进行搜索,也是以Document列表的形式返回。
Field
- 一个Document可以包含多个信息域,例如一篇文章可以包含“标题”、“正文”、“最后修改时间”等信息域,这些信息域就是通过Field在Document中存储的。
- Field有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个Field进行存储;通过索引属性你可以控制是否对该Field进行索引。
- 如果对标题和正文进行全文搜索,所以我们要把索引属性设置为真,同时我们希望能直接从搜索结果中提取文章标题,所以我们把标题域的存储属性设置为真,但是由于正文域太大了,我们为了缩小索引文件大小,将正文域的存储属性设置为假,当需要时再直接读取文件;我们只是希望能从搜索解果中提取最后修改时间,不需要对它进行搜索,所以我们把最后修改时间域的存储属性设置为真,索引属性设置为假。上面的三个域涵盖了两个属性的三种组合,还有一种全为假的没有用到,事实上Field不允许你那么设置,因为既不存储又不索引的域是没有意义的。
Term:
- Term是搜索的最小单位,它表示文档的一个词语,Term由两部分组成:它表示的词语和这个词语所出现的Field的名称。
我们以拉勾招聘网站的搜索为例,在网站上输入关键字搜索显示的内容不是直接从数据库中来的,而是从索引库中获取的,网站的索引数据需要提前创建的。以下是创建的过程:
- 第一步:获得原始文档:就是从mysql数据库中通过sql语句查询需要创建索引的数据
- 第二步:创建文档对象(Document),把查询的内容构建成lucene能识别的Document对象,获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档,文档中包括一个一个的域(Field),这个域对应就是表中的列。
- 注意:每个 Document 可以有多个 Field,不同的 Document 可以有不同的 Field,同一个Document可以有相同的 Field(域名和域值都相同)。每个文档都有一个唯一的编号,就是文档 id。
- 第三步:分析文档
将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单元,可以将语汇单元理解为一个一个的单词。分好的词会组成索引库中最小的单元:term,一个term由域名和词组成 - 第四步:创建索引,
对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到 Document(文档)。
注意:创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫 倒排索引结构。
倒排索引结构是根据内容(词语)找文档,如下图:
倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。
2.3 倒排索引
倒排索引记录每个词条出现在哪些文档,及在文档中的位置,可以根据词条快速定位到包含这个词条的文档及出现的位置。
- 文档:索引库中的每一条原始数据,例如一个商品信息、一个职位信息
- 词条:原始数据按照分词算法进行分词,得到的每一个词
创建倒排索引,分为以下几步:
1)创建文档列表:
lucene首先对原始文档数据进行编号(DocID),形成列表,就是一个文档列表
2)创建倒排索引列表
对文档中数据进行分词,得到词条(分词后的一个又一个词)。对词条进行编号,以词条创建索引。然后记录下包含该词条的所有文档编号(及其它信息)。
搜索的过程:
当用户输入任意的词条时,首先对用户输入的数据进行分词,得到用户要搜索的所有词条,然后拿着这些词条去倒排索引列表中进行匹配。找到这些词条就能找到包含这些词条的所有文档的编号。然后根据这些编号去文档列表中找到文档
2.4 查询索引
查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档
- 第一步:创建用户接口:用户输入关键字的地方
- 第二步:创建查询 指定查询的域名和关键字
- 第三步:执行查询
- 第四步:渲染结果 (结果内容显示到页面上 关键字需要高亮)
3 Lucene实战
3.1 需求说明
生成职位信息索引库,从索引库检索数据
3.2 准备开发环境
第一步:创建一个maven工程 ,已经学过Spring Boot,我们就创建一个SpringBoot项目
第二步:导入依赖
第三步:创建引导类
第四步:配置properties文件
第五步:创建实体类、mapper、service
3.3创建索引
@RunWith(SpringRunner.class) //springboot做junit测试固定的写法
@SpringBootTest
public class LuceneManager {
@Autowired
private JobInfoServiceImpl jobInfoService;
@Test
public void testCreateIndex() throws Exception{
// 1、指定索引文件存储的位置 D:\class\index
Directory directory = FSDirectory.open(new File("D:\\class\\index"));
// 2、配置版本和分词器
Analyzer analyzer = new StandardAnalyzer();//标准分词器
IndexWriterConfig config = new
IndexWriterConfig(Version.LATEST,analyzer);
// 3、创建一个用来创建索引的对象 IndexWriter
IndexWriter indexWriter = new IndexWriter(directory,config);
indexWriter.deleteAll(); //先删除索引
// 4、获取原始数据
List<JobInfo> jobInfoList = jobInfoService.findAll();
// 有多少的数据就应该构建多少lucene的文档对象document
for (JobInfo jobInfo : jobInfoList) {
Document document = new Document();
// 域名 值 源数据是否存储
document.add(new LongField("id",jobInfo.getId(), Field.Store.YES));
document.add(new TextField("companyName",jobInfo.getCompanyName(),
Field.Store.YES));
document.add(new TextField("companyAddr",jobInfo.getCompanyAddr(),
Field.Store.YES));
document.add(new TextField("jobName",jobInfo.getJobName(),
Field.Store.YES));
document.add(new TextField("jobAddr",jobInfo.getJobAddr(),
Field.Store.YES));
document.add(new IntField("salary",jobInfo.getSalary(),
Field.Store.YES));
document.add(new StringField("url",jobInfo.getUrl(),
Field.Store.YES));
// StringField 不需要分词时使用 举例:url 、电话号码、身份证号
indexWriter.addDocument(document);
}
// 关闭资源
indexWriter.close();
}
}
生成的索引目录:D:\class\index
- 索引(Index):
- 在Lucene中一个索引是放在一个文件夹中的。
- 如下图,同一文件夹中的所有的文件构成一个Lucene索引。
- 段(Segment):
- 按层次保存了从索引,一直到词的包含关系:索引(Index) –> 段(segment) –> 文档
(- Document) –> 域(Field) –> 词(Term) - 也即此索引包含了那些段,每个段包含了那些文档,每个文档包含了那些域,每个域包含了那些词。
- 一个索引可以包含多个段,段与段之间是独立的,添加新文档可以生成新的段,不同的段可以合并。
- 如上图,具有相同前缀文件的属同一个段,图*一个段 “_0” 。
- segments.gen和segments_1是段的元数据文件,也即它们保存了段的属性信息。
Field的特性:
-Document(文档)是Field(域)的承载体, 一个Document由多个Field组成. Field由名称和值两部分组成,Field的值是要索引的内容, 也是要搜索的内容.
- 按层次保存了从索引,一直到词的包含关系:索引(Index) –> 段(segment) –> 文档
- 是否分词(tokenized)
是: 将Field的值进行分词处理, 分词的目的是为了索引. 如: 商品名称, 商品描述. 这些内容用户会通过输入关键词进行查询, 由于内容多样, 需要进行分词处理建立索引.
否: 不做分词处理. 如: 订单编号, 身份证号, 是一个整体, 分词以后就失去了意义, 故不需要分词. - 是否索引(indexed)
是: 将Field内容进行分词处理后得到的词(或整体Field内容)建立索引, 存储到索引域. 索引的目的是为了搜索. 如: 商品名称, 商品描述需要分词建立索引. 订单编号, 身份证号作为整体建立索引. 只要可能作为用户查询条件的词, 都需要索引.
否: 不索引. 如: 商品图片路径, 不会作为查询条件, 不需要建立索引. - 是否存储(stored)
是: 将Field值保存到Document中. 如: 商品名称, 商品价格. 凡是将来在搜索结果页面展现给用户的内容, 都需要存储.
否: 不存储. 如: 商品描述. 内容多格式大, 不需要直接在搜索结果页面展现, 不做存储. 需要的时候可以从关系数据库取.
常用的Field类型:
3.4查询索引
public class LuceneQuery {
@Test
public void testQueryIndex() throws Exception{
// 1、指定索引文件存储的位置 D:\class\index
Directory directory = FSDirectory.open(new File("D:\\class\\index"));
// 2、 创建一个用来读取索引的对象 indexReader
IndexReader indexReader = DirectoryReader.open(directory);
// 3、 创建一个用来查询索引的对象 IndexSearcher
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
// 使用term查询:指定查询的域名和关键字
Query query = new TermQuery(new Term("companyName","北京"));
TopDocs topDocs = indexSearcher.search(query, 100);//第二个参数:最多显示多
少条数据
int totalHits = topDocs.totalHits; //查询的总数量
System.out.println("符合条件的总数:"+totalHits);
ScoreDoc[] scoreDocs = topDocs.scoreDocs; //获取命中的文档 存储的是文档的id
for (ScoreDoc scoreDoc : scoreDocs) {
int docID = scoreDoc.doc;
// 根据id查询文档
Document document = indexSearcher.doc(docID);
System.out.println( "id:"+ document.get("id"));
System.out.println( "companyName:"+ document.get("companyName"));
System.out.println( "companyAddr:"+ document.get("companyAddr"));
System.out.println( "salary:"+ document.get("salary"));
System.out.println( "url:"+ document.get("url"));
System.out.println("***********************************************************
***");
}
}
}
3.5中文分词器的使用
使用方式:
- 第一步:导依赖
<!--IK中文分词器-->
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
- 第二步:可以添加配置文件
放入到resources文件夹中。
- 第三步 创建索引时使用IKanalyzer
考虑一个问题:一个大型网站中的索引数据会很庞大的,所以使用lucene这种原生的写代码的方式就不合适了,所以需要借助一个成熟的项目或软件来实现,目前比较有名是solr和elasticSearch
4.Elastic search介绍和安装
Elasticsearch是一个需要安装配置的软件。
ELK技术栈说明
- Elastic有一条完整的产品线:Elasticsearch、Logstash、Kibana等,前面说的三个就是大家常说的ELK技术栈(开源实时日志分析平台)。
- Logstash 的作用就是一个数据收集器,将各种格式各种渠道的数据通过它收集解析之后格式化输出到Elasticsearch ,最后再由Kibana 提供的比较友好的 Web 界面进行汇总、分析、搜索。
- ELK 内部实际就是个管道结构,数据从 Logstash 到 Elasticsearch 再到 Kibana 做可视化展示。这三个组件各自也可以单独使用,比如 Logstash 不仅可以将数据输出到Elasticsearch ,也可以到数据库、缓存等
4.1.简介
4.1.1.Elastic
Elastic官网:https://www.elastic.co/cn/
Elastic有一条完整的产品线:Elasticsearch、Logstash、Kibana等,前面说的三个就是大家常说的ELK技术栈。
4.1.2.Elasticsearch
Elasticsearch官网:https://www.elastic.co/cn/products/elasticsearch
功能:
- 分布式的搜索引擎:百度、Google、站内搜索
- 全文检索:提供模糊搜索等自动度很高的查询方式,并进行相关性排名,高亮等功能
- 数据分析引擎(分组聚合):电商网站—一周内手机销量Top10
- 对海量数据进行近乎实时处理:水平扩展,每秒钟可处理海量事件,同时能够自动管理索引和查询在集群中的分布方式,以实现极其流畅的操作。
如上所述,Elasticsearch具备以下特点:
高速、扩展性、最相关的搜索结果
- 分布式:节点对外表现对等,每个节点都可以作为入门,加入节点自动负载均衡
- JSON:输入输出格式是JSON
- Restful风格,一切API都遵循Rest原则,容易上手
- 近实时搜索,数据更新在Elasticsearch中几乎是完全同步的,数据检索近乎实时
- 安装方便:没有其它依赖,下载后安装很方便,简单修改几个参数就可以搭建集群
- 支持超大数据:可以扩展到PB级别的结构化和非结构化数据
4.1.3.版本
目前Elasticsearch最新的版本是7.x,企业内目前用的比较多是6.x,我们以6.2.4进行讲解,需要JDK1.8及以上。
4.2.安装和配置
为了快速看到效果我们直接在本地window下安装Elasticsearch。环境要求:JDK8及以上版本
- 第一步:把今天资料文件夹中准备好的软件放到一个没有中文没有空格的位置,解压即可
- 第二步:修改配置文件
1、修改索引数据和日志数据存储的路径
第33行和37行,修改完记得把注释打开
path.data: d:\class\es\data
#
# Path to log files:
#
path.logs: d:\class\es\log
- 第三步:进入bin目录中直接双击 图下的命令文件。
如果启动失败(估计好多同学都会启动失败的),需要修改虚拟机内存的大小找到jvm.options文件 如图修改 - Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。
- Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常
4.3.访问
可以看到绑定了两个端口:
9300:集群节点间通讯接口,接收tcp协议
9200:客户端访问接口,接收Http协议
我们在浏览器中访问:http://127.0.0.1:9200
4.4.安装kibana
4.4.1.什么是Kibana
Kibana是一个基于Node.js的Elasticsearch索引库数据统计工具,可以利用Elasticsearch的聚合功能,生成各种图表,如柱形图,线状图,饼图等。而且还提供了操作Elasticsearch索引数据的控制台,并且提供了一定的API提示,非常有利于我们学习Elasticsearch的语法。
4.4.2.安装
因为Kibana依赖于node,需要在windows下先安装Node.js,双击运行课前资料提供的node.js的安装包:
一路下一步即可安装成功,然后在任意DOS窗口输入名:
4.4.3.配置运行
进入安装目录下的config目录,修改kibana.yml文件的第21行(注释放开即可)
修改elasticsearch服务器的地址:
elasticsearch.url: "http://127.0.0.1:9200"
进入安装目录下的bin目录:
双击运行
发现kibana的监听端口是5601
我们访问:http://127.0.0.1:5601
4.4.4.控制台
在页面右侧,我们就可以输入请求,访问Elasticsearch了。
4.5.安装ik分词器
Lucene的IK分词器早在2012年已经没有维护了,现在我们要使用的是在其基础上维护升级的版本,并且开发为Elasticsearch的集成插件了,与Elasticsearch一起维护升级,版本也保持一致
https://github.com/medcl/elasticsearch-analysis-ik
4.5.1.安装
- 解压elasticsearch-analysis-ik-6.2.4.zip后,将解压后的文件夹拷贝到elasticsearch-6.2.4\plugins下,并重命名文件夹为ik
- 重新启动ElasticSearch,即可加载IK分词器
4.6 安装Head插件
4.6.1 elasticsearch-head 简介
elasticsearch-head 简介
- elasticsearch-head是一个界面化的集群操作和管理工具,可以对集群进行傻瓜式操作。你可以通过插件把它集成到es(首选方式),也可以安装成一个独立webapp。
es-head主要有三个方面的操作:
- 显示集群的拓扑,并且能够执行索引和节点级别操作
- 搜索接口能够查询集群中原始json或表格格式的检索数据
- 能够快速访问并显示集群的状态
官方的文档: https://github.com/mobz/elasticsearch-head
4.6.2 elasticsearch-head 安装
elasticsearch-head 安装(基于谷歌浏览器)
(1)直接下载压缩包,地址:https://files.cnblogs.com/files/sanduzxcvbnm/elasticsearch-head.7z
(2)解压
(3)在谷歌浏览器中点击“加载已解压的压缩程序”,找到elasticsearch-head文件夹,点击打开即可进行安装。
5.使用kibana对索引库操作
5.1.基本概念
5.1.1.节点、集群、分片及副本
1、节点 (node)
- 一个节点是一个Elasticsearch的实例。
- 在服务器上启动Elasticsearch之后,就拥有了一个节点。如果在另一台服务器上启动Elasticsearch,这就是另一个节点。甚至可以通过启动多个Elasticsearch进程,在同一台服务器上拥有多个节点。
2、集群(cluster)
- 多个协同工作的Elasticsearch节点的集合被称为集群。
- 在多节点的集群上,同样的数据可以在多台服务器上传播。这有助于性能。这同样有助于稳定性,如果每个分片至少有一个副本分片,那么任何一个节点宕机后,Elasticsearch依然可以进行服务,返回所有数据。
- 但是它也有缺点:必须确定节点之间能够足够快速地通信,并且不会产生脑裂效应(集群的2个部分不能彼此交流,都认为对方宕机了)。
3、分片 (shard)
-
索引可能会存储大量数据,这些数据可能超过单个节点的硬件限制。例如,十亿个文档的单个索引占用了1TB的磁盘空间,可能不适合单个节点的磁盘,或者可能太慢而无法单独满足来自单个节点的搜索请求。
-
为了解决此问题,Elasticsearch提供了将索引细分为多个碎片的功能。创建索引时,只需定义所需的分片数量即可。每个分片本身就是一个功能齐全且独立的“索引”,可以托管在群集中的任何节点上。
-
分片很重要,主要有两个原因:
- 它允许您水平分割/缩放内容量
- 它允许您跨碎片(可能在多个节点上)分布和并行化操作,从而提高性能/吞吐量
-
分片如何分布以及其文档如何聚合回到搜索请求中的机制完全由Elasticsearch管理,并且对您作为用户是透明的。
在随时可能发生故障的网络/云环境中,非常有用,强烈建议您使用故障转移机制,以防碎片/节点因某种原因脱机或消失。为此,Elasticsearch允许您将索引分片的一个或多个副本制作为所谓的副本分片(简称副本)。
4、副本(replica)
- 分片处理允许用户推送超过单机容量的数据至Elasticsearch集群。副本则解决了访问压力过大时单机无法处理所有请求的问题。
- 分片可以是主分片,也可以是副本分片,其中副本分片是主分片的完整副本。副本分片用于搜索,或者是在原有的主分片丢失后成为新的主分片。
- 注意:可以在任何时候改变每个分片的副本分片的数量,因为副本分片总是可以被创建和移除的。这并不适用于索引划分为主分片的数量,在创建索引之前,必须决定主分片的数量。过少的分片将限制可扩展性,但是过多的分片会影响性能。默认设置的5份是一个不错的开始。
5.1.2 文档、类型、索引及映射
1、文档 (document)
Elasticsearch是面向文档的,这意味着索引和搜索数据的最小单位是文档。
在Elasticsearch中文档有几个重要的属性。
- 它是自我包含的。一篇文档同时包含字段和它们的取值。
- 它可以是层次的。文档中还包含新的文档,字段还可以包含其他字段和取值。例如,“location”字段可以同时包含“city”和“street“两个字段。
- 它拥有灵活的结构。文档不依赖于预先定义的模式。并非所有的文档都需要拥有相同的字段,它们不受限于同一个模式。
2、类型 (type)
- 类型是文档的逻辑容器,类似于表格是行的容器。在不同的类型中,最好放入不同结构的文档。例如,可以用一个类型定义聚会时的分组,而另一个类型定义人们参加的活动。
3、索引 (index)
- 索引是映射类型的容器。一个Elasticsearch索引是独立的大量的文档集合。 每个索引存储在磁盘上的同组文件中,索引存储了所有映射类型的字段,还有一些设置。
4、映射(mapping)
- 所有文档在写入索引前都将被分析,用户可以设置一些参数,决定如何将输入文本分割为词条,哪些词条应该被过滤掉,或哪些附加处理有必要被调用(比如移除HTML标签)。这就是映射扮演的角色:存储分析链所需的所有信息。
- Elasticsearch也是基于Lucene的全文检索库,本质也是存储数据,很多概念与MySQL类似的。
对比关系:
详细说明:
索引库(indices) | indices是index的复数,代表许多的索引, |
---|---|
概念 | 说明 |
类型(type) | 类型是模拟mysql中的table概念,一个索引库下可以有不同类型的索引(目前6.X以后的版本只能有一个类型),类似数据库中的表概念。数据库表中有表结构,也就是表中每个字段的约束信息;索引库的类型中对应表结构的叫做 映射(mapping) ,用来定义每个字段的约束。 |
文档(document) | 存入索引库原始的数据。比如每一条商品信息,就是一个文档 |
字段(field) | 文档中的属性 |
映射配置(mappings) | 字段的数据类型、属性、是否索引、是否存储等特性 |
5.2.创建索引库
5.2.1.语法
Elasticsearch采用Rest风格API,因此其API就是一次http请求,你可以用任何工具发起http请求
创建索引的请求格式:
- 请求方式:PUT
- 请求路径:/索引库名
- 请求参数:json格式
{
"settings": {
"属性名": "属性值"
}
}
settings:就是索引库设置,其中可以定义索引库的各种属性,目前我们可以不设置,都走默认
5.2.3.使用kibana创建
kibana的控制台,可以对http请求进行简化,示例:
相当于是省去了elasticsearch的服务器地址
而且还有语法提示,非常舒服。
5.3.查看索引库
Get请求可以帮我们查看索引信息,格式:
GET /索引库名
5.4.删除索引库
删除索引使用DELETE请求
语法
DELETE /索引库名
示例
6.使用kibana对类型及映射操作
-
有了 索引库 ,等于有了数据库中的 database 。接下来就需要索引库中的 类型 了,也就是数据库中的表 。创建数据库表需要设置字段约束,索引库也一样,在创建索引库的类型时,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做 字段映射(mapping)
-
注意:Elasticsearch7.x取消了索引type类型的设置,不允许指定类型,默认为_doc,但字段仍然是有的,我们需要设置字段的约束信息,叫做字段映(mapping)
-
字段的约束我们在学习Lucene中我们都见到过,包括到不限于:
- 字段的数据类型
- 是否要存储
- 是否要索引
- 是否分词
- 分词器是什么
# 6.1.创建字段映射
语法
请求方式依然是PUT
PUT /索引库名/_mapping/typeName
{
"properties": {
"字段名": {
"type": "类型",
"index": true,
"store": true,
"analyzer": "分词器"
}
}
}
- 类型名称:就是前面将的type的概念,类似于数据库中的表
字段名:任意填写,下面指定许多属性,例如:
t- ype:类型,可以是text、keyword、long、short、date、integer、object等- index:是否索引,默认为true
- store:是否存储,默认为false
- analyzer:分词器,这里的 ik_max_word 即使用ik分词器
示例
发起请求:
PUT lagou/_mapping/goods
{
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"images": {
"type": "keyword",
"index": "false"
},
"price": {
"type": "float"
}
}
}
响应结果:
{
"acknowledged": true
}
上述案例中,就给lagou这个索引库添加了一个名为 goods 的类型,并且在类型中设置了3个字段:
- title:商品标题
- images:商品图片
- price:商品价格
6.2.查看映射关系
语法:
GET /索引库名/_mapping
查看某个索引库中的所有类型的映射。如果要查看某个类型映射,可以再路径后面跟上类型名称。即:
GET /索引库名/_mapping/类型名
示例:
GET /lagou/_mapping/goods
响应:
{
"lagou": {
"mappings": {
"goods": {
"properties": {
"images": {
"type": "keyword",
"index": false
},
"price": {
"type": "float"
},
"title": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
}
}
6.3.映射属性详解
1)type
Elasticsearch中支持的数据类型非常丰富:
一级分类 | 二级分类 | 具体类型 | 使用 |
---|---|---|---|
核心类型 | 字符串类型 | text,keyword | 结构化搜索,全文文本搜索、聚合、排 |
- | 序等整数类型 | integer,long,short,byte | 字段的长度越短,索引和搜索的效率越高。 |
- | 浮点类型 | double,float,half_float,scaled_float | - |
- | 逻辑类型 | boolean | - |
- | 日期类型 | date | - |
- | 范围类型 | range | - |
- | 二进制类型 | binary | 该 binary 类型接受二进制值作为Base64编码的字符串。该字段默认情况下不存储(store),并且不可搜索 |
复合类型 | 数组类型 | array | - |
- | 对象类型 | object 用于单个JSON对象 | - |
- | 嵌套类型 | nested | 用于JSON对象数组 |
地理类型 | 地理坐标类型 | geo_point 纬度/经度积分 | |
- | 地理地图 | geo_shape 用于多边形等复杂形状 | |
特殊类型 | IP类型 | ip 用于IPv4和IPv6地址 | |
- | 范围类型 | completion | 提供自动完成建议 |
- | 令牌计数类型 | token_count | 计算字符串中令牌的数量 |
我们说几个关键的:
String类型,又分两种:
- text:使用文本数据类型的字段,它们会被分词,文本字段不用于排序,很少用于聚合,如文章标题、正文。
- keyword:关键字数据类型,用于索引结构化内容的字段,不会被分词,必须完整匹配的内容,如邮箱,身份证号。支持聚合
这两种类型都是比较常用的,但有的时候,对于一个字符串字段,我们可能希望他两种都支持,此时,可以利用其多字段特性
"properties": {
"title":{
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"sort":{
"type": "keyword"
}
},
"index": true
}
}
Numerical:数值类型,分两类
- 基本数据类型:long、interger、short、byte、double、float、half_float
- double 双精度64位
- float 单精度32位
- half_float 半精度16位
- 浮点数的高精度类型:scaled_float
- 带有缩放因子的缩放类型浮点数,依靠一个 long 数字类型通过一个固定的( double 类型)缩放因数进行缩放.
- 需要指定一个精度因子,比如10或100。elasticsearch会把真实值乘以这个因子后存储,取出时再还原。
Date:日期类型
- elasticsearch可以对日期格式化为字符串存储,但是建议我们存储为毫秒值,存储为long,节省空间。
Array:数组类型
- 进行匹配时,任意一个元素满足,都认为满足排序时,如果升序则用数组中的最小值来排序,如果降序则用数组中的最大值来排序
字符串数组:["one", "two"]
整数数组:[1,2]
数组的数组:[1, [2, 3]],等价于[1,2,3]
对象数组:[ { "name": "Mary", "age": 12 }, { "name": "John", "age": 10 }]
Object:对象
- JSON文档本质上是分层的:文档包含内部对象,内部对象本身还包含内部对象。
{
"region": "US",
"manager.age": 30,
"manager.name ": "John Smith"
}
索引方法如下:
{
"mappings": {
"properties": {
"region": { "type": "keyword" },
"manager": {
"properties": {
"age": { "type": "integer" },
"name": { "type": "text" }
}
}
}
}
}
如果存储到索引库的是对象类型,例如上面的girl,会把girl编程两个字段:girl.name和girl.age
- ip地址
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"ip_addr": {
"type": "ip"
}
}
}
}
}
PUT my_index/_doc/1
{
"ip_addr": "192.168.1.1"
}
GET my_index/_search
{
"query": {
"term": {
"ip_addr": "192.168.0.0/16"
}
}
}
2)index
index影响字段的索引情况。
- true:字段会被索引,则可以用来进行搜索过滤。默认值就是true,只有当某一个字段的index值设置为true时,检索ES才可以作为条件去检索。
- false:字段不会被索引,不能用来搜索
index的默认值就是true,也就是说你不进行任何配置,所有字段都会被索引。
但是有些字段是我们不希望被索引的,比如商品的图片信息(URL),就需要手动设置index为false。
3)store
- 是否将数据进行额外存储。
- 在学习lucene时,我们知道如果一个字段的store设置为false,那么在文档列表中就不会有这个字段的值,用户的搜索结果中不会显示出来。
- 但是在Elasticsearch中,即便store设置为false,也可以搜索到结果。
- 原因是Elasticsearch在创建文档索引时,会将文档中的原始数据备份,保存到一个叫做 _source 的属性中。而且我们可以通过过滤 _source 来选择哪些要显示,哪些不显示。
- 而如果设置store为true,就会在 _source 以外额外存储一份数据,多余,因此一般我们都会将store设置为false,事实上,store的默认值就是false。
- 在某些情况下,这对 store 某个领域可能是有意义的。例如,如果您的文档包含一个 title ,一个date 和一个非常大的 content 字段,则可能只想检索the title 和the date 而不必从一个大 _source字段中提取这些字段:
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"title": {
"type": "text",
"store": true
},
"date": {
"type": "date",
"store": true
},
"content": {
"type": "text"
}
}
}
}
}
4)boost
- 网站权重:网站权重是指搜索引擎给网站(包括网页)赋予一定的权威值,对网站(含网页)权威的评估评价。一个网站权重越高,在搜索引擎所占的份量越大,在搜索引擎排名就越好。提高网站权重,不但利于网站(包括网页)在搜索引擎的排名更靠前,还能提高整站的流量,提高网站信任度。所以提高网站的权重具有相当重要的意义。 权重即网站在SEO中的重要性,权威性。英文:Page Strength。1、权重不等于排名 2、权重对排名有着非常大的影响 3、整站权重的提高有利于内页的排名。
权重,新增数据时,可以指定该数据的权重,权重越高,得分越高,排名越靠前。
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"title": {
"type": "text",
"boost": 2
},
"content": {
"type": "text"
}
}
}
}
}
title 字段上的匹配项的权重是字段上的匹配项的权重的两倍 content ,默认 boost 值为 1.0 。
提升仅适用于Term查询(不提升prefix,range和模糊查询)。
6.4.一次创建索引库和类型
第一步:
PUT /lagou
第二步:
PUT lagou/_mapping/goods
{
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"images": {
"type": "keyword",
"index": "false"
},
"price": {
"type": "float"
}
}
}
刚才 的案例中我们是把创建索引库和类型分开来做,其实也可以在创建索引库的同时,直接制定索引库中的类型,基本语法
put /索引库名
{
"settings":{
"索引库属性名":"索引库属性值"
},
"mappings":{
"类型名":{
"properties":{
"字段名":{
"映射属性名":"映射属性值"
}
}
}
}
}
7.使用kibana对文档操作
文档,即索引库中某个类型下的数据,会根据规则创建索引,将来用来搜索。可以类比做数据库中的每一行数据。
7.1.新增文档
7.1.1.新增并随机生成id
通过POST请求,可以向一个已经存在的索引库中添加文档数据。
POST /索引库名/类型名
{
"key":"value"
}
示例:
POST /lagou/goods/
{
"title":"小米手机",
"images":"http://image.lagou.com/12479122.jpg",
"price":2699.00
}
响应:
{
"_index": "lagou",
"_type": "goods",
"_id": "tURGznQB29tVfg_iWHfl",
"_version": 1,
"result": "created",
"_shards": {
"total": 3,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 2
}
- 可以看到结果显示为: created ,应该是创建成功了。
- 另外,需要注意的是,在响应结果中有个 _id 字段,这个就是这条文档数据的 唯一标示 ,以后的增删改查都依赖这个id作为唯一标示。
- 可以看到id的值为: tURGznQB29tVfg_iWHfl ,这里我们新增时没有指定id,所以是ES帮我们随机生成的id。
7.2.查看文档
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把刚刚生成数据的id带上。
通过kibana查看数据:
GET /lagou/goods/tURGznQB29tVfg_iWHfl
查看结果:
{
"_index": "lagou",
"_type": "goods",
"_id": "tURGznQB29tVfg_iWHfl",
"_version": 1,
"found": true,
"_source": {
"title": "小米手机",
"images": "http://image.lagou.com/12479122.jpg",
"price": 2699
}
}
- _source :源文档信息,所有的数据都在里面。
- _id :这条文档的唯一标示
- 自动生成的id,长度为20个字符,URL安全,base64编码,GUID(全局唯一标识符),分布式系统并行生成时不可能会发生冲突
- 在实际开发中不建议使用ES生成的ID,太长且为字符串类型,检索时效率低。建议:将数据表中唯一的ID,作为ES的文档ID
7.3.新增文档并自定义id
如果我们想要自己新增的时候指定id,可以这么做:
POST /lagou/goods/2
{
"title":"大米手机",
"images":"http://image.lagou.com/12479122.jpg",
"price":2899.00
}
7.4.修改数据
PUT:修改文档
POST:新增文档
把刚才新增的请求方式改为PUT,就是修改了。不过修改必须指定id,id对应文档存在,则修改,id对应文档不存在,则新增
比如,我们把使用id为3,不存在,则应该是新增:
PUT /lagou/goods/3
{
"title":"超米手机",
"images":"http://image.lagou.com/12479122.jpg",
"price":3899.00,
"stock": 100,
"saleable":true
}
7.5.删除数据
删除使用DELETE请求,同样,需要根据id进行删除:
7.6.智能判断
- 刚刚我们在新增数据时,添加的字段都是提前在类型中定义过的,如果我们添加的字段并没有提前定义过,能够成功吗?
- 事实上Elasticsearch非常智能,你不需要给索引库设置任何mapping映射,它也可以根据你输入的数据来判断类型,动态添加数据映射
POST /lagou/goods/3
{
"title":"超大米手机",
"images":"http://image.lagou.com/12479122.jpg",
"price":3299.00,
"stock": 200,
"saleable":true,
"subTitle":"大米"
}
7.7.动态映射模板
1)模板名称,随便起
2)匹配条件,凡是符合条件的未定义字段,都会按照这个规则来映射
3)映射规则,匹配成功后的映射规则
举例,我们可以把所有未映射的string类型数据自动映射为keyword类型:
PUT lagou3
{
"mappings": {
"goods": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
}
},
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword",
"index":false,
"store":true
}
}
}
]
}
}
}
在这个案例中,我们把做了两个映射配置:
- title字段:统一映射为text类型,并制定分词器
- 其它字段:只要是string类型,统一都处理为keyword类型。
这样,未知的string类型数据就不会被映射为text和keyword并存,而是统一以keyword来处理!
8.查询
- 基本查询
- 结果过滤
- 高级查询
- 排序
8.1.基本查询:
基本语法
GET /索引库名/_search
{
"query":{
"查询类型":{
"查询条件":"查询条件值"
}
}
}
这里的query代表一个查询对象,里面可以有不同的查询属性
- 查询类型:
- 例如: match_all , match , term , range 等等
- 查询条件:查询条件会根据类型的不同,写法也有差异,后面详细讲解
8.1.1 查询所有(match_all)
GET /lagou/_search
{
"query":{
"match_all": {}
}
}
- query :代表查询对象
- match_all :代表查询所有
- took:查询花费时间,单位是毫秒
- time_out:是否超时
- _shards:分片信息
- hits:搜索结果总览对象
- total:搜索到的总条数
- max_score:所有结果中文档得分的最高分
- hits:搜索结果的文档对象数组,每个元素是一条搜索到的文档信息
- _index:索引库
- _type:文档类型
- _id:文档id
- _score:文档得分
- _source:文档的源数据
文档得分:使用ES时,对于查询出的文档无疑会有文档相似度之别。而理想的排序是和查询条件相关性越高排序越靠前,而这个排序的依据就是_score
8.1.2 匹配查询(match)
GET /lagou/_search
{
"query":{
"match":{
"title":"小米电视"
}
}
}
在上面的案例中,不仅会查询到电视,而且与小米相关的都会查询到,多个词之间是 or 的关系。
- and关系
某些情况下,我们需要更精确查找:比如在电商平台精确搜索商品时,我们希望这个关系(查询条件切分词之后的关系)变成 and (既要满足你,又要满足我),可以这样做
GET /lagou/_search
{
"query":{
"match":{
"title":{"query":"小米电视","operator":"and"}
}
}
}
8.1.3 词条匹配(term)
term 查询被用于精确值 匹配,这些精确值可能是数字、时间、布尔或者那些未分词的字符串,keyword类型的字符串
- 效果类似于:select * from tableName where name=‘value’;
GET /lagou/_search
{
"query":{
"term":{
"price":2699.00
}
}
}
8.1.4 布尔组合(bool)
bool 把各种其它查询通过 must (与)、 must_not (非)、 should (或)的方式进行组合
GET /lagou/_search
{
"query":{
"bool":{
"must": { "match": { "title": "大米" }},
"must_not": { "match": { "title": "电视" }},
"should": { "match": { "title": "手机" }}
}
}
}
8.1.5 范围查询(range)
range 查询找出那些落在指定区间内的数字或者时间
GET /lagou/_search
{
"query":{
"range": {
"price": {
"gte": 1000.0,
"lt": 2800.00
}
}
}
}
range 查询允许以下字符:
操作符 | 说明 |
---|---|
gt | 大于 |
gte | 大于等于 |
lt | 小于 |
lte | 小于等于 |
8.1.6 模糊查询(fuzzy)
fuzzy 查询是 term 查询的模糊等价,很少直接使用它。
fuzzy 查询是 term 查询的模糊等价。它允许用户搜索词条与实际词条的拼写出现偏差,但是偏差的编辑距离不得超过2:
GET /lagou/_search
{
"query": {
"fuzzy": {
"title": "appla"
}
}
}
8.2.结果过滤
默认情况下,elasticsearch在搜索的结果中,会把文档中保存在 _source 的所有字段都返回。
如果我们只想获取其中的部分字段,我们可以添加 _source 的过滤
8.2.1.直接指定字段
GET /lagou/_search
{
"_source": ["title","price"],
"query": {
"term": {
"price": 2699
}
}
}
8.2.2.指定includes和excludes
我们也可以通过:
includes:来指定想要显示的字段
excludes:来指定不想要显示的字段
二者都是可选的。
GET /lagou/_search
{
"_source": {
"includes":["title","price"]
},
"query": {
"term": {
"price": 2699
}
}
}
8.3 过滤(filter)
Elasticsearch 使用的查询语言(DSL)拥有一套查询组件,这些组件可以以无限组合的方式进行搭配。
这套组件可以在以下两种情况下使用:过滤情况(filtering context)和查询情况(query context)。
如何选择查询与过滤:
- 通常的规则是,使用查询(query)语句来进行 全文 搜索或者其它任何需要影响 相关性得分 的搜索。
除此以外的情况都使用过滤(filters)。 - 条件查询中进行过滤
- 所有的查询都会影响到文档的评分及排名。如果我们需要在查询结果中进行过滤,并且不希望过滤条件影响评分,那么就不要把过滤条件作为查询条件来用。而是使用 filter 方式:
GET /lagou/_search
{
"query":{
"bool":{
"must":{ "match": { "title": "小米手机" }},
"filter":{
"range":{"price":{"gt":2000.00,"lt":3800.00}}
}
}
}
}
- 无查询条件,直接过滤
- 如果一次查询只有过滤,没有查询条件,不希望进行评分,我们可以使用 constant_score 取代只有filter 语句的 bool 查 询。在性能上是完全相同的,但对于提高查询简洁性和清晰度有很大帮助。
GET /lagou/_search
{
"query":{
"constant_score": {
"filter": {
"range":{"price":{"gt":2000.00,"lt":3000.00}}
}
}
}
}
8.4 排序
8.4.1 单字段排序
sort 可以让我们按照不同的字段进行排序,并且通过 order 指定排序的方式
GET /lagou/_search
{
"query": {
"match": {
"title": "小米手机"
}
},
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
8.4.2 多字段排序
假定我们想要结合使用 price和 _score(得分) 进行查询,并且匹配的结果首先按照价格排序,然后按照相关性得分排序
GET /lagou/_search
{
"query":{
"bool":{
"must":{ "match": { "title": "小米手机" }},
"filter":{
"range":{"price":{"gt":200000,"lt":300000}}
}
}
},
"sort": [
{ "price": { "order": "desc" }},
{ "_score": { "order": "desc" }}
]
}
8.5.分页
Elasticsearch中数据都存储在分片中,当执行搜索时每个分片独立搜索后,数据再经过整合返回。那么,如果要实现分页查询该怎么办呢?
- elasticsearch的分页与mysql数据库非常相似,都是指定两个值:
- from:目标数据的偏移值(开始位置),默认from为0
- size:每页大小
GET /lagou/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "asc"
}
}
],
"from": 3,
"size": 3
}
8.6.高亮
高亮原理:
- 服务端搜索数据,得到搜索结果
- 把搜索结果中,搜索关键字都加上约定好的标签
- 前端页面提前写好标签的CSS样式,即可高亮
elasticsearch中实现高亮的语法比较简单:
GET /lagou/_search
{
"query": {
"match": {
"title": "手机"
}
},
"highlight": {
"pre_tags": "<em>",
"post_tags": "</em>",
"fields": {
"title": {}
}
}
}
在使用match查询的同时,加上一个highlight属性:
- pre_tags:前置标签
- post_tags:后置标签
- fields:需要高亮的字段
- title:这里声明title字段需要高亮
9. 聚合aggregations
聚合可以让我们极其方便的实现对数据的统计、分析。例如:
- 什么品牌的手机最受欢迎?
- 这些手机的平均价格、最高价格、最低价格?
- 这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
9.1 基本概念
Elasticsearch中的聚合,包含多种类型,最常用的两种,一个叫 桶 ,一个叫 度量 :
- 桶(bucket) 类似于 group by
桶的作用,是按照某种方式对数据进行分组,每一组数据在ES中称为一个 桶 ,例如我们根据国籍对人划分,可以得到 中国桶 、 英国桶 , 日本桶 ……或者我们按照年龄段对人进行划分:010,1020,2030,3040等。
Elasticsearch中提供的划分桶的方式有很多:
- Date Histogram Aggregation:根据日期阶梯分组,例如给定阶梯为周,会自动每周分为一组
- Histogram Aggregation:根据数值阶梯分组,与日期类似,需要知道分组的间隔(interval)
- Terms Aggregation:根据词条内容分组,词条内容完全匹配的为一组
- Range Aggregation:数值和日期的范围分组,指定开始和结束,然后按段分组
- ……
综上所述,我们发现bucket aggregations 只负责对数据进行分组,并不进行计算,因此往往bucket中往往会嵌套另一种聚合:metrics aggregations即度量
- 度量(metrics) 相当于聚合的结果
分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在ES中称为 度量
比较常用的一些度量聚合方式:
- Avg Aggregation:求平均值
- Max Aggregation:求最大值
- Min Aggregation:求最小值
- Percentiles Aggregation:求百分比
- Stats Aggregation:同时返回avg、max、min、sum、count等
- Sum Aggregation:求和
- Top hits Aggregation:求前几
- Value Count Aggregation:求总数
- ……
注意:在ES中,需要进行聚合、排序、过滤的字段其处理方式比较特殊,因此不能被分词,必须使用keyword 或 数值类型 。这里我们将color和make这两个文字类型的字段设置为keyword类型,这个类型不会被分词,将来就可以参与聚合
9.2 聚合为桶
首先,我们按照 汽车的颜色 color来 划分 桶 ,按照颜色分桶,最好是使用TermAggregation类型,按照颜色的名称来分桶
GET /car/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
}
}
}
}
- size: 查询条数,这里设置为0,因为我们不关心搜索到的数据,只关心聚合结果,提高效率
- aggs:声明这是一个聚合查询,是aggregations的缩写
- popular_colors:给这次聚合起一个名字,可任意指定。
- terms:聚合的类型,这里选择terms,是根据词条内容(这里是颜色)划分
- field:划分桶时依赖的字段
结果:
- field:划分桶时依赖的字段
- terms:聚合的类型,这里选择terms,是根据词条内容(这里是颜色)划分
- popular_colors:给这次聚合起一个名字,可任意指定。
{
"took": 33,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 8,
"max_score": 0,
"hits": []
},
"aggregations": {
"popular_colors": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "红",
"doc_count": 4
},
{
"key": "绿",
"doc_count": 2
},
{
"key": "蓝",
"doc_count": 2
}
]
}
}
}
- hits:查询结果为空,因为我们设置了size为0
- aggregations:聚合的结果
- popular_colors:我们定义的聚合名称
- buckets:查找到的桶,每个不同的color字段值都会形成一个桶
- key:这个桶对应的color字段的值
- doc_count:这个桶中的文档数量
9.3 桶内度量
- 前面的例子告诉我们每个桶里面的文档数量,这很有用。 但通常,我们的应用需要提供更复杂的文档度量。 例如,每种颜色汽车的平均价格是多少?
- 因此,我们需要告诉Elasticsearch 使用哪个字段 , 使用何种度量方式 进行运算,这些信息要嵌套在 桶内, 度量 的运算会基于 桶 内的文档进行
- 现在,我们为刚刚的聚合结果添加 求价格平均值的度量:
GET /car/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
},
"aggs":{
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
- aggs:我们在上一个aggs(popular_colors)中添加新的aggs。可见度量也是一个聚合
- avg_price:聚合的名称
- avg:度量的类型,这里是求平均值
- field:度量运算的字段
10.Elasticsearch集群
我们都是使用单点的elasticsearch,接下来我们会学习如何搭建Elasticsearch的集群。
10.1.单点的问题
单点的elasticsearch存在哪些可能出现的问题呢?
- 单台机器存储容量有限,无法实现高存储
- 单服务器容易出现单点故障,无法实现高可用
-单服务的并发处理能力有限,无法实现高并发
所以,为了应对这些问题,我们需要对elasticsearch搭建集群
10.2.集群的结构
10.2.1.数据分片
首先,我们面临的第一个问题就是数据量太大,单点存储量有限的问题。
大家觉得应该如何解决?
没错,我们可以把数据拆分成多份,每一份存储到不同机器节点(node),从而实现减少每个节点数据量的目的。这就是数据的分布式存储,也叫做: 数据分片(Shard) 。
10.2.2.数据备份
- 数据分片解决了海量数据存储的问题,但是如果出现单点故障,那么分片数据就不再完整,这又该如何解决呢?
- 没错,就像大家为了备份手机数据,会额外存储一份到移动硬盘一样。我们可以给每个分片数据进行备份,存储到其它节点,防止数据丢失,这就是数据备份,也叫 数据副本(replica) 。
- 数据备份可以保证高可用,但是每个分片备份一份,所需要的节点数量就会翻一倍,成本实在是太高了!
- 为了在高可用和成本间寻求平衡,我们可以这样做:
- 首先对数据分片,存储到不同节点
- 然后对每个分片进行备份,放到对方节点,完成互相备份
- 这样可以大大减少所需要的服务节点数量,如图,我们以3分片,每个分片备份一份为例:
在这个集群中,如果出现单节点故障,并不会导致数据缺失,所以保证了集群的高可用,同时也减少了节点中数据存储量。并且因为是多个节点存储数据,因此用户请求也会分发到不同服务器,并发能力也得到了一定的提升。
10.3.搭建集群
- 集群需要多台机器,我们这里用一台机器来模拟,因此我们需要在一台虚拟机中部署多个elasticsearch节点,每个elasticsearch的端口都必须不一样。
- 一台机器进行模拟:将我们的ES的安装包复制三份,修改端口号,data和log存放位置的不同。
- 实际开发中:将每个ES节点放在不同的服务器上。
- 我们计划集群名称为:lagou-elastic,部署3个elasticsearch节点,分别是:
- node-01:http端口9201,TCP端口9301
- node-02:http端口9202,TCP端口9302
- node-03:http端口9203,TCP端口9303
- http:表示使用http协议进行访问时使用 端口,elasticsearch-head、kibana、postman,默认端口号是9200。
- tcp:集群间的各个节点进行通讯的端口,默认9300
第一步:复制es软件粘贴3次,分别改名
第二步:修改每一个节点的配置文件 config下的- elasticsearch.yml,下面已第一份配置文件为例三个节点的配置文件几乎一致,除了:node.name、path.data、path.log、http.port、transport.tcp.port
node1
#允许跨域名访问
http.cors.enabled: true
#当设置允许跨域,默认为*,表示支持所有域名
http.cors.allow-origin: "*"
#允许所有节点访问
network.host: 0.0.0.0
# 集群的名称,同一个集群下所有节点的集群名称应该一致
cluster.name: lagou-elastic
#当前节点名称 每个节点不一样
node.name: node-01
#数据的存放路径 每个节点不一样,不同es服务器对应的data和log存储的路径不能一样
path.data: d:\class\es-9201\data
#日志的存放路径 每个节点不一样
path.logs: d:\class\es-9201\logs
# http协议的对外端口 每个节点不一样,默认:9200
http.port: 9201
# TCP协议对外端口 每个节点不一样,默认:9300
transport.tcp.port: 9301
#三个节点相互发现,包含自己,使用tcp协议的端口号
discovery.zen.ping.unicast.hosts:
["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#声明大于几个的投票主节点有效,请设置为(nodes / 2) + 1
discovery.zen.minimum_master_nodes: 2
# 是否为主节点
node.master: true
node2、node3一样
第三步:启动集群
- 把三个节点分别启动,启动时不要着急,要一个一个地启动
使用head插件查看:
10.4.测试集群中创建索引库
配置kibana,再重启
搭建集群以后就要创建索引库了,那么问题来了,当我们创建一个索引库后,数据会保存到哪个服务节点上呢?如果我们对索引库分片,那么每个片会在哪个节点呢?
settings:就是索引库设置,其中可以定义索引库的各种属性,目前我们可以不设置,都走默认。这里给搭建看看集群中分片和备份的设置方式,
PUT /lagou
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
}
}
这里有两个配置:
- number_of_shards:分片数量,这里设置为3
- number_of_replicas:副本数量,这里设置为1,每个分片一个备份,一个原始数据,共2份。
通过chrome浏览器的head查看,我们可以查看到分片的存储结构:
可以看到,lagou这个索引库,有三个分片,分别是0、1、2,每个分片有1个副本,共6份。
- node-01上保存了1号分片和2号分片的副本
- node-02上保存了0号分片和2号分片的副本
-node-03上保存了0号分片和1号分片的副本
10.5.集群工作原理
10.5.1.shad与replica机制
(1)一个index包含多个shard,也就是一个index存在多个服务器上
(2)每个shard都是一个最小工作单元,承载部分数据,比如有三台服务器,现在有三条数据,这三条数
据在三台服务器上各方一条.
(3)增减节点时,shard会自动在nodes中负载均衡
(4)primary shard(主分片)和replica shard(副本分片),每个document肯定只存在于某一个primary shard以及其对应的replica shard中,不可能存在于多个primary shard
(5)replica shard是primary shard的副本,负责容错,以及承担读请求负载
(6)primary shard的数量在创建索引的时候就固定了,replica shard的数量可以随时修改
(7)primary shard的默认数量是5,replica默认是1(每个主分片一个副本分片),默认有10个
shard,5个primary shard,5个replica shard
(8)primary shard不能和自己的replica shard放在同一个节点上(否则节点宕机,primary shard和副本都丢失,起不到容错的作用),但是可以和其他primary shard的replica shard放在同一个节点上
10.5.2.集群写入数据
- 客户端选择一个node发送请求过去,这个node就是coordinating node (协调节点)
- coordinating node,对document进行路由,将请求转发给对应的node。(根据一定的算法选择对应的节点进行存储)
- 实际上的node上的primary shard处理请求,将数据保存在本地,然后将数据同步到replica node
- coordinating node,如果发现primary node和所有的replica node都搞定之后,就会返回请求到客户端
这个路由简单的说就是取模算法,比如说现在有3太服务器,这个时候传过来的id是5,那么5%3=2,就放在第2台服务器
10.5.3.ES查询数据
倒排序算法
查询有个算法叫倒排序:简单的说就是:通过分词把词语出现的id进行记录下来,再查询的时候先去查到哪些id包含这个数据,然后再根据id把数据查出来
查询过程
- 客户端发送一个请求给coordinate node
- 协调节点将搜索的请求转发给所有的shard对应的primary shard 或replica shard
- query phase(查询阶段):每一个shard 将自己搜索的结果(其实也就是一些唯一标识),返回给协调节点,有协调节点进行数据的合并,排序,分页等操作,产出最后的结果
- fetch phase(获取阶段) ,接着由协调节点,根据唯一标识去各个节点进行拉取数据,最终返回给客户端
11.Elasticsearch客户端
11.1.客户端介绍
在elasticsearch官网中提供了各种语言的客户端:https://www.elastic.co/guide/en/elasticsearch/client/index.html
注意点击进入后,选择版本到 6.2.4 ,因为我们之前按照的都是 6.2.4 版本:
11.2.创建Demo工程
11.2.1.初始化项目
11.2.2.pom文件
注意,这里我们直接导入了SpringBoot的启动器,方便后续讲解。不过还需要手动引入elasticsearch的High-level-Rest-Client的依赖:
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<!--Apache开源组织提供的用于操作JAVA BEAN的工具包-->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.1</version>
</dependency>
<!--ES高级Rest Client-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.4.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
11.2.3.配置文件
我们在resource下创建application.yml
11.3.索引库及映射
创建索引库的同时,我们也会创建type及其映射关系,但是这些操作不建议使用java客户端完成,原因如下:
- 索引库和映射往往是初始化时完成,不需要频繁操作,不如提前配置好
- 官方提供的创建索引库及映射API非常繁琐,需要通过字符串拼接json结构:
package com.lagou.es.pojo;
public class Product {
private Long id;
private String title; //标题
private String category;// 分类
private String brand; // 品牌
private Double price; // 价格
private String images; // 图片地址
}
分析一下数据结构:
- id:可以认为是主键,将来判断数据是否重复的标示,不分词,可以使用keyword类型
- title:搜索字段,需要分词,可以用text类型
- category:商品分类,这个是整体,不分词,可以使用keyword类型
- brand:品牌,与分类类似,不分词,可以使用keyword类型
- price:价格,这个是double类型
-images:图片,用来展示的字段,不搜索,index为false,不分词,可以使用keyword类型
我们可以编写这样的映射配置
PUT /lagou
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"item": {
"properties": {
"id": {
"type": "keyword"
},
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"category": {
"type": "keyword"
},
"brand": {
"type": "keyword"
},
"images": {
"type": "keyword",
"index": false
},
"price": {
"type": "double"
}
}
}
}
}
11.4.索引数据操作
有了索引库,我们接下来看看如何新增索引数据
操作MYSQL数据库:
1.获取数据库连接
2.完成数据的增删改查
3.释放资源
11.4.1.初始化客户端
我们先编写一个测试类:
public class ElasticSearchTest {
private RestHighLevelClient client;
// Json工具
private Gson gson = new Gson();
@Before
public void init(){
// 初始化HighLevel客户端
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("127.0.0.1", 9201, "http"),
new HttpHost("127.0.0.1", 9202, "http"),
new HttpHost("127.0.0.1", 9203, "http")
)
);
}
@After
public void close() throws IOException {
// 关闭客户端
client.close();
}
}
11.4.2.新增文档
@Test
public void testInsert() throws IOException {
//1.文档数据
Product product = new Product();
product.setBrand("华为");
product.setCategory("手机");
product.setId(1L);
product.setImages("http://image.huawei.com/1.jpg");
product.setPrice(5999.99);
product.setTitle("华为P50就是棒");
//2.将文档数据转换为json格式
String source = gson.toJson(product);
//3.创建索引请求对象 访问哪个索引库、哪个type、指定文档ID
//public IndexRequest(String index, String type, String id)
IndexRequest request = new
IndexRequest("lagou","item",product.getId().toString());
request.source(source, XContentType.JSON);
//4.发出请求
IndexResponse response = restHighLevelClient.index(request,
RequestOptions.DEFAULT);
System.out.println(response);
}
11.4.3.查看文档
@Test
public void testFindIndex() throws IOException {
// 创建get请求,并指定id
GetRequest request = new GetRequest("lagou", "item", "1");
// 查询,得到响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 解析响应,应该是json
String source = response.getSourceAsString();
// 转换json数据
Product item = gson.fromJson(source, Product.class);
System.out.println(item);
}
11.4.4.修改文档
新增时,如果传递的id是已经存在的,则会完成修改操作,如果不存在,则是新增。
11.4.5.删除文档
根据id删除:
@Test
public void testDeleteIndex() throws IOException {
// 准备删除的请求,参数为id
DeleteRequest request = new DeleteRequest("lagou", "item", "1");
// 发起请求
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
System.out.println("response = " + response);
}
11.5 搜索数据
11.5.1.查询所有match_all
@Test
public void testMatchAll() throws IOException {
// 创建搜索对象
SearchRequest request = new SearchRequest();
// 查询构建工具
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 添加查询条件,通过QueryBuilders获取各种查询
sourceBuilder.query(QueryBuilders.matchAllQuery());
request.source(sourceBuilder);
// 搜索
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 解析
SearchHits hits = response.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
// 取出source数据
String json = hit.getSourceAsString();
// 反序列化
Product item = gson.fromJson(json, Item.class);
System.out.println("item = " + item);
}
}
- 注意,上面的代码中,搜索条件是通过 sourceBuilder.query(QueryBuilders.matchAllQuery())来添加的。这个 query() 方法接受的参数是: QueryBuilder 接口类型。
- 这个接口提供了很多实现类,分别对应我们在之前中学习的不同类型的查询,例如:term查询、match查询、range查询、boolean查询等
-
因此,我们如果要使用各种不同查询,其实仅仅是传递给 sourceBuilder.query() 方法的参数不同而已。而这些实现类不需要我们去 new ,官方提供了 QueryBuilders 工厂帮我们构建各种实现类
11.5.2.关键字搜索match
其实搜索类型的变化,仅仅是利用QueryBuilders构建的查询对象不同而已,其他代码基本一致:
因此,我们可以把这段代码封装,然后把查询条件作为参数传递:
private void basicQuery(SearchSourceBuilder sourceBuilder) throws IOException {
// 创建搜索对象
SearchRequest request = new SearchRequest();
request.source(sourceBuilder);
// 搜索
SearchResponse response = client.search(request,
RequestOptions.DEFAULT);
// 解析
SearchHits hits = response.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
// 取出source数据
String json = hit.getSourceAsString();
// 反序列化
Product item = gson.fromJson(json, Item.class);
System.out.println("item = " + item);
}
}
11.5.3.范围查询range
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price");
方法 | 说明 |
---|---|
gt(Object from) | 大于 |
gte(Object from) | 大于等于 |
lt(Object from) | 小于 |
lte(Object from) | 小于等于 |
11.5.4.source过滤
_source:存储原始文档
- 默认情况下,索引库中所有数据都会返回,如果我们想只返回部分字段,可以通过source filter来控制。
@Test
public void testSourceFilter() throws IOException {
// 创建搜索对象
SearchRequest request = new SearchRequest();
// 查询构建工具
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 添加查询条件,通过QueryBuilders获取各种查询
sourceBuilder.query(QueryBuilders.matchAllQuery());
// 添加source过滤
sourceBuilder.fetchSource(new String[]{"id", "title", "price"}, null);
basicQuery(sourceBuilder);
}
11.6排序
依然是通过sourceBuilder来配置
public void testSortQuery() throws IOException {
// 创建搜索对象
SearchRequest request = new SearchRequest();
// 查询构建工具
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 添加查询条件,通过QueryBuilders获取各种查询
sourceBuilder.query(QueryBuilders.matchAllQuery());
// 添加排序
sourceBuilder.sort("price", SortOrder.ASC);
basicQuery(sourceBuilder);
}
11.7.分页
分页需要视图层传递两个参数给我们:
- 当前页:page
- 每页大小:size
而elasticsearch中需要的不是当前页,而是起始位置,还好有公式可以计算出: - from–>起始位置,0表示第一条
- 起始位置:start = (page - 1) * size
- 第一页:(1-1)*5 = 0
- 第二页:(2-1)*5 = 5
public void testSortAndPageQuery() throws IOException {
// 创建搜索对象
SearchRequest request = new SearchRequest();
// 查询构建工具
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 添加查询条件,通过QueryBuilders获取各种查询
sourceBuilder.query(QueryBuilders.matchAllQuery());
// 添加排序
sourceBuilder.sort("price", SortOrder.ASC);
// 添加分页
int page = 1;
int size = 3;
int start = (page - 1) * size;
// 配置分页
sourceBuilder.from(start);
sourceBuilder.size(3);
basicQuery(sourceBuilder);
}
12.Spring Data Elasticsearch
12.1.什么是SpringDataElasticsearch
- Spring Data Elasticsearch(以后简称SDE)是Spring Data项目下的一个子模块。
- Spring Data 的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如MySQL),还是非关系数据库(如Redis),或者类似Elasticsearch这样的索引数据库。从而简化开发人员的代码,提高开发效率。
- Spring Data Elasticsearch的页面:https://projects.spring.io/spring-data-elasticsearch/
特征:- 支持Spring的基于 @Configuration 的java配置方式,或者XML配置方式
- 提供了用于操作ES的便捷工具类 ElasticsearchTemplate 。包括实现文档到POJO之间的自动智能映射。
- 利用Spring的数据转换服务实现的功能丰富的对象映射
- 基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式,可以定义JavaBean:类名、属性
- 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自动得到实现)。当然,也支持人工定制查询
12.2配置SpringDataElasticsearch
我们在pom文件中,引入SpringDataElasticsearch的启动器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
然后,只需要在resources下新建application.yml文件,引入elasticsearch的host和port即可:
spring:
data:
elasticsearch:
cluster-name: lagou-elastic
cluster-nodes: 127.0.0.1:9301,127.0.0.1:9302,127.0.0.1:9303
- 需要注意的是,SpringDataElasticsearch底层使用的不是Elasticsearch提供的RestHighLevelClient,而是TransportClient,并不采用Http协议通信,而是访问elasticsearch对外开放的tcp端口,我们之前集群配置中,设置的分别是:9301,9302,9303
添加引导类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EsApplication {
public static void main(String[] args) {
SpringApplication.run(EsApplication.class,args);
}
}
12.3.索引库操作
12.3.1.创建索引库
准备一个pojo对象
然后准备一个新的实体类,作为下面与索引库对应的文档:
我们先创建一个测试类,然后注入ElasticsearchTemplate:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringElasticsearchTest {
@Autowired
private ElasticsearchTemplate esTemplate;
}
下面是创建索引库的API示例:
@Test
public void testCreateIndex(){
// 创建索引库,并制定实体类的字节码
esTemplate.createIndex(Goods.class);
}
- 发现没有,创建索引库需要指定的信息,比如:索引库名、类型名、分片、副本数量、还有映射信息都没有填写,这是怎么回事呢?
- 实际上,与我们自定义工具类类似,SDE也是通过实体类上的注解来配置索引库信息的,我们需要在Goods上添加下面的一些注解:
@Document(indexName = "lagou", type = "product", shards = 3, replicas = 1)
public class Product {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title; //标题
@Field(type = FieldType.Keyword)
private String category;// 分类
@Field(type = FieldType.Keyword)
private String brand; // 品牌
@Field(type = FieldType.Double)
private Double price; // 价格
@Field(type = FieldType.Keyword, index = false)
private String images; // 图片地址
//
几个用到的注解:
- @Document:声明索引库配置
- indexName:索引库名称
- type:类型名称,默认是“docs”
- shards:分片数量,默认5
- replicas:副本数量,默认1
- @Id:声明实体类的id
- @Field:声明字段属性
- type:字段的数据类型
- analyzer:指定分词器类型
-index:是否创建索引
12.3.2.创建映射
刚才的注解已经把映射关系也配置上了,所以创建映射只需要这样
12.4索引数据CRUD
SDE的索引数据CRUD并没有封装在ElasticsearchTemplate中,而是有一个叫做 ElasticsearchRepository的接口:
我们需要自定义接口,继承ElasticsearchRespository:
package com.lagou.es.repository;
import com.lagou.es.pojo.Goods;
import
org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
}
12.4.1 创建索引数据
@Autowired
private GoodsRepository goodsRepository;
@Test
public void addDocument(){
Goods goods = new Goods(1L, "小米手机9", " 手机",
"小米", 3499.00, "http://image.lagou.com/13123.jpg");
// 添加索引数据
goodsRepository.save(goods);
}
批量创建:
@Test
public void addDocuments(){
// 准备文档数据:
List<Goods> list = new ArrayList<>();
list.add(new Goods(1L, "小米手机7", "手机", "小米", 3299.00, "/13123.jpg"));
list.add(new Goods(2L, "坚果手机R1", "手机", "锤子", 3699.00, "/13123.jpg"));
list.add(new Goods(3L, "华为META10", "手机", "华为", 4499.00, "/13123.jpg"));
list.add(new Goods(4L, "小米Mix2S", "手机", "小米", 4299.00, "/13123.jpg"));
list.add(new Goods(5L, "荣耀V10", "手机", "华为", 2799.00, "/13123.jpg"));
// 添加索引数据
goodsRepository.saveAll(list);
}
12.4.2.查询索引数据
默认提供了根据id查询,查询所有两个功能:
@Test
public void testQueryById(){
Optional<Goods> goodsOptional = goodsRepository.findById(3L);
System.out.println(goodsOptional.orElse(null));
}
@Test
public void testQueryAll(){
Iterable<Goods> list = goodsRepository.findAll();
list.forEach(System.out::println);
}
12.4.3.自定义方法查询
ProductRepository提供的查询方法有限,但是它却提供了非常强大的自定义查询功能:
只要遵循SpringData提供的语法,我们可以任意定义方法声明:
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
/**
* 根据价格区间查询
* @param from 开始价格
* @param to 结束价格
* @return 符合条件的goods
*/
List<Product> findByPriceBetween(Double from, Double to);
}
Keyword | Sample | Elasticsearch Query String |
---|---|---|
And | findByNameAndPrice | {“bool” : {“must” : [ {“field”: {“name” : “?”}}, {“field” :{“price” : “?”}} ]}} |
Or | findByNameOrPrice | {“bool” : {“should” : [{“field” : {“name” : “?”}},{“field” : {“price” : “?”}}]}} |
Is | findByName | {“bool” : {“must” : {“field” :{“name” : “?”}}}} |
Not | findByNameNot | {“bool” : {“must_not” :{“field” : {“name” : “?”}}}} |
Between | findByPriceBetween | {“bool” : {“must” : {“range” :{“price” : {“from” : ?,“to” :?,“include_lower” :true,“include_upper” :true}}}}} |
LessThanEqual | findByPriceLessThan | {“bool” : {“must” : {“range” :{“price” : {“from” : null,“to”: ?,“include_lower” :true,“include_upper” :true}}}}} |
GreaterThanEqual | findByPriceGreaterThan | {“bool” : {“must” : {“range” :{“price” : {“from” : ?,“to” :null,“include_lower” :true,“include_upper” :true}}}}} |
Before | findByPriceBefore | {“bool” : {“must” : {“range” :{“price” : {“from” : null,“to”: ?,“include_lower” :true,“include_upper” :true}}}}} |
After | findByPriceAfter{“bool” : {“must” : {“range” :{“price” : {“from” : ?,“to” :null,“include_lower” :true,“include_upper” :true}}}}} | |
Like | findByNameLike | {“bool” : {“must” : {“field” :{“name” : {“query” : “?*”,“analyze_wildcard” :true}}}}} |
StartingWith | findByNameStartingWith{“bool” : {“must” : {“field” :{“name” : {“query” : “?*”,“analyze_wildcard” :true}}}}} | |
EndingWith | findByNameEndingWith | {“bool” : {“must” : {“field” :{“name” : {“query” :"*?",“analyze_wildcard” :true}}}}} |
Contains/Containing | findByNameContaining | {“bool” : {“must” : {“field” :{“name” : {“query” : “?”,“analyze_wildcard” :true}}}}} |
In | findByNameIn(Collectionnames) | {“bool” : {“must” : {“bool” :{“should” : [ {“field” :{“name” : “?”}}, {“field” :{“name” : “?”}} ]}}}} |
NotIn | findByNameNotIn(Collectionnames) | {“bool” : {“must_not” :{“bool” : {“should” : {“field”: {“name” : “?”}}}}}} |
Near | findByStoreNear | Not Supported Yet |
True | findByAvailableTrue | {“bool” : {“must” : {“field” :{“available” : true}}}} |
False | findByAvailableFalse | {“bool” : {“must” : {“field” :{“available” : false}}}} |
OrderBy | findByAvailableTrueOrderByNameDesc | {“sort” : [{ “name” : {“order”: “desc”} }],“bool” : {“must”: {“field” : {“available” :true}}}} |
12.5.原生查询
- 如果觉得上述接口依然不符合你的需求,SDE也支持原生查询,这个时候还是使用ElasticsearchTemplate
- 而查询条件的构建是通过一个名为 - NativeSearchQueryBuilder 的类来完成的,不过这个类的底层还是使用的原生API中的 QueryBuilders 、AggregationBuilders 、 HighlightBuilders 等工具。
需求:
查询title中包含小米手机的商品,以价格升序排序,分页查询:每页展示2条,查询第1页。
对查询结果进行聚合分析:获取品牌及个数
@Test
public void testNativeQuery(){
// 原生查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 1.1 source过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new
String[0]));
// 1.2搜索条件
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
// 1.3分页及排序条件
queryBuilder.withPageable(
PageRequest.of(0, 2,
Sort.by(Sort.Direction.ASC, "price")));
// 1.4高亮显示
// queryBuilder.withHighlightBuilder(new HighlightBuilder().field("title"));
// 1.5聚合
queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"
));
// 构建查询条件,并且查询
AggregatedPage<Goods> result = esTemplate.queryForPage(queryBuilder.build(),
Goods.class);
// 2、解析结果:
// 2.1分页结果
long total = result.getTotalElements();
int totalPages = result.getTotalPages();
List<Goods> list = result.getContent();
System.out.println("总条数 = " + total);
System.out.println("总页数 = " + totalPages);
System.out.println(list);
// 2.2.聚合结果
Aggregations aggregations = result.getAggregations();
Terms terms = aggregations.get("brandAgg");
terms.getBuckets().forEach(b -> {
System.out.println("品牌 = " + b.getKeyAsString());
System.out.println("count = " + b.getDocCount());
});
}
高亮展示:
1、自定义搜索结果映射
public class EsResultMapper implements SearchResultMapper {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T>
aClass, Pageable pageable) {
// 记录总条数
long totalHits = response.getHits().getTotalHits();
// 记录列表(泛型) - 构建Aggregate使用
List<T> list = new ArrayList<>();
// 获取搜索结果(真正的的记录)
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
if (hits.getHits().length <= 0) {
return null;
}
// 将原本的JSON对象转换成Map对象
Map<String, Object> map = hit.getSourceAsMap();
// 获取高亮的字段Map
Map<String, HighlightField> highlightFields =
hit.getHighlightFields();
for (Map.Entry<String, HighlightField> highlightField :
highlightFields.entrySet()) {
// 获取高亮的Key
String key = highlightField.getKey();
// 获取高亮的Value
HighlightField value = highlightField.getValue();
// 实际fragments[0]就是高亮的结果,无需遍历拼接
Text[] fragments = value.getFragments();
// 因为高亮的字段必然存在于Map中,就是key值
map.put(key, fragments[0].toString());
}
// 把Map转换成对象
Gson gson = new Gson();
T item = gson.fromJson(gson.toJson(map), aClass);
list.add(item);
}
// 返回的是带分页的结果
return new AggregatedPageImpl<>(list, pageable, totalHits);
}
}
2、高亮实现:
@Test
public void testNativeQuery() {
// 原生查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 1.1 source过滤
//queryBuilder.withSourceFilter(new FetchSourceFilter(new String[0], new
String[0]));
// 1.2搜索条件
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米手机"));
// 1.3分页及排序条件
queryBuilder.withPageable(
PageRequest.of(0, 6,
Sort.by(Sort.Direction.ASC, "price")));
// 1.4高亮显示
//queryBuilder.withHighlightBuilder(new
HighlightBuilder().field("title"));
// 构建高亮查询
HighlightBuilder.Field field = new
HighlightBuilder.Field("title").preTags("<font style='color:red'>").postTags("
</font>");
queryBuilder.withHighlightFields(field); // 名字高亮
NativeSearchQuery build = queryBuilder.build();
// 1.5聚合
queryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brand"
));
// 构建查询条件,并且查询
AggregatedPage<Product> result =
template.queryForPage(queryBuilder.build(), Product.class,new EsResultMapper());
// 2、解析结果:
long total = result.getTotalElements();
int totalPages = result.getTotalPages();
List<Product> list = result.getContent();
list.stream().forEach(product -> System.out.println(product));
}