Bootstrap

ElasticSearch学习笔记把:Springboot整合ES(二)

一、前言

上一篇文章中我们学习了ES中的Term级别的查询,包括 term、terms、terms_set、rang等,今天我们使用Java代码实现一遍上述的查询。

二、项目依赖

POM

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>springboot-es</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>springboot-es</name>
  <description>springboot-es</description>
  <properties>
    <java.version>17</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.7.6</spring-boot.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    <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.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>


  </dependencies>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>17</source>
          <target>17</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring-boot.version}</version>
        <configuration>
          <mainClass>com.example.SpringbootEsApplication</mainClass>
          <skip>true</skip>
        </configuration>
        <executions>
          <execution>
            <id>repackage</id>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

</project>

配置文件:

server:
  port: 8088
spring:
  elasticsearch:
    uris: http://localhost:9200

三、代码编写

1、term查询

案例:查询城市位于北京的酒店

GET /hotel/_search
{
  "query": {
    "term": {
      "city": {
        "value": "北京"
      }
    }
  }
}
   @Resource
    private ElasticsearchOperations elasticsearchOperations;

    /**
     * 根据城市搜索酒店,使用term匹配
     */
    @GetMapping("/termQueryByCity")
    public List<Hotel> termQueryByCity(String city) {
        Criteria criteria = Criteria.where("city").is(city);
        Query query = new CriteriaQuery(criteria);
        final SearchHits<Hotel> search = elasticsearchOperations.search(query, Hotel.class);
        //没有命中任何文档
        if (!search.hasSearchHits()) {
            return new ArrayList<>();
        }
        final List<SearchHit<Hotel>> searchHits = search.getSearchHits();
        return searchHits.stream().map(SearchHit::getContent).collect(Collectors.toList());
    }

2、terms查询

案例:查询城市为北京或者青岛的酒店

GET /hotel/_search
{
  "query": {
    "terms": {
      "city": [
        "北京",
        "青岛"
      ]
    }
  }
}
   /**
     * 根据多个城市搜索,使用terms搜索
     */
    @GetMapping("/termsQueryByCities")
    public List<Hotel> termsQueryByCities(String cities) {
        if (!StringUtils.hasText(cities)) {
            return new ArrayList<>();
        }
        Criteria criteria = Criteria.where("city").in(Arrays.asList(cities.split(",")));
        Query query = new CriteriaQuery(criteria);
        final SearchHits<Hotel> search = elasticsearchOperations.search(query, Hotel.class);
        //没有命中任何文档
        if (!search.hasSearchHits()) {
            return new ArrayList<>();
        }
        final List<SearchHit<Hotel>> searchHits = search.getSearchHits();
        return searchHits.stream().map(SearchHit::getContent).collect(Collectors.toList());
    }
在这里我们通过接口传入多个城市,并且使用in作为搜索条件构建Query
3、terms_set

案例:搜索符合C++和Python的程序员,且需要符合最低满足个数

GET /programmer/_search
{
  "query": {
    "terms_set": {
      "skill": {
        "terms": [ "C++", "Python"],
        "minimum_should_match_field":"required_matches"
      }
    }
  }
}

由于没有直接的API可以支持terms_set 所以这里这里需直接编写DSL脚本,好在SpringData为我们提供了StringQuery 用于通过更灵活的查询方式。

 /**
     * terms_set 查询
     */
    @GetMapping("/termsSetQueryBySkills")
    public List<Programmer> termsSetQueryBySkills(String skills) {
        if (!StringUtils.hasText(skills)) {
            return new ArrayList<>();
        }
        final String join = Arrays.stream(skills.split(","))
            .map(skill -> "\"" + skill + "\"")
            .collect(Collectors.joining(", "));

        String dslQuery = """
                {
                  "terms_set": {
                    "skill": {
                      "terms": [%s],
                      "minimum_should_match_field": "required_matches"
                    }
                  }
                }
            """.formatted(join);
        Query stringQuery = new StringQuery(dslQuery);
        final SearchHits<Programmer> search = elasticsearchOperations.search(stringQuery, Programmer.class);
        if (!search.hasSearchHits()) {
            return new ArrayList<>();
        }
        return search.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
    }
简单的来说,StringQuery允许我们直接通过字符串的形式提供DSL用于搜索。
4、range查询

