springboot操作Elasticsearch

一、版本说明

使用版本 ElasticSearch 7.17.3  springboot 2.7 Spring Data Elasticsearch 4.4.8 jdk不需要安装,会使用ES内置的JDK。

安装指定版本的ES,安装指定版本的Spring Boot,安装Spring Boot时选择依赖Spring Data Elasticsearch,spring boot指定了Spring Data Elasticsearch的版本。版本必须对应,否则Spring Data Elasticsearch和ES无法对应起来。

Spring Data Elasticsearch - Reference Documentation  查看版本对应关系。

看Spring Data Elasticsearch官方文档一定注意版本,版本不同,API差距很大,并且分清不同package下的同名类,很容易出错。

 && https://docs.spring.io/spring-data/elasticsearch/docs/4.4.8/reference/html/#preface.metadata &&  将url路径中的current或具体版本号修改为Spring Data Elasticsearch版本的版本号就可以查看对应版本文档

下图是查看spring boot中使用Spring Data Elasticsearch的版本号

二、安装ES 创建spring boot项目 

Elasticsearch是一个分布式全文搜索引擎,支持单节点模式(Single-Node Mode)和集群模式(Cluster Mode)部署,一 般来说,小公司的业务场景往往使用Single-Node Mode部署即可。以Single-Node Mode部署感受下ES,后续搭建分布式集群深入学习。

安装ES

1. 虚拟机环境准备

操作系统:CentOS 7.x 64 bit

客户端连接工具:SecureCRT关闭虚拟机的防火墙

systemctl stop firewalld.service #停止firewall

systemctl disable firewalld.service #禁止firewall开机启动

firewall-cmd --state # 查看防火墙

2. Elasticsearch Single-Node Mode部署

我们在虚拟机上部署Single-Node Mode Elasticsearch

          下载Elasticsearch 地址: https://www.elastic.co/cn/downloads/elasticsearch 最新版本 。本课程使用7.3.0版本 。选择Linux版本下载 

安装ES 

 1.上传、解压tar.gz文件 

cd /opt/lagou/software
tar -zxvf elasticsearch-7.3.0-linux-x86_64.tar.gz -C ../servers

2.重命名

cd ../servers/
mv elasticsearch-7.3.0/ elasticsearch/

3 配置Elasticsearch

(1)编辑vim /opt/lagou/servers/elasticsearch/confifig/elasticsearch.yml

        单机安装请取消注释:node.name: node-1,否则无法正常启动。

        修改网络和端口,取消注释master节点,单机只保留一个node

node.name: node-1
network.host: 192.168.1.250
#
# Set a custom port for HTTP:
#
http.port: 9200
cluster.initial_master_nodes: ["node-1"]

(2)按需修改vim /opt/lagou/servers/elasticsearch/confifig/jvm.options内存设置

      vim /opt/lagou/servers/elasticsearch/config/jvm.options

     根据实际情况修改占用内存,默认都是1G,单机1G内存,启动会占用700m+然后在安装kibana后,基本上无 法运行了,运行了一会就挂了报内存不足。 内存设置超出物理内存,也会无法启动,启动报错。

      -Xms2g

      -Xmx2g

Exception in thread "main" java.nio.file.AccessDeniedException: /opt/es/elasticsearch-7.17.3/config/jvm.options.d  虚拟机内存设置好,或设置大一点 1G内存肯定不行,至少4G

3.添加es用户,es默认root用户无法启动,需要改为其他用户

useradd estest

#修改密码

passwd estest

改变es目录拥有者账号

chown -R estest /opt/lagou/servers/elasticsearch/

4.修改/etc/sysctl.conf

      ES因为需要大量的创建索引文件,需要大量的打开系统的文件,所以我们需要解除linux系统当中打开文件最大数目的限制,不然ES启动就会抛错

修改文件句柄数

vim /etc/sysctl.conf

末尾添加:vm.max_map_count=655360

