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还没实现,这个待笔者再研究研究,放到之后的文章中再做讲解,希望对你有所帮助。