案例:查询价格区间在300~500之间的酒店

GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 300,
        "lte": 500
      }
    }
  }
}
这个DSL翻译成SQL 就是 select * from hotel where price >= 300 and price <=500

 /**
     * 范围搜索
     */
    @GetMapping("/rangeQuery")
    public List<Hotel> rangeQuery(Integer startPrice,Integer endPrice){
        Criteria criteria = Criteria.where("price").between(startPrice,endPrice);
        Query query = new CriteriaQuery(criteria);
        final SearchHits<Hotel> search = elasticsearchOperations.search(query, Hotel.class);
        if(!search.hasSearchHits()){
            return new ArrayList<>();
        }
        return search.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
    }
5、prefix查询

案例:搜索姓张的程序员

GET /programmer/_search
{
  "query": {
    "prefix": {
      "name": {
        "value": "张"
      }
    }
  }
}

    /**
     * 前缀匹配
     */
    @GetMapping("/prefixQuery")
    public List<Programmer> prefixQuery(String prefix){
        Criteria criteria = Criteria.where("name").startsWith(prefix);
        Query query = new CriteriaQuery(criteria);
        final SearchHits<Programmer> search = elasticsearchOperations.search(query, Programmer.class);
        if(!search.hasSearchHits()){
            return new ArrayList<>();
        }
        return search.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
    }
6、IDS查询
GET /hotel/_search
{
  "query": {
    "ids" : {
      "values" : ["001","002"]
    }
  }
}


    /**
     * ID批量查询
     */
    @GetMapping("/idsQuery")
    public List<Hotel> idsQuery(String ids){
        Criteria criteria = Criteria.where("id").in(Arrays.asList(ids.split(",")));
        Query query = new CriteriaQuery(criteria);
        final SearchHits<Hotel> search = elasticsearchOperations.search(query, Hotel.class);
        if(!search.hasSearchHits()){
            return new ArrayList<>();
        }
        return search.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
    }

四、完整代码

package com.example.controller;

import com.example.entity.Hotel;
import com.example.entity.Programmer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author hardy(叶阳华)
 * @Description
 * @Date 2024/11/29 10:48
 * @Modified By: Copyright(c) cai-inc.com
 */
@RestController
@RequestMapping(value = "/es/query")
public class EsQueryController {

    @Resource
    private ElasticsearchOperations elasticsearchOperations;

    /**
     * 根据城市搜索酒店,使用term匹配
     */
    @GetMapping("/termQueryByCity")
    public List<Hotel> termQueryByCity(String city) {
        Criteria criteria = Criteria.where("city").is(city);
        Query query = new CriteriaQuery(criteria);
        final SearchHits<Hotel> search = elasticsearchOperations.search(query, Hotel.class);
        //没有命中任何文档
        if (!search.hasSearchHits()) {
            return new ArrayList<>();
        }
        final List<SearchHit<Hotel>> searchHits = search.getSearchHits();
        return searchHits.stream().map(SearchHit::getContent).collect(Collectors.toList());
    }

    /**
     * 根据多个城市搜索,使用terms搜索
     */
    @GetMapping("/termsQueryByCities")
    public List<Hotel> termsQueryByCities(String cities) {
        if (!StringUtils.hasText(cities)) {
            return new ArrayList<>();
        }
        Criteria criteria = Criteria.where("city").in(Arrays.asList(cities.split(",")));
        Query query = new CriteriaQuery(criteria);
        final SearchHits<Hotel> search = elasticsearchOperations.search(query, Hotel.class);
        //没有命中任何文档
        if (!search.hasSearchHits()) {
            return new ArrayList<>();
        }
        final List<SearchHit<Hotel>> searchHits = search.getSearchHits();
        return searchHits.stream().map(SearchHit::getContent).collect(Collectors.toList());
    }