执行sysctl -p 让其生效

sysctl -p

5.修改/etc/security/limits.conf

修改linux系统对文件描述符的限制级别vim /etc/security/limits.conf

末尾添加:

* soft nofile 65536

* hard nofile 65536

* soft nproc 4096

* hard nproc 4096

6.启动es

切换刚刚新建的用户

su estest

启动命令

/opt/lagou/servers/elasticsearch/bin/elasticsearch

7.配置完成:浏览器访问测试。 http://192.168.1.250:9200

8.Google商店安装head插件 就可以查看ES的运行状态或者使用Apipost自己调用接口。

我自己写的接口文件:  es接口查询

安装IK分词器


安装
使用 root 用户操作!! 每台机器都要配置。配置完成之后,需要重启ES 服务
(1)  elasticsearch 安装目录的 plugins 目录下新建 analysis - ik 目录   
#新建analysis-ik文件夹
mkdir analysis-ik
#切换至 analysis-ik文件夹下
cd analysis-ik
#上传资料中的 elasticsearch-analysis-ik-7.3.0.zip
#解压
unzip elasticsearch-analysis-ik-7.3.3.zip
#解压完成后删除zip
rm -rf elasticsearch-analysis-ik-7.3.0.zip

注意:
-bash: unzip: command not found
yum install -y unzip

(2) 重启Elasticsearch Kibana

#杀死es
ps -ef|grep elasticsearch|grep bootstrap |awk '{print $2}' |xargs kill -9
#启动
nohup /opt/lagou/servers/es/elasticsearch/bin/elasticsearch >/dev/null 2>&1 &

 测试

IK 分词器有两种分词模式: ik_max_word ik_smart 模式。
ik_max_word ( 常用 )
会将文本做最细粒度的拆分
ik_smart
会做最粗粒度的拆分
测试一波输入下面的请求:
POST _analyze
{
"analyzer": "ik_max_word",
"text": "南京市长江大桥"
}

ik_max_word 分词模式运行得到结果:
{
"tokens": [{
"token": "南京市",
"start_offset": 0,
"end_offset": 3,
"type": "CN_WORD",
"position": 0
},
{
"token": "南京",
"start_offset": 0,
"end_offset": 2,
"type": "CN_WORD",
"position": 1
},
{
"token": "市长",
"start_offset": 2,
"end_offset": 4,
"type": "CN_WORD",
"position": 2
},
{
"token": "长江大桥",
"start_offset": 3,
"end_offset": 7,
"type": "CN_WORD",
"position": 3
},
{
"token": "长江",
"start_offset": 3,
"end_offset": 5,
"type": "CN_WORD",
"position": 4
},
{
"token": "大桥",
"start_offset": 5,
"end_offset": 7,
"type": "CN_WORD",
"position": 5
}]
}
POST _analyze
{
"analyzer": "ik_smart",
"text": "南京市长江大桥"
}

ik_smart 分词模式运行得到结果:
 
{
"tokens": [{
"token": "南京市",
"start_offset": 0,
"end_offset": 3,
"type": "CN_WORD",
"position": 0
},
{
"token": "长江大桥",
"start_offset": 3,
"end_offset": 7,
"type": "CN_WORD",
"position": 1
}
]
}

如果现在假如江大桥是一个人名,是南京市市长,那么上面的分词显然是不合理的,该怎么办?


  词典使用
       扩展词 :就是不想让哪些词被分开,让他们分成一个词。比如上面的江大桥    
       停用词 :有些词在文本中出现的频率非常高。但对本文的语义产生不了多大的影响。例如英文的 a an the of 等。或中文的” 的、了、呢等 。这样的词称为停用词。停用词经常被过滤掉,不会被进行索引。在检索的过程中,如 果用户的查询词中含有停用词,系统会自动过滤掉。停用词可以加快索引的速度,减少索引库文件的大小。
      
