ElasticSearch使用
前言
Lucene使用起来有点麻烦,其实现在企业中使用原生Lucene来进行搜索的很少了,使用Solr和ElasticSearch比较多,Solr和ElasticSearch都是基于Lucene开发的搜索服务器,使用它们远远比使用原生Lucene简单容易的多。尤其是ElasticSearch,功能非常强大,提供了许多好用强大的插件。最近接触到了ElasticSearch,所以这里做一下记录。
1、ElasticSearch介绍
ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。
我们建立一个网站或应用程序,并要添加搜索功能,但是想要完成搜索工作的创建是非常困难的。我们希望搜索解决方案要运行速度快,我们希望能有一个零配置和一个完全免费的搜索模式,我们希望能够简单地使用JSON通过HTTP来索引数据,我们希望我们的搜索服务器始终可用,我们希望能够从一台开始并扩展到数百台,我们要实时搜索,我们要简单的多租户,我们希望建立一个云的解决方案。因此我们利用Elasticsearch来解决所有这些问题及可能出现的更多其它问题。
总结:
- elasticsearch是一个基于Lucene的高扩展的分布式搜索服务器,支持开箱即用。
- elasticsearch隐藏了Lucene的复杂性,对外提供Restful 接口来操作索引、搜索。
elasticsearch优点:
- 扩展性好,可部署上百台服务器集群,处理PB级数据。
- 近实时的去索引数据、搜索数据。
关于elasticsearch和solr的选择问题:
- 如果公司现在用的solr可以满足需求就不需要更换。
- 如果公司准备进行全文检索项目的开发,建议优先考虑elasticsearch,因为像Github这样大规模的搜索都在用它。
2、原理和应用
(1)索引结构
以图说明:
黑色部分是物理结构,上边黄色部分是逻辑结构,逻辑结构也是为了更好的
去描述ElasticSearch的工作原理及去使用物理结构中的索引文件。
逻辑结构部分是一个倒排索引表:
- 将要搜索的文档内容分词,所有不重复的词组成分词列表。
- 将搜索的文档最终以Document方式存储起来。
- 每个词和docment都有关联。
如下图:
现在,如果想搜索quick brown,只需要查找包含每个词条的文档,搜索到的结果如下:
两个文档都匹配,但是明显第一个文档比第二个匹配度更高。因为第一个文档既包含quick又包含brown,第二个文档只包含brown。如果我们使用仅计算匹配词条数量的简单相似性算法 ,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。
(2)RESTful应用方法
Elasticsearch提供RESTful Api接口进行索引、搜索,并且支持多种客户端。
ElasticSearch在项目中的应用如下:
说明:
- 用户在前端搜索关键字。
- 项目前端通过http方式请求项目服务端。.
- 项目服务端通过Http RESTful方式请求ES集群进行搜索。
- ES集群从索引库检索数据。
- 检索出的数据反馈给前端界面进行展示。
3、ElasticSearch的安装
(1)安装es
安装要求:
- 新版本要求至少jdk1.8以上。
- 支持tar、zip、rpm等多种安装方式。在windows下开发建议使用ZIP安装方式。
- 支持docker方式安装。
去官网下载好适合版本和系统的安装包,这里选用6.2.1版本,解压后的目录如下:
bin目录如下:
里面是一些脚本目录,包括:启动、停止等可执行脚本。
config目录如下:
里面是一些配置文件。
data是自己建的,索引库就存在在这里。
lib里是一些jar包,logs里是日志文件,modules里面是模块目录,包括了es的功能模块。plugins里面是插件,es支持插件机制,比如IK分词器放在这里。
(2)配置文件
a、3个配置文件
ES的配置文件的地址根据安装形式的不同而不同:使用zip、tar安装,配置文件的地址在安装目录的config下;使用RPM安装,配置文件在/etc/elasticsearch下;使用MSI安装,配置文件的地址在安装目录的config下,并且会自动将config目录地址写入环境变量
ES_PATH_CONF。因为这里是使用的是zip安装,所以配置文件在config目录下,如下:
说明:
- elasticsearch.yml : 用于配置Elasticsearch运行参数。
- jvm.options : 用于配置Elasticsearch JVM设置。
- log4j2.properties: 用于配置Elasticsearch日志。
以下就这3个配置文件进行分别说明。
b、elasticsearch.yml
配置格式是YAML,可以采用两种方式来进行配置:
方式一:层次方式,如下:
path: data: /var/lib/elasticsearch logs: /var/log/elasticsearch
方式二:属性方式如下:
path.data: /var/lib/elasticsearch path.logs: /var/log/elasticsearch
这里就统一采用属性方式配置了,具体信息如下:
cluster.name: xuechengnode.name: xc_node_1network.host: 0.0.0.0http.port: 9200transport.tcp.port: 9300node.master: truenode.data: truediscovery.zen.ping.unicast.hosts: ["0.0.0.0:9300", "0.0.0.0:9301"]discovery.zen.minimum_master_nodes: 1node.ingest: truebootstrap.memory_lock: falsenode.max_local_storage_nodes: 2path.data: C:\dev\elasticsearch\es_1\datapath.logs: C:\dev\elasticsearch\es_1\logshttp.cors.enabled: truehttp.cors.allow-origin: /.*/
参数说明如下:
- cluster.name:配置elasticsearch的集群名称,默认是elasticsearch。
- node.name:节点名,通常一台物理服务器就是一个节点,es会默认随机指定一个名字,建议指定一个有意义的名称,方便管理,一个或多个节点组成一个cluster集群,集群是一个逻辑的概念,节点是物理概念。
- path.conf: 设置配置文件的存储路径,tar或zip包安装默认在es根目录下的config文件夹,rpm安装默认在/etc/。
- elasticsearch path.data: 设置索引数据的存储路径,默认是es根目录下的data文件夹,可以设置多个存储路径,用逗号隔开。
- path.logs::设置日志文件的存储路径,默认是es根目录下的logs文件夹。
- path.plugins::设置插件的存放路径,默认是es根目录下的plugins文件夹。
- bootstrap.memory_lock::设置为true可以锁住ES使用的内存,避免内存与swap分区交换数据。
- network.host::设置绑定主机的ip地址,设置为0.0.0.0表示绑定任何ip,允许外网访问,生产环境建议设置为具体的ip。
- http.port:设置对外服务的http端口,默认为9200。
- ransport.tcp.port:集群结点之间通信端口。
- node.master:指定该节点是否有资格被选举成为master结点,默认是true,如果原来的master宕机会重新选举新的master。
- node.data:指定该节点是否存储索引数据,默认为true。
- discovery.zen.ping.unicast.hosts:设置集群中master节点的初始列表。
- discovery.zen.ping.timeout:设置ES自动发现节点连接超时的时间,默认为3秒,如果网络延迟高可设置大些。
- discovery.zen.minimum_master_nodes:主结点数量的最少值 ,此值的公式为:(master_eligible_nodes / 2) 1 ,比如:有3个符合要求的主结点,那么这里要设置为2。
- node.max_local_storage_nodes:单机允许的最大存储结点数,通常单机启动一个结点建议设置为1,开发环境如果单机启动多个节点可设置大于1。
c、jvm.options
设置最小及最大的JVM堆内存大小。在jvm.options中设 -Xms和-Xmx:
- 两个值设置为相等。
- 将Xmx设置为不超过物理内存的一半。
具体信息如下:
## GC configuration-XX: UseConcMarkSweepGC-XX:CMSInitiatingOccupancyFraction=75-XX: UseCMSInitiatingOccupancyOnly## optimizations# pre-touch memory pages used by the JVM during initialization-XX: AlwaysPreTouch## basic# explicitly set the stack size-Xss1m# set to headless, just in case-Djava.awt.headless=true# ensure UTF-8 encoding by default (e.g. filenames)-Dfile.encoding=UTF-8# use our provided JNA always versus the system one-Djna.nosys=true# turn off a JDK optimization that throws away stack traces for common# exceptions because stack traces are important for debugging-XX:-OmitStackTraceInFastThrow# flags to configure Netty-Dio.netty.noUnsafe=true-Dio.netty.noKeySetOptimization=true-Dio.netty.recycler.maxCapacityPerThread=0# log4j 2-Dlog4j.shutdownHookEnabled=false-Dlog4j2.disable.jmx=true-Djava.io.tmpdir=${ES_TMPDIR}## heap dumps# generate a heap dump when an allocation from the Java heap fails# heap dumps are created in the working directory of the JVM-XX: HeapDumpOnOutOfMemoryError# specify an alternative path for heap dumps# ensure the directory exists and has sufficient space#-XX:HeapDumpPath=/heap/dump/path## JDK 8 GC logging8:-XX: PrintGCDetails8:-XX: PrintGCDateStamps8:-XX: PrintTenuringDistribution8:-XX: PrintGCApplicationStoppedTime8:-Xloggc:logs/gc.log8:-XX: UseGCLogFileRotation8:-XX:NumberOfGCLogFiles=328:-XX:GCLogFileSize=64m# JDK 9 GC logging9-:-Xlog:gc*,gc age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m# due to internationalization enhancements in JDK 9 Elasticsearch need to set the provider to COMPAT otherwise# time/date parsing will break in an incompatible way for some date patterns and locals9-:-Djava.locale.providers=COMPAT
d、log4j2.properties
日志文件设置,ES使用log4j,注意日志级别的配置。
status = error# log action execution errors for easier debugginglogger.action.name = org.elasticsearch.actionlogger.action.level = debugappender.console.type = Consoleappender.console.name = consoleappender.console.layout.type = PatternLayoutappender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%nappender.rolling.type = RollingFileappender.rolling.name = rollingappender.rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}.logappender.rolling.layout.type = PatternLayoutappender.rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%.-10000m%nappender.rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}-%d{yyyy-MM-dd}-%i.log.gzappender.rolling.policies.type = Policiesappender.rolling.policies.time.type = TimeBasedTriggeringPolicyappender.rolling.policies.time.interval = 1appender.rolling.policies.time.modulate = trueappender.rolling.policies.size.type = SizeBasedTriggeringPolicyappender.rolling.policies.size.size = 128MBappender.rolling.strategy.type = DefaultRolloverStrategyappender.rolling.strategy.fileIndex = nomaxappender.rolling.strategy.action.type = Deleteappender.rolling.strategy.action.basepath = ${sys:es.logs.base_path}appender.rolling.strategy.action.condition.type = IfFileNameappender.rolling.strategy.action.condition.glob = ${sys:es.logs.cluster_name}-*appender.rolling.strategy.action.condition.nested_condition.type = IfAccumulatedFileSizeappender.rolling.strategy.action.condition.nested_condition.exceeds = 2GBrootLogger.level = inforootLogger.appenderRef.console.ref = consolerootLogger.appenderRef.rolling.ref = rollingappender.deprecation_rolling.type = RollingFileappender.deprecation_rolling.name = deprecation_rollingappender.deprecation_rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_deprecation.logappender.deprecation_rolling.layout.type = PatternLayoutappender.deprecation_rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%.-10000m%nappender.deprecation_rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_deprecation-%i.log.gzappender.deprecation_rolling.policies.type = Policiesappender.deprecation_rolling.policies.size.type = SizeBasedTriggeringPolicyappender.deprecation_rolling.policies.size.size = 1GBappender.deprecation_rolling.strategy.type = DefaultRolloverStrategyappender.deprecation_rolling.strategy.max = 4logger.deprecation.name = org.elasticsearch.deprecationlogger.deprecation.level = warnlogger.deprecation.appenderRef.deprecation_rolling.ref = deprecation_rollinglogger.deprecation.additivity = falseappender.index_search_slowlog_rolling.type = RollingFileappender.index_search_slowlog_rolling.name = index_search_slowlog_rollingappender.index_search_slowlog_rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_search_slowlog.logappender.index_search_slowlog_rolling.layout.type = PatternLayoutappender.index_search_slowlog_rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c] %marker%.-10000m%nappender.index_search_slowlog_rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_search_slowlog-%d{yyyy-MM-dd}.logappender.index_search_slowlog_rolling.policies.type = Policiesappender.index_search_slowlog_rolling.policies.time.type = TimeBasedTriggeringPolicyappender.index_search_slowlog_rolling.policies.time.interval = 1appender.index_search_slowlog_rolling.policies.time.modulate = truelogger.index_search_slowlog_rolling.name = index.search.slowloglogger.index_search_slowlog_rolling.level = tracelogger.index_search_slowlog_rolling.appenderRef.index_search_slowlog_rolling.ref = index_search_slowlog_rollinglogger.index_search_slowlog_rolling.additivity = falseappender.index_indexing_slowlog_rolling.type = RollingFileappender.index_indexing_slowlog_rolling.name = index_indexing_slowlog_rollingappender.index_indexing_slowlog_rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_indexing_slowlog.logappender.index_indexing_slowlog_rolling.layout.type = PatternLayoutappender.index_indexing_slowlog_rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c] %marker%.-10000m%nappender.index_indexing_slowlog_rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_indexing_slowlog-%d{yyyy-MM-dd}.logappender.index_indexing_slowlog_rolling.policies.type = Policiesappender.index_indexing_slowlog_rolling.policies.time.type = TimeBasedTriggeringPolicyappender.index_indexing_slowlog_rolling.policies.time.interval = 1appender.index_indexing_slowlog_rolling.policies.time.modulate = truelogger.index_indexing_slowlog.name = index.indexing.slowlog.indexlogger.index_indexing_slowlog.level = tracelogger.index_indexing_slowlog.appenderRef.index_indexing_slowlog_rolling.ref = index_indexing_slowlog_rollinglogger.index_indexing_slowlog.additivity = false
(3)启动es
进入bin目录,双击elasticsearch.bat。
启动完毕后浏览器访问:http://localhost:9200。显示如下:
说明es是启动成功的。
(4)安装head插件
head插件是ES的一个可视化管理插件,用来监视ES的状态,并通过head客户端和ES服务进行交互,比如创建映射、创建索引等。从ES6.0开始,head插件运行得先安装运行node.js。
a、安装node.js
这里直接省略。
b、下载head
去官网下载好合适版本,解压安装,安装后的目录如下:
c、运行head
在head安装目录下执行命令:npm run start,启动完成后如下:
d、连接到es
浏览器访问:http://localhost:9100。
标记的地方输入es的地址,然后连接,如图连接成功,可以看到目前之前一个节点xc_node_1,且集群的健康值为良好。
4、ElasticSearch入门
ES作为一个索引及搜索服务,对外提供丰富的REST接口,这里使用head插件来测试,目的是对ES的使用方法及流程有个初步的认识。
(1)创建索引库
ES的索引库是一个逻辑概念,它包括了分词列表及文档列表,同一个索引库中存储了相同类型的文档。它就相当于MySQL中的表,对于索引这个词,要区分一下名词和动词:
- 索引(名词):ES是基于Lucene构建的一个搜索服务,它要从索引库搜索符合条件索引数据。
- 索引(动词):索引库刚创建起来是空的,将数据添加到索引库的过程称为索引。
以下将使用两种方法创建索引库,它们的工作原理是相同的,都是客户端向ES服务发送命令。
a、使用postman
发送put请求:http://localhost:9200/demo,body如下:
参数说明:
- number_of_shards:设置分片的数量,在集群中通常设置多个分片,表示一个索引库将拆分成多片分别存储不同的结点,提高了ES的处理能力和高可用性,入门程序使用单机环境,这里设置为1。
- number_of_replicas:设置副本的数量,设置副本是为了提高ES的高可靠性,单机环境设置为0。
发送,响应如下:
使用head插件查看:
说明创建索引库demo成功。
b、使用head插件创建
将刚才创建的索引库删掉,测试使用head插件创建:
删除成功了。重新创建:
点击新建索引:
点击ok:
创建成功了。推荐使用head插件来创建索引库,非常方便。
(2)创建映射
a、概念说明
在索引中每个文档都包括了一个或多个field,创建映射就是向索引库中创建field的过程,下边是document和field与关系数据库的概念的类比:
文档(Document)----------------Row记录
字段(Field)-------------------Columns 列
注意:6.0之前的版本有type(类型)概念,type相当于关系数据库的表,ES官方将在ES9.0版本中彻底删除type。
那么索引库相当于关系数据库中的数据库还是表?
- 如果相当于数据库就表示一个索引库可以创建很多不同类型的文档,这在ES中也是允许的。
- 如果相当于表就表示一个索引库只能存储相同类型的文档,ES官方建议在一个索引库中只存储相同类型的文档。
b、创建映射
创建映射为post请求:
http://localhost:9200/索引库名称 /类型名称/_mapping。
由于ES6.0版本还没有将type彻底删除,所以暂时把type起一个没有特殊意义的名字。那么发送的url为:http://localhost:9200/demo/doc/_mapping。body如下:
发送,返回结果:
使用head插件查看:
说明映射创建成功。
c、创建文档
ES中的文档相当于MySQL数据库表中的记录。发送post或put请求:
http://localhost:9200/索引库名称/类型/id。
如果不指定id值ES会自动生成ID,那么发送的url如下:
http://localhost:9200/demo/doc/xkfyzsq001,使用post请求,body如下:
发送,返回结果:
使用head查看:
点击数据可查看详细内容:
d、搜索文档
现在搜索索引库里面的文档数据,以下将根据不同的条件来进行查询,查询都是get请求。
按照id查询:
http://localhost:9200/demo/doc/xkfyzsq001
发送,响应如下:
查询所有记录:
http://localhost:9200/demo/doc/_search
发送,响应如下:
查询name中包含风的记录:
http://localhost:9200/demo/doc/_search?q=name:风
发送,响应如下:
查询studymodel为201001的记录:
http://localhost:9200/demo/doc/_search?q=studymodel:201001
发送,响应如下:
参数说明如下:
- took:本次操作花费的时间,单位为毫秒。
- timed_out:请求是否超时。
- _shards:说明本次操作共搜索了哪些分片。
- hits:搜索命中的记录。
- hits.total : 符合条件的文档总数 hits.hits :匹配度较高的前N个文档。
- hits.max_score:文档匹配得分,这里为最高分。
- _score:每个文档都有一个匹配度得分,按照降序排列。
- _source:显示了文档的原始内容。
5、IK分词器
(1)测试分词器
在添加文档时会进行分词,索引中存放的就是一个一个的词(term),当你去搜索时就是拿关键字去匹配词,最终找到词关联的文档。
下面测试当前索引库使用的分词器。发送post请求:
http://localhost:9200/_analyze
body如下:
发送,响应如下:
可以看到,分词效果非常的差,每个中文默认为一个词。因为当前索引库使用的分词器对中文就是单字分词。下面将使用IK分词器来进行分词。
(2)安装IK分词器
使用IK分词器可以实现对中文分词的效果。去网上下载IK分词器,将下载好的zip包解压,将解压后的文件放到plugins下新建的ik文件夹下:
重新测试分词效果。发送post请求:
http://localhost:9200/_analyze
body如下:
可以看到,下面加了 “analyzer”:“ik_max_word”,发送请求,响应如下:
可以看到不再是单字分词了,而是有规律的分词。
(3)IK的两种分词模式
ik分词器有两种分词模式:ik_max_word和ik_smart模式。
a、ik_max_word
会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。
b、ik_smart
会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。
修改分词模式,再测试:
发送,响应如下:
(4)自定义词库
如果要让分词器支持一些专有词语,可以自定义词库。iK分词器自带一个main.dic的文件,此文件为词库文件。
在config目录中创建一个my.ini文件,注意尾UTF-8格式,然后在my.ini中自定义语汇:
然后在IKAnalyzer.cfg.xml中配置加载my.ini扩展词典,如下:
停用词典也可以扩展。配置好以后保存,重启es,测试分词效果:
发送,响应如下:
可以看到,扩展词典面规定的,会分为一个词。
6、映射
已经安装了ik分词器,如果在索引和搜索时去使用ik分词器呢?如何指定其它类型的field,比如日期类型、数值类型等,下面进行介绍。
(1)映射的维护
a、查询所有映射
get请求:http://localhost:9200/_mapping
响应如下:
b、创建映射
post请求:http://localhost:9200/demo/doc/_mapping
c、更新映射
映射创建成功可以添加新字段,已有字段不允许更新。
d、删除映射
通过删除索引来删除映射。
(2)映射常用类型
ES6.2核心的字段类型如下:
a、string类型
字符串包括 text和keyword两种类型。如下:
text类型
属性说明如下:
- analyzer:通过analyzer属性指定分词器。
指定name的字段类型为text,使用ik分词器的ik_max_word分词模式。这个是索引和搜索都使用ik_max_word。
- search_analyzer:指定搜索时使用的分词器。
指定name的字段类型为text,索引时使用ik分词器的ik_max_word分词模式,搜索时使用ik分词器的ik_smart模式。对于ik分词器建议是索引时使用ik_max_word将搜索内容进行细粒度分词,搜索时使用ik_smart提高搜索精确性。
- index:通过index属性指定是否索引。默认为true,即要进行索引,只有进行索引才可以从索引库搜索到。但是也有一些内容不需要索引,比如:商品图片地址只被用来展示图片,不进行搜索图片,此时可以将index设置为false。如果不索引,按照这个域搜是搜不到的。
- store:否在source之外存储,每个文档索引后会在ES中保存一份原始文档,存放在"_source"中,一般情况下不需要设置store为true,因为在_source中已经有一份原始文档了。
下进行测试,因为原索引库中存在映射,删除索引库,重新创建,然后创建索引。
put请求: http://localhost:9200/demo/doc/_mapping
body如下:
发送,响应如下:
映射创建成功,然后插入文档。
put请求:http://localhost:9200/demo/doc/4028e58161bcf7f40161bcf8b77c0000
body如下:
发送,响应如下:
文档插入成功。以下进行查询测试。
get请求:http://localhost:9200/demo/doc/_search?q=name:开发
响应结果:
get请求:http://localhost:9200/demo/doc/_search?q=description:开发
响应结果:
get请求:http://localhost:9200/demo/doc/_search?q=pic:group1
响应结果:
是查不到的,pic没有索引,不能进行搜索。
get请求:http://localhost:9200/demo/doc/_search?q=studymodel:201002
响应结果:
keyword类型
text文本字段在映射时要设置分词器,keyword字段为关键字字段,通常搜索keyword是按照整体搜索,所以创建keyword字段的索引时是不进行分词的,比如:邮政编码、手机号码、身份证等。keyword字段通常用于过滤、排序、聚合等。
下面进行测试,先删除索引库,重建。
先创建映射。put请求:http://localhost:9200/demo/doc/_mapping
body如下:
发送,响应如下:
映射创建成功。
插入文档。put请求:http://localhost:9200/demo/doc/j1111111111
body如下:
发送,响应如下:
文档插入成功。
进行搜索测试。get请求:http://localhost:9200/demo/doc/_search?q=name:java
响应如下:
查不到,因为keyword是精确匹配,修改url为:
http://localhost:9200/demo/doc/_search?q=name:java编程基础
响应如下:
可以查到,因为name类型设置成了keyword,必须精确匹配,也就是完全匹配上才行。
b、日期date类型
日期类型不用设置分词器,通常日期类型的字段用于排序。可以通过format来设置日期格式。
添加新的映射:
更新现有文档的timestamp的值。
查看head:
添加成功了。
c、数值类型
下边是ES支持的数值类型:
说明:
- 尽量选择范围小的类型,提高搜索效率。
- 对于浮点数尽量用比例因子,比如一个价格字段,单位为元,我们将比例因子设置为100这在ES中会按分存储。
由于比例因子为100,如果我们输入的价格是23.45则ES中会将23.45乘以100存储在ES中。如果输入的价格是23.456,ES会将23.456乘以100再取一个接近原始值的数,得出2346。使用比例因子的好处是整型比浮点型更易压缩,节省磁盘空间。
如果比例因子不适合,则从下表选择范围小的去用:更新已有映射,添加price:
插入文档:
head查看:
d、综合例子
以下以一个综合例子来说明。
删除索引库,重新创建。
创建映射:http://localhost:9200/demo/doc/_mapping
{ "properties": { "description": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "name": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "pic":{ "type":"text", "index":false }, "price": { "type": "float" }, "studymodel": { "type": "keyword" }, "timestamp": { "type": "date", "format": "yyyy‐MM‐dd HH:mm:ss||yyyy‐MM‐dd||epoch_millis" } } }
插入文档:
使用head查看:
那么映射的维护和映射的常用类型就介绍到这里。
7、索引管理
(1)es客户端
想要连接到elasticsearch,必须通过它提供的客户端来连接。那么es提供了多种不同的客户端,这里只说两种:
- TransportClient:ES提供的传统客户端,官方计划8.0版本删除此客户端。
- 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,不过当前它还处于完善中,有些功能还没有。
这里就用RestClient客户端了,使用Java High Level REST Client高版本的,如果有不支持的,那么再使用Java High Level REST Client低版本的。
(2)创建工程
创建新的maven工程:test-elasticsearch。
- pom依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.1.RELEASE</version></parent><dependencies><!-- 依赖的model模块 --><dependency><groupId>com.ycz</groupId><artifactId>ycz-model</artifactId><version>1.0-SNAPSHOT</version></dependency><!-- 依赖api模块 --><dependency><groupId>com.ycz</groupId><artifactId>ycz-api</artifactId><version>1.0-SNAPSHOT</version></dependency><!-- 依赖的web模块 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- commons工具包 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.4</version></dependency><!-- json转换包 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.73</version></dependency><!-- es客户端依赖 --><dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>6.3.1</version></dependency><!-- es依赖 --><dependency><groupId>org.elasticsearch</groupId><artifactId>elasticsearch</artifactId><version>6.3.1</version></dependency><!-- 测试包 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
ps:es6.3版本以前貌似不支持通过映射来删除索引库。
- yml配置
application.yml的配置信息如下:
server: port: 50000 spring: application: name: test-es ##自定义属性xuecheng: elasticsearch: ## 多个节点之间用逗号隔开 hostlist: 121.42.xxx.xxx:9200 # 索引库 course: # 索引库名称 index: xc_course # 索引库名称 type: doc #类型 # 索引库 media: index: xc_course_media type: doc # 源字段 source_field: courseid,media_id,media_url,teachplan_id,media_fileoriginalname
- 启动类
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,MongoAutoConfiguration.class,MongoDataAutoConfiguration.class})@EntityScan(basePackages = {"com.ycz.domain"})@ComponentScan(basePackages = {"com.ycz.api"})@ComponentScan(basePackages = {"com.ycz.test.es"})public class EsTestApplication { public static void main(String[] args) { SpringApplication.run(EsTestApplication.class, args); }}
说明一下:因为我这里没有在yml中配置dataSource数据源和mongodb的数据源,所以把有关数据源的自动配置类给排除掉了,不排除启动会报错。
- 启动测试
建议项目搭建完成后先启动,看是否能启动成功,因为每个人的配置环境不一样,有可能哪里不小心写错了,造成项目无法启动,我这里是可以启动的:项目启动成功不报错后就可以开始写代码了。
(3)es配置类
配置类的作用是向spring中注册es客户端,创建config目录,在目录下创建es的配置类,如下:
/* * es配置类 */@Configurationpublic class ElasticSearchConfig { // 获取配置信息中的es节点连接列表 @Value("${xuecheng.elasticsearch.hostlist}") private String hostlist; // 注册es高版本客户端 @Bean public RestHighLevelClient restHighLevelClient() { // 解析hostlist的配置信息 String[] hosts = hostlist.split(","); // 创建HttpHost数组,其中存放es主机和端口的配置信息 HttpHost[] httpHosts = new HttpHost[hosts.length]; for (int i = 0; i < hosts.length; i ) { String item = hosts[i]; // 获取主机 String host = item.split(":")[0]; // 获取端口 int port = Integer.parseInt(item.split(":")[1]); httpHosts[i] = new HttpHost(host, port, "http"); } // 创建客户端 RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(httpHosts)); return restHighLevelClient; } // 针对低版本 @Bean public RestClient restClient() { String[] hosts = hostlist.split(","); HttpHost[] httpHosts = new HttpHost[hosts.length]; for (int i = 0; i < hosts.length; i ) { String item = hosts[i]; String host = item.split(":")[0]; int port = Integer.parseInt(item.split(":")[1]); httpHosts[i] = new HttpHost(host, port, "http"); } RestClient restClient = RestClient.builder(httpHosts).build(); return restClient; }}
(4)删除索引库
下面使用RestClient客户端来删除已建立的索引库。在测试包中创建测试类,如下:
@SpringBootTest@RunWith(SpringRunner.class)public class TestIndex { //注入客户端 @Autowired RestHighLevelClient restHighLevelClient; //低版本 @Autowired RestClient restClient; //测试删除索引库 @Test public void testDelIndex() throws IOException { //创建删除索引请求对象 DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("demo"); //执行删除,返回响应对象 DeleteIndexResponse deleteIndexResponse = restHighLevelClient. indices().delete(deleteIndexRequest); //获取响应结果 boolean res = deleteIndexResponse.isAcknowledged(); if(res == true) { System.out.println("索引库删除成功!"); }else { System.out.println("索引库删除失败!"); } }}
先使用head插件查看:
目前是有一个名为demo的索引库的。
执行testDelIndex这个方法,控制台:
head插件再查看:
索引删除成功。
(5)创建索引库及映射
使用postman的话,创建索引库请求的body如下:
创建映射的话,请求的body如下:
那么现在使用客户端来创建索引库和映射mapping,在TestIndex中添加以下方法:
//测试创建索引库 @Test public void testCreateIndex() throws IOException { //创建创建索引请求对象 CreateIndexRequest createIndexRequest = new CreateIndexRequest("xc_course"); //设置索引库的分片数和副本 createIndexRequest.settings( Settings.builder() .put("number_of_shards", 1) .put("number_of_replicas", 0)); //构建XContentBuilder对象,用来创建映射 XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("properties") .startObject("name").field("type", "text").field("analyzer", "ik_max_word") .field("search_analyzer", "ik_smart").endObject().startObject("description").field("type", "text") .field("analyzer", "ik_max_word").field("search_analyzer", "ik_smart").endObject() .startObject("studymodel").field("type", "keyword").endObject().startObject("price") .field("type", "float").endObject().endObject().endObject(); //设置映射 createIndexRequest.mapping("doc", mapping); //执行,返回响应对象 CreateIndexResponse createIndexResponse = restHighLevelClient. indices().create(createIndexRequest); boolean res = createIndexResponse.isAcknowledged(); if(res == true) { System.out.println("索引库创建成功!"); }else { System.out.println("索引库创建失败!"); } }
执行这个方法,控制台:
使用head查看:
索引库创建成功,再看映射:
映射也创建成功了。
(6)添加文档
使用postman添加文档的话,请求的body如下:
现在使用客户端来添加,在TestIndex中添加以下方法:
//测试向索引库中添加文档 @Test public void testAddDoc() throws IOException { //准备要添加的数据,使用json传递 Map<String, Object> jsonMap = new HashMap<>(); jsonMap.put("name", "spring cloud实战"); jsonMap.put("description", "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud基础入门 3.实战Spring Boot 4.注册中心eureka。"); jsonMap.put("studymodel", "201001"); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy‐MM‐dd HH:mm:ss"); jsonMap.put("timestamp", dateFormat.format(new Date())); jsonMap.put("price", 55.6f); //创建索引请求对象 IndexRequest indexRequest = new IndexRequest("xc_course","doc"); //设置文档 indexRequest.source(jsonMap); //执行,返回响应对象 IndexResponse indexResponse = restHighLevelClient.index(indexRequest); //获取响应结果 DocWriteResponse.Result result = indexResponse.getResult(); if (result != null) { System.out.println(result); } }
执行,控制台:
使用head查看:
具体内容:
添加成功。
(7)查询文档
在TestIndex中添加以下方法:
//通过文档ID查询文档 @Test public void testSearchDocById() throws IOException { //请求对象 GetRequest getRequest = new GetRequest("xc_course", "doc", "WS3qznYBvN7iZrhPEeR2"); //执行,获取响应对象 GetResponse getResponse = restHighLevelClient.get(getRequest); //获取响应结果 boolean res = getResponse.isExists(); if(res == true) { Map<String,Object> source = getResponse.getSourceAsMap(); System.out.println(source); }else { System.out.println("文档不存在!"); } }
执行,控制台输出如下:
查询成功了。
(8)更新文档
ES更新文档的顺序是:先检索到文档、将原来的文档标记为删除、创建新文档、删除旧文档,创建新文档就会重建索引。
在TestIndex中添加以下方法:
//测试更新文档 @Test public void testUpdateDoc() throws IOException { //更新请求 UpdateRequest updateRequest = new UpdateRequest("xc_course", "doc", "WS3qznYBvN7iZrhPEeR2"); //要更新的内容 Map<String, Object> map = new HashMap<>(); map.put("name", "更新测试"); //设置文档 updateRequest.doc(map); //执行,获取响应对象 UpdateResponse updateResponse = restHighLevelClient.update(updateRequest); //获取响应结果 RestStatus status = updateResponse.status(); System.out.println(status); }
执行,控制台如下:
用head查看:
更新成功。
(9)删除文档
是根据文档来删除的。在TestIndex中添加以下方法:
//测试删除文档 @Test public void testDelDoc() throws IOException { String docId = "WS3qznYBvN7iZrhPEeR2"; //删除对象 DeleteRequest deleteRequest = new DeleteRequest("xc_course","doc",docId); //执行,获取响应对象 DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest); //获取响应结果 DocWriteResponse.Result result = deleteResponse.getResult(); if(result != null) { System.out.println(result); } }
执行,控制台如下:
使用head查看:
那么文档删除成功了。
8、搜索管理
(1)准备环境
先创建索引库和映射,测试包下建立新类:TestSearch,如下:
@SpringBootTest@RunWith(SpringRunner.class)public class TestSearch { @Autowired RestHighLevelClient restHighLevelClient; @Autowired RestClient restClient; //创建索引库及映射 @Test public void createIndex() throws IOException { //创建索引对象请求 CreateIndexRequest createIndexRequest = new CreateIndexRequest("xc_course"); //设置索引库的分片和副本数 createIndexRequest.settings( Settings.builder() .put("number_of_shards","1") .put("number_of_replicas", 0)); //构建XContentBuilder对象 XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("properties") .startObject("name").field("type", "text").field("analyzer", "ik_max_word") .field("search_analyzer", "ik_smart").endObject().startObject("description").field("type", "text") .field("analyzer", "ik_max_word").field("search_analyzer", "ik_smart").endObject() .startObject("studymodel").field("type", "keyword").endObject().startObject("price") .field("type", "float").endObject().startObject("pic").field("type", "text").field("index",false).endObject() .startObject("timestamp").field("type","date").field("format","yyyy-MM-dd HH:mm:ss||yyyy-MM-dd").endObject() .endObject().endObject(); //设置映射 createIndexRequest.mapping("doc", mapping); //执行 CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest); boolean res = createIndexResponse.isAcknowledged(); if(res == true) { System.out.println("索引库创建成功!"); }else { System.out.println("索引库创建失败!"); } }}
执行这个方法,控制台:
head插件查看:
然后插入几条文档,在TestSearch中添加以下方法:
//向索引库中添加文档 @Test public void addDoc() throws IOException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); List<Map<String,Object>> list = new ArrayList<>(); //添加3条数据 Map<String,Object> jsonMap = new HashMap<>(); jsonMap.put("name","Bootstrap开发"); jsonMap.put("description","Bootstrap是由Twitter推出的一个前台页面开发框架,是一个非常流行的开发框架,此框架集成了\r\n" "多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松\r\n" "的实现一个不受浏览器限制的精美界面效果。"); jsonMap.put("studymodel", "201002"); jsonMap.put("pic", "group1/M00/00/00/1.jpg"); jsonMap.put("price", 30.5f); jsonMap.put("timestamp",sdf.format(new Date())); Map<String,Object> jsonMap2 = new HashMap<>(); jsonMap2.put("name","java编程基础"); jsonMap2.put("description","java语言是世界第一编程语言,在软件开发领域使用人数最多。"); jsonMap2.put("studymodel", "201001"); jsonMap2.put("pic", "group1/M00/00/00/2.jpg"); jsonMap2.put("price", 50.5f); jsonMap2.put("timestamp",sdf.format(new Date())); Map<String,Object> jsonMap3 = new HashMap<>(); jsonMap3.put("name","spring开发基础"); jsonMap3.put("description","spring在java领域非常流行,java程序员都在用。"); jsonMap3.put("studymodel", "201001"); jsonMap3.put("pic", "group1/M00/00/00/3.jpg"); jsonMap3.put("price", 100.5f); jsonMap3.put("timestamp",sdf.format(new Date())); list.add(jsonMap); list.add(jsonMap2); list.add(jsonMap3); IndexRequest indexRequest = new IndexRequest("xc_course","doc"); for(int i=0;i<list.size();i ) { indexRequest.source(list.get(i)); IndexResponse indexResponse = restHighLevelClient.index(indexRequest); DocWriteResponse.Result result = indexResponse.getResult(); if(result != null) { System.out.println(result); } } }
执行这个方法,控制台:
用head查看:
查看详细:
简单搜索的话直接用head插件:
下面将使用DSL来进行搜索。
(2)DSL搜索
DSL(Domain Specific Language)是ES提出的基于json的搜索方式,在搜索时传入特定的json格式的数据来完成不同的搜索需求。DSL比URI搜索方式功能强大,在项目中建议使用DSL方式来完成搜索。
a、查询所有文档
在TestSearch中添加以下方法:
//测试搜索全部文档 @Test public void testSearchAll() throws IOException, ParseException { //搜索请求对象 SearchRequest searchRequest = new SearchRequest("xc_course"); //设置类型 searchRequest.types("doc"); //搜索源对象 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); //设置搜索方式,matchAllQuery为搜索全部 searchSourceBuilder.query(QueryBuilders.matchAllQuery()); //设置源字段过滤 //第一个参数是结果显示哪些field,第二个参数结果不显示哪些field searchSourceBuilder.fetchSource( new String[] {"name","studymodel","price","timestamp"},new String[] {}); //搜索请求对象中设置源对象 searchRequest.source(searchSourceBuilder); //执行搜索请求,获取响应对象 SearchResponse searchResponse = restHighLevelClient.search(searchRequest); //从响应结果中获取搜索结果 SearchHits searchHits = searchResponse.getHits(); //总记录数 long total = searchHits.getTotalHits(); //获取匹配的文档 SearchHit [] hits = searchHits.getHits(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); System.out.println("一共搜索到" total "条数据,如下:"); for(SearchHit hit:hits) { //获取文档id String docId = hit.getId(); //获取源文档内容 Map<String,Object> source = hit.getSourceAsMap(); //从源文档中获取field String name = (String)source.get("name"); String studymodel = (String)source.get("studymodel"); Double price = (Double)source.get("price"); Date timestamp = sdf.parse((String)source.get("timestamp")); System.out.println("文档ID:" docId); System.out.println("name==========>>" name); System.out.println("studymodel==========>>" studymodel); System.out.println("price==========>>" price); System.out.println("timestamp==========>>" timestamp); System.out.println("---------------------------------------"); } }
重点是matchAllQuery这个方法,执行这个方法,控制台如下:
b、分页查询
在TestSearch中添加以下方法:
//测试分页搜索查询 @Test public void testSearchPaged() throws IOException, ParseException { SearchRequest searchRequest = new SearchRequest("xc_course"); searchRequest.types("doc"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.matchAllQuery()); //页码 int page = 1; //每页记录条数 int size = 1; int start = (page -1) * size; //设置分页参数 searchSourceBuilder.from(start); searchSourceBuilder.size(size); searchSourceBuilder.fetchSource( new String[] {"name","studymodel","price","timestamp"},new String[] {}); searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits(); SearchHit [] hits = searchHits.getHits(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); System.out.println("一共搜索到" total "条数据,如下:"); for(SearchHit hit:hits) { String docId = hit.getId(); Map<String,Object> source = hit.getSourceAsMap(); String name = (String)source.get("name"); String studymodel = (String)source.get("studymodel"); Double price = (Double)source.get("price"); Date timestamp = sdf.parse((String)source.get("timestamp")); System.out.println("文档ID:" docId); System.out.println("name==========>>" name); System.out.println("studymodel==========>>" studymodel); System.out.println("price==========>>" price); System.out.println("timestamp==========>>" timestamp); System.out.println("---------------------------------------"); } }
和查询全部查不到,加了以下内容:
//页码 int page = 1; //每页记录条数 int size = 1; int start = (page -1) * size; //设置分页参数 searchSourceBuilder.from(start); searchSourceBuilder.size(size);
执行这个方法,控制台如下:
然后将page值改为2,再执行:
c、Term Query精确查询
Term Query为精确查询,在搜索时会整体匹配关键字,不再将关键字分词。
在TestSearch中添加已以下方法:
//测试termQuery精确查询 @Test public void testTermQuery() throws IOException, ParseException { SearchRequest searchRequest = new SearchRequest("xc_course"); searchRequest.types("doc"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); //设置搜索方式,termQuery为按关键字精确匹配,不分词 searchSourceBuilder.query(QueryBuilders.termQuery("name", "java")); searchSourceBuilder.fetchSource( new String[] {"name","studymodel","price","timestamp"},new String[] {}); searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits(); SearchHit [] hits = searchHits.getHits(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); System.out.println("一共搜索到" total "条数据,如下:"); for(SearchHit hit:hits) { String docId = hit.getId(); Map<String,Object> source = hit.getSourceAsMap(); String name = (String)source.get("name"); String studymodel = (String)source.get("studymodel"); Double price = (Double)source.get("price"); Date timestamp = sdf.parse((String)source.get("timestamp")); System.out.println("文档ID:" docId); System.out.println("name==========>>" name); System.out.println("studymodel==========>>" studymodel); System.out.println("price==========>>" price); System.out.println("timestamp==========>>" timestamp); System.out.println("---------------------------------------"); } }
执行,控制台输出:
关键是termQuery这个方法,只能匹配一个field。
d、按照id精确匹配
ES提供根据多个id值匹配的方法,可以传入多个id,在TestSearch中添加以下方法:
//按照id搜索 @Test public void testQueryByIds() throws IOException, ParseException { SearchRequest searchRequest = new SearchRequest("xc_course"); searchRequest.types("doc"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); //多个id放在数组中,传入数组参数就行了 String []ids = {"XS0h03YBvN7iZrhP1-TX","Xi0h03YBvN7iZrhP2OTS"}; //注意,这里是termsQuery方法,不是termQuery searchSourceBuilder.query(QueryBuilders.termsQuery("_id", ids)); searchSourceBuilder.fetchSource( new String[] {"name","studymodel","price","timestamp"},new String[] {}); searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits(); SearchHit [] hits = searchHits.getHits(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); System.out.println("一共搜索到" total "条数据,如下:"); for(SearchHit hit:hits) { String docId = hit.getId(); Map<String,Object> source = hit.getSourceAsMap(); String name = (String)source.get("name"); String studymodel = (String)source.get("studymodel"); Double price = (Double)source.get("price"); Date timestamp = sdf.parse((String)source.get("timestamp")); System.out.println("文档ID:" docId); System.out.println("name==========>>" name); System.out.println("studymodel==========>>" studymodel); System.out.println("price==========>>" price); System.out.println("timestamp==========>>" timestamp); System.out.println("---------------------------------------"); } }
执行这个方法,控制台如下:
注意,这里是termsQuery方法,不是termQuery。
e、match Query搜索
match Query即全文检索,它的搜索方式是先将搜索字符串分词,再使用各词条从索引中搜索。match query与Term query区别是match query在搜索前先将搜索关键字分词,再拿各词语去索引中搜索。match Query只能匹配一个field。
请求的body如下:
参数说明:
- query:搜索的关键字,对于英文关键字如果有多个单词则中间要用半角逗号分隔,而对于中文关键字中间可以用逗号分隔也可以不用。
- operator:or表示只要有一个词在文档中出现则就符合条件,and表示每个词都在文档中出现则才符合条件。
- minimum_should_match:指定文档匹配词的占比。比如上面的搜索关键字会被分为三个词,"80%"表示,三个词在文档的匹配占比为80%,即3*0.8=2.4,向上取整得2,表示至少有两个词在文档中要匹配成功。
上边的搜索流程如下:
- 将“spring开发”分词,分为spring、开发两个词。
- 再使用spring和开发两个词去匹配索引中搜索。
- 由于设置了operator为or,只要有一个词匹配成功则就返回该文档。
下面使用代码进行实现,在TestSearch中添加以下方法:
//测试match Query分词搜索 @Test public void testMatchQuery() throws ParseException, IOException { SearchRequest searchRequest = new SearchRequest("xc_course"); searchRequest.types("doc"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); //设置搜索方式,matchAllQuery为分词搜索 searchSourceBuilder.query(QueryBuilders.matchQuery("name", "spring开发框架") .minimumShouldMatch("80%")); searchSourceBuilder.fetchSource( new String[] {"name","studymodel","price","timestamp"},new String[] {}); searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits(); SearchHit [] hits = searchHits.getHits(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); System.out.println("一共搜索到" total "条数据,如下:"); for(SearchHit hit:hits) { String docId = hit.getId(); Map<String,Object> source = hit.getSourceAsMap(); String name = (String)source.get("name"); String studymodel = (String)source.get("studymodel"); Double price = (Double)source.get("price"); Date timestamp = sdf.parse((String)source.get("timestamp")); System.out.println("文档ID:" docId); System.out.println("name==========>>" name); System.out.println("studymodel==========>>" studymodel); System.out.println("price==========>>" price); System.out.println("timestamp==========>>" timestamp); System.out.println("---------------------------------------"); } }
执行这个方法,控制台如下:
f、multi Query
termQuery和matchQuery一次只能匹配一个Field,multiQuery一次可以匹配多个字段。
说明:查询会拿spring和框架这两个词去匹配name和description这两个field,多个词匹配的话可以提升字段的boost权重来提高得分。提升boost,通常关键字匹配上name的权重要比匹配上description的权重高,这里可以对name的权重提升。“name^10” 表示权重提升10倍,执行上边的查询,发现name中包括spring关键字的文档排在前边。
在TestSearch中添加以下方法:
//测试multi Query搜索匹配多个field,并提升boost权重 @Test public void testMultiQuery() throws ParseException, IOException { SearchRequest searchRequest = new SearchRequest("xc_course"); searchRequest.types("doc"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); //设置搜索方式,multiMatchQuery为匹配多个field,提升name的权重 searchSourceBuilder.query(QueryBuilders.multiMatchQuery("spring框架", "name","description") .minimumShouldMatch("50%") .field("name", 10)); searchSourceBuilder.fetchSource( new String[] {"name","studymodel","price","timestamp","description"},new String[] {}); searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits(); SearchHit [] hits = searchHits.getHits(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); System.out.println("一共搜索到" total "条数据,如下:"); for(SearchHit hit:hits) { String docId = hit.getId(); Map<String,Object> source = hit.getSourceAsMap(); String name = (String)source.get("name"); String studymodel = (String)source.get("studymodel"); Double price = (Double)source.get("price"); Date timestamp = sdf.parse((String)source.get("timestamp")); String description = (String)source.get("description"); System.out.println("文档ID:" docId); System.out.println("name==========>>" name); System.out.println("description==========>>" description); System.out.println("studymodel==========>>" studymodel); System.out.println("price==========>>" price); System.out.println("timestamp==========>>" timestamp); System.out.println("---------------------------------------"); } }
执行这个方法,控制台输出:
关键字是spring框架,按照50%匹配度,有spring或框架的记录都会搜索到,可以看到,第一条记录的name中包含spring,而第二条记录的name中不包含spring和框架,但是第二条记录的description中有框架这个词,由于设置了name的权重提升了10倍,因此name字段优先匹配关键字,所以排在了最前面。
g、布尔查询
布尔查询对应于Lucene的BooleanQuery查询,实现将多个查询组合起来。
参数说明:
- must:文档必须匹配must所包括的查询条件,相当于 “AND”。
- should:文档应该匹配should所包括的查询条件其中的一个或多个,相当于 “OR”。
- must_not:文档不能匹配must_not所包括的该查询条件,相当于“NOT”。
在TestSearch中添加以下方法:
//测试布尔查询 @Test public void testBoolQuery() throws IOException, ParseException { SearchRequest searchRequest = new SearchRequest("xc_course"); searchRequest.types("doc"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); //MultiMatchQueryBuilder查询对象 MultiMatchQueryBuilder matchQueryBuilder = QueryBuilders.multiMatchQuery("spring框架", "name","description") .minimumShouldMatch("50%") .field("name", 10); //TermQueryBuilder查询对象 TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("studymodel", "201001"); //创建布尔查询对象 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //将前面的两个查询对象组合在布尔查询对象里 boolQueryBuilder.must(matchQueryBuilder); boolQueryBuilder.must(termQueryBuilder); //将布尔查询对象设置在搜索源对象中 searchSourceBuilder.query(boolQueryBuilder); searchSourceBuilder.fetchSource( new String[] {"name","studymodel","price","timestamp","description"},new String[] {}); searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits(); SearchHit [] hits = searchHits.getHits(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); System.out.println("一共搜索到" total "条数据,如下:"); for(SearchHit hit:hits) { String docId = hit.getId(); Map<String,Object> source = hit.getSourceAsMap(); String name = (String)source.get("name"); String studymodel = (String)source.get("studymodel"); Double price = (Double)source.get("price"); Date timestamp = sdf.parse((String)source.get("timestamp")); String description = (String)source.get("description"); System.out.println("文档ID:" docId); System.out.println("name==========>>" name); System.out.println("description==========>>" description); System.out.println("studymodel==========>>" studymodel); System.out.println("price==========>>" price); System.out.println("timestamp==========>>" timestamp); System.out.println("---------------------------------------"); } }
执行这个方法,控制台输出:
可以看到,布尔查询组合了MultiMatchQuery查询对象和TermQuery查询对象,这种方式在开发中用的还是比较多的。
h、过滤器
过滤是针对搜索的结果进行过滤,过滤器主要判断的是文档是否匹配,不去计算和判断文档的匹配度得分,所以过滤器性能比查询要高,且方便缓存,推荐尽量使用过滤器去实现查询或者过滤器和查询共同使用。过滤器在布尔查询中使用,下边是在搜索结果的基础上进行过滤。
参数说明:
- range:范围过滤,保留大于等于60 并且小于等于100的记录。
- term:项匹配过滤,保留studymodel等于"201001"的记录。
注意:range和term一次只能对一个Field设置范围过虑。
在TestSearch中添加以下方法:
//测试过滤器对搜索结果执行过滤 @Test public void testFilter() throws IOException, ParseException { SearchRequest searchRequest = new SearchRequest("xc_course"); searchRequest.types("doc"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); MultiMatchQueryBuilder matchQueryBuilder = QueryBuilders.multiMatchQuery("spring框架", "name","description") .minimumShouldMatch("50%") .field("name", 10); BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); boolQueryBuilder.must(matchQueryBuilder); //设置布尔查询的过滤器 //过滤studymodel为201001的问你当 boolQueryBuilder.filter(QueryBuilders.termQuery("studymodel", "201001")); //过滤价格在50到100之间的记录 boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(50).lte(100)); searchSourceBuilder.query(boolQueryBuilder); searchSourceBuilder.fetchSource( new String[] {"name","studymodel","price","timestamp","description"},new String[] {}); searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits(); SearchHit [] hits = searchHits.getHits(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); System.out.println("一共搜索到" total "条数据,如下:"); for(SearchHit hit:hits) { String docId = hit.getId(); Map<String,Object> source = hit.getSourceAsMap(); String name = (String)source.get("name"); String studymodel = (String)source.get("studymodel"); Double price = (Double)source.get("price"); Date timestamp = sdf.parse((String)source.get("timestamp")); String description = (String)source.get("description"); System.out.println("文档ID:" docId); System.out.println("name==========>>" name); System.out.println("description==========>>" description); System.out.println("studymodel==========>>" studymodel); System.out.println("price==========>>" price); System.out.println("timestamp==========>>" timestamp); System.out.println("---------------------------------------"); } }
执行这个方法,控制台如下:
因为price值不满足过滤条件,无法过滤到此条文档,将lte的值改为110,再执行,控制台如下:
这时候价格就满足过滤条件了,可以将文档记录过滤出来。
i、排序
可以在字段上添加一个或多个排序,支持在keyword、date、float等类型上添加,text类型的字段上不允许添加排序。
过滤0–120元价格范围的文档,并且对结果进行排序,先按studymodel降序,再按价格升序。
测试代码如下:
//测试过滤器后排序 @Test public void testSort() throws IOException, ParseException { SearchRequest searchRequest = new SearchRequest("xc_course"); searchRequest.types("doc"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //过滤价格在0到120之间的记录 boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(0).lte(120)); //排序 //按照studymodel降序 searchSourceBuilder.sort(new FieldSortBuilder("studymodel").order(SortOrder.DESC)); //按照price升序 searchSourceBuilder.sort(new FieldSortBuilder("price").order(SortOrder.ASC)); searchSourceBuilder.query(boolQueryBuilder); searchSourceBuilder.fetchSource( new String[] {"name","studymodel","price","timestamp","description"},new String[] {}); searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits(); SearchHit [] hits = searchHits.getHits(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); System.out.println("一共搜索到" total "条数据,如下:"); for(SearchHit hit:hits) { String docId = hit.getId(); Map<String,Object> source = hit.getSourceAsMap(); String name = (String)source.get("name"); String studymodel = (String)source.get("studymodel"); Double price = (Double)source.get("price"); Date timestamp = sdf.parse((String)source.get("timestamp")); String description = (String)source.get("description"); System.out.println("文档ID:" docId); System.out.println("name==========>>" name); System.out.println("description==========>>" description); System.out.println("studymodel==========>>" studymodel); System.out.println("price==========>>" price); System.out.println("timestamp==========>>" timestamp); System.out.println("---------------------------------------"); } }
执行这个方法,控制台如下:
可以看到,是按照studymodel先进行降序的,然后按照price进行升序。
j、高亮显示
高亮显示可以将搜索结果一个或多个字突出显示,以便向用户展示匹配关键字的位置。
在搜索语句中添加highlight即可实现。
测试代码如下:
//测试关键字高亮 @Test public void testHighlight() throws IOException, ParseException { SearchRequest searchRequest = new SearchRequest("xc_course"); searchRequest.types("doc"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.multiMatchQuery("spring框架", "name","description") .minimumShouldMatch("50%") .field("name",10)); searchSourceBuilder.fetchSource( new String[] {"name","studymodel","price","timestamp","description"},new String[] {}); //高亮对象 HighlightBuilder highlightBuilder = new HighlightBuilder(); //设置前后标签 highlightBuilder.preTags("<tag>"); highlightBuilder.postTags("</tag>"); //设置需要高亮的字段 highlightBuilder.fields().add(new HighlightBuilder.Field("name")); highlightBuilder.fields().add(new HighlightBuilder.Field("description")); //搜索源对象中添加高亮对象 searchSourceBuilder.highlighter(highlightBuilder); searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = restHighLevelClient.search(searchRequest); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits(); SearchHit [] hits = searchHits.getHits(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); System.out.println("一共搜索到" total "条数据,如下:"); for(SearchHit hit:hits) { String docId = hit.getId(); Map<String,Object> source = hit.getSourceAsMap(); String name = (String)source.get("name"); String studymodel = (String)source.get("studymodel"); Double price = (Double)source.get("price"); Date timestamp = sdf.parse((String)source.get("timestamp")); String description = (String)source.get("description"); //取出高亮字段 Map<String, HighlightField> highlightFields = hit.getHighlightFields(); if(highlightFields!=null) { //取出name高亮字段 HighlightField nameHighlightField = highlightFields.get("name"); //取出description高亮字段 HighlightField desHighlightField = highlightFields.get("description"); //将name更换为高亮的 if(nameHighlightField!=null) { Text [] fragments = nameHighlightField.getFragments(); StringBuilder sb = new StringBuilder(); for(Text str:fragments) { sb.append(str.toString()); } name = sb.toString(); } //将description更换为高亮的 if(desHighlightField!=null) { Text []fragments = desHighlightField.getFragments(); StringBuilder sb = new StringBuilder(); for(Text str:fragments) { sb.append(str.toString()); } description = sb.toString(); } } System.out.println("文档ID:" docId); System.out.println("name==========>>" name); System.out.println("description==============>" description); System.out.println("studymodel==========>>" studymodel); System.out.println("price==========>>" price); System.out.println("timestamp==========>>" timestamp); System.out.println("---------------------------------------"); } }
执行这个方法,控制台如下:
可以看到,spring框架关键字分词之后,每个分词都是关键字,匹配到的name或descrition中含有spring或框架的都被打上了设置好的tag标签,其实这个标签很有用,前端可以事先给这个标签写一个样式,那么搜索结果返回到前端之后关键字就可以使用这个样式,就变成高亮的了,这个在开发中用的很多。
9、集群管理
(1)集群结构
ES通常以集群方式工作,这样做不仅能够提高 ES的搜索能力还可以处理大数据搜索的能力,同时也增加了系统的容错能力及高可用,ES可以实现PB级数据的搜索。
ES集群结构如下图:
说明:
- 结点:ES集群由多个服务器组成,每个服务器即为一个Node结点(该服务只部署了一个ES进程)。
- 分片:当我们的文档量很大时,由于内存和硬盘的限制,同时也为了提高ES的处理能力、容错能力及高可用能力,我们将索引分成若干分片,每个分片可以放在不同的服务器,这样就实现了多个服务器共同对外提供索引及搜索服务。一个搜索请求过来,会分别从各各分片去查询,最后将查询到的数据合并返回给用户。
- 副本:为了提高ES的高可用同时也为了提高搜索的吞吐量,我们将分片复制一份或多份存储在其它的服务器,这样即使当前的服务器挂掉了,拥有副本的服务器照常可以提供服务。
- 主节点:一个集群中会有一个或多个主节点,主节点的作用是集群管理,比如增加节点,移除节点等,主节点挂掉后ES会重新选一个主节点。
- 节点转发:每个节点都知道其它节点的信息,我们可以对任意一个节点发起请求,接收请求的节点会转发给其它节点查询数据。
(2)搭建集群
下面创建一个两节点的集群,并且索引的分片设置2片,每片1个副本。
a、节点的三个角色
- 主节点:master节点主要用于集群的管理及索引。比如新增节点、分片分配、索引的新增和删除等。
- 数据节点:data节点上保存了数据分片,它负责索引和搜索操作。
- 客户端节点:client节点仅作为请求客户端存在,client的作用也作为负载均衡器,client节点不存数据,只是将请求均衡转发到其它节点。
相关参数如下:
- node.master:#是否允许为主结点。
- node.data:#允许存储数据作为数据结点。
- node.ingest:#是否允许成为协调节点。
4种组合方式:
- master=true,data=true:即是主节点又是数据节点。
- master=false,data=true:仅是数据节点。
- master=true,data=false:仅是主节点,不存储数据。
- master=false,data=false:即不是主节点也不是数据节点,此时可设置ingest为true表示它是一个客户端。
b、创建节点1
节点1对外服务的http端口是9200。
集群管理端口是9300。
配置elasticsearch.yml信息如下:
cluster.name: xuechengnode.name: xc_node_1network.host: 0.0.0.0http.port: 9200transport.tcp.port: 9300node.master: truenode.data: truediscovery.zen.ping.unicast.hosts: ["0.0.0.0:9300", "0.0.0.0:9301"]discovery.zen.minimum_master_nodes: 1node.ingest: truebootstrap.memory_lock: falsenode.max_local_storage_nodes: 2path.data: C:\dev\elasticsearch\es_1\datapath.logs: C:\dev\elasticsearch\es_1\logshttp.cors.enabled: truehttp.cors.allow-origin: /.*/
节点1是主节点,也是数据节点,允许成为数据节点。
c、创建节点2
节点2对外服务的http端口是9201。
集群管理端口是9301。
配置elasticsearch.yml信息如下:
cluster.name: xuechengnode.name: xc_node_2network.host: 0.0.0.0http.port: 9201transport.tcp.port: 9301node.master: truenode.data: truediscovery.zen.ping.unicast.hosts: ["0.0.0.0:9300", "0.0.0.0:9301"]discovery.zen.minimum_master_nodes: 1node.ingest: truebootstrap.memory_lock: falsenode.max_local_storage_nodes: 2path.data: C:\dev\elasticsearch\es_2\datapath.logs: C:\dev\elasticsearch\es_2\logshttp.cors.enabled: truehttp.cors.allow-origin: /.*/
启动节点1、节点2。使用head查看:
星星的为主节点,圆形为副节点。其实按照配置来说,这两个节点都是主节点,如果节点1挂了,那么节点2会顶上去,成为主节点。
查看节点状态:
查看集群健康值:
d、创建索引库
head连上任意一个节点,创建索引库:
分片为2,其中1个副本。点击OK。
创建成功后,这个集群一个有4个分片,其中2个副本。
e、关于集群的健康值
用3种颜色来表示集群的健康状态: green 、yellow 或者 red 。
- green:所有的主分片和副本分片都正常运行。
- yellow:所有的主分片都正常运行,但有些副本分片运行不正常。
- red:存在主分片运行不正常。
-
目前这个2节点的集群健康值为green,表示所有主、副分片都是正常运行的。
(3)测试集群
创建映射:
添加文档:
连到9201查看:
可以看到,向其中一个节点添加文档,其他节点可以看到数据。
然后现在我关闭节点1:
可以看到节点2现在成为了主节点,集群的健康值为yellow,因为节点1挂掉了。现在只能从节点2查询。
10、总结
使用ElasticSearch可以很好的解决数据量庞大查询的问题,经常被用于搜索中。现在是大数据时代,用好ES很重要。