《Spring Boot 实战派》--13.集成NoSQL数据库,实现Elasticsearch和Solr搜索引擎
第13章 集成NoSQL数据库,实现Elasticsearch和Solr搜索引擎
关于搜索引擎 我们很难实现 Elasticseach 和 Solr两大搜索框架的效果;
所以本章针对两大搜索框架,非常详细地讲解 它们的原理和具体使用方法,
首先介绍什么是搜索引擎 、如何用 MySQL实现简单的搜索引擎,以及Elasticseach 的 概念和 接口类;
然后介绍Elasticseach 精准、模糊、范围、组合、分页、聚合查询; 最后介绍 Solr的概念、安装、配置和使用,并对两大搜索框架进行比较。13.1 Elasticseach --- 搜索应用服务器
13.1.1什么是搜索引擎
搜索引擎(search engine)通常意义上是指:根据特定策略,运用特定的爬虫程序从互联网 上搜集信息,然后对信息进行处理后,为用户提供检索服务,将检索到的相关信息展示给用户的 系统。
本章主要讲解的是搜索的索引和检索,不涉及爬虫程序的内容爬取。大部分公司的业务也不会 有爬取工作,而只提供查询服务,而且Elasticsearch也只是提供这方面的功能。
13.1.2用数据库实现搜索功能
在极少量的数据应用中,可以利用关系型数据库的Select语句实现搜索功能。比如有一个电子 商务系统,采用MySQL数据库,其产品数据表如图13-1所示。
需要实现产品搜索功能,可以使用如下SQL语句:
select * from product where 字段 like %关键词%' select * from product where 字段 like '关键词%'
上述SQL语句中,如果要使用索引提高性能,则like就必须写成like 'a%,或'%a'形式。两 边都加上“%”是不会触发索引的。
我们先不考虑性能,只从商业效果上来看下面的演示是否能满足用户的需求。
1.简单查询
假如,用户想搜索“红富士”的苹果,当搜索关键词“红富士”之后,MySQL执行SQL语句, 如图13-2所示。
查询结果是“name”字段中出现“红富士”的产品信息。
2.多字段模糊查询
上面已经实现了简单的查询。但是,我们发现产品“金帅”的“body”字段中是"金帅,苹果 中一种好吃的苹果,和红富士一样好吃”,这包含了 “红富士”关键词,如何才能把“body”字段 中的关键词也搜索到呢?
现在对SQL语句进行改进,改为如图13-3所示的SQL语句。可以看到,"body"字段中有 “红富士”的词也被检索出来了。
3.分词查询
由于用户输入时可能会存在输入错误的情况,假设用户输入的是“红富s”,那么数据库是无法 查询到结果的。这时需要用分词算法对输入数据进行分词,可以分为:
红' 富、S
红富、S
红、富S
然后对词进行分别查询,查询结果如图13-4所示。
我们会发现结果中把“name”和“body”字段都搜索出来了,但是出现了一个问题一苹果XS 手机壳不该出现却出现了。当然这里排在最后,可能对用户影响不大,但是如果加上排序字段,
根 据最新添加来排序呢?则变成了如图13-5所示的效果。
这就会影响用户的搜索体验,本意是查询水果的,结果手机配件却排在了第一位,问题在哪里 呢?其中一个原因就是分词没分好,然后没有权重设置(权重这里暂时不涉及)。所以,此时需要一 个很好的分词系统。
从MySQL 5.7开始内置了 ngram全文检索插件,用来支持中文分词,并且对MylSAM和 InnoDB引擎有效。在使用中文检索分词插件ngram之前,需要在MySQL配置文件里面设置它的 分词大小,比如设置为2:
ngram_token_size=2
然后可以使用match命令进行查询。
select * from product where match (name,body) against ('红富 s');
分词的结果就变成了:
红富、富S、红S
查询结果如图13-6所示。
乍一看效果很好,但实际并不太科学,配置限定死了,没法根据应用环境来配置。比如搜索“苹 果XR"这个手机,因为分词长度为“2”,那么苹果这种水果也会被搜索岀来,所以结果不尽如 人意。
通过以上实例可以看出,如果用MySQL等关系型或NoSQL数据库去实现搜索引擎还是很麻 烦的,即使在不考虑性能的情况下,考虑的问题依然非常多。
如果加上性能因素来考量,即使对数 据库进行索引,在面对复杂情况下的查询时,效果和性能都是不尽如人意的。
所以,在现在信息爆炸的时代,处理大规模数据就显得力不从心,这时需要一种专业、配置简 单、性能极优的搜索引擎系统,如果能开源、可实现分布式、支持RESTfulAPI,则学习、使用成 本、性能和实
现就非常完美了。这时,Elasticsearch和Solr就出现在我们面前了。
13.1.3 认识 Elasticsearch
Elasticsearch是一个分布式、RESTful风格的搜索和数据分析引擎。通过它,能够执行及合 并多种类型的搜索(结构化数据、非结构化数据、地理位置、指标),解决不断涌现出的各种需求。
Elasticsearch使用的是标准的RESTful风格的API,使用JSON提供多种语言(Java、 Python、Net、SQL和PHP)的支持,它可以快速地存储、搜索和分析海量数据。
Elasticsearch是用Java语言开发的,并使用Lucene作为其核心来实现所有索引和搜索的功 能。它的目的是:通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。
Elasticsearch是一个幵源的高扩展的分布式全文检索引擎,可以近乎实时地存储、检索数据; 本身扩展性很好,允许多台服务器协同工作,每台服务器可以运行多个实例。单个实例称为一个节 点(node),—组节点构成一个集群(cluster)?分片是底层的工作单元,文档保存在分片内,分片 又被分配到集群内的各个节点里,每个分片仅保存全部数据的一部分。
当Elasticsearch的节点启动后,它会使用多播(multicast)或单播(用户更改了配置)寻找 集群中的其他节点,并与之建立连接。
13.1.4 Elasticsearch 应用案例
?GitHub: 2013年年初,GitHub把Solr缓存改成了 Elasticsearch,以便用户搜索20TB 的数据,包括13亿个文件和1300亿行代码。
?维基百科:启动以Elasticsearch为基础的核心搜索架构SoundCloud,为1.8亿用户提供 即时而精准的音乐搜索服务。
?百度:百度使用Elasticsearch作为数据分析引擎,20多个业务线采集服务器上的各类数 据及用户自定义数据,通过对各种数据进行多维分析,辅助定位异常。其单集群最大100台 机器,200个Elasticsearch节点,每天导入超过30TB的数据。
除这些公司外,Stack Overflow.新浪、阿里、360、携程、有赞、苏宁都在使用它。它被广 泛地用于各大公司的站内搜索、IT系统搜索(OA、CRM、ERPk数据分析等工作中。
13.1.5 对比 Elasticsearch 与 MySQL
尽管将Elasticsearch与MySQL进行对比并不科学,但是这样的对比能区分日asticsearch 和MySQL数据库的区别,便于快速用熟悉的知识来理解Elasticsearcho所以,本节采用对比的
方式来讲解Elasticsearch。Elasticsearch与MySQL的结构对比见表13-1。
表 13-1 Elasticsearch 与 MySQL 的结构对比
ElasticSearch |
MySQL |
ElasticSearch |
MySQL |
index |
database |
everything is indexed |
index |
type |
table |
query dsl |
sql |
document |
row |
get url |
select *from table |
field |
column |
put url |
update table set |
mapping |
schema |
(1 )关系型数据库中的数据库,相当于Elasticsearch中的索引(index )
(2) —个数据库下面有多张表(table),相当于一个索引(index)下面有多个类型(type)。
(3) —个数据库表(table)下的数据由多行(row)多列(column,属性)组成,相当于一个 type由多个文档(document)和多个field组成。
(4) 在关系型数据库中,schema定义了表、每个表的字段,还有表和字段之间的关系;在 Elasticsearch中,mapping定义索引下的type的字段处理规则,即索引如何建立、索引类型、 是否保存原始索引JSON文档、是否压缩原始JSON文档、是否需要分词处理、 如何进行分词处 理等。
(5) 在 MySQL 数据库中的增(insert )、删(delete )、改(update )、查(select)操作相 当于 Elasticsearch 中的增(put/post )、删(delete )、改(update )、查(get )。
客户端主要通过“方法(PUT/POST/GET/DELETE ) + http://ip:端口/索引名称/类型/主键” 来访问内容。
13.1.6 认识 ElasticSearchRepository
Spring-data-elasticsearch 是 Spring 提供的操作 Elasticsearch 的数据接口,它封装了大 量的基础操作,通过它可以很方便地操作Elasticsearch的数据。
通过继承ElasticsearchRepository来完成基本的CRUD及分页操作,和普通的JPA没有什 么区别。比如下面实体Product的Repository继承ElasticsearchRepository后,可以在 Elasticsearch文档中进行查找和比较等操作。具体使用方法见以下代码:
?Component public interface ProductRepository extends ElasticsearchRepository{ Product findByld(long id); Product findByName(String name); List findByPriceBetween(double price 1, double price2); )
ElasticsearchRepository有几个特有的search方法,用来构建一些Elasticsearch查询, 主要由QueryBuilde「和SearchQuery两个参数来完成一些特殊查询。
实现类NativeSearchQuery实现了 Que「yBuilde「和Sea「chQue「y方法,要构建复杂查询, 可以通过构建NativeSearchQuery类来实现。
—般情况下,不是直接新建NativeSearchQuery类,而是使用NativeSearchQueryBuilder 来完成NativeSearchQuery的构建。具体用法见以下代码:
NativeSearchQueryBuilder .withQuery(QueryBuilder1) ,withFilter(QueryBuilder2) .withSort(SortBuilder1) .withXxx().build()
13.1.7 认识 ElasticsearchTemplate
ElasticsearchTemplate是Spring对Elasticsearch的API进行的封装,主要用来对索引进 行仓U建、删除等操作。它继承了 Elasticsearchdperations 和 ApplicationContextAware 接口。 ElasticSearchTemplate 提供一些比 ElasticsearchRepository 更底层的方法。
ElasticsearchOperations接口中常用的方法如下。
- createlndex()方法:创建索引,返回值为布尔类型数据。
- indexExists()方法:查询索引是否存在,返回值为布尔类型数据。
- putMapping()方法:创建映射,返回值为布尔类型数据。
- getMapping()方法:得到映射,返回值为一个Map数据。
- deletelndex()方法:删除索引,返回值为布尔类型数据。
13.1.8 认识注解?Document
注解?Document作用于类,用于标记实体类为文档对象。
存储在Elasticsearch中的一条数据,即是一个文档,类似关系型数据库的一行数据。 Elasticsearch会索引每个文档的内容,以便搜索。它使用JSON格式,将数据存储到曰asticsearch 中,实际上是将JSON格式的字符串发送给了 Elasticsearch。
1.、document的核心元数据
document有三个核心元数据,分别是_index、_type、_id。
(1 ) Jndexo代表一个document存放在哪个index中,类似的数据放在一个索引中,非类似 的数据放在不同的索引中。index中包含了很多类似的document,这些document的field很大一 部分是相同的。索引名称必须小写,不能用下画线开头,不包含逗号。
(2) _type。代表document属于index的哪个类别,一个索引通常会划分为多个type,逻辑 上对index不同的数据进行分类。type名称可以是大写或小写,但是不能用下画线开头,不能包含 逗号。
(3 ) _ido代表document的唯一标识,与Jndex和_type —起可以标识和定位一个 documento默认自动创建id,也可以手动指定document的id。
2、document id的手动指定和自动生成
(1 )手动指定document id。
如果需要从某些其他系统中导入一些数据到Elasticsearch,则会采用手动指定id的形式,因 为一般情况下系统中已有数据的唯一标识,可以用作Elasticsea「ch中的document的ido
其语法格式为:
put /index/type/id { "json" } (2)自动生成 document id。 其语法格式为: post /index/type ( "json" )
自动生成的id长度为20个字符,URL安全、Base64编码、GUID、分布式系统并行生成时 不会发生冲突。
3、document的_source元数据,以及定制返回结果
.source元数据是在创建document时放在body中的JSON数据。在默认情况下,查找数据 时会返回全部数据。如果要定制返回结果,贝U可以指定_sou「ce中返回哪些fieldo
例如:
GET / Jndex/ _type/1 ?_source=field
13.1.9管理索引
1、创建索引
(1) 根据类的信息自动生成创建索引。
下面代码是根据实体类创建一个名为“ec”的索引,并定义tpye是“product”。由于是单机 环境,所以定义副本为0,分片为默认值5。
@Document(indexName = "product", type = "product", replicas = 0, shards = 5) public class Product implements Serializable { )
代码解释如下。
- indexName :对应索引库名称,可以理解为数据库名。必须小写,否则会报 uorg.elasticsearch.indices.InvalidlndexNameException"异常。
- type:对应在索引库中的类型,可以将其理解为“表名”。
- shards:分片数量,默认值为5。
- replicas:副本数量,默认值为1。如果是单机环境,则健康状态为“yellow”。如果要成为 “green”,则指定值为0即可。
(2) 手动创建索引。
可以使用createlndex方法手动指定indexName和Settings,再进行映射。在使用前,要先 注入ElasticsearchTemplate。使用方法如下。
- 根据索引名创建索引:lasticsearchTemplate.create!ndex("indexname");
- 根据类名创建索引:lasticsearchTemplate.createlndex(Product.class);
2、查询索引
- 根据索引名查询:elasticsearchTemplate.indexExists("indexname");
- 根据类名查询:elasticsearchTemplate.indexExists(Product.class);
3、删除索引
可以根据索引名和类名对索引进行删除。
- 根据索引名删除:elasticsearchTemplate.deletelndex("indexname");
- 根据类名删除:elasticsearchTemplate.deletelndex(Product.class);
13.2 实例55:用ELK管理Spring Boot应用程序的日志
ELK 是 Elasticsearch+Logstash+Kibana 的简称。
Logstash负责将数据信息从输入端传输到输出端,比如将信息从MySQL传入Elasticsearch, 还可以根据自己的需求在中间加上滤网。Logstash提供了很多功能强大的滤网,以满足各种应 用场景。
Logstash有以下两种工作方式:
(1) 每一台机器启动一个Logstash服务,读取本地的数据文件,生成流传给Elasticsearch o
(2) Logback引入Logstash包,然后直接生产JSON流,传给一个中心的Logstash服务 器,Logstash 服务器再传给 Elasticsearch。最后,Elasticsearch 将其流传给 Kibana。
Kibana是一个开源的分析与可视化平台,和Elasticsearch —起使用。可以用Kibana搜索、 查看、交互存放在日asticsearch索弓I里的数据。使用各种不同的图标、表格、地图等,Kibana能 够很轻易地展示高级数据分析与可视化。
ELK架构为数据分布式存储、日志解析和可视化创建了一个功能强大的管理链。三者相互配合, 取长补短,共同完成分布式大数据处理工作。
本实例通过Logstash收集本地的log文件流,传输给日asticsearch。
本实例的源代码可以在“/13/ELKDemo”目录下找到。
13.2.1 安装 Elasticsearch
(1) 通过官网下载Elasticsearch o
(2) 在下载完成后,首先将其解压到合适的目录,然后进入解压目录下的bin目录,双击 elasticsearch.bat文件启动Elasticsearch。这里需要确保安装的Java版本在1.8及以上。
(3) 访问“http:〃127.0.0.1:9200/”,当看到返回如下一串JSON格式的代码时,则说明已经 安装成功了。
{ "name": "1q71xef", "cluster_name": "elasticsearch", 〃省略 "tagline": "You Know, for Search" }
根据应用需要,还可以安装日asticsearch必要的一些插件,如Head、kibana、IK (中文分 词)、grapho
在Elasticsearch 6.0.0或更新版本中,创建的索引只会包含一个映射类型(mapping type ) o在Elasticsearch 5.x中创建的具有多个映射类型的索引在Elasticsearch 6.x中依然会 正常工作。
在Elasticsearch 7.0.0中,映射类型被完全移除了。
13.2.2 安装 Logstash
1、安装 Logstash
(1) 访问 Elasticsearch 官网下载 Logstash
(2) 将下载文件解压到自定义的目录即可。
2、配置 Logstash
(1)在解压文件的config目录下新建Iog4j_to_es.conf文件,写入以下代码:
input{ tcp( host =>"localhost" port =>9601 mode 二〉"server" tags 二〉["tags”] ##JSON格式 codec => jsonjines } } output( elasticsearch( hosts=>"127.0.0.1:9200" index 二〉"demolog" ) stdout( codec=>rubydebug} }
这里一定要注意:这是UTF-8的格式,不要带BOM。如果启动时岀现错误,则可以用“logstash -f ../config/xxx.conf -tn命令检查配置文件是否错误。
(2) 新建文件run.bato 写入代码 “logstash -f ../config/log4j_to_es.conf”,保存。然后双 击该配置文件,启动Logstash o
(3) 访问“localhost:9600”,如出现以下内容,则代表配置成功。
{,,host,,:"zhonghua,,,,,version":"6.5.0,,J"http_address,,:,,127.0.0.1:9600,,J"id,,:"03472165-2b17-4e5f-a1a1-f4 8ea4deb9a1","name":"zhonghua","build_date,,:"2018-11-09T19:43:40+00:00","build_sha":"4b3a404d6751 261 d155458c1 a8454a22167b1954")"build_snapshot":false)
13.2.2 安装 Kibana
Kibana是官方推出的Elasticsearch数据可视化工具。
(1 )通过访问Elasticsearch官网下载Kibana。
(2) 解压下载的压缩文件,进入解压目录,双击Kibana目录的bin/kibana.bat,以启动 Kibana,当出现以下提示时,代表启动成功。
log [08:23:47.611] [info][status][plugin:spaces@6.5.0] Status changed from yellow to green - Ready
(3) 访问localhost:5601就可以进入Kibana控制台。
单击控制台左边导航栏的“Dev-tools”按钮,可以进入Dev-tools界面。单击“Get to work“, 然后在控制台输入“GET/_cat/health?”命令,可以查看服务器状态。如果在右侧返回的结果中看 到green或yellow ,则表示服务器状态正常。
yellow表示所有主分片可用,但不是所有副本分片都可用。如果Elasticsearch只是安 装在本地,且设置了副本大于0,则会出现黄色,这是正常的状态。因为并没有分布式部 署,是单节点。另外,由于Elasticsearch默认有1个副本,主分片和副本不能在同一个 节点上,因此副本就是未分配(unassigned )
所以,在设计实体时可以设置@。0血111511:(血(1。乂村2111。= "goods",type = "goods",shards = 5, yreplicas = 0),即 “repIicas=0” 就会变成 green o
13.2.4 配置 Spring Boot 项目
(1)添加项目依赖,见以下代码:
org.springframework.boot spring-boot-starter-log4j 1.3.8.RELEASE net.logstash.logback logstash-logback-encoder 5.3
(2)添加配置文件logback.xml,这里在Spring Boot项目里添加一个配置文件,见以下代码:
<?xml version="1.0" encoding="UTF-8"?>Oppender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> localhost:9601 %d(HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n
13.2.5创建日志计划任务
在Spring Boot项目中创建logTest类,用于测试将日志通过Logstash发送到日asticsearch, 见以下代码
?Component public class logTest ( private Logger logger = LoggerFactory.getLogger(logTest.class); @Scheduled(fixedRate = 1000) public void logtest() ( logger.trace("trace 日志”); logger.debugC'debug 日志”); logger.info("info 日志”); logger.warn("warn 日志, logger.error("error 日志”); } }
(1)在入口类中添加注解@EnableScheduling,开启计划任务,然后运行项目。
(2 )访问,,http://localhost:5601 "o
选择左侧导航栏的“Management —Index Patterns —>■ Create index pattern"命令,输 入"demolog",单击"Next”按钮,选择时间过滤器字段名,单击“Create index pattern”按钮, 创建完成。
进入Kibana的Discover,就可以查看日志信息了。
13.2.6用Kibana查看管理日志
在Kibana的Discover页面中,可以交互式地探索自己的数据。这里可以访问与所选择的索引 默认匹配的每个索引中的文档,可以提交查询请求、过滤搜索结构,并查看文档数据,
还可以看到 匹配查询请求的文档数量,以及字段值统计信息。
如果选择的索引模式配置了 time字段,则文档随时间的分布将显示在页面顶部的直方图中。 Discover
Kibana的功能非常强大,还可以进行可视化设计,创建热点图、区域图、饼图、时间线等,也 可以监控Elasticsearch的健康状态。如果安装了 APM支持,还可以进行性能监控。
13.3 实例 56:在 Spring Boot 中集成 Elasticsearch,实现增加、 删除、修改、查询文档的功能
本实例讲解如何在Spring Boot中实现增加、删除、修改、查询文档的功能,以理解Spring Boot 的相关Starter的使用和Elasticsea「ch的知识点和具体应用。
本实例的源代码可以在"/13/ElasticsearchProductDemo”目录下找到。
13.3.1 集成 Elasticsearch
Spring Boot 提 供 了 Starter ( spring-boot-starter-data-elasticsearch ) 来集成 Elasticsearch o
?优点:开发速度快,不要求熟悉Elasticsearch的一些API,能快速上手。即使之前对 Elasticsearch不了解,也能通过方法名或SQL语句快速写出自己需要的逻辑。而具体转换 成API层的操作则是由框架底层实现的。
?缺点:使用的Spring Boot的版本对Elasticsearch的版本也有了要求,不能超过某些版本 号,在部署时需要注意。如果采用API方式,则能解决这个问题。
(1 )添加依赖,见以下代码:
org.springframework.boot
spring-boot-starter-data-elasticsearch
(2)添加application.properties配置 见以下代码:
spring.data.elasticsearch.cluster-name=elasticsearch #节点的地址。注意,API模式下端口号是9300,千万不要写成9200 spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300 #是否开启本地存储 spring.data.elasticsearch.repositories.enable=true
13.3.2创建实体
(1)创建实体。
这里根据类的信息自动生成,也可以手动指定索引名称。ElasticsearchTemplate中提供了创 建索引的API,因为进行本机测试,没做集群,所以replicas副本先设置为0。见以下代码:
〃省略 〃索引名称可以理解为数据库名,必须为小写,否贝U会报uorg.elasticsearch.indices.lnvalidlndexNameException,, 异常 @Document(indexName = "ec", type = "product", replicas = 0, shards = 5) //type (类型)可以理解为表名 @Data public class Product implements Serializable ( /** ?Description: @ld 注解必须是 springframework 包下的 * org.springframework.data.annotation.Id 7 private Long id; @Field(type = FieldType.Text, analyzer ="ik_max_word")//ik_max_word 使用 IK 分词器 private String name; @Field(type = FieldType.Keyword)//在存储数据时,不会对 category 进行分词 〃分类 private String category; 〃价格 @Field(type = FieldType.Double) private Double price; @Field(index = false, type = FieldType.Keyword)// index二false,表示不建立索引 〃图片地址 private String images; private String body; } 〃省略
代码解释如下。
?@ld注解:作用于成员变量,标记一个字段作为id主键。
??Field注解:作用于成员变量,标记为文档的字段,需要指定字段映射属性type。
- index:是否索引,布尔类型,默认为true。
- store:是否存储,布尔类型,默认为false。
- analyzer:分词器名称,这里的ik_max_word即使用IK分词器。
(2)创建数据操作接口。
继承ElasticsearchRepository即可创建数据操作接口,这样不用写方法,就具备了
Elasticsearch文档的增加、删除、修改和查询功能。见以下代码:
package com.example.demo.repository; 〃省略 ?Component public interface ProductRepository extends ElasticsearchRepository( Product findByld(long id); Product findByName(String name); }
13.3.3实现增加、删除、修改和查询文档的功能
在测试类中,实现对Elasticsearch文档进行增加、删除、修改和查询的功能,见以下代码:
〃省略 @SpringBootTest @RunWith(SpringRunner.class) public class ProductControllerTest ( 〃每页数量 private Integer PAGESIZE=10; @Autowired private ProductRepository productRepository; @Test public void save() { long id= System.currentTimeMillis(); Product product = new Product(id, ”红富士”,”水菓”,7.99,”/img/p1.jpg”,%是一个测试商品, productRepository.save(product); System.out.println(product.getld()); ) @Test public void getProduct() ( Product product = p「oductRepository.findByName("红富士, System.out.println(product.getld()); ) @Test public void update() ( long id=1557032203515L; Product product = new Product(id, ”金帅水果”,7.99,”/img/p1 .jpg”,”金帅也和红富士一样,非常好吃,脆脆的, productRepository.save(product); ) @Test public void getProductByld() { Product product = productRepository.findByld(1557032203515L); System.out.println(product.getName()+product.getBody()); } @Test public void delete() ( long id=1557032203515L; productRepository.deleteByld(id); ) @Test public void getAII() ( lterablelist = productRepository.findAII(Sort.by("id").ascending()); for (Product product: list) ( System.out.println(product); } )
请根据自己测试“save”方法返回的id值进行测试。这里的id值(1557032203515L ) 是笔者本机上得到的,读者不能用这个id值进行测试。
启动项目,运行测试getAII,控制台返回如下值:
Product(id=1557031659306, name=红富士,category二水果 price=7.99, images=/img/p1.jpg, body二这是一个 测试商品) Product(id=1557032088459, name二金帅,category=水果,price二7.99, images=/img/p1.jpg, body=金帅也和红 富士一样,非常好吃,脆脆的) Product(id=1557032203515, name=红富士,category=水果 price=7.99, images=/img/p1 .jpg, body二这是一个 测试商品) Product(id=1557034189287, name=红富士,category二水果,price=7.99, images=/img/p1.jpg, body=这是一个 测试商品)
13.4 Elasticsearch 查询
可以根据Spring Data提供的方法名称,实现自己想自定义的查询功能:无须写实现类,只要 继承ElasticsearchRepository接口即可。
如“findByTitle”表示根据“title”进行查询,具体方 法见表
关键词 |
例 子 |
And |
findByNameAndPrice |
Or |
findByNameOrPrice |
Is |
findByName |
Not |
findByNameNot |
Between |
findByPriceBetween |
关键词 |
例 子 |
LessThanEqual |
findByPriceLessThan |
GreaterThanEqual |
findByPriceGreaterThan |
Before |
findByPriceBefore |
After |
findByPriceAfter |
Like |
findByNameLike |
StartingWith |
findByNameStartingWith |
EndingWith |
findByNameEndingWith |
Contains/Containing |
findByNameContaining |
In |
findByNameln(Collection |
Notln |
findByNameNotln(Collection |
Near |
findByStoreNear |
TRUE |
findByAvailableT rue |
FALSE |
findByAvailableFalse |
OrderBy |
findByAvailableTrueOrderByNameDesc |
如果要查询价格在7?8元的商品,贝!J可以在接口类加上“List〈P「oduct> findByFYiceBetween (double pricel, double price2); ” 方法,见以下代码: |
|
?Component public interface ProductRepository extends ElasticsearchRepository Product findByName(String name); List } |
然后,在测试类中直接使用自定义的“findByP「iceBetween”方法查询出数据,见以下代码:
@Test public void queryByPriceBetween() ( lterablelist = productRepository.findByPriceBetween(7.00, 8.00); for (Product product: list) ( System.out.println(product); )
其他的使用方法以此类推。
13.4.2精准查询
1、单参数 termQuery
用法见以下代码:
QueryBuilder queryBuilder=QueryBuilders.termQuery("字段名","查询值”);
它是不分词查询。因为不分词,所以汉字只能查询一个字,而多字母的英语单词算一个字。
具体实现见以下代码:
@Test public void termQueryO ( 〃构建查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilderQueryBuilder 二 new NativeSearchQueryBuilderQ; 〃查询词,只能查询一个汉字,或一个英文单词 nativeSearchQueryBuilderQueryBuilder.withQuery(QueryBuilders.termQuery("name","富")); //搜索,获取结果 Pageproducts 二 productRepository.search(nativeSearchQueryBuilderQueryBuilder.buildO); 〃总条数 for (Product product: products) ( System.out.println(product); ) }
2、多参数termsQuery
terms可以提供"个查询的参数对一个字段进行查询,用法见以下代码。注意,这里是te「m的 复数形式terms o
QueryBuilder queryBuilder=QueryBuilders.termsQuery("字段名查询值","查询值,
具体实现见以下代码:
@Test 〃多参数 termsQuery public void termsQueryO ( //构建查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilderQueryBuilder 二 new NativeSearchQueryBuilder(); 〃查询词,只能查询一个汉字或一个英文单词 nativeSearchQueryBuilderQueryBuilder.withQuery(QueryBuilders.termsQuery("name",'富',"帅")); 〃搜索,获取结果 Pageproducts = productRepository.search(nativeSearchQueryBuilderQueryBuilder.build()); //总条数 for (Product product: products) ( System.out.println(product); } }
3、分词查询matchQuery
分词查询采用默认的分词器,用法见以下代码:
QueryBuilder queryBuilder2 = QueryBui(ders.matchQuery("字段名","查询值”);
具体实现见以下代码:
@Test 〃分词查询采用默认的分词器 public void matchQueryO ( 〃查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilderQueryBuilder = new NativeSearchQueryBuilder(); 〃查询词 nativeSearchQueryBuilderQueryBuilder.withQuery(QueryBuilders.matchQuery("name",”红士”)); 〃搜索,获取结果 Pageproducts= productRepository.search(nativeSearchQueryBuilderQueryBuilder.build()); for (Product product: products) ( System.out.println(product); ) )
4、 多字段查询 multiMatchQuery
多字段查询采用multiMatchQuery方法,用法见以下代码:
QueryBuilder queryBuilder= QueryBuilders.multiMatchQuery("查询值”,”字段名”,”字段名”,"字段名,
具体实现见以下代码:
@Test 〃多字段查询 public void multiMatchQueryO { //构建查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); nativeSea「chQueryBuilde「.withQuery(QueryBuilde「s.multiMatchQuery("红富士金帅"「name", "body")); //搜索,获取结果 Pageproducts = productRepository.search(nativeSearchQueryBuilder.build()); 〃总条数 for (Product product: products) ( System.out.println(product); ) }
13.4.3模糊查询
常见的模糊查询的方法有4种。
1.左右模糊
用法见以下代码:
QiteryBuilders.queryStringQueryf'W 询值”).field("字段名”);
具体实现见以下代码:
@Test public void queryStringQuery() ( 〃查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilderQueryBuilder = new NativeSearchQueryBuilder(); 〃左右模糊 nativeSearchQueryBuilderQueryBuilder.withQuery(QueryBuilders.queryStringQuery("我覚得红富士 好吃 ”).field(” name”)); 〃搜索,获取结果 Pageproducts= productRepository.search(nativeSearchQueryBuilderQueryBuilder.build()); for (Product product: products) { System.out.println(product); ) }
2、前缀查询 prefixQuery
如果字段没分词,则匹配整个字段前缀,用法见以下代码:
QueryBuilders.prefixQueryC* 字段名查询值”);
具体实现见以下代码:
@Test public void prefixQueryO { 〃查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilderQueryBuilder 二 new NativeSearchQueryBuilder(); 〃左右模糊 ativeSearchQueryBuilderQueryBuilder.withQuery(QueryBuilders.prefixQuery("name","±")); //搜索,获取结果 Pageproducts= productRepository.search(nativeSearchQueryBuilderQueryBuilder.build()); for (Product product: products) ( System.out.println(product); ) }
3、通配符查询 wildcard query
使用通配符方式进行查询,支持通配符“*”和“? ”。“*”代表任意字符串,“? ”代表任意一 个字符。
(1 )使用通配符“*”。
通配符“*”可以匹配多个值,用法见以下代码:
QueryBuilders.wildcardQueryf^ 段名”,”查询值 *");
具体实现见以下代码:
@Test public void wildcardQuery() { 〃查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilderQueryBuilder = new NativeSearchQueryBuilder(); nativeSea「chQue「yBuilde「QueryBuilder.withQuery(QueryBuilde「s.wildcardQuery("name"」^*")); 〃搜索,获取结果 Pageproducts二 productRepository.search(nativeSearchQueryBuilderQueryBuilder.build()); for (Product product: products) ( System.out.println(product); ) )
(2)使用通配符“? ”。
通配符“?”匹配一个词,用法见以下代码:
QueryBuilders.wildcardQuery(”字段名”,”查?值”);
具体实现见以下代码:
@Test 〃通配符查询 public void wildcardQuery2() { 〃查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilderQueryBuilder = new NativeSearchQueryBuilder(); nativeSearchQueryBuilderQueryBuilder.withQuery(QueryBuilders.wildcardQuery(nname","金?")); 〃搜索,获取结果 Pageproducts= productRepository.search(nativeSearchQueryBuilderQueryBuilder.build()); for (Product product: products) ( System.out.println(product); ) )
4.分词模糊查询fuzzy query
分词模糊查询即匹配截取字符串为字前或后加1个词的文档,这里通过增加fuzziness (模糊) 属性来查询,fuzziness的含义是检索的term前后增加或减少〃个词的匹配查询。用法见以下代码:
QueryBuilders.fuzzyQueryC 字段名","查询值'').fuzziness(Fuzziness.ONE);
具体实现见以下代码:
@Test public void fuzzyQueryO ( 〃查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilderQueryBuilder =new NativeSearchQueryBuilderQ; nativeSearchQueryBuilderQueryBuilder ,withQuery(QueryBuilders.fuzzyQuery("name","±").fuzziness(Fuzziness.ONE)); //搜索,获取结果 Pageproducts= productRepository.search(nativeSearchQueryBuilderQueryBuilder.buildO); for (Product product: products) ( System.out.println(product); ) }
5.相似内容推荐
相似内容的推荐是给定一篇文档信息,然后向用户推荐与该文档相似的文档。通过 Elasticsearch的More like this查询接口,可以非常方便地实现基于内容的推荐,
用法见以下代码: QueryBuilders.moreLikeThisQuery(new StringQ {"字段名"}).addLikeText("查询值”);
如果不指定字段名,则默认全部,常用在相似内容的推荐上。
13.4.4范围查询
将文档与具有一定范围内字词的字段进行匹配,用法如下。
- 闭区间查询:QueryBuilde「queryBuilder 二 QueryBuilders.rangeQuery("字段名").from (“值 1n).to('值 2”);
- 开区间查询:QueryBuilder queryBuilder 二 QueryBuilde「s.「angeQue「y(”字段名”),from(" 值 2").includeUpper(false).includeLower(false);/Z默认是true,也就是包含
- 大于:QueryBuilder queryBuilder 二 QueryBuilders.rangeQuery("字段名").gt("查询值”);
- 大于或等于:QueryBuilder queryBuilde「二 Que「yBuilde「s.「angeQue「y(”字段名").gte(" 查询值,
- 小于:Que「yBuilde「queryBuilder 二 Que「yBuilde「s.「angeQue「y(”字段名”).lt(”查询值”);
- 小于或等于:QueryBuilder queryBuilder = QueryBuilders.rangeQuery("字段名").lte(" 查询值”);
13.4.5组合查询
组合查询是可以设置多个条件的查询方式,用来组合多个查询,有4种方式。
- must:代表文档必须完全匹配条件,相当于and,会参与计算分值。
- mustnot:代表必须不满足匹配条件。
- filter:代表返回的文档必须满足filte「条件,但不会参与计算分值。
- should:代表返回的文档可能满足条件,也可能不满足条件,有多个should时满足任何一 个就可以,相当于o「,可以通过minimum_should_match设置至少满足几个。
13.4.6分页查询
使用NativeSearchQueryBuilder实现分页查询,用法见以下代码:
@Test public void termQuery() { 〃分页 int page = 0; int size=5;//每页文档数 〃构建查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilderQueryBuilder = new NativeSearchQueryBuilder(); 〃查询词,只能查询一个汉字,或一个英文单词 nativeSearchQueryBuilderQueryBuilder.withQuery(QueryBuilders.termQuery("name","富")); //搜索,获取结果 nativeSearchQueryBuilderQueryBuilder.withPageable(PageRequest.of(page, size)); Pageproducts = productRepository.search(nativeSearchQueryBuilderQueryBuilder.buildO); //总条数 for (Product product: products) ( System.out.println(product); } )
如果要逬行排序,只要在分页查询上构建withSort参数即可,用法见以下代码:
@Test 〃分页查询+排序 public void searchByPageAndSort() ( //分页: int page = 0; int size = 5;//每页文档数 〃构建查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilderQueryBuilder = new NativeSearchQueryBuilder(); 〃查询词,只能查询一个汉字,或一个英文单词 nativeSearchQueryBuilderQueryBuilder.withQuery(QueryBuilders.termQuery("name","富")); 〃搜索,获取结果 nativeSearchQueryBuilderQueryBuilder.withSort(SortBuilders.fieldSort(,,id,').order(SortOrder.DESC)); nativeSearchQueryBuilderQueryBuilder.withPageable(PageRequest.of(page, size)); Pageproducts = productRepository.search(nativeSearchQueryBuilderQueryBuilder.build()); 〃总条数 for (Product product: products) ( System.out.println(product); ) }
13.4.7聚合查询
聚合(aggregation )是Elasticsearch的一个强大功能,可以极其方便地实现对数据的统计、 分析工作。搜索是查找某些具体的文档,聚合就是对这些搜索到的文档进行统计,可以聚合出更加 细致的数据。它有两个重要概念。
- Bucket (桶/集合):满足特定条件的文档的集合,即分组。
- Metric (指标/度量):对桶内的文档进行统计计算(最小值、最大值),简单理解就是进行 、A-A-运算。
聚合由AggregationBuilders类来构建,它提供的静态方法见表13-3。
功 能 |
语 法 |
统计数量 |
ValueCountBuilder vcb= AggregationBuilders.count("count id").field("id"); |
去重统计数量 |
CardinalityBuilder cb= AggregationBuilders.cardinality("distinct count id").field("id"); |
聚合过滤 |
FilterAggregationBuilder fab二 AggregationBuilders.filter("id_filter").filter(QueryBuilders.query StringQuery("id:1")); |
按字段分组 |
TermsBuilder tb= AggregationBuilders.terms("group name").field("name"); |
求和 |
SumBuilder sumBuilder= AggregationBuilders.sum("suin price").field("price"); |
求平均值 |
AvgBuilder ab= AggregationBuilders.avg("avg price").field("price"); |
求最大值 |
MaxBuilder mb= AggregationBuilders.max("max price").field("price"); |
求最小值 |
MinBuilder min 二 AggregationBuilders.min("min price").field("price"); |
按日期间隔分组 |
DateHistogramBuilder dhb二 AggregationBuilders.dateHistogram("dhb dt").field("date"); |
获取聚合结果 |
TopHitsBuilder thb二 AggregationBuilders.topHits("top hit result"); |
嵌套的聚合 |
NestedBuilder nb= AggregationBuilders.nested("negsted quests").path("quests"); |
反转嵌套 |
AggregationBuilders.reverseNested("res negsted").path("kps "); |
具体用法见以下代码:
//测试桶 public String searchBybucket() ( NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); queryBuilder.withSourceFilter(new FetchSourceFilterfnew String[]{""}, null)); 〃指定索引的类型,只先从各分片中查询匹配的文档,再重新排序和排名,取前size个文档 queryBuilder.withSearchType(SearchType.QUERY_THEN_FETCH); 〃指定要查询的索引库的名称和类型,其实就是文档?Document中设置的indedName和type queryBuilder.withlndices("goods").withTypes("goods"); 〃添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand queryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brand")); //查询,需要把结果强转为AggregatedPage类型,AggregatedPage:聚合查询的结果类。它是Page的子接口 AggregatedPage aggPage = (AggregatedPage ) goodsRepository.search(queryBuilder.build()); 〃从结果中取出名为brands的聚合解析 〃强转为St「ingTerm类型 StringTerms agg = (StringTerms) aggPage.getAggregation("brands"); //获取桶 List buckets = agg.getBuckets(); 〃遍历 for (StringTerms.Bucket bucket: buckets) { //获取桶中的key System.out.println(bucket.getKeyAsStringO); //获取桶中的文档数量 System.out.println(bucket.getDocCount()); ) return buckets; )
还可以嵌套聚合,在聚合AggregationBuilders中使用subAggregation,用法见以下代码:
queryBuilder.addAggregation( AggregationBuilders.terms("brands").field("brand") ,subAggregation(AggregationBuilders.avg("price_avg").field("price")) //在品牌聚合桶内进行嵌套聚 合);
这里一定要注意Spring Boot和Elasticsea「ch的版本是否对应。
13.5 实例57:实现产品搜索引擎
本实例通过实现一个产品信息的搜索引擎来帮助读者理解本章所讲的知识点及具体使用。创建 实体和接口的方法在前面小节中已经讲解过,本节只讲解创建控制器实现搜索API和搜索的视图展 示方法。
本实例的源代码可以在,713/ElasticsearchProductSearch”目录下找到。
(1 )创建搜索控制器,用于构建搜索框架,见以下代码:
〃省略 ?Controller public class Searchcontroller { @Autowired private ProductRepository productRepository; @GetMapping("search") public ModelAndView searchByPageAndSort(lnteger start,String key) ( //分页: if (start == null) ( start = 0; } int size=2;〃每页文档数 〃构建查询条件 NativeSearchQueryBuilder nativeSearchQueryBuilderQueryBuilder 二 new NativeSearchQueryBuilder(); //nativeSearchQueryBuilderQueryBuilder.withQuery(QueryBuilders.matchQuery("name", key)); nativeSearchQueryBuilderQueryBuilder.withQuery(QueryBuilders.multiMatchQuery(key,"name", ” body”)); 〃搜索,获取结果 nativeSearchQueryBuilderQueryBuilder.withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC)); nativeSearchQueryBuilderQueryBuilder.withPageable(PageRequest.of(start, size)); Pageproducts = productRepository.search(nativeSearchQueryBuilderQueryBuilder.buildO); 〃总条数 for (Product product: products) ( System.out.println(product); } ModelAndView mav = new ModelAndView("search"); mav.addObject("page", products); mav.addObject("keys", key); return mav; ) }
(2)创建显示视图。
创建用于展示数据的前端页面,具体见以下代码:
搜索词:mav