Linux部署Tomcat
以下操作使用es用户
1) 上传tomcat安装包到linux123服务器
为了避免权限问题上传到此目录下:/opt/lagou/servers/es/

cd /opt/lagou/servers/es/
2 )解压tar -zxvf apache-tomcat-8.5.59.tar.gz
mv apache-tomcat-8.5.59/ tomcat/

3 )配置自定义词典文件
自定义扩展词库
cd /opt/lagou/servers/es/tomcat/webapps/ROOT
vim ext_dict.dic
添加:江大桥

自定义停用词
cd /opt/lagou/servers/es/tomcat/webapps/ROOT
vim stop_dict.dic
添加:
的
了
啊

4 启动tomcat
/opt/lagou/servers/es/tomcat/bin/startup.sh

浏览器访问
http://linux123:8080/ext_dict.dic

(3)配置IK分词器

添加自定义扩展,停用词典
使用root用户修改,或者直接把整个文件夹改为es用户所有!!

cd /opt/lagou/servers/es/elasticsearch/plugins/analysis-ik/config
vim IKAnalyzer.cfg.xml


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://linux123:8080/ext_dict.dic</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<entry key="remote_ext_stopwords">http://linux123:8080/stop_dict.dic</entry>
</properties>
(4) 重启服务
#杀死es
ps -ef|grep elasticsearch|grep bootstrap |awk '{print $2}' |xargs kill -9
#启动
nohup /opt/lagou/servers/es/elasticsearch/bin/elasticsearch >/dev/null 2>&1 &

新建 Spring Boot项目

(1)新建spring boot 项目,选择Spring Data Elasticserach

 (2)新建repository实体类,映射Es中的index

package com.eb.fri.casebase.beanMappingESIndex;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;


@Document(indexName = "case")
@Data
public class Case {
    @Id
    String id;

    //单位
    @Field(type = FieldType.Text, analyzer = "ik_max_word")


    //发布时间 fielddata允许排序
    @Field(type = FieldType.Date,pattern = "yyyy-MM-dd HH:mm:ss")
    String publish_time;

   //date类型的数据想要排序,不需要设置fieldata。只需要看下索引是date类型即可。date类型可以进行排序。Test类型可以进行分词。
}

(3)新建接口继承ElasticsearchRepository

package com.eb.fri.casebase.repository;

import com.eb.fri.casebase.beanMappingESIndex.Case;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import java.util.List;
import java.util.Optional;

public interface ElasticsearchCaseRepository extends ElasticsearchRepository<Case, String> {

    @Override
    Optional<Case> findById(String s);


    /**
     * 模糊查询分页实现
     *
     * @param pageable
     * @return //
     */
    @Query(value = "   {\"fuzzy\": {\"article\": \"?0\"}}")
    Page<Case> findByArticleFussy(String kw, Pageable pageable);

   

}

(4)application.yml配置

spring:
  elasticsearch:
    uris: 192.168.1.250:9200
    connection-timeout: 10

(5)配置spring boot内置支持的logback日志

      日志配置文件如果是下面文件名和位置,会被spring boot自动识别。 

    

    <logger name="tracer" level="trace"/> 这句是开启Spring Data Elasticsearch的 查询语句日志。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--======================================= 本地变量 ======================================== -->
    <!--在没有定义${LOG_HOME}系统变量的时候,可以设置此本地变量。提交测试、上线时,要将其注释掉,使用系统变量。 -->
    <!-- <property name="LOG_HOME" value="D:/data/logs" /> -->

    <!-- 应用名称:和统一配置中的项目代码保持一致(小写) -->
    <property name="APP_NAME" value="base" />
    <!--日志文件保留天数 -->
    <property name="LOG_MAX_HISTORY" value="180" />
    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径 -->
    <!--应用日志文件保存路径 -->
    <property name="LOG_APP_HOME" value="${catalina.home}/${APP_NAME}/app" />

    <!--=========================== 按照每天生成日志文件:默认配置=================================== -->
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 按照每天生成日志文件:主项目日志 -->
    <appender name="APP"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名 -->
            <FileNamePattern>${LOG_APP_HOME}/base.%d{yyyy-MM-dd}.log
            </FileNamePattern>
            <!--日志文件保留天数 -->
            <MaxHistory>${LOG_MAX_HISTORY}</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{500} - %msg%n</pattern>
        </encoder>
    </appender>
    <!--=============================== 定义各个框架的输出级别 ====================================== -->

    <root level="DEBUG">
        <appender-ref ref="APP" />
        <appender-ref ref="STDOUT" />

    </root>

    <!--配置es日志-->
    <logger name="tracer" level="trace"/>