    /**
     * terms_set 查询
     */
    @GetMapping("/termsSetQueryBySkills")
    public List<Programmer> termsSetQueryBySkills(String skills) {
        if (!StringUtils.hasText(skills)) {
            return new ArrayList<>();
        }
        final String join = Arrays.stream(skills.split(","))
            .map(skill -> "\"" + skill + "\"")
            .collect(Collectors.joining(", "));

        String dslQuery = """
                {
                  "terms_set": {
                    "skill": {
                      "terms": [%s],
                      "minimum_should_match_field": "required_matches"
                    }
                  }
                }
            """.formatted(join);
        Query stringQuery = new StringQuery(dslQuery);
        final SearchHits<Programmer> search = elasticsearchOperations.search(stringQuery, Programmer.class);
        if (!search.hasSearchHits()) {
            return new ArrayList<>();
        }
        return search.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
    }

    /**
     * 判断是否存在某个字段
     */
    @GetMapping("/exist")
    public boolean exist(String title) {
        // 构建 exists 查询
        NativeSearchQuery query = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.existsQuery(title))
            .build();
        final SearchHits<Programmer> search = elasticsearchOperations.search(query, Programmer.class);
        return search.hasSearchHits();
    }

    /**
     * 范围搜索
     */
    @GetMapping("/rangeQuery")
    public List<Hotel> rangeQuery(Integer startPrice,Integer endPrice){
        Criteria criteria = Criteria.where("price").between(startPrice,endPrice);
        Query query = new CriteriaQuery(criteria);
        final SearchHits<Hotel> search = elasticsearchOperations.search(query, Hotel.class);
        if(!search.hasSearchHits()){
            return new ArrayList<>();
        }
        return search.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
    }

    /**
     * 前缀匹配
     */
    @GetMapping("/prefixQuery")
    public List<Programmer> prefixQuery(String prefix){
        Criteria criteria = Criteria.where("name").startsWith(prefix);
        Query query = new CriteriaQuery(criteria);
        final SearchHits<Programmer> search = elasticsearchOperations.search(query, Programmer.class);
        if(!search.hasSearchHits()){
            return new ArrayList<>();
        }
        return search.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
    }

    /**
     * ID批量查询
     */
    @GetMapping("/idsQuery")
    public List<Hotel> idsQuery(String ids){
        Criteria criteria = Criteria.where("id").in(Arrays.asList(ids.split(",")));
        Query query = new CriteriaQuery(criteria);
        final SearchHits<Hotel> search = elasticsearchOperations.search(query, Hotel.class);
        if(!search.hasSearchHits()){
            return new ArrayList<>();
        }
        return search.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
    }
}

package com.example.entity;

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;

/**
 * @Author hardy(叶阳华)
 * @Description
 * @Date 2024/11/29 10:59
 * @Modified By: Copyright(c) cai-inc.com
 */
@Data
@Document(indexName = "programmer",createIndex = false)
public class Programmer {

    @Id
    private String id;

    @Field(type = FieldType.Keyword)
    private String name;

    @Field(type = FieldType.Keyword)
    private String skill;

    @Field(type = FieldType.Integer,value = "required_matches")
    private Integer requiredMatches;


}

package com.example.entity;

import com.fasterxml.jackson.annotation.JsonFormat;
import java.math.BigDecimal;
import java.util.Date;
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;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;

/**
 * @Author hardy(叶阳华)
 * @Description
 * @Date 2024/11/26 14:06
 * @Modified By: Copyright(c) cai-inc.com
 */
@Data
@Document(indexName = "hotel", createIndex = false)
public class Hotel {

    /**
     * ID
     */
    @Id
    private String id;

    /**
     * 标题:text类型,使用ik_max_word作为分词器
     */
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
    private String title;

    /**
     * 所在城市:所在城市没必要分词
     */
    @Field(type = FieldType.Keyword)
    private String city;

    /**
     * 价格
     */
    @Field(type = FieldType.Double)
    private BigDecimal price;

    /**
     * 便利措施
     */
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
    private String amenities;

    /**
     * 创建时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @Field(value = "create_time", type = FieldType.Date, format = {}, pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    /**
     * 是否满员
     */
    @Field(value = "full_room", type = FieldType.Boolean)
    private Boolean fullRoom;


    /**
     * 位置
     */
    @GeoPointField
    private GeoPoint location;

    @Field(type = FieldType.Integer)
    private Integer praise;
}

五、结束语

这次我们用Java代码把主要的Term级别的查询实现了一遍,当然还有一个比较复杂的Fuzzy还没实现,这个待笔者再研究研究,放到之后的文章中再做讲解,希望对你有所帮助。

;