全文检索 Elasticearch
教学目标
1. 了解Elasticsearch的应用场景
2. 掌握索引维护的方法
3. 掌握基本的搜索Api的使用方法
约束
1.阅读本教程之前需要掌握Lucene的索引方法、搜索方法 。
1 ElasticSearch介绍
1.1 介绍
官方网址:https://www.elastic.co/cn/products/elasticsearch
Github:https://github.com/elastic/elasticsearch
总结:
1、elasticsearch是一个基于Lucene的高扩展的分布式搜索服务器,支持开箱即用。
2、elasticsearch隐藏了Lucene的复杂性,对外提供Restful 接口来操作索引、搜索。
突出优点:
1.扩展性好,可部署上百台服务器集群,处理PB级数据。
2.近实时的去索引数据、搜索数据。
es和solr选择哪个?
1.如果你公司现在用的solr可以满足需求就不要换了。
2.如果你公司准备进行全文检索项目的开发,建议优先考虑elasticsearch,因为像Github这样大规模的搜索都在用它。
1.2原理与应用
1.2.1索引结构
下图是ElasticSearch的索引结构,下边黑色部分是物理结构,上边黄色部分是逻辑结构,逻辑结构也是为了更好的去描述ElasticSearch的工作原理及去使用物理结构中的索引文件。
逻辑结构部分是一个倒排索引表:
1、将要搜索的文档内容分词,所有不重复的词组成分词列表。
2、将搜索的文档最终以Document方式存储起来。
3、每个词和docment都有关联。
如下:
现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 ,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。
1.2.3 RESTful应用方法
如何使用es?
Elasticsearch提供 RESTful Api接口进行索引、搜索,并且支持多种客户端。
下图是es在项目中的应用方式:
1)用户在前端搜索关键字
2)项目前端通过http方式请求项目服务端
3)项目服务端通过Http RESTful方式请求ES集群进行搜索
4)ES集群从索引库检索数据。
2 ElasticaSearch安装
2.1 安装
bin:脚本目录,包括:启动、停止等可执行脚本
config:配置文件目录
data:索引目录,存放索引文件的地方
logs:日志目录
modules:模块目录,包括了es的功能模块
plugins :插件目录,es支持插件机制
2.2 配置文件
2.2.1 三个配置文件
ES的配置文件的地址根据安装形式的不同而不同:
使用zip、tar安装,配置文件的地址在安装目录的confifig下。
使用RPM安装,配置文件在/etc/elasticsearch下。
使用MSI安装,配置文件的地址在安装目录的confifig下,并且会自动将confifig目录地址写入环境变量 ES_PATH_CONF。
本教程使用的zip包安装,配置文件在ES安装目录的confifig下。
配置文件如下:
elasticsearch.yml : 用于配置Elasticsearch运行参数 jvm.options : 用于配置Elasticsearch JVM设置
log4j2.properties: 用于配置Elasticsearch日志
2.2.2 elasticsearch.yml
配置格式是YAML,可以采用如下两种方式:
方式1:层次方式
path: data: /var/lib/elasticsearch logs: /var/log/elasticsearch
方式2:属性方式
path.data: /var/lib/elasticsearch path.logs: /var/log/elasticsearch
本项目采用方式2,例子如下:
cluster.name: xuecheng
node.name: xc_node_1
network.host: 0.0.0.0
http.port: 9200
transport.tcp.port: 9300
node.master: true
node.data: true
discovery.zen.minimum_master_nodes: 1
bootstrap.memory_lock: false
node.max_local_storage_nodes: 1
path.data: D:\ElasticSearch\elasticsearch‐6.2.1\data
path.logs: D:\ElasticSearch\elasticsearch‐6.2.1\logs
http.cors.enabled: true
http.cors.allow‐origin: /.*/
注意path.data和path.logs路径配置正确。
常用的配置项如下:
cluster.name:配置elasticsearch的集群名称,默认是elasticsearch。建议修改成一个有意义的名称。
node.name:节点名,通常一台物理服务器就是一个节点,es会默认随机指定一个名字,建议指定一个有意义的名称,方便管理一个或多个节点组成一个cluster集群,集群是一个逻辑的概念,节点是物理概念,后边章节会详细介绍。
path.conf: 设置配置文件的存储路径,tar或zip包安装默认在es根目录下的confifig文件夹,rpm安装默认在/etc/elasticsearch path.data: 设置索引数据的存储路径,默认是es根目录下的data文件夹,可以设置多个存储路径,用逗号隔开。 path.logs: 设置日志文件的存储路径,默认是es根目录下的logs文件夹 path.plugins: 设置插件的存放路径,默认是es根目录下的plugins文件夹
bootstrap.memory_lock: true 设置为true可以锁住ES使用的内存,避免内存与swap分区交换数据。
network.host: 设置绑定主机的ip地址,设置为0.0.0.0表示绑定任何ip,允许外网访问,生产环境建议设置为具体的ip。 http.port: 9200 设置对外服务的http端口,默认为9200。
transport.tcp.port: 9300 集群结点之间通信端口node.master: 指定该节点是否有资格被选举成为master结点,默认是true,如果原来的master宕机会重新选举新的master。 node.data: 指定该节点是否存储索引数据,默认为true。discovery.zen.ping.unicast.hosts: ["host1:port", "host2:port", "..."] 设置集群中master节点的初始列表。 discovery.zen.ping.timeout: 3s 设置ES自动发现节点连接超时的时间,默认为3秒,如果网络延迟高可设置大些。
discovery.zen.minimum_master_nodes:主结点数量的最少值 ,此值的公式为:(master_eligible_nodes / 2) + 1 ,比如:有3个符合要求的主结点,那么这里要设置为2。
node.max_local_storage_nodes:单机允许的最大存储结点数,通常单机启动一个结点建议设置为1,开发环境如果单机启动多个节点可设置大于1.
2.2.3 jvm.options
设置最小及最大的JVM堆内存大小:
在jvm.options中设置 -Xms和-Xmx:
1) 两个值设置为相等
2) 将 Xmx 设置为不超过物理内存的一半。
2.2.4 log4j2.properties
日志文件设置,ES使用log4j,注意日志级别的配置。
2.3 启动ES
进入bin目录,在cmd下运行:elasticsearch.bat
浏览器输入:http://localhost:9200
显示结果如下(配置不同内容则不同)说明ES启动成功:
3 ES快速入门
ES作为一个索引及搜索服务,对外提供丰富的REST接口,快速入门部分的实例使用head插件来测试,目的是对ES的使用方法及流程有个初步的认识。
3.1 创建索引库
ES的索引库是一个逻辑概念,它包括了分词列表及文档列表,同一个索引库中存储了相同类型的文档。它就相当于MySQL中的表,或相当于Mongodb中的集合。
关于索引这个语:
索引(名词):ES是基于Lucene构建的一个搜索服务,它要从索引库搜索符合条件索引数据。
索引(动词):索引库刚创建起来是空的,将数据添加到索引库的过程称为索引。
下边介绍两种创建索引库的方法,它们的工作原理是相同的,都是客户端向ES服务发送命令。
1)使用postman或curl这样的工具创建:
put http://localhost:9200/索引库名称
{
"settings": {
"index": {
"number_of_shards": 1,
"number_of_replicas": 0
}
}
}
number_of_shards:设置分片的数量,在集群中通常设置多个分片,表示一个索引库将拆分成多片分别存储不同 的结点,提高了ES的处理能力和高可用性,入门程序使用单机环境,这里设置为1。
number_of_replicas:设置副本的数量,设置副本是为了提高ES的高可靠性,单机环境设置为0.
如下是创建的例子,创建xc_course索引库,共1个分片,0个副本:
3.2 创建映射
3.2.1 概念说明
在索引中每个文档都包括了一个或多个field,创建映射就是向索引库中创建field的过程,下边是document和field与关系数据库的概念的类比:
文档(Document)----------------Row记录
字段(Field)-------------------Columns 列
注意:6.0之前的版本有type(类型)概念,type相当于关系数据库的表,ES官方将在ES9.0版本中彻底删除type。
上边讲的创建索引库相当于关系数据库中的数据库还是表?
1、如果相当于数据库就表示一个索引库可以创建很多不同类型的文档,这在ES中也是允许的。
2、如果相当于表就表示一个索引库只能存储相同类型的文档,ES官方建议 在一个索引库中只存储相同类型的文档。
3.2.2 创建映射
我们要把课程信息存储到ES中,这里我们创建课程信息的映射,先来一个简单的映射,如下:
发送:post http://localhost:9200/索引库名称/类型名称/_mapping
创建类型为xc_course的映射,共包括三个字段:name、description、studymondel
由于ES6.0版本还没有将type彻底删除,所以暂时把type起一个没有特殊意义的名字。
post 请求:http://localhost:9200/my_course/doc/_mapping
表示:在xc_course索引库下的doc类型下创建映射。doc是类型名,可以自定义,在ES6.0中要弱化类型的概念, 给它起一个没有具体业务意义的名称。
{
"properties": {
"name": {
"type": "text"
},
"description": {
"type": "text"
},
"studymodel": {
"type": "keyword"
}
}
}
映射创建成功在kibon中查询
3.3 创建文档
ES中的文档相当于MySQL数据库表中的记录。
发送:put 或Post http://localhost:9200/xc_course/doc/id值
(如果不指定id值ES会自动生成ID)
http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000
{
"name": "Bootstrap开发框架",
"description": "Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包 含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的 精美界面效果。",
"studymodel": "201001"
}
使用postman测试:
查询数据
3.4 搜索文档
1、根据课程id查询文档
发送:get http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000
使用postman测试:
2、查询所有记录
发送 get http://localhost:9200/xc_course/doc/_search
3、查询名称中包括spring 关键字的的记录
发送:get http://localhost:9200/xc_course/doc/_search?q=name:bootstrap
4、查询学习模式为201001的记录
发送 get http://localhost:9200/xc_course/doc/_search?q=studymodel:201001
3.4.1查询结果分析
分析上边查询结果:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0.2876821,
"hits": [{
"_index": "xc_course",
"_type": "doc",
"_id": "4028e58161bcf7f40161bcf8b77c0000",
"_score": 0.2876821,
"_source": {
"name": "Bootstrap开发框架",
"description": "Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较 为广泛。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现 一个不受浏览器限制的精美界面效果。",
"studymodel": "201001"
}
}]
}
}
took:本次操作花费的时间,单位为毫秒。
timed_out:请求是否超时
_shards:说明本次操作共搜索了哪些分片
hits:搜索命中的记录
hits.total : 符合条件的文档总数 hits.hits :匹配度较高的前N个文档
hits.max_score:文档匹配得分,这里为最高分
_score:每个文档都有一个匹配度得分,按照降序排列。
_source:显示了文档的原始内容。
4 IK分词器
4.1测试分词器
在添加文档时会进行分词,索引中存放的就是一个一个的词(term),当你去搜索时就是拿关键字去匹配词,最终找到词关联的文档。
测试当前索引库使用的分词器:
post 发送:localhost:9200/_analyze
{"text":"测试分词器,后边是测试内容:spring cloud实战"}
结果如下:
会发现分词的效果将 “测试” 这个词拆分成两个单字“测”和“试”,这是因为当前索引库使用的分词器对中文就是单字分词。
4.2 安装IK分词器
测试分词效果:
发送:post localhost:9200/_analyze
{"text":"测试分词器,后边是测试内容:spring cloud实战","analyzer":"ik_max_word" }
4.3 两种分词模式
ik分词器有两种分词模式:ik_max_word和ik_smart模式。
1、ik_max_word
会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、 华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。
2、ik_smart
会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。
测试两种分词模式:
发送:post localhost:9200/_analyze
{"text":"中华人民共和国人民大会堂","analyzer":"ik_smart" }
4.4 自定义词库
如果要让分词器支持一些专有词语,可以自定义词库。
5 映射
上边章节安装了ik分词器,如果在索引和搜索时去使用ik分词器呢?如何指定其它类型的fifield,比如日期类型、数值类型等。
本章节学习各种映射类型及映射维护方法。
5.1 映射维护方法
1、查询所有索引的映射:
GET: http://localhost:9200/_mapping
2、创建映射
post 请求:http://localhost:9200/xc_course/doc/_mapping
一个例子:
{
"properties": {
"name": {
"type": "text"
},
"description": {
"type": "text"
},
"studymodel": {
"type": "keyword"
}
}
}
3、更新映射
映射创建成功可以添加新字段,已有字段不允许更新。
4、删除映射
通过删除索引来删除映射。
5.2 常用映射类型
5.2.1 text文本字段
下图是ES6.2核心的字段类型如下:
字符串包括text和keyword两种类型:
1、text
1)analyzer
通过analyzer属性指定分词器。
下边指定name的字段类型为text,使用ik分词器的ik_max_word分词模式。
"name": {
"type": "text",
"analyzer": "ik_max_word"
}
上边指定了analyzer是指在索引和搜索都使用ik_max_word,如果单独想定义搜索时使用的分词器则可以通过 search_analyzer属性。
对于ik分词器建议是索引时使用ik_max_word将搜索内容进行细粒度分词,搜索时使用ik_smart提高搜索精确性。
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
2)index
通过index属性指定是否索引。
默认为index=true,即要进行索引,只有进行索引才可以从索引库搜索到。
但是也有一些内容不需要索引,比如:商品图片地址只被用来展示图片,不进行搜索图片,此时可以将index设置为false。
删除索引,重新创建映射,将pic的index设置为false,尝试根据pic去搜索,结果搜索不到数据
"pic": {
"type": "text",
"index":false
}
5.2.1.1 测试
删除xc_course/doc下的映射
创建新映射:Post http://localhost:9200/xc_course/doc/_mapping
{
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"description": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"pic": {
"type": "text",
"index": false
},
"studymodel": {
"type": "text"
}
}
}
插入文档:
http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000
{
"name": "Bootstrap开发框架",
"description": "Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包 含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的 精美界面效果。",
"pic": "group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
"studymodel": "201002"
}
查询测试:
Get http://localhost:9200/xc_course/_search?q=name:开发
Get http://localhost:9200/xc_course/_search?q=description:开发
Get http://localhost:9200/xc_course/_search?
q=pic:group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg
Get http://localhost:9200/xc_course/_search?q=studymodel:201002
通过测试发现:name和description都支持全文检索,pic不可作为查询条件。
5.2.2 keyword关键字字段
上边介绍的text文本字段在映射时要设置分词器,keyword字段为关键字字段,通常搜索keyword是按照整体搜索,所以创建keyword字段的索引时是不进行分词的,比如:邮政编码、手机号码、身份证等。keyword字段通常用于过虑、排序、聚合等。
5.2.2.1测试
更改映射:
{
"properties": {
"studymodel": {
"type": "keyword"
},
"name": {
"type": "keyword"
}
}
}
插入文档:
{
"name": "java编程基础",
"description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
"pic":"group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
"studymodel": "201001"
}
根据studymodel查询文档
搜索:http://localhost:9200/xc_course/_search?q=name:java
name是keyword类型,所以查询方式是精确查询。
5.2.3 date日期类型
日期类型不用设置分词器。
通常日期类型的字段用于排序。
1)format
通过format设置日期格式
例子:
下边的设置允许date字段存储年月日时分秒、年月日及毫秒三种格式。
{
"properties": {
"timestamp": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
}
}
}
插入文档:
Post :http://localhost:9200/xc_course/doc/3
{
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。",
"studymodel": "201001",
"pic":"group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
"timestamp":"2018‐07‐04 18:28:58"
}
6.2.4 数值类型
下边是ES支持的数值类型
1、尽量选择范围小的类型,提高搜索效率
2、对于浮点数尽量用比例因子,比如一个价格字段,单位为元,我们将比例因子设置为100这在ES中会按 分 存 储,映射如下:
"price": {
"type": "scaled_float",
"scaling_factor": 100
},
由于比例因子为100,如果我们输入的价格是23.45则ES中会将23.45乘以100存储在ES中。
如果输入的价格是23.456,ES会将23.456乘以100再取一个接近原始值的数,得出2346。
使用比例因子的好处是整型比浮点型更易压缩,节省磁盘空间。
如果比例因子不适合,则从下表选择范围小的去用:
更新已有映射,并插入文档:
http://localhost:9200/xc_course/doc/3
{
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。",
"studymodel": "201001",
"pic":"group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
"timestamp":"2018‐07‐04 18:28:58",
"price":38.6
}
6.2.5 综合例子
创建如下映射
post:http://localhost:9200/xc_course/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"
}
}
}
插入文档:
Post: http://localhost:9200/xc_course/doc/1
{
"name": "Bootstrap开发",
"description": "Bootstrap是由Twitter 推出的一个前台页面开发框架,是一个非常流行的开发框架,此框架集成了多种页面效果。此开发框架包含了大量 的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精 美界面效果。",
"studymodel": "201002",
"price": 38.6,
"timestamp": "2018-04-25 19:11:35",
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg"
}
6索引管理
6.1 搭建工程
6.1.1 ES客户端
ES提供多种不同的客户端:
1、TransportClient
ES提供的传统客户端,官方计划8.0版本删除此客户端。
2、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 High Level REST Client,如果它有不支持的功能,则使用Java Low Level REST Client。
添加依赖:
6.1.2 创建搜索工程
创建搜索工程(maven工程):xc-service-search,添加RestHighLevelClient依赖及junit依赖。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.zb</groupId>
<artifactId>esdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>esdemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</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-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.2.1</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.2.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、配置文件
server:
port: ${port:40100}
spring:
application:
name: esdemo
xc:
elasticsearch:
hostlist: 192.168.1.110:9200,192.168.1.110:9201
3、配置类
创建com.zb.config包
在其下创建配置类
package com.zb.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ElasticsearchConfig {
@Value("${xc.elasticsearch.hostlist}")
private String hostlist;
@Bean
public RestHighLevelClient restHighLevelClient() {
//解析hostlist配置信息
String[] split = hostlist.split(",");
//创建HttpHost数组,其中存放es主机和端口的配置信息
HttpHost[] httpHostArray = new HttpHost[split.length];
for (int i = 0; i < split.length; i++) {
String item = split[i];
httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http");
}
//创建RestHighLevelClient客户端
return new RestHighLevelClient(RestClient.builder(httpHostArray));
}
//项目主要使用RestHighLevelClient,对于低级的客户端暂时不用
@Bean
public RestClient restClient() {
//解析hostlist配置信息
String[] split = hostlist.split(",");
//创建HttpHost数组,其中存放es主机和端口的配置信息
HttpHost[] httpHostArray = new HttpHost[split.length];
for (int i = 0; i < split.length; i++) {
String item = split[i];
httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http");
}
return RestClient.builder(httpHostArray).build();
}
}
6.2创建索引库
6.2.1 API
创建索引:
put http://localhost:9200/索引名称
{
"settings": {
"index": {
"number_of_shards": 1,
"number_of_replicas": 0
}
}
}
创建映射:
发送:put http://localhost:9200/索引库名称/类型名称/_mapping
创建类型为xc_course的映射,共包括三个字段:name、description、studymodel
http://localhost:9200/xc_course/doc/_mapping
{
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"description": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"studymodel": {
"type": "keyword"
},
"price": {
"type": "float"
},
"timestamp": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
6.2.2 Java Client
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestIndex {
@Autowired
private RestHighLevelClient restHighLevelClient;
@Autowired
private RestClient restClient;
/*删除索引库*/
@Test
public void testDeleteIndex() {
try {
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("xc_course");
DeleteIndexResponse deleteIndexResponse = restHighLevelClient.indices().delete(deleteIndexRequest);
boolean acknowledged = deleteIndexResponse.isAcknowledged();
System.out.println(acknowledged);
} catch (IOException e) {
e.printStackTrace();
}
}
//添加文档
@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", 5.6f);
//索引请求对象
IndexRequest indexRequest = new IndexRequest("nj_course", "doc");
//指定索引文档内容
indexRequest.source(jsonMap);
//索引响应对象
IndexResponse indexResponse = restHighLevelClient.index(indexRequest);
//获取响应结果
DocWriteResponse.Result result = indexResponse.getResult();
System.out.println(result);
}
@Test
public void testAddIndex() throws IOException {
CreateIndexRequest createIndexRequest = new CreateIndexRequest("xc_course");
createIndexRequest.settings(Settings.builder().put("number_of_shards", 1).put("number_of_replicas", 0));
//设置映射
createIndexRequest.mapping("doc", " {\n" +
" \t\"properties\": {\n" +
" \"name\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\":\"ik_max_word\",\n" +
" \"search_analyzer\":\"ik_smart\"\n" +
" },\n" +
" \"description\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\":\"ik_max_word\",\n" +
" \"search_analyzer\":\"ik_smart\"\n" +
" },\n" +
" \"studymodel\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"price\": {\n" +
" \"type\": \"float\"\n" +
" }\n" +
" }\n" +
"}", XContentType.JSON);
//创建索引操作客户端
IndicesClient indices = restHighLevelClient.indices();
//创建响应对象
CreateIndexResponse createIndexResponse = indices.create(createIndexRequest);
//得到响应结果
boolean acknowledged = createIndexResponse.isAcknowledged();
System.out.println(acknowledged);
}
@Test
public void getDoc() throws IOException {
GetRequest getRequest = new GetRequest(
"nj_course",
"doc",
"rnynaWsBDjrihc96zXhk");
GetResponse getResponse = restHighLevelClient.get(getRequest);
boolean exists = getResponse.isExists();
Map<String, Object> sourceAsMap = getResponse.getSourceAsMap();
System.out.println(sourceAsMap);
}
//更新文档
@Test
public void updateDoc() throws IOException {
UpdateRequest updateRequest = new UpdateRequest("nj_course", "doc",
"rnynaWsBDjrihc96zXhk");
Map<String, String> map = new HashMap<>();
map.put("name", "jsp boot");
updateRequest.doc(map);
UpdateResponse update = restHighLevelClient.update(updateRequest);
RestStatus status = update.status();
System.out.println(status);
}
//根据id删除文档
@Test
public void testDelDoc() throws IOException {
//删除文档id
String id = "eqP_amQBKsGOdwJ4fHiC";
//删除索引请求对象
DeleteRequest deleteRequest = new DeleteRequest("xc_course","doc",id);
//响应对象
DeleteResponse deleteResponse = client.delete(deleteRequest);
//获取响应结果
DocWriteResponse.Result result = deleteResponse.getResult();
System.out.println(result);
}
}
7搜索管理
7.1 准备环境
7.1.1 创建映射
创建xc_course索引库。
创建如下映射
创建xc_course索引库。
创建如下映射
post:http://localhost:9200/xc_course/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"
}
}
}
7.1.2 插入原始数据
http://localhost:9200/xc_course/doc/1
{
"name": "Bootstrap开发",
"description": "Bootstrap是由Twitter推出的一个前台页面开发框架,是一个非常流行的开发框架,此框架集成了 多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松 的实现一个不受浏览器限制的精美界面效果。",
"studymodel": "201002",
"price": 68.6,
"timestamp": "2018-04-25 19:11:35",
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg"
}
http://localhost:9200/xc_course/doc/2
{
"name": "java编程基础",
"description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
"studymodel": "201002",
"price": 68.6,
"timestamp": "2018-04-25 19:11:35",
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg"
}
http://localhost:9200/xc_course/doc/3
{
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。",
"studymodel": "201001",
"price": 88.6,
"timestamp": "2018-04-25 19:11:35",
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg"
}
7.1.3 简单搜索
简单搜索就是通过url进行查询,以get方式请求ES。
格式:get ../_search?q=.....
q:搜索字符串。
例子:?q=name:spring 搜索name中包括spring的文档。
7.3.1 查询所有文档
查询所有索引库的文档。
发送:post http://localhost:9200/_search
查询指定索引库指定类型下的文档。(通过使用此方法)
发送:post http://localhost:9200/xc_course/doc/_search
{
"query": {
"match_all": {}
}
}
结果说明:
took:本次操作花费的时间,单位为毫秒。
timed_out:请求是否超时
_shards:说明本次操作共搜索了哪些分片
hits:搜索命中的记录
hits.total : 符合条件的文档总数 hits.hits :匹配度较高的前N个文档
hits.max_score:文档匹配得分,这里为最高分
_score:每个文档都有一个匹配度得分,按照降序排列。
_source:显示了文档的原始内容。
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestSearch {
@Autowired
RestHighLevelClient client;
@Autowired
RestClient restClient;
//搜索type下的全部记录
@Test
public void testSearchAll() throws IOException {
SearchRequest searchRequest = new SearchRequest("xc_course");
searchRequest.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
//source源字段过虑
searchSourceBuilder.fetchSource(new String[]{"name","studymodel"}, new String[]{});
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest);
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
String index = hit.getIndex();
String type = hit.getType();
String id = hit.getId();
float score = hit.getScore();
String sourceAsString = hit.getSourceAsString();
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
String name = (String) sourceAsMap.get("name");
String studymodel = (String) sourceAsMap.get("studymodel");
String description = (String) sourceAsMap.get("description");
System.out.println(name);
System.out.println(studymodel);
System.out.println(description);
}
}
}
7.3.2 分页查询
ES支持分页查询,传入两个参数:from和size。
form:表示起始文档的下标,从0开始。
size:查询的文档数量。
发送:post http://localhost:9200/xc_course/doc/_search
{
"from": 0,
"size": 1,
"query": {
"match_all": {}
},
"_source": ["name", "studymodel"]
}
JavaClient
SearchRequest searchRequest = new SearchRequest("xc_course");
searchRequest.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
//分页查询,设置起始下标,从0开始
searchSourceBuilder.from(0);
//每页显示个数
searchSourceBuilder.size(10);
//source源字段过虑
searchSourceBuilder.fetchSource(new String[]{"name","studymodel"}, new String[]{});
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest);
7.3.3 Term Query
Term Query为精确查询,在搜索时会整体匹配关键字,不再将关键字分词。
发送:post http://localhost:9200/xc_course/doc/_search
{
"query": {
"term": {
"name": "java"
}
},
"_source": ["name", "studymodel"]
}
上边的搜索会查询name包括“java”这个词的文档。
JavaClient:
SearchRequest searchRequest = new SearchRequest("xc_course");
searchRequest.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.termQuery("name","spring"));
//source源字段过虑
searchSourceBuilder.fetchSource(new String[]{"name","studymodel"}, new String[]{});
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest);
7.3.4 根据id精确匹配
ES提供根据多个id值匹配的方法:
测试:
post: http://127.0.0.1:9200/xc_course/doc/_search
{
"query": {
"ids": {
"type": "doc",
"values": ["3", "4", "100"]
}
}
}
JavaClient:
String[] split = new String[]{"1","2"};
List<String> idList = Arrays.asList(split);
searchSourceBuilder.query(QueryBuilders.termsQuery("_id", idList));
7.3.5 match Query
1、基本使用
match Query即全文检索,它的搜索方式是先将搜索字符串分词,再使用各各词条从索引中搜索。
match query与Term query区别是match query在搜索前先将搜索关键字分词,再拿各各词语去索引中搜索。
发送:post http://localhost:9200/xc_course/doc/_search
{
"query": {
"match": {
"description": {
"query": "spring框架",
"operator": "or"
}
}
}
}
query:搜索的关键字,对于英文关键字如果有多个单词则中间要用半角逗号分隔,而对于中文关键字中间可以用 逗号分隔也可以不用。
operator:or 表示 只要有一个词在文档中出现则就符合条件,and表示每个词都在文档中出现则才符合条件。
上边的搜索的执行过程是:
1、将“spring开发”分词,分为spring、开发两个词
2、再使用spring和开发两个词去匹配索引中搜索。
3、由于设置了operator为or,只要有一个词匹配成功则就返回该文档。
JavaClient:
SearchRequest searchRequest = new SearchRequest("xc_course");
searchRequest.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//匹配关键字
searchSourceBuilder.query(QueryBuilders.matchQuery("name","spring开发").operator(Operator.OR));
searchSourceBuilder.fetchSource(new String[]{"name","studymodel"}, new String[]{});
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest);
2、minimum_should_match
上边使用的operator = or表示只要有一个词匹配上就得分,如果实现三个词至少有两个词匹配如何实现?
使用minimum_should_match可以指定文档匹配词的占比:
比如搜索语句如下:
{
"query": {
"match": {
"description": {
"query": "spring开发框架",
"minimum_should_match": "80%"
}
}
}
}
“spring开发框架”会被分为三个词:spring、开发、框架
设置"minimum_should_match": "80%"表示,三个词在文档的匹配占比为80%,即3*0.8=2.4,向上取整得2,表示至少有两个词在文档中要匹配成功。
对应的RestClient如下:
//匹配关键字
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("description", "前台页面开发框架 架
构")
.minimumShouldMatch("80%");//设置匹配占比
searchSourceBuilder.query(matchQueryBuilder);
7.3.6 multi Query
上边学习的termQuery和matchQuery一次只能匹配一个Field,本节学习multiQuery,一次可以匹配多个字段。
1、基本使用
单项匹配是在一个field中去匹配,多项匹配是拿关键字去多个Field中匹配。
例子:
发送:post http://localhost:9200/xc_course/doc/_search
拿关键字 “spring css”去匹配name 和description字段。
{
"query": {
"multi_match": {
"query": "spring css",
"minimum_should_match": "50%",
"fields": ["name", "description"]
}
}
}
2、提升boost
匹配多个字段时可以提升字段的boost(权重)来提高得分
例子:
提升boost之前,执行下边的查询:
通过查询发现Bootstrap排在前边。
{
"query": {
"multi_match": {
"query": "spring css",
"minimum_should_match": "50%",
"fields": ["name", "description"]
}
}
}
提升boost,通常关键字匹配上name的权重要比匹配上description的权重高,这里可以对name的权重提升。
{
"query": {
"multi_match": {
"query": "spring框架",
"minimum_should_match": "50%",
"fields": ["name", "description^10"]
}
}
}
JavaClient:
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("spring框架",
"name", "description")
.minimumShouldMatch("50%");
multiMatchQueryBuilder.field("name",10);//提升boost
7.3.7 布尔查询
布尔查询对应于Lucene的BooleanQuery查询,实现将多个查询组合起来。
三个参数:
must:文档必须匹配must所包括的查询条件,相当于 “AND” should:文档应该匹配should所包括的查询条件其中的一个或多个,相当于 "OR" must_not:文档不能匹配must_not所包括的该查询条件,相当于“NOT”
分别使用must、should、must_not测试下边的查询:
发送:POST http://localhost:9200/xc_course/doc/_search
{
"_source": ["name", "studymodel", "description"],
"from": 0,
"size": 1,
"query": {
"bool": {
"must": [{
"multi_match": {
"query": "spring框架",
"minimum_should_match": "50%",
"fields": ["name^10", "description"]
}
}, {
"term": {
"studymodel": "201002"
}
}]
}
}
}
must:表示必须,多个查询条件必须都满足。(通常使用must)
should:表示或者,多个查询条件只要有一个满足即可。
must_not:表示非。
JavaClient:
//BoolQuery,将搜索关键字分词,拿分词去索引库搜索
//创建搜索请求对象
SearchRequest searchRequest= new SearchRequest("xc_course");
searchRequest.types("doc");
//创建搜索源配置对象
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.fetchSource(new String[]{"name","pic","studymodel"},new String[]{});
//multiQuery
String keyword = "spring开发框架";
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("spring框架", "name", "description") .minimumShouldMatch("50%");
multiMatchQueryBuilder.field("name",10);
//TermQuery
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("studymodel", "201001");
//布尔查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.must(multiMatchQueryBuilder);
boolQueryBuilder.must(termQueryBuilder);
//设置布尔查询对象
searchSourceBuilder.query(boolQueryBuilder);
searchRequest.source(searchSourceBuilder);//设置搜索源配置
SearchResponse searchResponse = client.search(searchRequest);
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
for(SearchHit hit:searchHits){
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
System.out.println(sourceAsMap);
}
7.3.8 过虑器
过虑是针对搜索的结果进行过虑,过虑器主要判断的是文档是否匹配,不去计算和判断文档的匹配度得分,所以过虑器性能比查询要高,且方便缓存,推荐尽量使用过虑器去实现查询或者过虑器和查询共同使用。
过虑器在布尔查询中使用,下边是在搜索结果的基础上进行过虑:
{
"_source": ["name", "studymodel", "description", "price"],
"query": {
"bool": {
"must": [{
"multi_match": {
"query": "spring框架",
"minimum_should_match": "50%",
"fields": ["name^10", "description"]
}
}],
"filter": [{
"term": {
"studymodel": "201001"
}
}, {
"range": {
"price": {
"gte": 60,
"lte": 100
}
}
}]
}
}
}
range:范围过虑,保留大于等于60 并且小于等于100的记录。
term:项匹配过虑,保留studymodel等于"201001"的记录。
注意:range和term一次只能对一个Field设置范围过虑。
client:
SearchRequest searchRequest = new SearchRequest("xc_course");
searchRequest.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","description"}, new String[]{});
searchRequest.source(searchSourceBuilder);
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders
.multiMatchQuery("spring框架", "name", "description");
multiMatchQueryBuilder.minimumShouldMatch("50%");
multiMatchQueryBuilder.field("name",10);
BoolQueryBuilder boolQueryBuilder=QueryBuilders.boolQuery();
boolQueryBuilder.must(multiMatchQueryBuilder);
boolQueryBuilder.filter(QueryBuilders.termQuery("studymodel","201001"));
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(60).lte(100));
searchSourceBuilder.query(boolQueryBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest);
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
7.3.9 排序
可以在字段上添加一个或多个排序,支持在keyword、date、flfloat等类型上添加,text类型的字段上不允许添加排序。
发送 POST http://localhost:9200/xc_course/doc/_search
过虑0--10元价格范围的文档,并且对结果进行排序,先按studymodel降序,再按价格升序
{
"_source": ["name", "studymodel", "description", "price"],
"query": {
"bool": {
"filter": [{
"range": {
"price": {
"gte": 0,
"lte": 100
}
}
}]
}
},
"sort": [{
"studymodel": "desc"
}, {
"price": "asc"
}]
}
client:
SearchRequest searchRequest = new SearchRequest("xc_course");
searchRequest.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//source源字段过虑
searchSourceBuilder.fetchSource(new String[]{"name", "studymodel"}, new String[]{});
//boolquery
//定义一个booleanquery
BoolQueryBuilder boolQueryBuilder= QueryBuilders.boolQuery();
//过滤器
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(0).lte(100));
//创建排序
searchSourceBuilder.sort(new FieldSortBuilder("studymodel").order(SortOrder.DESC));
searchSourceBuilder.sort(new FieldSortBuilder("price").order(SortOrder.ASC));
searchSourceBuilder.query(boolQueryBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest);
//搜索结果
SearchHits hits = searchResponse.getHits();
//匹配到的数据
SearchHit[] searchHits = hits.getHits();
7.3.10 高亮显示
高亮显示可以将搜索结果一个或多个字突出显示,以便向用户展示匹配关键字的位置。
在搜索语句中添加highlight即可实现,如下:
Post: http://127.0.0.1:9200/xc_course/doc/_search
{
"_source": ["name", "studymodel", "description", "price"],
"query": {
"bool": {
"must": [{
"multi_match": {
"query": "开发框架",
"minimum_should_match": "50%",
"fields": ["name^10", "description"],
"type": "best_fields"
}
}],
"filter": [{
"range": {
"price": {
"gte": 0,
"lte": 100
}
}
}]
}
},
"sort": [{
"price": "asc"
}],
"highlight": {
"pre_tags": ["<span style=’color:red;’>"],
"post_tags": ["</span>"],
"fields": {
"name": {},
"description": {}
}
}
}
client代码如下:
public void testHighlight() throws IOException {
SearchRequest searchRequest = new SearchRequest("xc_course");
searchRequest.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","description"}, new String[]{});
searchRequest.source(searchSourceBuilder);
MultiMatchQueryBuilder multiMatchQueryBuilder=QueryBuilders.multiMatchQuery("开发框架","name","description");
multiMatchQueryBuilder.field("name",10);
BoolQueryBuilder boolQueryBuilder =QueryBuilders.boolQuery();
boolQueryBuilder.must(multiMatchQueryBuilder);
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(0).lte(100));
searchSourceBuilder.query(boolQueryBuilder);
//设置高亮
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);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest);
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
String sourceAsString = hit.getSourceAsString();
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
String name = (String) sourceAsMap.get("name");
Double price = (Double) sourceAsMap.get("price");
//取出name高亮字段
Map<String, HighlightField> highlightFieldMap = hit.getHighlightFields();
if(highlightBuilder!=null){
HighlightField nameField= highlightFieldMap.get("name");
if(nameField!=null){
Text[] nameTxt = nameField.getFragments();
StringBuffer nameStr= new StringBuffer();
for (Text text:nameTxt) {
nameStr.append(text);
}
name=nameStr.toString();
}
}
System.out.println(name);
System.out.println(price);
}
}
分组查询
public void myGroup() throws Exception {
SearchRequest searchRequest = new SearchRequest("xc_course");
searchRequest.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
TermsAggregationBuilder termsBuilder = AggregationBuilders.terms("studymodelgroup").field("studymodel");
searchSourceBuilder.aggregation(termsBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//得到这个分组的数据集合
Terms terms = response.getAggregations().get("studymodelgroup");
for(int i=0;i<terms.getBuckets().size();i++){
//statistics
String id =terms.getBuckets().get(i).getKey().toString();//id
Long sum =terms.getBuckets().get(i).getDocCount();//数量
System.out.println("=="+terms.getBuckets().get(i).getDocCount()+"------"+terms.getBuckets().get(i).getKey());
}
}
在es中,只有keyword类型的字符串可以分组聚合,读取数据库会将字符串读取为text类型,属于动态映射,想要分组查询,利用静态映射自定义字符串类型即可
商品搜索
创建映射
{
"properties": {
"sn": {
"type": "text"
},
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"price": {
"type": "float"
},
"num": {
"type": "integer"
},
"alert_num": {
"type": "integer"
},
"image": {
"type": "text"
},
"images": {
"type": "text"
},
"weight": {
"type": "integer"
},
"create_time": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
},
"update_time": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
},
"spu_id": {
"type": "text"
},
"category_id": {
"type": "integer"
},
"category_name": {
"type": "keyword"
},
"brand_name": {
"type": "keyword"
},
"spec": {
"type": "keyword"
},
"sale_num": {
"type": "integer"
},
"comment_num": {
"type": "integer"
},
"status": {
"type": "keyword"
},
"version": {
"type": "integer"
},
"specMap":{
"properties":{
"test":{
"type": "keyword"
}
}
}
}
}
创建索引
public void createIndex() throws Exception {
CreateIndexRequest request = new CreateIndexRequest("tb_sku", Settings.builder().put("number_of_shards", "1").put("number_of_replicas", "0").build());
request.mapping("doc", "{\n" +
"\t\"properties\": {\n" +
"\t\t\"sn\": {\n" +
"\t\t\t\"type\": \"text\"\n" +
"\t\t},\n" +
"\t\t\"name\": {\n" +
"\t\t\t\"type\": \"text\",\n" +
"\t\t\t\"analyzer\": \"ik_max_word\",\n" +
"\t\t\t\"search_analyzer\": \"ik_smart\"\n" +
"\t\t},\n" +
"\t\t\"price\": {\n" +
"\t\t\t\"type\": \"float\"\n" +
"\t\t},\n" +
"\t\t\"num\": {\n" +
"\t\t\t\"type\": \"integer\"\n" +
"\t\t},\n" +
"\t\t\"alert_num\": {\n" +
"\t\t\t\"type\": \"integer\"\n" +
"\t\t},\n" +
"\t\t\"image\": {\n" +
"\t\t\t\"type\": \"text\"\n" +
"\t\t},\n" +
"\t\t\"images\": {\n" +
"\t\t\t\"type\": \"text\"\n" +
"\t\t},\n" +
"\t\t\"weight\": {\n" +
"\t\t\t\"type\": \"integer\"\n" +
"\t\t},\n" +
"\t\t\"create_time\": {\n" +
"\t\t\t\"type\": \"date\",\n" +
"\t\t\t\"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd\"\n" +
"\t\t},\n" +
"\t\t\"update_time\": {\n" +
"\t\t\t\"type\": \"date\",\n" +
"\t\t\t\"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd\"\n" +
"\t\t},\n" +
"\t\t\"spu_id\": {\n" +
"\t\t\t\"type\": \"text\"\n" +
"\t\t},\n" +
"\t\t\"category_id\": {\n" +
"\t\t\t\"type\": \"integer\"\n" +
"\t\t},\n" +
"\t\t\"category_name\": {\n" +
"\t\t\t\"type\": \"keyword\"\n" +
"\t\t},\n" +
"\t\t\"brand_name\": {\n" +
"\t\t\t\"type\": \"keyword\"\n" +
"\t\t},\n" +
"\t\t\"spec\": {\n" +
"\t\t\t\"type\": \"keyword\"\n" +
"\t\t},\n" +
"\t\t\"sale_num\": {\n" +
"\t\t\t\"type\": \"integer\"\n" +
"\t\t},\n" +
"\t\t\"comment_num\": {\n" +
"\t\t\t\"type\": \"integer\"\n" +
"\t\t},\n" +
"\t\t\"status\": {\n" +
"\t\t\t\"type\": \"keyword\"\n" +
"\t\t},\n" +
"\t\t\"version\": {\n" +
"\t\t\t\"type\": \"integer\"\n" +
"\t\t},\n" +
"\t\t\"specMap\":{\n" +
"\t\t\t\"properties\":{\n" +
"\t\t\t\t\"颜色\":{\n" +
"\t\t\t\t\t\"type\": \"keyword\"\n" +
"\t\t\t\t},\n" +
"\t\t\t\t\"版本\":{\n" +
"\t\t\t\t\t\"type\": \"keyword\"\n" +
"\t\t\t\t},\n" +
"\t\t\t\t\"尺码\":{\n" +
"\t\t\t\t\t\"type\": \"keyword\"\n" +
"\t\t\t\t},\n" +
"\t\t\t\t\"内存\":{\n" +
"\t\t\t\t\t\"type\": \"keyword\"\n" +
"\t\t\t\t}\n" +
"\t\t\t}\n" +
"\t\t}\n" +
"\t}\n" +
"}", XContentType.JSON);
CreateIndexResponse response = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());
}
导入数据
public void addDoc() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/shop_goods", "root", "ok");
String sql = "select * from tb_sku";
PreparedStatement pstm = connection.prepareStatement(sql);
ResultSet rs = pstm.executeQuery();
while (rs.next()) {
Map<String, Object> jsonMap = new HashMap<>();
jsonMap.put("sn", rs.getString("sn"));
jsonMap.put("name", rs.getString("name"));
jsonMap.put("price", rs.getFloat("price"));
jsonMap.put("num", rs.getInt("num"));
jsonMap.put("alert_num", rs.getInt("alert_num"));
jsonMap.put("image", rs.getString("image"));
jsonMap.put("images", rs.getString("images"));
jsonMap.put("weight", rs.getInt("weight"));
jsonMap.put("spu_id", rs.getString("spu_id"));
jsonMap.put("category_id", rs.getInt("category_id"));
jsonMap.put("category_name", rs.getString("category_name"));
jsonMap.put("brand_name", rs.getString("brand_name"));
jsonMap.put("create_time", rs.getDate("create_time").toString());
jsonMap.put("update_time", rs.getDate("update_time").toString());
jsonMap.put("spec", rs.getString("spec"));
jsonMap.put("sale_num", rs.getInt("sale_num"));
jsonMap.put("comment_num", rs.getInt("comment_num"));
jsonMap.put("status", rs.getString("status"));
jsonMap.put("version", rs.getInt("version"));
//获取spec-->Map(String)--Map类型
Map<String, Object> specMap= JSON.parseObject(jsonMap.get("spec").toString()) ;
//如果需要生成动态的域,只需要将该域存入到一个Map<String,Object>对象中即可,该Map<String,Object>的key会生成一个域,域的名字为该Map的key
//当前Map<String,Object>后面Object的值会做为当前域(key)对应的值
jsonMap.put("specMap",specMap);
IndexRequest request = new IndexRequest("tb_sku", "doc", rs.getString("id"));
request.source(jsonMap);
IndexResponse indexResponse = restHighLevelClient.index(request, RequestOptions.DEFAULT);
DocWriteResponse.Result result = indexResponse.getResult();
System.out.println(result);
}
}
品牌统计
用户搜索的时候,除了使用分类搜索外,还有可能使用品牌搜索,所以我们还需要显示品牌数据和规格数据,品牌数据和规格数据的显示比较容易,都可以考虑使用分类统计的方式进行分组实现。
品牌统计分析
看下面的SQL语句,我们在执行搜索的时候,第1条SQL语句是执行搜,第2条语句是根据品牌名字分组查看有多少品牌,大概执行了2个步骤就可以获取数据结果以及品牌统计,我们可以发现他们的搜索条件完全一样。
-- 查询所有
SELECT * FROM tb_sku WHERE name LIKE '%手机%';
-- 根据品牌名字分组查询
SELECT brand_name FROM tb_sku WHERE name LIKE '%手机%' GROUP BY brand_name;
我们每次执行搜索的时候,需要显示商品品牌名称,这里要显示的品牌名称其实就是符合搜素条件的所有商品的品牌集合,我们可以按照上面的实现思路,使用ES根据分组名称做一次分组查询即可实现。
品牌分组统计实现
整体代码如下:
public Map search(Map<String, String> searchMap) throws Exception {
//1.获取关键字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "华为";//赋值给一个默认的值
}
SearchRequest searchRequest = new SearchRequest("tb_sku");
searchRequest.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(AggregationBuilders.terms("skuCategorygroup").field("category_name").size(50));
searchSourceBuilder.aggregation(AggregationBuilders.terms("skuBrandgroup").field("brand_name").size(50));
searchSourceBuilder.query(QueryBuilders.matchQuery("name", keywords));
searchRequest.source(searchSourceBuilder);
SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//得到这个分组的数据集合
Terms termsCategory = response.getAggregations().get("skuCategorygroup");
Terms termsBrand = response.getAggregations().get("skuBrandgroup");
List<String> categoryList = getStringsCategoryList(termsCategory);
List<String> brandList = getStringsBrandList(termsBrand);
//6.返回结果
Map<String ,Object> myresultMap = new HashMap<String ,Object>();
myresultMap.put("categoryList", categoryList);
myresultMap.put("brandList", brandList);
return myresultMap;
}
/**
* 获取品牌列表
*
* @return
*/
private List<String> getStringsBrandList(Terms terms) {
List<String> brandList = new ArrayList<>();
if (terms != null) {
for(int i=0;i<terms.getBuckets().size();i++){
//statistics
String brand =terms.getBuckets().get(i).getKey().toString();//id
brandList.add(brand);
}
}
return brandList;
}
/**
* 获取分类列表数据
*
* @return
*/
private List<String> getStringsCategoryList(Terms terms) {
List<String> categoryList = new ArrayList<>();
if (terms != null) {
for(int i=0;i<terms.getBuckets().size();i++){
//statistics
String category =terms.getBuckets().get(i).getKey().toString();//id
categoryList.add(category);
}
}
return categoryList;
}
规格统计
用户搜索的时候,除了使用分类、品牌搜索外,还有可能使用规格搜索,所以我们还需要显示规格数据,规格数据的显示相比上面2种实现略微较难一些,需要对数据进行处理,我们也可以考虑使用分类统计和品牌统计的方式进行分组实现。
规格统计分析
看下面的SQL语句,我们在执行搜索的时候,第1条SQL语句是执行搜,第2条语句是根据规格分组查看有多少规格,大概执行了2个步骤就可以获取数据结果以及规格统计,我们可以发现他们的搜索条件完全一样。
-- 查询所有
SELECT * FROM tb_sku WHERE name LIKE '%手机%';
-- 根据规格名字分组查询
SELECT spec FROM tb_sku WHERE name LIKE '%手机%' GROUP BY spec;
上述SQL语句执行后的结果如下图:
1.获取所有规格数据
2.将所有规格数据转换成Map
3.定义一个Map<String,Set>,key是规格名字,防止重复所以用Map,valu是规格值,规格值有多个,所以用集合,为了防止规格重复,用Set去除重复
4.循环规格的Map,将数据填充到定义的Map<String,Set>中
我们每次执行搜索的时候,需要显示商品规格数据,这里要显示的规格数据其实就是符合搜素条件的所有商品的规格集合,我们可以按照上面的实现思路,使用ES根据分组名称做一次分组查询,并去除重复数据即可实现。
规格统计分组实现
如图:获取规格分组结果:
封装调用分组结果的方法:
/**
* 获取规格列表数据
*
* @return
*/
private Map<String, Set<String>> getStringsSpecList(Terms terms) {
Map<String, Set<String>> specMap = new HashMap<String, Set<String>>();
Set<String> specList = new HashSet<>();
List<String> categoryList = new ArrayList<>();
if (terms != null) {
for(int i=0;i<terms.getBuckets().size();i++){
//statistics
String category =terms.getBuckets().get(i).getKey().toString();//id
specList.add(category);
}
}
for (String specjson : specList) {
Map<String, String> map = JSON.parseObject(specjson, Map.class);
for (Map.Entry<String, String> entry : map.entrySet()) {//
String key = entry.getKey(); //规格名字
String value = entry.getValue(); //规格选项值
//获取当前规格名字对应的规格数据
Set<String> specValues = specMap.get(key);
if (specValues == null) {
specValues = new HashSet<String>();
}
//将当前规格加入到集合中
specValues.add(value);
//将数据存入到specMap中
specMap.put(key, specValues);
}
}
return specMap;
}
条件筛选
用户有可能会根据分类搜索、品牌搜索,还有可能根据规格搜索,以及价格搜索和排序操作。根据分类和品牌搜索的时候,可以直接根据指定域搜索,而规格搜索的域数据是不确定的,价格是一个区间搜索,所以我们可以分为三段时间,先实现分类、品牌搜素,再实现规格搜索,然后实现价格区间搜索。
分类、品牌筛选
需求分析
页面每次向后台传入对应的分类和品牌,后台据分类和品牌进行条件过滤即可。
上图整体代码如下:
public Map search(Map<String, String> searchMap) throws Exception {
//1.获取关键字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "华为";//赋值给一个默认的值
}
SearchRequest searchRequest = new SearchRequest("tb_sku");
searchRequest.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(AggregationBuilders.terms("skuCategorygroup").field("category_name").size(50));
searchSourceBuilder.aggregation(AggregationBuilders.terms("skuBrandgroup").field("brand_name").size(50));
searchSourceBuilder.aggregation(AggregationBuilders.terms("skuSpecgroup").field("spec").size(50));
//创建bool组合查询对象
BoolQueryBuilder boolQueryBuilder=new BoolQueryBuilder();
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keywords, "name");
boolQueryBuilder.must(multiMatchQueryBuilder);
if (!StringUtils.isEmpty(searchMap.get("brand"))) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brand_name", searchMap.get("brand")));
}
if (!StringUtils.isEmpty(searchMap.get("category"))) {
boolQueryBuilder.filter(QueryBuilders.termQuery("category_name", searchMap.get("category")));
}
//资源与查询绑定
searchSourceBuilder.query(boolQueryBuilder);
//资源与查询绑定
searchSourceBuilder.query(boolQueryBuilder);
//将绑定的信息存储到请求中
searchRequest.source(searchSourceBuilder);
SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//得到这个分组的数据集合
Terms termsCategory = response.getAggregations().get("skuCategorygroup");
Terms termsBrand = response.getAggregations().get("skuBrandgroup");
Terms specBrand = response.getAggregations().get("skuSpecgroup");
List<String> categoryList = getStringsCategoryList(termsCategory);
List<String> brandList = getStringsBrandList(termsBrand);
Map<String, Set<String>> secpMap = getStringsSpecList(specBrand);
//6.返回结果
Map<String ,Object> myresultMap = new HashMap<String ,Object>();
myresultMap.put("categoryList", categoryList);
myresultMap.put("brandList", brandList);
myresultMap.put("secpMap", secpMap);
SearchHits hits = response.getHits();
SearchHit[] searchHits= hits.getHits();
for (SearchHit hit : searchHits) {
String id = hit.getId();
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
String name = sourceAsMap.get("name").toString();
String spec = sourceAsMap.get("spec").toString();
System.out.println(searchMap+"\t"+name+"\t"+spec);
}
return myresultMap;
}
测试
测试效果如下:
规格过滤
需求分析
规格这一块,需要向后台发送规格名字以及规格值,我们可以按照一定要求来发送数据,例如规格名字以特殊前缀提交到后台:spec_网络制式:电信4G、spec_显示屏尺寸:4.0-4.9英寸
后台接到数据后,可以根据前缀spec_来区分是否是规格,如果以spec_xxx开始的数据则为规格数据,需要根据指定规格找信息。
上图是规格的索引存储格式,真实数据在spechMap.规格名字中,所以找数据也是按照如下格式去找:
spechMap.规格名字
public Map search(Map<String, String> searchMap) throws Exception {
//1.获取关键字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "华为";//赋值给一个默认的值
}
SearchRequest searchRequest = new SearchRequest("tb_sku");
searchRequest.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(AggregationBuilders.terms("skuCategorygroup").field("category_name").size(50));
searchSourceBuilder.aggregation(AggregationBuilders.terms("skuBrandgroup").field("brand_name").size(50));
searchSourceBuilder.aggregation(AggregationBuilders.terms("skuSpecgroup").field("spec").size(50));
//创建bool组合查询对象
BoolQueryBuilder boolQueryBuilder=new BoolQueryBuilder();
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keywords, "name");
boolQueryBuilder.must(multiMatchQueryBuilder);
if (!StringUtils.isEmpty(searchMap.get("brand"))) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brand_name", searchMap.get("brand")));
}
if (!StringUtils.isEmpty(searchMap.get("category"))) {
boolQueryBuilder.filter(QueryBuilders.termQuery("category_name", searchMap.get("category")));
}
if (searchMap != null) {
for (String key : searchMap.keySet()) {
if (key.startsWith("spec_")) {
boolQueryBuilder.filter(QueryBuilders.termQuery("specMap."+ key.substring(5) +”.keyword”, searchMap.get(key)));
}
}
}
//资源与查询绑定
searchSourceBuilder.query(boolQueryBuilder);
//资源与查询绑定
searchSourceBuilder.query(boolQueryBuilder);
//将绑定的信息存储到请求中
searchRequest.source(searchSourceBuilder);
SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//得到这个分组的数据集合
Terms termsCategory = response.getAggregations().get("skuCategorygroup");
Terms termsBrand = response.getAggregations().get("skuBrandgroup");
Terms specBrand = response.getAggregations().get("skuSpecgroup");
List<String> categoryList = getStringsCategoryList(termsCategory);
List<String> brandList = getStringsBrandList(termsBrand);
Map<String, Set<String>> secpMap = getStringsSpecList(specBrand);
//6.返回结果
Map<String ,Object> myresultMap = new HashMap<String ,Object>();
myresultMap.put("categoryList", categoryList);
myresultMap.put("brandList", brandList);
myresultMap.put("secpMap", secpMap);
SearchHits hits = response.getHits();
SearchHit[] searchHits= hits.getHits();
for (SearchHit hit : searchHits) {
String id = hit.getId();
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
String name = sourceAsMap.get("name").toString();
String spec = sourceAsMap.get("spec").toString();
String specMap = sourceAsMap.get("specMap").toString();
System.out.println(name+"\t"+spec+"\t"+specMap);
}
return myresultMap;
}
测试
Map<String,String> result = new HashMap<>();
result.put("brand","华为");
result.put("category","手机");
result.put("keywords","华为");
result.put("spec_颜色","白色");
result.put("spec_颜色","蓝色");
result.put("spec_版本","8GB+256GB");
SkuService skuService = context.getBean(SkuService.class);
Map map = skuService.search(result);
价格区间查询
价格区间查询,每次需要将价格传入到后台,前端传入后台的价格大概是price=0-500或者price=500-1000依次类推,最后一个是price=3000,后台可以根据-分割,如果分割得到的结果最多有2个,第1个表示x<price,第2个表示price<=y。
代码如下:
String price = searchMap.get("price");
if (!StringUtils.isEmpty(price)) {
String[] split = price.split("-");
if (!split[1].equalsIgnoreCase("*")) {
boolQueryBuilder.filter(
QueryBuilders.rangeQuery("price")
.from(split[0], true).to(split[1], true));
} else {
boolQueryBuilder.filter(
QueryBuilders.rangeQuery("price").gte(split[0]));
}
}
测试
Map<String,String> result = new HashMap<>();
result.put("brand","华为");
result.put("category","手机");
result.put("keywords","华为");
result.put("spec_颜色","白色");
result.put("spec_颜色","蓝色");
result.put("spec_版本","8GB+256GB");
result.put("spec_版本","8GB+256GB");
result.put("price","10000-35000");
SkuService skuService = context.getBean(SkuService.class);
Map map = skuService.search(result);
搜索分页
分页分析
页面需要实现分页搜索,所以我们后台每次查询的时候,需要实现分页。用户页面每次会传入当前页和每页查询多少条数据,当然如果不传入每页显示多少条数据,默认查询30条即可。
分页实现
Integer index =Integer.parseInt(searchMap.get("index")) ;
Integer size =Integer.parseInt(searchMap.get("size")) ;
searchSourceBuilder.from((index-1)*size);
searchSourceBuilder.size(size);
测试
Map<String,String> result = new HashMap<>();
result.put("brand","华为");
result.put("category","手机");
result.put("keywords","华为");
result.put("spec_颜色","白色");
result.put("spec_颜色","蓝色");
result.put("index","1");
result.put("size","5");
SkuService skuService = context.getBean(SkuService.class);
Map map = skuService.search(result);
搜索排序
排序分析
排序这里总共有根据价格排序、根据评价排序、根据新品排序、根据销量排序,排序要想实现非常简单,只需要告知排序的域以及排序方式即可实现。
价格排序:只需要根据价格高低排序即可,降序价格高->低,升序价格低->高
评价排序:评价分为好评、中评、差评,可以在数据库中设计3个列,用来记录好评、中评、差评的量,每次排序的时候,好评的比例来排序,当然还要有条数限制,评价条数需要超过N条。
新品排序:直接根据商品的发布时间或者更新时间排序。
销量排序:销量排序除了销售数量外,还应该要有时间段限制。
排序实现
这里我们不单独针对某个功能实现排序,我们只需要在后台接收2个参数,分别是排序域名字和排序方式,代码如下:
前端页面传递要排序的字段(field)和要排序的类型(ASC,DESC),后台接收.
//构建排序查询
String sortRule = searchMap.get("sortRule");
String sortField = searchMap.get("sortField");
if (!StringUtils.isEmpty(sortRule) && !StringUtils.isEmpty(sortField)) {
searchSourceBuilder.sort(new FieldSortBuilder(sortField).order(sortRule.equals("DESC") ? SortOrder.DESC : SortOrder.ASC));
}
测试
Map<String,String> result = new HashMap<>();
result.put("brand","华为");
result.put("category","手机");
result.put("keywords","华为");
result.put("spec_颜色","白色");
result.put("spec_颜色","蓝色");
result.put("index","1");
result.put("size","5");
result.put("sortField","price");
result.put("sortRule","ASC");
SkuService skuService = context.getBean(SkuService.class);
Map map = skuService.search(result);
高亮显示
高亮显示是指根据商品关键字搜索商品的时候,显示的页面对关键字给定了特殊样式,让它显示更加突出,如上图商品搜索中,关键字编程了红色,其实就是给定了红色样式。
代码如下
//资源与查询绑定
searchSourceBuilder.query(boolQueryBuilder);
//设置高亮对象
HighlightBuilder highlightBuilder=new HighlightBuilder();
highlightBuilder.preTags("<div style='color:red;'>");
highlightBuilder.postTags("</div>");
highlightBuilder.fields().add(new HighlightBuilder.Field("name"));
searchSourceBuilder.highlighter(highlightBuilder);
//将绑定的信息存储到请求中
searchRequest.source(searchSourceBuilder)
........
//获取高亮的结果数据
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if(highlightBuilder!=null){
HighlightField nameField= highlightFields.get("name");
if(nameField!=null){
Text[] nameTxt = nameField.getFragments();
StringBuffer nameStr= new StringBuffer();
for (Text text:nameTxt) {
nameStr.append(text);
}
name=nameStr.toString();
}
}
完整代码功能
public Map search(Map<String, String> searchMap) throws Exception {
//1.获取关键字的值
String keywords = searchMap.get("keywords");
if (StringUtils.isEmpty(keywords)) {
keywords = "华为";//赋值给一个默认的值
}
SearchRequest searchRequest = new SearchRequest("tb_sku");
searchRequest.types("doc");
//分页查询
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
Integer index =Integer.parseInt(searchMap.get("index")) ;
Integer size =Integer.parseInt(searchMap.get("size")) ;
searchSourceBuilder.from((index-1)*size);
searchSourceBuilder.size(size);
//分组查询
searchSourceBuilder.aggregation(AggregationBuilders.terms("skuCategorygroup").field("category_name").size(50));
searchSourceBuilder.aggregation(AggregationBuilders.terms("skuBrandgroup").field("brand_name").size(50));
searchSourceBuilder.aggregation(AggregationBuilders.terms("skuSpecgroup").field("spec").size(50));
//创建bool组合查询对象
BoolQueryBuilder boolQueryBuilder=new BoolQueryBuilder();
//关键字查询
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keywords, "name");
boolQueryBuilder.must(multiMatchQueryBuilder);
//条件查询
if (!StringUtils.isEmpty(searchMap.get("brand"))) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brand_name", searchMap.get("brand")));
}
if (!StringUtils.isEmpty(searchMap.get("category"))) {
boolQueryBuilder.filter(QueryBuilders.termQuery("category_name", searchMap.get("category")));
}
if (searchMap != null) {
for (String key : searchMap.keySet()) {
if (key.startsWith("spec_")) {
boolQueryBuilder.filter(QueryBuilders.termQuery("specMap."+ key.substring(5) , searchMap.get(key)));
}
}
}
//价格过滤查询
String price = searchMap.get("price");
if (!StringUtils.isEmpty(price)) {
String[] split = price.split("-");
if (!split[1].equalsIgnoreCase("*")) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").from(split[0], true).to(split[1], true));
} else {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(split[0]));
}
}
//构建排序查询
String sortRule = searchMap.get("sortRule");
String sortField = searchMap.get("sortField");
if (!StringUtils.isEmpty(sortRule) && !StringUtils.isEmpty(sortField)) {
searchSourceBuilder.sort(new FieldSortBuilder(sortField).order(sortRule.equals("DESC") ? SortOrder.DESC : SortOrder.ASC));
}
//资源与查询绑定
searchSourceBuilder.query(boolQueryBuilder);
//资源与查询绑定
searchSourceBuilder.query(boolQueryBuilder);
//设置高亮对象
HighlightBuilder highlightBuilder=new HighlightBuilder();
highlightBuilder.preTags("<div style='color:red;'>");
highlightBuilder.postTags("</div>");
highlightBuilder.fields().add(new HighlightBuilder.Field("name"));
searchSourceBuilder.highlighter(highlightBuilder);
//将绑定的信息存储到请求中
searchRequest.source(searchSourceBuilder);
SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//得到这个分组的数据集合
Terms termsCategory = response.getAggregations().get("skuCategorygroup");
Terms termsBrand = response.getAggregations().get("skuBrandgroup");
Terms specBrand = response.getAggregations().get("skuSpecgroup");
List<String> categoryList = getStringsCategoryList(termsCategory);
List<String> brandList = getStringsBrandList(termsBrand);
Map<String, Set<String>> secpMap = getStringsSpecList(specBrand);
//6.返回结果
Map<String ,Object> myresultMap = new HashMap<String ,Object>();
myresultMap.put("categoryList", categoryList);
myresultMap.put("brandList", brandList);
myresultMap.put("secpMap", secpMap);
SearchHits hits = response.getHits();
SearchHit[] searchHits= hits.getHits();
for (SearchHit hit : searchHits) {
String id = hit.getId();
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
String name = sourceAsMap.get("name").toString();
String spec = sourceAsMap.get("spec").toString();
String specMap = sourceAsMap.get("specMap").toString();
String myprice = sourceAsMap.get("price").toString();
//获取高亮的结果数据
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if(highlightBuilder!=null){
HighlightField nameField= highlightFields.get("name");
if(nameField!=null){
Text[] nameTxt = nameField.getFragments();
StringBuffer nameStr= new StringBuffer();
for (Text text:nameTxt) {
nameStr.append(text);
}
name=nameStr.toString();
}
}
System.out.println(name+"\t"+spec+"\t"+specMap+"\t"+myprice);
}
return myresultMap;
}
/**
* 获取品牌列表
*
* @return
*/
private List<String> getStringsBrandList(Terms terms) {
List<String> brandList = new ArrayList<>();
if (terms != null) {
for(int i=0;i<terms.getBuckets().size();i++){
//statistics
String brand =terms.getBuckets().get(i).getKey().toString();//id
brandList.add(brand);
}
}
return brandList;
}
/**
* 获取分类列表数据
*
* @return
*/
private List<String> getStringsCategoryList(Terms terms) {
List<String> categoryList = new ArrayList<>();
if (terms != null) {
for(int i=0;i<terms.getBuckets().size();i++){
//statistics
String category =terms.getBuckets().get(i).getKey().toString();//id
categoryList.add(category);
}
}
return categoryList;
}
/**
* 获取规格列表数据
*
* @return
*/
private Map<String, Set<String>> getStringsSpecList(Terms terms) {
Map<String, Set<String>> specMap = new HashMap<String, Set<String>>();
Set<String> specList = new HashSet<>();
List<String> categoryList = new ArrayList<>();
if (terms != null) {
for(int i=0;i<terms.getBuckets().size();i++){
//statistics
String category =terms.getBuckets().get(i).getKey().toString();//id
specList.add(category);
}
}
for (String specjson : specList) {
Map<String, String> map = JSON.parseObject(specjson, Map.class);
for (Map.Entry<String, String> entry : map.entrySet()) {//
String key = entry.getKey(); //规格名字
String value = entry.getValue(); //规格选项值
//获取当前规格名字对应的规格数据
Set<String> specValues = specMap.get(key);
if (specValues == null) {
specValues = new HashSet<String>();
}
//将当前规格加入到集合中
specValues.add(value);
//将数据存入到specMap中
specMap.put(key, specValues);
}
}
return specMap;
}
|