</configuration>

(5)controller调用

简单的CRUD使继承自ElasticsearchRepository接口的接口,复杂查询使用NativeSearchQuery构造查询条件。

package com.eb.fri.casebase.controller;


import com.alibaba.fastjson.JSONObject;
import com.eb.fri.casebase.beanMappingESIndex.Case;
import com.eb.fri.casebase.repository.ElasticsearchCaseRepository;
import com.eb.fri.casebase.utils.SnowFlake;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.QueryStringQueryBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @author liguofang
 * 对ES数据的查询 包括全文搜索、模糊搜索等
 */
@RestController
@RequestMapping(value = "/es")
public class ESController {

    @Autowired
    ElasticsearchCaseRepository esrepo;

    @Autowired
    ElasticsearchOperations op;




    @ResponseBody
    @PostMapping(value = "/case/save")
    public String searchBy(@RequestBody Case caseObj) {

        caseObj.setId(SnowFlake.getID());

        Case res = esrepo.save(caseObj);
        return res.toString();

    }

 

    @GetMapping("/case/delete/{id}")
    public String deleteById(@PathVariable("id") Long id) {
        return "";
    }


    /**
     * 查询指定单位的文章,根据作者,文章内容等字段
     *
     * @param pageable
     * @return
     */
    @GetMapping("/case/searchDeptArticle")
    public SearchHits<Case> searchDeptArticle(String dept, String fields, @PageableDefault(sort = {"publish_time"}, direction = Sort.Direction.DESC) Pageable pageable) {
        JSONObject props_json = JSONObject.parseObject(fields);
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        boolQuery.must(QueryBuilders.matchQuery("dept", dept));
        for (String k : props_json.keySet()) {
            boolQuery.should(QueryBuilders.matchQuery(k, props_json.getString(k)));
        }
        NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
                //查询条件
                .withQuery(boolQuery)

                //分页
                .withPageable(pageable)
                .build();
        SearchHits<Case> searchHits = op.search(nativeSearchQuery, Case.class);
        return searchHits;
    }

}

三、DSL操作

查询表达式 | Elasticsearch: 权威指南 | Elastic  官方中文文档版本比较旧,新版本没有中文版

Fuzzy query | Elasticsearch Guide [7.17] | Elastic  指定版本

理解bool组合查询

下面的查询用于查找 title 字段匹配 how to make millions 并且不被标识为 spam 的文档。那些被标识为 starred 或在2014之后的文档,将比另外那些文档拥有更高的排名。如果 两者 都满足。 即使数据中不满足should的数据也会查询出来,只不过评分低,排名靠后。

{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }},
            { "range": { "date": { "gte": "2014-01-01" }}}
        ]
    }
}

注意数据是否返回、对评分是否有影响。 

must 满足条件才返回

文档 必须 匹配这些条件才能被查询返回。

must_not 不满足条件才返回

文档 必须不 匹配这些条件才能被查询返回。

should 不影响返回,只影响得分

如果满足这些语句中的任意语句,将增加 _score ,否则,无任何影响。它们主要用于修正每个文档的相关性得分。不影响数据是否查询返回。

