ElasticSearch的学习
介绍
ElasticSearch(简称ES)是一个开源的分布式搜索和数据分析引擎,是用Java开发并且是当前最流行的开源的企业级搜索引擎,能够达到近实时搜索,它专门设计用于处理大规模的文本数据和实现高性能的全文检索。
ElasticSearch是基于Restfull风格进行数据操作
应用场景:全文检索、日志分析、商业智能决策 等
ElasticStack生态介绍
ElasticSearch:核心搜索和分析引擎,提供存储、索引和分布式搜索功能
Logstach:数据处理管道,负责数据收集、处理和传输
Beats:边缘数据采集器,负责从各种来源采集数据并发送到Logstash或者ElasticSearch
Kibana:可视化和管理工具,提供数据展示和交互式查询
分词器
分词器可以让搜索的时候,能将指定的搜索词分成几个不同部分的搜索词来进行分别搜索。官方称之为文本分析器,顾名思义,是对文本进行分析处理的一种手段,基本处理逻辑为按照预先制定的分词规则,把原始文档分割成若干更小粒度的词项,粒度大小取决于分词器规则。
一般中文使用ik分词器,使用"analyzer":"ik_max_word","search_analyzer":"ik_smart"的方案。
ElasticSearch的核心概念
全文检索(Full-Text-Serach):全文检索是一种从大量文本数据中快速检索出包含指定词汇或者词语的信息的技术。
倒排索引:在一个文档集合中,每个文档都可以视为一个词语的集合,倒排索引则是将词语映射到包含这个词语的文档的数据结构
如何实现倒排索引:
- 文档预处理
- 构建词典
- 创建倒排列表
- 存储索引文件
- 查询处理
索引:类似于MySQL中的表,用于存储不同的数据
映射:类似于表中的schema,用于表示索引中的数据的结构
文档:索引中的一个数据实体,类似于MySQL中的一行记录
索引操作
创建索引
PUT /index_name
index_name代表索引名
可以直接用上述的语法去创建一个空的索引,也可以直接在创建的时候直接初始化好表的信息,例如:
PUT /index_name
{
"settings":{
//索引设置
},
"mapping":{
//字段映射
}
}
必要的参数:
-
索引名称(index_name):索引名必须是小写字母,可以包含数字和下划线
-
索引设置(settings)
- 分片数量(numberofshards):一个索引的分片数量决定了索引的并行度和数据分布,默认是一个
- 副本数量(numberofreplicas):副本提高了数据的可用性和容错能力,默认是一个
-
映射(mappings)
- 字段属性(properties):定义索引中文档的字段及其类型。常用的字段包括:text,keyword,integer,float,date等。示例:
{ "properties":{ "field_name1":{ "type":"text", …… }, "field_name2":{ "type":"integer", …… } } }
删除索引
DELETE /index_name
修改索引
PUT /index_name_settings
修改的时候,带上要修改的跟设置相关的参数,例如:
PUT /index_name_settings
{
"index": {
"number_of_replicas": 2
}
}
PUT /index_name/_mapping
修改的时候(添加新字段),带上要修改的跟映射相关的参数,例如:
PUT /index_name/_mapping
{
"properties":{
"content":{
"type":"text",
"analyzer":"ik_max_word",
"search_analyzer":"ik_smart"
}
}
}
索引库和mapping一旦创建就无法更改,但是可以添加新的字段。
索引别名
aliases是一个和setting和mapping同级的参数,用于指定索引的别名。例如:
PUT /index2/_mapping
{
"aliases":{
"index_alias":{}
}
"properties":{
"content":{
"type":"text",
"analyzer":"ik_max_word",
"search_analyzer":"ik_smart"
}
}
}
如此,就创建了一个索引index2,index2这个索引就有了index_alias这个别名
也可以给现有的索引增加一个别名:
POST /_aliases
{
"actions":[
{
"add":{
"index":"my_index",
"aliases":"my_index_alias"
}
}
]
}
如此,现有的my_index索引就有了一个别名叫做my_index_alias
索引别名有什么作用呢?
如果要多索引检索,即同时检索多个索引,有两种方案:
-
使用逗号对多个索引名称进程分隔
POST index1,index2,index3,index4/_search
-
使用通配符的方式
POST index*/_search
这种方式会有很大的局限性,建议使用别名的方式
使用别名的方式:
-
使用别名关联已有索引
POST /_aliases { "actions":[ { "add":{ "index":"my_index1", "aliases":"my_index_alias" } }, { "add":{ "index":"my_index2", "aliases":"my_index_alias" } }, { "add":{ "index":"my_index3", "aliases":"my_index_alias" } } ] }
查询的时候,GET my_index_alias 就会查询关联这个别名的所有的表
若索引和和别名指向相同,则在相同检索条件下的检索效率是一致的,因为索引别名只是物理索引的软链接的名称而已。
对相同索引别名的物理索引建议由一致的映射,以提升检索效率。
推荐充分发挥索引别名在检索方面的优势,但是在写入和更新的时候还要使用物理索引。
mapping的字段的属性
mapping是对索引库中文档的约束,常见的mapping的字段的属性包括:
- type:字段数据类型,创建的类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址,不可分词,是一个不可分割的整体)
- 数值:long、integer、short、byte、double、float
- 布尔:boolean
- 日期:date
- 对象:object
如果要使用数组的形式,type直接指定数组元素的类型即可,数据值也是以[xxx,xxx,xxx,……]数组的形式。
- index:是否创建索引,给了值为true,就会为其创建倒排索引
- analyzer:使用那种分词器,结合text类型的字段使用
- properties:该字段的子字段
- copy_to:可以将该字段的属性值拼接到指定哪个属性中
文档操作
新增文档
新增文档的DSL语法:
POST /索引库名/_doc/文档id
{
"字段1":"值1",
"字段2":{
"子属性1":"值2",
"子属性2":"值3"
}
}
如果不指定新建的文档id,就会自动随机生成一个id,这种情况显然不好。建议在新增文档的时候,加上指定的文档id。
查看文档
GET /索引库名/_doc/文档id
删除文档
DELETE /索引库名/_doc/文档id
更新文档
方式一:全量修改,会删除旧文档,添加新文档
PUT /索引库名/_doc/文档id
{
"字段1":"值1",
"字段2":{
"子属性1":"值2",
"子属性2":"值3"
}
}
如果文档id对应的文档存在,就删除旧文档,添加新文档。如果不存在,就直接增加一个新文档。
方式二:增量修改,修改指定字段值。
POST /索引库名/_update/文档id
{
"doc":{
"字段1":"值1",
"字段2":{
"子属性1":"值2",
"子属性2":"值3"
}
}
}
这种方式只会修改一条文档中的指定的字段值
DSL查询文档
DSL Query的分类
ElasticSearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
- 查询所有:查询出所有数据。match_all
- 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引中匹配。
- 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型的字段。
- 地理(geo)查询:根据经纬度查询。
- 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。
DSL Query查询语法
查询的基本语法如下:
GET /index_name/_search
{
"query":{
"查询类型":{
"查询条件":"条件值"
}
}
}
全文检索查询
全文检索查询,会对用户的输入内容进行分词,常用于搜索框搜索
-
match查询:全文检索查询中的一种,会对用户输入内容进行分词,然后去倒排索引库检索。
GET /index_name/_search { "query":{ "match":{ "字段名":"搜索内容" } } }
-
multi_match:与match查询类似,只不过允许同时查询多个字段。
GET /hotel/_search { "query":{ "multi_match":{ "query":"搜索词", "fields":["字段1","字段2","字段3",……] } } }
推荐使用copy_to将要共同查询的字段汇总到一个总字段之后,使用match查询。使用multi_match查询的时候,参与查询的字段越多,查询性能越差。
精确查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的方式有:
-
term:根据词条精确值查询
GET /index_name/_search { "query":{ "term":{ "字段名":{ "value":"搜索值" } } } }
-
range:根据值的范围查询
GET /index_name/_search { "query":{ "range":{ "字段名":{ "gte":"最小值", "lte":"最大值" } } } }
- gte是表示大于等于,如果只想表示大于,使用gt
- lte是表示小于等于,如果只想表示小于,使用lt
地理查询
根据经纬度查询。常见方式有:
-
geo_bounding_box:查询geo_point值落在某个矩形范围内的所有文档
GET /index_name/_search { "query":{ "geo_bounding_box":{ "字段名":{ "top_left":{ //左上角 "lat":"纬度值", "lon":"经度值" }, "bottom_right":{ //右下角 "lat":"纬度值", "lon":"经度值" } } } } }
- top_left属性就是矩形左上角的点的经纬度
- bottom_right属性就是矩形右下角的点的经纬度
-
geo_distance:查询到指定中心点小于某个距离值的所有文档
GET /index_name/_search { "query":{ "geo_distance":{ "distance":"距离(直接以m、km等等为距离单位即可)" "字段名":"字段值" } } }
复合查询
符合查询可以将其他简单查询组合起来,实现更复杂的搜索逻辑。
function score:算分函数查询,可以控制文档相关性算分,控制文档排名
相关性算分:当我利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果的时候按照分值降序排列。TF(词条频率)= 词条出现次数/文档中词条总数。还有一个TF-IDF算法,即也考虑逆文档频率,逆文档频率 = log(文档总数/包含词条的文档总数)。但是目前使用的是更高级的BM25算法,这种算法不会受词频影响较大,得分增长比较平缓,后面趋于水平。
Function Score Query
使用function score query,可以修改文档的相关性算分(query score),根据新得到的算分排序。
GET /index_name/_search
{
"query":{
"function_score":{
//原始查询条件,搜索文档并根据相关性打分(query_score)
"query":{
"查询类型":{
"字段名":"搜索内容"
}
},
"function":[
{
"filter":{"查询类型":{"字段名":"字段值"}},
"算分函数":……
}
],
"boost_mode":"multiply" //加权模式
}
}
}
算分函数,算分函数的结果称为function score,将来会与query score运算,得到新的算分,常见的有:
- weight:给一个常量值,作为函数结果(function score)
- field_value_factor:用文档中的某个字段值作为函数结果
- random_score:随机生成一个值,作为函数结果
- script_score:自定义计算公式,公式结果作为函数结果
加权模式,定义function score与query score的运算方式,包括:
- multiply:两者相乘。默认就是这个运算方式。
- replace:用function score替换这个query score
- 其他:sum、avg、max、min
Boolean Query
布尔查询是一个或者多个查询子句的组合,子查询的组合方式有:
- must:必须匹配每个子查询,类似于“与”
- should:选择性匹配子查询,类似于“或”
- must_not:必须不匹配,不参与算分,类似于“非”
- filter:必须匹配,不参与算分
语法:
GET /hotel/_search
{
"query":{
"bool":{
"must":[
{"查询方式":{"字段名":"字段值"}},
{"查询方式":{"字段名":"字段值"}},
……
],
"should":[
{"查询方式":{"字段名":"字段值"}},
{"查询方式":{"字段名":"字段值"}},
……
],
"must_not":[
{"查询方式":{"字段名":"字段值"}},
……
],
"filter":[
{"查询方式":{"字段名":"字段值"}},
……
]
}
}
}
搜索结果处理
排序
elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序的字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
GET /index_name/_search
{
//正常搜索内容部分
"query":{
"搜索方式":{
"字段名":"搜索内容"
}
},
"sort":[
{
"字段名":"排序方式(desc/asc)"
},
{
"字段名":"排序方式(desc/asc)"
},
……
]
}
sort数组中的一个元素代表根据一个字段排序及其排序方式,排序的优先级是按数组中的顺序,从前到后,依次递减。
如果是地理坐标类型的数据,sort数组里面的元素的写法要有些区别:
"sort":[
{
"_geo_distance":{
"字段名":"经度值,纬度值", //坐标
"order":"asc", //排序方式
"unit":"km" //单位
}
},
……
]
分页
elasticsearch默认情况下只返回top10的数据,如果要查询更多数据,就需要修改分页参数了。
elasticsearch中通过修改from、size参数来控制要返回的分页结果:
GET /hotel/_search
{
"query":{
"搜索方式":{
"字段名":"搜索内容"
}
},
"from":60, //分页开始的位置如果想第n页,值为(n-1)*size
"size":20
}
深度分页问题
es分页是选取从第一个到指定分页中最后一个的所有数据,再把前面的不要的数据丢掉,在集群的时候,就会面临问题。
es一般是分布式的,所以会面临深度分页问题。比如要搜数据中的前一千条,就是从每个集群中搜索指定的一千条数据,再汇总这些每个一千条,从汇总中取出新的前一千条。
深度分页,es提供了俩种解决方案:
search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
scroll:将排序数据形成快照,保存在内存中。官方已经不推荐使用。
高亮
高亮就是在搜索结果中把搜索关键字突出显示。
语法:
GET /hotel/_search
{
"query":{
"搜索方式":{
"字段名":"搜索内容"
}
},
"highlight":{
"fields":{
"字段名":{
"pre_tags":"<em>", //用来标记高亮字段的前置标签
"post_tags":"</em>" //用来标记高亮字段的后置标签
},
"字段名2":{
"pre_tags":"<em>", //用来标记高亮字段的前置标签
"post_tags":"</em>", //用来标记高亮字段的后置标签
"require_field_match":"false"
}
}
}
}
但是默认情况下,es搜索字段必须要和高亮字段一致,如果不是一致的字段,要加上require_field_match配置项
数据聚合
聚合(aggregations)可以实现对文档数据的统计、分析、运算。常见的聚合有三类:
-
桶(Bucket)聚合:用来对文档做分组。常见的有以下两种聚合类型
- TermAggregation:按照文档字段值进行分组。
- DateHistogram:按照日期阶梯进行分组,例如一周为一组,或者一个月为一组。
DSL语法:
GET /hotel/_search { "size":0, //定义size为零,结果中不包含文档,只包含聚合结果 "aggs":{ "聚合名":{ //给聚合自定义一个名字 "聚合类型":{ "field":"字段名", "size":20 //希望聚合的结果的数量 } } } }
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。
可以加一个order字段,进行修改结果排序方式:
GET /hotel/_search { "size":0, //定义size为零,结果中不包含文档,只包含聚合结果 "aggs":{ "聚合名":{ //给聚合自定义一个名字 "聚合类型(同查询类型)":{ "field":"字段名", "order":{ "字段名":"asc/desc" //排序方式 }, "size":20 //希望聚合的结果的数量 } } } }
默认情况下,Bucket聚合是对索引库中的所有的文档做聚合,我们可以限定文档要聚合的文档范围,只要添加query条件即可:
GET /hotel/_search { "query":{ "搜索方式":{ "字段名":"搜索内容" } }, "size":0, //定义size为零,结果中不包含文档,只包含聚合结果 "aggs":{ "聚合名":{ //给聚合自定义一个名字 "聚合类型(同查询类型)":{ "field":"字段名", "order":{ "字段名":"asc/desc" //排序方式 }, "size":20 //希望聚合的结果的数量 } } } }
-
度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等。
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同时求Max、Min、Avg、Sum等。
GET /hotel/_search { "size":0, //定义size为零,结果中不包含文档,只包含聚合结果 "aggs":{ "聚合名":{ //给聚合自定义一个名字 "聚合类型(同查询类型)":{ "field":"字段名", "size":20 //希望聚合的结果的数量 } "aggs":{ //是上一层聚合的子聚合,也就是分组后对每组进行计算 "聚合名称":{ //子聚合名称 "stats":{ //metric聚合类型 "field":"字段名" //要聚合的字段 } } } } } }
排序的时候,就可以使用子聚合中的结果来排序,直接将子聚合的聚合名作为字段名即可,使用stats的时候,就使用 子聚合名.avg 、 子聚合名.min 等等作为字段名即可。
-
管道(pipeline)聚合:其他聚合的结果为基础再做聚合。
自动补全
自定义分词器
elasticsearch中分词器(analyzer)的组成包含三部分:
- character filter:在tokenizer之前对文本进行处理。例如删除字符、替换字符。
- tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
- tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等。
要实现在拼音分词的之前,能先进行ik分词,可以在创建索引的时候,通过settings来配置自定义的analyzer分词器,而且,也要设置一些拼音分词器的参数,让其能达到更好的拼音分词效果:
PUT /test
{
"settings":{
"analysis":{
"analyzer":{ //用于创建自定义分词
"自定义分词器名称":{
"tokenizer":"ik_max_word",
"filter":"pinyin"
}
},
"filter": { // 自定义tokenizer filter
"py": { // 自定义过滤器名称
"type": "pinyin", // 过滤器类型,这里是pinyin
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings":{
"properties":{
"字段名":{
"type":"字段类型",
"analyzer":"my_analyzer",
"search_analyzer":"ik_smart"
}
}
}
}
因为不同的同音中文对应的拼音会一致,为了防止搜出同音词,应该要在倒排索引的时候使用拼音分词器,而搜索的时候不应该使用拼音分词器。要再添加一个search_analyzer,设置为ik_smart,如此搜索的时候,就会使用search_analyzer配置的分词器而不会走analyzer的分词器。
自动补全查询
completion suggester查询
这个查询会匹配用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中的字段的类型有一些约束:
- 参与补全查询的字段必须是completion类型。
- 字段的内容一般是用来补全的多个词条形成的数组。
查询语法如下:
GET /test/_search
{
"suggest":{
"title_suggest":{
"text":"搜索内容",
"completion":{
"field":"title", //补全查询的字段
"skip_duplicates":true,
"size":10
}
}
}
}
可以在后端的业务里,将要进行自动补全查询的字段,加到一个list里面,再将这个list赋值给这个自动补全查询字段。
数据同步
当关于某个实体的数据更新或者增加的时候,当然在es和数据库中的数据都要做相应的数据更新。
- 方案一:同步调用
- 即先更新数据库,数据库中更新完数据之后,再调用更新索引中的文档的接口,更新es中的数据。
- 优点:简单
- 缺点:耦合度高
- 方案二:异步调用
- 更新数据库的操作时候,发送一条消息给mq,mq监听消息之后,调用更新索引的文档的接口,更新es中的数据。
- 优点:低耦合
- 缺点:依赖mq的可靠性
- 方案三:监听binlog
- 使用一些中间件,比如canal,监听binlog,来监听mysql中的增删改操作,当相应的数据发生改变的时候,触发相应的更新es中的操作。
- 优点:完全解除服务间的耦合
- 缺点:增加数据库负担
SpringBoot整合ElasticSearch(Elasticsearch Java API Client操作索引库)
初始化
maven导入依赖:
<!-- elasticsearch的导入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
配置类配置基本的es连接信息(记得Java这里的import不要导入错了依赖)
yaml:
elasticsearch:
host: xxx.xxx.xxx.xxx #主机地址
port: 9200 #es端口
scheme: http #协议
连接信息配置类:
@ConfigurationProperties(prefix = "elasticsearch")
@Component
@Data
public class EsProperties {
private String host;
private Integer port;
private String scheme;
}
es操作的组件的配置类:
builder中的new的HttpHost对象就是在配置好client的连接信息。
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import jakarta.annotation.Resource;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ElasticSearchConfig {
@Resource
private EsProperties esProperties;
@Bean
public ElasticsearchClient esClient() {
RestClient restClient = RestClient
.builder(new HttpHost(esProperties.getHost(),esProperties.getPort(),esProperties.getScheme()))
.build();
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
return new ElasticsearchClient(transport);
}
}
索引操作
由于我感觉,索引操作好像一般不会放在springboot中进行,一般进行的应该都是文档操作之类的,就简单展示一下索引操作的Java代码:
// 索引名字
String indexName = "student";
// 索引是否存在
BooleanResponse books = esClient.indices().exists(e -> e.index(indexName));
System.out.println("索引是否存在:" + books.value());
// 创建索引
esClient.indices().create(c -> c
.index(indexName)
.mappings(mappings -> mappings // 映射
.properties("name", p -> p
.text(t -> t // text类型,index=false
.index(false)
)
)
.properties("age", p -> p
.long_(t -> t) // long类型
)
)
);
// 删除索引
esClient.indices().delete(d -> d.index(indexName));
esClient就是上面的配置文件中配置的ioc容器中的ElasticSearchClient类型的组件,直接使用autowired或者resource注解注入即可。
文档操作
新增:
// 新增
CreateResponse createResponse = esClient.create(c -> c
.index(indexName) // 索引名字
.id(account.getId()) // id
.document(account) // 实体类
);
我使用的是es8的方式,使用ElasticsearchClient进行操作,这种方式中有着较多的lambda的写法。且会有多层调用,可以像我这种写法,能将参数分析清楚。
删除:
DeleteResponse deleteResp = esClient.delete(d -> d.index(indexName).id("1"));
批量新增:
List<Account> accountList = ...
BulkRequest.Builder br = new BulkRequest.Builder();
for (Account acc : accountList) {
br.operations(op -> op
.create(c -> c
.index(indexName)
.id(acc.getId())
.document(acc)
)
);
}
BulkResponse bulkResp = esClient.bulk(br.build());
批量新增操作要使用到Bulk。
根据id查找:
GetResponse<ArticleVo> getResp = esClient.get(g ->
g.index("article_index1")
.id("17770146669-1732611392477")
, ArticleVo.class);
if (getResp.found()) {
ArticleVo source = getResp.source(); // 这就是得到的实体类
source.setArticleId(getResp.id());
System.out.println(source);
}
高亮、分页、排序查找:
三个知识点我就放在一起了,其实和普通的DSL的写法大致,只要DSL掌握的好,再按这个结果写,写起来会很简单。
public ResultData<List<ArticleESVo>> searchArticlePage(String message, Integer pageSize, Integer pageNum) throws IOException {
SearchResponse<ArticleESVo> search = esClient.search(s -> s
.index("article_index1") //指明索引
.from((pageNum-1) * pageSize) //分页开始数
.size(pageSize) //每页的页大小
.sort(so ->so //排序配置
.field(f -> f
.field("publicTimeView") //排序字段
.order(SortOrder.Desc)
.field("likes") //排序字段
.order(SortOrder.Desc)
)
)
.query(q -> q //查询配置
.match(t -> t
.field("all") //查询字段,这里直接查all
.query(message) //用户的查询内容
)
)
.highlight(h ->h //高光配置
.preTags("<span color='red'>") //高光部分的前置标签
.postTags("</span>") //高光部分的后置标签
.fields("title",hi ->hi) //要高光的字段
.fields("mainContent",hi2->hi2) //要高光的字段
.requireFieldMatch(false) //设置为不需要匹配查询字段也行
)
, ArticleESVo.class
);
List<ArticleESVo> articleESVoList = new ArrayList<>();
System.out.println(search + "es给回来的数据数据是这样子的");
List<Hit<ArticleESVo>> hits = search.hits().hits(); //目标实体的数据都在这里面,但是还是存在一个Hit对象里面,高光和普通数据,都要从这个hit里面获取。
for (Hit<ArticleESVo> hit : hits) {
List<String> listTitleHighLight = hit.highlight().get("title"); //获取高光部分的内容
List<String> listMainContentHighLight = hit.highlight().get("mainContent"); //获取高光部分的内容
ArticleESVo articleESVo = hit.source();
if (listTitleHighLight != null){
articleESVo.setTitle(listTitleHighLight.get(0)); //如果标题有高光,就替换掉
}
if (listMainContentHighLight != null){
articleESVo.setMainContent(listMainContentHighLight.get(0)); //如果文章摘要有高光,就替换掉
}
articleESVoList.add(articleESVo); //将目标数据加到要返回的集合中
}
System.out.println(articleESVoList + " 最终结果");
return ResultData.success(articleESVoList);
}
高光字段是存在hit参数里面的highlight参数里面的,通过get方法指定高光处理后的字段值,再覆盖掉原来source中获取的实体类的相应字段(source中的实体对象中的字段信息是没有经过高光处理的),如果没有指定的高光信息,就不能赋值过去的,不然会报空指针异常,所以要先进行一个判断。
自动补全:
public ResultData<List<String>> suggestSearch(String message) throws IOException {
SearchResponse<ArticleESVo> search = esClient.search(s -> s
.index("article_index")
.suggest(sug -> sug
.suggesters("suggest_article", fs -> fs
.text(message).completion(te -> te
.field("suggestion")
.skipDuplicates(true)
.size(10)
)
)
)
, ArticleESVo.class);
List<String> list = new ArrayList<>();
System.out.println(search + "推荐结果");
Suggestion<ArticleESVo> suggest_article = search.suggest().get("suggest_article").get(0);
List<CompletionSuggestOption<ArticleESVo>> options = suggest_article.completion().options();
for (CompletionSuggestOption<ArticleESVo> option : options) {
String text = option.text();
list.add(text);
}
return ResultData.success(list);
}
可以看出,自动补全的Elasticsearch Java API Client的写法看起来和DSL的写法非常相似,可见,掌握好DSL的语法,对使用Elasticsearch Java API Client非常有帮助。我这里的写法是用一个String类型的集合,存储所有的自动补全词条,再将其返回给前端。