个人技术博客:www.zhenganwen.top
环境
- 64位Win10、8G内存、JDK8
- ES安装包:elasticsearch-6.2.1
- ES中文分词插件:ik-6.4.0
- 官方文档
安装ES
ES项目结构
解压elasticsearch-6.2.1.zip
,解压后得到的目录为==ES根目录==,其中各目录作用如下:
- bin,存放启动ES等命令脚本
- config,存放ES的配置文件,ES启动时会读取其中的内容
- elasticsearch.yml,ES的集群信息、对外端口、内存锁定、数据目录、跨域访问等属性的配置
- jvm.options,ES使用Java写的,此文件用于设置JVM相关参数,如最大堆、最小堆
- log4j2.properties,ES使用log4j作为其日志框架
- data,数据存放目录(索引数据)
- lib,ES依赖的库
- logs,日志存放目录
- modules,ES的各功能模块
- plugins,ES的可扩展插件存放目录,如可以将ik中文分词插件放入此目录,ES启动时会自动加载
属性配置
默认的elasticsearch.yml中的所有属性都被注释了,我们需要设置一些必要的属性值,在文尾添加如下内容(集群相关的配将在后文详细说明):
1 | 复制代码cluster.name: xuecheng #集群名称,默认为elasticsearch |
JVM参数设置
默认ES启动需要分配的堆内存为1G,如果你的机器内存较小则可在jvm.options
中调整为512M:
1 | 复制代码-Xms512m |
启动ES
双击/bin/elasticsearch.bat
启动脚本即可启动ES,关闭该命令行窗口即可关闭ES。
启动后访问:http://localhost:9200,如果得到如下响应则ES启动成功:
1 | 复制代码{ |
elasticsearch-head可视化插件
ES是基于Lucene开发的产品级搜索引擎,封装了很多内部细节,通过此插件我们可以通过Web的方式可视化查看其内部状态。
此插件无需放到ES的/plugins
目录下,因为它是通过JS与ES进行交互的。
1 | 复制代码git clone git://github.com/mobz/elasticsearch-head.git |
浏览器打开:http://localhost:9100,并连接通过ES提供的http端口连接ES:
ES快速入门
首先我们要理解几个概念:索引库(index)、文档(document)、字段(field),我们可以类比关系型数据库来理解:
ES | MySQL |
---|---|
索引库index | 数据库database |
type | 表table |
文档document | 行row |
字段field | 列column |
但是自ES6.x开始,type
的概念就慢慢被弱化了,官方将在ES9正式剔除它。因此我们可以将索引库类比为一张表。一个索引库用来存储一系列结构类似的数据。虽然可以通过多个type
制造出一个索引库“多张表”的效果,但官方不建议这么做,因为这会降低索引和搜索性能,要么你就新建另外一个索引库。类比MySQL来看就是,一个库就只放一张表。
名词索引 & 动词索引
名词索引指的是索引库,一个磁盘上的文件。
一个索引库就是一张倒排索引表,将数据存入ES的过程就是先将数据分词然后添加到倒排索引表的过程。
以添加“中华人民共和国”、“中华上下五千年”到索引库为例,倒排索引表的逻辑结构如下:
term | doc_id |
---|---|
中华 | 1、2 |
人民 | 1 |
共和国 | 1 |
上下 | 2 |
五 | 2 |
千年 | 2 |
doc_id | doc |
---|---|
1 | 中华人民共和国 |
2 | 中华上下五千年 |
这种将数据分词并建立各分词到文档之间的关联关系的过程称为==索引==(动词)
Postman
Postman是一款HTTP客户端工具,能否方便地发送各种形式的RESTful请求。
下文将以Postman来测试ES的RESTful API,请求的根URL为:http://localhost:9200
索引库管理
创建索引库
创建一个名为“xc_course”的用于存放学成在线(教育平台)的课程数据的索引库:
- PUT /xc_course
1 | 复制代码{ |
创建成功了吗?我们可以通过elasticsearch-head
来查看,刷新localhost:9100:
删除索引库
DELET /xc_course
查看索引信息
GET /xc_course
映射管理
创建映射
映射可以类比MySQL的表结构定义,如有哪些字段,字段是什么类型。
创建映射的请求格式为:POST /index_name/type_name/_mapping。
不是说type
已经弱化了吗?为什么这里还要指定type的名称?因为在ES9才正式剔除type的概念,在此之前需要一个过渡期,因此我们可以指定一个无意义的type名,如“doc”:
POST /xc_course/doc/_mapping
1 | 复制代码{ |
查看映射(类比查看表结构)
GET /xc_course/doc/_mapping
1 | 复制代码{ |
也可以通过head插件查看:
文档管理
添加文档
PUT /index/type/id
如果不指定id,那么ES会为我们自动生成:
PUT /xc_course/doc
1 | 复制代码{ |
响应如下:
1 | 复制代码{ |
根据id查询文档
GET /index/type/id
于是我们拿到我们刚添加数据生成的id来查询:
GET /xc_course/doc/Hib0QmoB7xBOMrejqjF3
1 | 复制代码{ |
查询全部文档
GET /index/type/_search
1 | 复制代码{ |
IK中文分词器
ES默认情况下是不支持中文分词的,也就是说对于添加的中文数据,ES将会把每个字当做一个term(词项),这不利于中文检索。
测试ES默认情况下对中文分词的结果:
POST /_analyze
你会发现ES的固定API都会带上
_
前缀,如_mapping
、_search
、_analyze
1 | 复制代码{ |
分词结果如下:
1 | 复制代码{ |
下载ik-6.4.0并解压到ES/plugins/
目录下,并将解压后的目录改名为==ik==,==重启ES==,该插件即会被自动加载。
重启ES后再测试分词效果:
POST http://localhost:9200/_analyze
1 | 复制代码{ |
ik_max_word分词策略是尽可能的分出多的term,即细粒度分词:
1 | 复制代码{ |
而ik_smart则是出粒度分词(设置"analyzer" : "ik_smart"
):
1 | 复制代码{ |
自定义词库
ik分词器仅提供了常用中文短语的词库,而对于实时性的热门网络短语则无法识别,因此有时为了增加分词准确性,我们需要自己扩展词库。
首先我们测试ik对网络词汇“蓝瘦香菇”的分词效果:
PUT /_analyze
1 | 复制代码{ |
分词如下:
1 | 复制代码{ |
我们在ES的/plugins/ik/config
目录下增加自定义的词库文件my.dic
并添加一行“蓝瘦香菇”(词典文件的格式是每一个词项占一行),并在ik的配置文件/plugins/ik/config/IKAnalyzer.cfg.xml
中引入该自定义词典:
1 | 复制代码<!--用户可以在这里配置自己的扩展字典 --> |
==重启ES==,ik分词器将会把我们新增的词项作为分词标准:
1 | 复制代码{ |
映射
新增字段
PUT /xc_course/doc/_mapping
1 | 复制代码{ |
GET /xc_course/doc/_mapping
1 | 复制代码{ |
已有的映射可以新增字段但不可以更改已有字段的定义!
PUT /xc_course/doc/_mapping
1 | 复制代码{ |
报错:已定义的price不能从double类型更改为integer类型:
1 | 复制代码{ |
如果一定要更改某字段的定义(包括类型、分词器、是否索引等),那么只有删除此索引库重新建立索引并定义好各字段,再迁入数据。因此在索引库创建时要考虑好映射的定义,因为仅可扩展字段但不可重新定义字段。
常用的映射类型——type
ES6.2的核心数据类型如下:
keyword
此类型的字段不会被分词,该字段内容被表示为就是一个短语不可分割。如各大商标和品牌名可使用此类型。并且在该字段查询内容时是精确匹配,如在type为keyword的brand
字段搜索“华为”不会搜出字段值为“华为荣耀”的文档。
date
type为date的字段还可以额外指定一个format
,如
1 | 复制代码{ |
新增文档的create_time
字段值可以是日期+时间或仅日期
数值类型
1、尽量选择范围小的类型,提高搜索效率
2、对于浮点数尽量用比例因子,比如一个价格字段,单位为元,我们将比例因子设置为100这在ES中会按==分==存
储,映射如下:
1 | 复制代码"price": { |
由于比例因子为100,如果我们输入的价格是23.45则ES中会将23.45乘以100存储在ES中。如果输入的价格是23.456,ES会将23.456乘以100再取一个接近原始值的数,得出2346。
使用比例因子的好处是整型比浮点型更易压缩,节省磁盘空间。
是否建立索引——index
index默认为true,即需要分词并根据分词所得词项建立倒排索引(词项到文档的关联关系)。有些字段的数据是无实际意义的,如课程图片的url仅作展示图片之用,不需要分词建立索引,那么可以设置为false:
PUT /xc_course/doc/_mapping
1 | 复制代码{ |
索引分词器 & 搜索分词器
索引分词器——analyzer
将数据添加到索引库时使用的分词器,建议使用ik_max_word,比如“中华人民共和国”,如果使用ik_smart,那么整个“中华人民共和国”将被作为一个term(此项)存入倒排索引表,那么在搜索“共和国”时就搜不到此数据(词项与词项之间是精确匹配的)。
搜索分词器——search_analyzer
搜索分词器则是用于将用户的检索输入分词的分词器。
建议使用ik_smart,比如搜索“中华人民共和国”,不应该出现“喜马拉雅共和国”的内容。
是否额外存储——store
是否在source之外存储,每个文档索引后会在 ES中保存一份原始文档,存放在_source
中,一般情况下不需要设置
store为true,因为在_source
中已经有一份原始文档了。
综合实战
创建一个课程集合的映射:
- 首先删除已建立映射的索引
DELET /xc_course
2. 新增索引
PUT /xc_course
3. 创建映射
PUT /xc_course/doc/_mapping
1 | 复制代码{ |
- 添加文档
POST /xc_course/doc
1 | 复制代码{ |
- 检索Java
GET http://localhost:9200/xc_course/doc/_search?q=name:java
6. 检索学习模式
GET http://localhost:9200/xc_course/doc/_search?q=studypattern:20101
索引管理和Java客户端
从此章节开始我们将对ES的每个RESTful API实现配套的Java代码。毕竟虽然前端可以通过HTTP访问ES,但是ES的管理和定制化业务还是需要一个后端作为枢纽。
ES提供的Java客户端——RestClient
RestClient是官方推荐使用的,它包括两种:Java Low Level REST Client和 Java High Level REST Client。ES在6.0之后提供 Java High Level REST Client, 两种客户端官方更推荐使用 Java High Level REST Client,不过当前它还处于完善中,有些功能还没有(如果它有不支持的功能,则使用Java Low Level REST Client。)。
依赖如下:
1 | 复制代码<dependency> |
Spring整合ES
依赖
1 | 复制代码<dependency> |
配置文件
application.yml
:
1 | 复制代码server: |
启动类
1 | 复制代码@SpringBootApplication |
ES配置类
1 | 复制代码package com.xuecheng.search.config; |
测试类
1 | 复制代码package com.xuecheng.search; |
ES客户端API
首先我们将之前创建的索引库删除:
DELETE /xc_course
然后回顾一下创建索引库的RESTful形式:
PUT /xc_course
1 | 复制代码{ |
创建索引库
1 | 复制代码@Test |
对比RESTful形式,通过CreateIndexRequest方式发起此次请求,第3行通过构造函数指明了要创建的索引库名(对应URI /xc_course
),第14行构造了请求体(你会发现settings
方法和JSON请求格式很相似)。
操作索引库需要使用
IndicesClient
对象。
删除索引库
1 | 复制代码@Test |
创建索引库时指定映射
1 | 复制代码@Test |
添加文档
添加文档的过程就是“索引”(动词)。需要使用IndexRequest
对象进行索引操作。
1 | 复制代码public static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
响应结果包含了ES为我们生成的文档id,这里我测试得到的id为fHh6RWoBduPBueXKl_tz
根据id查询文档
1 | 复制代码@Test |
根据id更新文档
ES更新文档有两种方式:全量替换和局部更新
全量替换:ES首先会根据id查询文档并删除然后将该id作为新文档的id插入。
局部更新:只会更新相应字段
全量替换:
POST /index/type/id
局部更新:
POST /index/type/_update
Java客户端提供的是局部更新,即仅对提交的字段进行更新而其他字段值不变
1 | 复制代码@Test |
根据id删除文档
1 | 复制代码@Test |
搜索管理
准备环境
为了有数据可搜,我们重新创建映射并添加一些测试数据
创建映射
DELETE /xc_course
PUT /xc_course
1 | 复制代码{ |
PUT /xc_course/doc/_mapping
1 | 复制代码{ |
添加测试数据
PUT /xc_course/doc/1
1 | 复制代码{ |
PUT /xc_course/doc/2
1 | 复制代码{ |
PUT /xc_course/doc/3
1 | 复制代码{ |
简单搜索
- 搜索指定索引库中的所有文档
GET /xc_course/_search
- 搜索指定type中的所有文档
GET /xc_course/doc/_search
DSL搜索
DSL(Domain Specific Language)是ES提出的基于json的搜索方式,在搜索时传入特定的json格式的数据来完成不同的搜索需求。
DSL比URI搜索方式功能强大,在项目中建议使用DSL方式来完成搜索。
DSL搜索方式是使用POST提交,URI为以_search
结尾(在某index或某type范围内搜索),而在JSON请求体中定义搜索条件。
查询所有文档——matchAllQuery
POST /xc_course/doc/_search
1 | 复制代码{ |
query
用来定义搜索条件,_source
用来指定返回的结果集中需要包含哪些字段。这在文档本身数据量较大但我们只想获取其中特定几个字段数据时有用(既可过滤掉不必要字段,又可提高传输效率)。
结果说明:
- took,本次操作花费的时间,单位毫秒
- time_out,请求是否超时(ES不可用或网络故障时会超时)
- _shard,本次操作共搜索了哪些分片
- hits,命中的结果
- hits.total,符合条件的文档数
- hits.hits,命中的文档集
- hits.max_score,hits.hits中各文档得分的最高分,文档得分即查询相关度
- _source,文档源数据
1 | 复制代码{ |
Java代码实现:
1 | 复制代码@Test |
DSL核心API
- new SearchRequest(index),指定要搜索的索引库
- searchRequest.type(type),指定要搜索的type
- SearchSourceBuilder,构建DSL请求体
- searchSourceBuilder.query(queryBuilder),构造请求体中
“query”:{}
部分的内容- QueryBuilders,静态工厂类,方便构造queryBuilder,如
searchSourceBuilder.query(QueryBuilders.matchAllQuery())
就相当于构造了“query”:{ "match_all":{} }
- searchRequest.source(),将构造好的请求体设置到请求对象中
分页查询
PUT http://localhost:9200/xc_course/doc/_search
1 | 复制代码{ |
其中from
的含义是结果集偏移,而size
则是从偏移位置开始之后的size
条结果。
1 | 复制代码{ |
这里虽然hits.total
为3,但是只返回了第一条记录。因此我们在做分页功能时需要用到一个公式:from = (page-1)*size
Java代码实现
1 | 复制代码@Test |
提取结果集中的文档
1 | 复制代码SearchResponse response = restHighLevelClient.search(request); |
词项匹配——termQuery
词项匹配是==精确匹配==,只有当倒排索引表中存在我们指定的词项时才会返回该词项关联的文档集。
如搜索课程名包含java
词项的文档
1 | 复制代码{ |
结果如下:
1 | 复制代码"hits": { |
但如果你指定"term"
为{ "name":"java编程" }
就搜索不到了:
1 | 复制代码"hits": { |
因为“java编程基础”在索引时会被分为“java”、“编程”、“基础”三个词项添加到倒排索引表中,因此没有一个叫“java编程”的词项和此次查询匹配。
term
查询是精确匹配,term.name
不会被search_analyzer
分词,而是会作为一个整体和倒排索引表中的词项进行匹配。
根据id精确匹配——termsQuery
查询id为1和3的文档
POST http://localhost:9200/xc_course/doc/_search
1 | 复制代码{ |
Java实现
1 | 复制代码@Test |
==大坑==
根据id精确匹配也是term查询的一种,但是调用的API是
termsQuery("_id", ids)
,注意是termsQuery
而不是termQuery
。
全文检索—— matchQuery
输入的关键词会被search_analyzer
指定的分词器分词,然后根据所得词项到倒排索引表中查找文档集合,每个词项关联的文档集合都会被查出来,如查“bootstrap基础”会查出“java编程基础”:
POST
1 | 复制代码{ |
因为“bootstrap基础”会被分为“bootstrap”和“基础”两个词项,而词项“基础”关联文档“java编程基础”。
1 | 复制代码@Test |
operator
上述查询等同于:
1 | 复制代码{ |
即对检索关键词分词后每个词项的查询结果取并集。
operator
可取值or
、and
,分别对应取并集和取交集。
如下查询就只有一结果(课程名既包含“java”又包含“基础”的只有“java编程基础”):
1 | 复制代码{ |
Java代码
1 | 复制代码@Test |
minimum_should_match
上边使用的operator = or
表示只要有一个词匹配上就得分,如果实现三个词至少有两个词匹配如何实现?
使用minimum_should_match
可以指定文档匹配词的占比,比如搜索语句如下:
1 | 复制代码{ |
“spring开发框架”会被分为三个词:spring、开发、框架。
设置"minimum_should_match":"80%"
表示,三个词在文档的匹配占比为80%,即3*0.8=2.4,向上取整得2,表
示至少有两个词在文档中才算匹配成功。
1 | 复制代码@Test |
多域检索——multiMatchQuery
上边学习的termQuery
和matchQuery
一次只能匹配一个Field,本节学习multiQuery
,一次可以匹配多个字段(即扩大了检索范围,之前一直都是在name
字段中检索)。
如检索课程名或课程描述中包含“spring”或“css”的文档:
1 | 复制代码{ |
Java:
1 | 复制代码@Test |
boost权重
观察上述查出的文档得分:
1 | 复制代码"hits": [ |
你会发现文档3
中spring
词项在文档出现的次数占文档词项总数的比例较高因此得分(_score
)较高。那我们猜想,是不是我们在文档1
的课程描述中多添加几个css
能否提升其_score
呢?
于是我们更新一下文档1:
1 | 复制代码@Test |
再次查询发现文档1的得分果然变高了:
1 | 复制代码"hits": [ |
那我们有这样一个业务需求:课程出现spring或css肯定是与spring或css相关度更大的课程,而课程描述出现则不一定。因此我们想提高课程出现关键词项的得分权重,我们可以这么办(在name
字段后追加一个^
符号并指定权重,默认为1):
1 | 复制代码{ |
Java:
1 | 复制代码@Test |
布尔查询——boolQuery
布尔查询对应于Lucene的BooleanQuery查询,实现==将多个查询组合起来==。
三个参数
must
:文档必须匹配must所包括的查询条件,相当于 “AND”should
:文档应该匹配should所包括的查询条件其中的一个或多个,相当于 “OR”must_not
:文档不能匹配must_not所包括的该查询条件,相当于“NOT”
如查询课程名包含“spring”==且==课程名或课程描述跟“开发框架”有关的:
1 | 复制代码{ |
Java
1 | 复制代码@Test |
必须满足的条件放到must
中(boolQueryBuilder.must(条件)
),必须排斥的条件放到must_not
中,只需满足其一的条件放到should
中。
查询课程名必须包含“开发”但不包含“java”的,且包含“spring”或“boostrap”的课程:
1 | 复制代码{ |
当然实际项目不会这么设置条件,这里只是为了演示效果,这里为了演示方便用的都是
termQuery
,事实可用前面任意一种Query
。
Java
1 | 复制代码@Test |
过滤器——filter
过滤是针对搜索的结果进行过滤,==过滤器主要判断的是文档是否匹配,不去计算和判断文档的匹配度得分==,所以==过滤器性能比查询要高,且方便缓存==,推荐尽量使用过滤器去实现查询或者过滤器和查询共同使用。
过滤器仅能在布尔查询中使用。
全文检索“spring框架”,并过滤掉学习模式代号不是“201001”和课程价格不在10~100之间的
1 | 复制代码{ |
Java
1 | 复制代码@Test |
排序
查询课程价格在10~100之间的,并按照价格升序排列,当价格相同时再按照时间戳降序排列
1 | 复制代码{ |
Java
1 | 复制代码@Test |
高亮
1 | 复制代码{ |
结果:
1 | 复制代码"hits": [ |
hits
结果集中的每个结果出了给出源文档_source
之外,还给出了相应的高亮结果highlight
Java
1 | 复制代码@Test |
比较难理解的API是
HighlightFields
和highlightField.getFragments()
,我们需要对比响应JSO的结构来类比理解。
我们可以通过highlightFields.get()
来获取highlight.name
和highlight.description
对应的highlightField
,但是为什么hightField.getFragment
返回的是一个Text[]
而不是Text
呢。我们猜测ES将文档按照句子分成了多个段,仅对出现关键词项的段进行高亮并返回,于是我们检索css测试一下果然如此:
因此你需要注意返回的
highlight
可能并不包含所有原字段内容
集群管理
ES通常以集群方式工作,这样做不仅能够提高 ES的搜索能力还可以处理大数据搜索的能力,同时也增加了系统的容错能力及高可用,ES可以实现PB级数据的搜索。
下图是ES集群结构的示意图:
集群相关概念
节点
ES集群由多个服务器组成,每个服务器即为一个Node节点(该服务只部署了一个ES进程)。
分片
当我们的文档量很大时,由于内存和硬盘的限制,同时也为了提高ES的处理能力、容错能力及高可用能力,我们将索引分成若干分片(可以类比MySQL中的分区来看,一个表分成多个文件),每个分片可以放在不同的服务器,这样就实现了多个服务器共同对外提供索引及搜索服务。
一个搜索请求过来,会分别从各各分片去查询,最后将查询到的数据合并返回给用户。
副本
为了提高ES的高可用同时也为了提高搜索的吞吐量,我们将分片复制一份或多份存储在其它的服务器,这样即使当前的服务器挂掉了,拥有副本的服务器照常可以提供服务。
主节点
一个集群中会有一个或多个主节点,主节点的作用是集群管理,比如增加节点,移除节点等,主节点挂掉后ES会重新选一个主节点。
节点转发
每个节点都知道其它节点的信息,我们可以对任意一个v发起请求,接收请求的节点会转发给其它节点查询数据。
节点的三个角色
主节点
master节点主要用于集群的管理及索引 比如新增节点、分片分配、索引的新增和删除等。
数据节点
data 节点上保存了数据分片,它负责索引和搜索操作。
客户端节点
client 节点仅作为请求客户端存在,client的作用也作为负载均衡器,client 节点不存数据,只是将请求均衡转发到其它节点。
配置
可在/config/elasticsearch.yml
中配置节点的功能:
- node.master: #是否允许为主节点
- node.data: #允许存储数据作为数据节点
- node.ingest: #是否允许成为协调节点(数据不在当前ES实例上时转发请求)
四种组合方式:
- master=true,data=true:即是主节点又是数据节点
- master=false,data=true:仅是数据节点
- master=true,data=false:仅是主节点,不存储数据
- master=false,data=false:即不是主节点也不是数据节点,此时可设置ingest为true表示它是一个客户端。
搭建集群
下我们来实现创建一个2节点的集群,并且索引的分片我们设置2片,每片一个副本。
解压elasticsearch-6.2.1.zip
两份为es-1
、es-2
配置文件elasticsearch.yml
节点1:
1 | 复制代码cluster.name: xuecheng |
节点2:
1 | 复制代码cluster.name: xuecheng |
测试分片
创建索引,索引分两片,每片有一个副本:
PUT http://localhost:9200/xc_course
1 | 复制代码{ |
通过head
插件查看索引状态:
测试主从复制
写入数据
POST http://localhost:9200/xc_course/doc
1 | 复制代码{ |
两个结点均有数据:
集群的健康
通过访问 GET /_cluster/health 来查看Elasticsearch 的集群健康情况。
用三种颜色来展示健康状态: green 、 yellow 或者 red 。
- green:所有的主分片和副本分片都正常运行。
- yellow:所有的主分片都正常运行,但有些副本分片运行不正常。
- red:存在主分片运行不正常。
本文转载自: 掘金