filter 必须满足条件才返回

必须 匹配条件才会查询返回。这些语句对评分没有贡献,只是根据过滤标准来排除或包含文档。

将查询移到 bool 查询的 filter 语句中,这样它就自动的转成一个不评分的 filter 了。

{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }}
        ],
        "filter": {
          "bool": { 
              "must": [
                  { "range": { "date": { "gte": "2014-01-01" }}},
                  { "range": { "price": { "lte": 29.99 }}}
              ],
              "must_not": [
                  { "term": { "category": "ebooks" }}
              ]
          }
        }
    }
}

关键区分:输入是否进行分词、索引内字段Test类型一定进行分词、针对全部字段匹配还是单个字段匹配。输入是否进行错别字修改

分词查询:输入单词会进行分词,索引内的字段数据会进行分词。然后进行单个字段匹配或者全部字段匹配。 给定相似度排序结果。  match queryforstring实现

模糊查询:输入单词修改某几个字,然后和索引内分词的单个字段进行匹配。只能单字段,不能全部字段。 fussy regex实现

精确匹配:term实现。输入单词不进行分词,必须和文段分词完全匹配才可以。term实现。

对于分词的理解: Elasticsearch入门到精通第二篇-查询分析_木秀林的博客-CSDN博客

三、操作API

 开发流程

主要工作是开启Spring Data Elasticsearch日志后,对比输出的查询dsl和直接调用es的接口的dsl是否相同。 下面是es的官网中文文档

Spring Data Elasticsearch - Reference Documentation  4.4.8版本文档

Elasticsearch学习(7)—— 查询API - 叶枫啦啦的个人空间 - OSCHINA - 中文开源技术交流社区  

如何使用Spring Data Elasticsearch

(1) 自定义映射索引的实体类

package com.eb.fri.casebase.beanMappingESIndex;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;


@Document(indexName = "case")
@Data
public class Case {
    @Id
    String id;

    //单位
    @Field(type = FieldType.Text, analyzer = "ik_max_word")


    //发布时间 fielddata允许排序
    @Field(type = FieldType.Date,pattern = "yyyy-MM-dd HH:mm:ss")
    String publish_time;

   //date类型的数据想要排序,不需要设置fieldata。只需要看下索引是date类型即可。date类型可以进行排序。Test类型可以进行分词。
}
注解:@Document用来声明Java对象与ElasticSearch索引的关系
             indexName 索引名称(是字母的话必须是小写字母)
             type 索引类型
             shards 主分区数量,默认5
             replicas 副本分区数量,默认1
             createIndex 索引不存在时,是否自动创建索引,默认true
                        不建议自动创建索引(自动创建的索引 是按着默认类型和默认分词器)
注解:@Id 表示索引的主键
注解:@Field 用来描述字段的ES数据类型,是否分词等配置,等于Mapping描述,其属性包括如下
             index 设置字段是否索引,默认是true,如果是false则该字段不能被查询
             store 标记原始字段值是否应该存储在 Elasticsearch 中,默认值为false,以便于快速检索。虽然store占用磁盘空间,但是减少了计算。
             type 数据类型(text、keyword、date、object、geo等)
             analyzer 对字段使用分词器,注意一般如果要使用分词器,字段的type一般是text。
             format 定义日期时间格式
注解:@CompletionField 定义关键词索引 要完成补全搜索
             analyzer 对字段使用分词器,注意一般如果要使用分词器,字段的type一般是text。
             searchAnalyzer 显示指定搜索时分词器,默认是和索引是同一个,保证分词的一致性。
             maxInputLength:设置单个输入的长度,默认为50 UTF-16 代码点

具体映射规则查看官网 Spring Data Elasticsearch - Reference Documentation 

注意事项

(1) test类型的字段想排序 必须设置fieldata 但是springboot的api设置无效。必须es api执行才可以。 

{
  "properties": {
    "publish_time": {
      "type": "text",
      "fielddata": true
    }
  }
}

(2)date类型可以进行排序。

(3)排序字段为_score  _score才会有相似度分数值,如果排序字段为其他的字段,那么_score值为NaN  

(2)自定义接口实现ElasticsearchRepository接口

      首先实现映射索引的自定义实体类。       

     然后实现接口就可以使用一些关键词实现CRUD ES数据。常用来一些简单的操作,不能够实现复杂查询语句。因此,结合ElasticsearchOperations来查询。

package com.eb.fri.casebase.repository;

import com.eb.fri.casebase.beanMappingESIndex.Case;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import java.util.List;
import java.util.Optional;

public interface ElasticsearchCaseRepository extends ElasticsearchRepository<Case, String> {

    //继承来的方法
    @Override
    Optional<Case> findById(String s);

     //根据关键字实现查询指定的字段
    List<SearchHit<Case>> findByAuthor(String kw);

    /**
     * 分页实现
     *
     * @param pageable
     * @return //
     */
    @Query(value = "{\"match\": {\"article\": {\"query\": \"?0\"}}}")
    Page<Case> findByArticle(String kw, Pageable pageable);


}

(2)使用ElasticsearchOperations 结合Queries实现查询

构造查询语句的三种方式

CriteriaQuery 隔离具体DSL,根据语义进行构造查询语句

StringQuery  使用ES DSL JSON请求体

NativeSearchQuery ES所有原生API都进行了实现

通过 NativeSearchQueryBuilder.withQuery(QueryBuilder1).withFilter(QueryBuilder2).withSort(SortBuilder1).withXXXX().build(); 这样的方式来完成 NativeSearchQuery 的构建。

QueryBuilder 主要用来构建查询条件、过滤条件,SortBuilder 主要是构建排序。

要构建 QueryBuilder,我们可以使用工具类 QueryBuilders,里面有大量的方法用来完成各种各样的 QueryBuilder 的构建,字符串的、Boolean 型的、match 的、地理范围的等等。

要构建 SortBuilder,可以使用 SortBuilders 来完成各种排序。

然后就可以通过 NativeSearchQueryBuilder 来组合这些 QueryBuilder 和 SortBuilder,再组合分页的参数等等,最终就能得到一个 SearchQuery 了。

SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery (QueryBuilders.queryStringQuery ("spring boot OR 书籍")).build ();

  @GetMapping("/case/searchDeptArticle")
    public SearchHits<Case> searchDeptArticle(String dept, String props, @PageableDefault(sort = {"publish_time"}, direction = Sort.Direction.DESC) Pageable pageable) {
        JSONObject props_json = JSONObject.parseObject(props);
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        boolQuery.must(QueryBuilders.matchQuery("dept", dept));
        for (String k : props_json.keySet()) {
            boolQuery.should(QueryBuilders.matchQuery(k, props_json.getString(k)));
        }
        NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
                //查询条件
                .withQuery(boolQuery)

                //分页
                .withPageable(pageable)
                .build();
        SearchHits<Case> searchHits = op.search(nativeSearchQuery, Case.class);
        return searchHits;
    }
Query query = new NativeSearchQueryBuilder()
    .addAggregation(terms("lastnames").field("lastname").size(10)) //
    .withQuery(QueryBuilders.matchQuery("firstname", firstName))
    .build();

SearchHits<Person> searchHits = operations.search(query, Person.class);

ElasticSearch系列(六)springboot中使用QueryBuilders、NativeSearchQuery实现复杂查询_雨剑yyy的博客-CSDN博客

QueryBuilders使用

QueryBuilders是ES中的查询条件构造器。下面结合一些具体的查询场景,分析其常用方法。

假设ES中已经有title为 “总裁关心浦东开发开放” 的数据;

ik_smart分词结果:

{
    "tokens": [
        {
            "token": "总裁",
            "start_offset": 3,
            "end_offset": 6,
            "type": "CN_WORD",
            "position": 1
        },
        {
            "token": "关心",
            "start_offset": 6,
            "end_offset": 8,
            "type": "CN_WORD",
            "position": 2
        },
        {
            "token": "浦东",
            "start_offset": 8,
            "end_offset": 10,
            "type": "CN_WORD",
            "position": 3
        },
        {
            "token": "开发",
            "start_offset": 10,
            "end_offset": 12,
            "type": "CN_WORD",
            "position": 4
        },
        {
            "token": "开放",
            "start_offset": 12,
            "end_offset": 14,
            "type": "CN_WORD",
            "position": 5
        }
    ]
}

精确查询


精确,指的是查询关键字(或者关键字分词后),必须与目标分词结果完全匹配。

1.指定字符串作为关键词查询,关键词支持分词

//查询title字段中,包含 ”开发”、“开放" 这个字符串的document;相当于把"浦东开发开放"分词了,再查询;
QueryBuilders.queryStringQuery("开发开放").defaultField("title");

//不指定feild,查询范围为所有feild
QueryBuilders.queryStringQuery("青春");

//指定多个feild
QueryBuilders.queryStringQuery("青春").field("title").field("content");

2.以关键字“开发开放”,关键字不支持分词

QueryBuilders.termQuery("title", "开发开放")
QueryBuilders.termsQuery("fieldName", "fieldlValue1","fieldlValue2...")


3.以关键字“开发开放”,关键字支持分词

QueryBuilders.matchQuery("title", "开发开放")
QueryBuilders.multiMatchQuery("fieldlValue", "fieldName1", "fieldName2", "fieldName3")


模糊查询


模糊,是指查询关键字与目标关键字可以模糊匹配。

1.左右模糊查询,其中fuzziness的参数作用是在查询时,es动态的将查询关键词前后增加或者删除一个词,然后进行匹配

    QueryBuilders.fuzzyQuery("title", "开发开放").fuzziness(Fuzziness.ONE)

2.前缀查询,查询title中以“开发开放”为前缀的document;

    QueryBuilders.prefixQuery("title", "开发开放")

3.通配符查询,支持*和?,?表示单个字符;注意不建议将通配符作为前缀,否则导致查询很慢

   QueryBuilders.wildcardQuery("title", "开*放")
   QueryBuilders.wildcardQuery("title", "开?放")

注意,
在分词的情况下,针对fuzzyQuery、prefixQuery、wildcardQuery不支持分词查询,即使有这种doucment数据,也不一定能查出来,因为分词后,不一定有“开发开放”这个词;

查询总结

   title为 “总裁关心浦东开发开放” 的数据

查询关键词  开发开放
queryStringQuery    查询目标中含有开发、开放、开发开放的    无    无
matchQuery    同queryStringQuery
termQuery    无结果,因为它不支持分词 无
prefixQuery    无结果,因为它不支持分词 无有,目标分词中以”开“开头的
fuzzyQuery    无结果,但是与fuzziness参数有关系
wildcardQuery    开发开放*无结果    开*,有放*,无

 范围查询

//闭区间查询
QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2");

//开区间查询,默认是true,也就是包含
QueryBuilders.rangeQuery("fieldName").from("fieldValue1").to("fieldValue2").includeUpper(false).includeLower(false);

//大于
QueryBuilders.rangeQuery("fieldName").gt("fieldValue");
//大于等于
QueryBuilders.rangeQuery("fieldName").gte("fieldValue");
//小于
QueryBuilders.rangeQuery("fieldName").lt("fieldValue");
//小于等于
QueryBuilders.rangeQuery("fieldName").lte("fieldValue");


组合查询boolQuery()

QueryBuilders.boolQuery()
QueryBuilders.boolQuery().must();//文档必须完全匹配条件,相当于and
QueryBuilders.boolQuery().mustNot();//文档必须不匹配条件,相当于not
QueryBuilders.boolQuery().should();//至少满足一个条件,这个文档就符合should,相当于or



具体demo如下:

public void testBoolQuery() {
   NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.boolQuery()
                    .should(QueryBuilders.termQuery("title", "开发"))
                    .should(QueryBuilders.termQuery("title", "青春"))
                    .mustNot(QueryBuilders.termQuery("title", "潮头"))
            )
            .withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC))
            .withPageable(PageRequest.of(0, 50))
            .build();
    List<ArticleEntity> articleEntities = elasticsearchRestTemplate.queryForList(nativeSearchQuery, ArticleEntity.class);
    articleEntities.forEach(item -> System.out.println(item.toString()));
}

以上是查询title分词中,包含“开发”或者“青春”,但不能包含“潮头”的document;也可以多个must组合。

SortBuilders排序


上述demo中,我们使用了排序条件:

//按照id字段降序
.withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC))

注意排序时,有个坑,就是在以id排序时,比如降序,结果可能并不是我们想要的。因为根据id排序,es实际上会根据_id进行排序,但是_id是string类型的,排序后的结果会与整型不一致。

建议:在创建es的索引mapping时,将es的id和业务的id分开,比如业务id叫做myId:

@Id
@Field(type = FieldType.Long, store = true)
private Long myId;

@Field(type = FieldType.Text, store = true, analyzer = "ik_smart")
private String title;

@Field(type = FieldType.Text, store = true, analyzer = "ik_smart")
private String content;

这样,后续排序可以使用myId进行排序。

分页


使用如下方式分页:

@Test
public void testPage() {
    NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.matchQuery("title", "青春"))
            .withSort(SortBuilders.fieldSort("myId").order(SortOrder.DESC))
            .withPageable(PageRequest.of(0, 50))
            .build();
    AggregatedPage<ArticleEntity> page = elasticsearchRestTemplate.queryForPage(nativeSearchQuery, ArticleEntity.class);
    List<ArticleEntity> articleEntities = page.getContent();
    articleEntities.forEach(item -> System.out.println(item.toString()));
}

注意,如果不指定分页参数,es默认只显示10条。

高亮显示


查询title字段中的关键字,并高亮显示:

@Test
public void test() {
    String preTag = "<font color='#dd4b39'>";
    String postTag = "</font>";
    NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.matchQuery("title", "开发"))
            .withPageable(PageRequest.of(0, 50))
            .withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC))
            .withHighlightFields(new HighlightBuilder.Field("title").preTags(preTag).postTags(postTag))
            .build();

    AggregatedPage<ArticleEntity> page = elasticsearchRestTemplate.queryForPage(nativeSearchQuery, ArticleEntity.class,
            new SearchResultMapper() {
                @Override
                public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
                    List<ArticleEntity> chunk = new ArrayList<>();
                    for (SearchHit searchHit : response.getHits()) {
                        if (response.getHits().getHits().length <= 0) {
                            return null;
                        }
                        ArticleEntity article = new ArticleEntity();
                        article.setMyId(Long.valueOf(searchHit.getSourceAsMap().get("id").toString()));
                        article.setContent(searchHit.getSourceAsMap().get("content").toString());
                        HighlightField title = searchHit.getHighlightFields().get("title");
                        if (title != null) {
                            article.setTitle(title.fragments()[0].toString());
                        }
                        chunk.add(article);
                    }
                    if (chunk.size() > 0) {
                        return new AggregatedPageImpl<>((List<T>) chunk);
                    }
                    return null;
                }

                @Override
                public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
                    return null;
                }
            });


    List<ArticleEntity> articleEntities = page.getContent();
    articleEntities.forEach(item -> System.out.println(item.toString()));
}

结果:

 title=勇立潮头——总裁关心浦东<font color='#dd4b39'>开发</font>开放40, content=外交部

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fang·up·ad

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值