springboot操作elasticsearch进行复杂查询

复杂搜索

最近在做一个搜索的项目,里面关于很多字段的搜索,一开始在网上查了一下,发现网上大部分都是一些简单搜索。正常情况我们通常都是很多字段的搜索,类似下图,其中省份又要精确到市级,县级。所以我分享一下我接触到的一些复杂搜索。
在这里插入图片描述
我的环境是 springboot2.6.13,elk集群是8.2。就一个依赖

	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
	</dependency>

看了一下依赖项,发现好像es只支持到7.15。经过我的测试,虽然我的es集群是8.2,但是也可以进行搜索,只是后面地理位置搜索的时候遇到了一个小问题。
在这里插入图片描述
项目配置文件

spring.elasticsearch.uris=http://xxxxx1:9200,http://xxxxx2:9200,http://xxxxx3:9200,http://xxxxx4:9200,http://xxxxx5:9200
spring.elasticsearch.username=xxxxxxx
spring.elasticsearch.password=xxxxxxx

es的索引结构,这里我就简化了搜索不需要的字段。我没有使用中文分词器,因为搜索的关键词不是一个词语的时候搜不出来。

{
    "settings": {
        "number_of_replicas": 1, // 一个副本
        "number_of_shards": 5, // 5个分片
        "index.store.type": "niofs",
        "index.unassigned.node_left.delayed_timeout": "5m"
    },
    "mappings":{
        "properties":{
            "id":{
                "type":"long",
                "index": true
            },
            "company":{ // 公司名
                "type":"text",
                "index": true,
                "fields":{
                    "keyword":{
                        "type":"keyword"
                    }
                }
            },
            "intro":{ // 公司简介
                "type":"text",
                "index": true
            },
            "people":{ // 人名
                "type":"text",
                "index": true,
                "fields":{
                    "keyword":{
                        "type":"keyword"
                    }
                }
            },
            "addre":{ // 地址
                "type":"text",
                "index": true
            },
            "business_status":{ // 营业状态
                "type":"keyword"
            },
            "province_id":{ // 省级编码
                "type":"integer"
            },
            "province_name":{
                "type":"keyword"
            },
            "city_id":{ // 市级编码
                "type":"integer"
            },
            "city_name":{
                "type":"keyword"
            },
            "county_id":{ // 县级编码
                "type":"integer"
            },
            "county_name":{
                "type":"keyword"
            },
            "register":{ // 注册金额
                "type":"double"
            },
            "setup_time":{ // 成立时间
                "type":"date",
                "format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
            },
            "is_mobile":{ // 是否有手机号 1有0无
                "type": "keyword"
            },
            "is_else_mobile":{ // 是否有固话 1有0无
                "type": "keyword"
            },
            "is_email":{ // 是否有邮箱 1有0无
                "type": "keyword"
            },
            "location": {  // 地理位置
                "type": "geo_point"
            },
            "score_sort": { // 自定义排序规则,数据处理的时候给每条数据记录评分,给予数据权重
                "type": "byte"
            },
            "update_time":{
                "type":"date",
                "format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
            },
            "create_time":{
                "type":"date",
                "format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
            }
        }
    }
}
  • score_sort 这个字段,是我自定义排序规则的字段,很多时候单纯的ID倒序或者时间倒序满足不了复杂的排序需求。比如有些场景,svip的信息要放最前面,然后是vip的信息,然后是普通信息。又或者给了广告费之类的信息排前面。这时候就需要我们自定义排序规则,甚至定义多个用于排序的字段。
  • 用于搜索的信息(非原始数据),通常都是通过数据处理,然后在通过logstash或者自己写的脚本或者别的方法把数据同步过去。在数据处理的时候根据需求自定义排序规则,定义排序字段。
  • 这里我的规则的是(简化后的),搜索公司名的时候,我希望公司名里包含“有限公司”的排在前面,score_sort=3,然后是“公司”的score_sort=2,然后在是别的 店、商行之类的就score_sort=1。当然实际开发中这个排序规则要复杂很多,别的有重要意义的字段也要考虑进去排序规则中,具体根据需求来。

es索引实体类

import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.elasticsearch.common.geo.GeoPoint;
import org.springframework.data.annotation.Id;
import org.springframework.stereotype.Component;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import java.util.HashMap;

@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "xxxxx_search")
public class CompanyInfoSearchEs {

    @Id
    private Integer id;
    // @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    @Field(type = FieldType.Text)
    private String company;

    @Field(type = FieldType.Text)
    private String intro;

    private String industry;

    @Field(type = FieldType.Text)
    private String people;

    @Field(type = FieldType.Text)
    private String addre;

    private String business_status;
    private Integer province_id;
    private String province_name;
    private Integer city_id;
    private String city_name;
    private Integer county_id;
    private String county_name;
    private Double register;
    private String setup_time;
    private String is_mobile;
    private String is_else_mobile;
    private String is_email;
    private String img_path;
    private Byte score_sort;

    private GeoPoint location; // 地理位置经纬度信息
    private String update_time;
    private String create_time;

	// 后面处理搜索结果的时候用到
    public HashMap<String, Object> toHashMap() {
        HashMap<String, Object> map = new HashMap<>();
        map.put("id", id);
        map.put("company", company);
        map.put("intro", intro);
        map.put("people", people);
        map.put("addre", addre);
        map.put("businessStatus", business_status);
        map.put("register", register);
        map.put("setupTime", setup_time);
        map.put("isMobile", is_mobile);
        map.put("isElseMobile", is_else_mobile);
        map.put("isEmail", is_email);
        map.put("imgPath", img_path);
        map.put("location", location);
        return map;
    }


}

搜索接口

import com.xxxxx.dto.CompanyInfoDTO;
import com.xxxxx.service.impl.CompanyInfoSearchEsImp;
import com.xxxxx.utils.RetResponse;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import lombok.extern.slf4j.Slf4j;

import java.util.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;


@Slf4j
@RestController
@RequestMapping(value = "/searchCompany")
public class SearchCompany {

    @Resource
    private CompanyInfoSearchEsImp companyInfoSearchEsImp;

    /**
     * 企业信息搜索
     */
    @PostMapping(value = "/search")
    public RetResponse<HashMap<String, Object>> searchPc(@RequestBody @Valid CompanyInfoDTO requestMap, BindingResult result) {
        System.out.println("SearchCompanyInfo: " + requestMap.toString());

        if (result.hasErrors()) {
            log.warn(Objects.requireNonNull(result.getFieldError()).getDefaultMessage());
            return RetResponse.newError(202, result.getFieldError().getDefaultMessage());
        }

        HashMap<String, Object> retCompanyInfo = companyInfoSearchEsImp.searchCompanyInfo(requestMap);
		// 当精准搜索不到数据的时候切换模糊搜索在搜一次
        if (Integer.parseInt(retCompanyInfo.get("total").toString()) == 0) {
            requestMap.setPrecision(0);
            retCompanyInfo = companyInfoSearchEsImp.searchCompanyInfo(requestMap);
        }
        return RetResponse.newSuccess(retCompanyInfo);
    }
}

下面的调搜索接口时需要传的json参数,

{
    "searchType": 0, // 搜索类型 0:全部,1:企业搜索,2:法人搜索,3:公司地址搜索 默认传 0
    "precision": 1, // 搜索类型 1:精准搜索,0:模糊搜索 默认传 1
    "keyword": "电脑", // 关键词 --默认不传
    "province": [], // 省级编码  --默认不传
    "city": [], // 市级编码 --默认不传
    "county": [], // 县级编码 --默认不传

    "setupTimeUp": "2021-06-11", // 开始时间 格式 “2024-06-11” ---默认不传 开始时间和结束时间要么都不传,要么都传
    "setupTimeDown": "2024-09-01", // 结束时间 格式 “2024-06-11” ---默认不传

    "amountUp": 1, // 注册金额 单位 万 开始 ---默认不传 开始和结束要么都不传,要么都传 
    "amountDown": 10, // 注册金额 单位 万 结束 ---默认不传

    "status": "开业", // 开业 | 注销 | 吊销 | 迁出 | 停业 | 撤销 | 解散 | 个体转企业 | 其他 ---默认不传
    "isMobile": "1", // 是否有手机号:1是,0否 ---默认不传
    "isElseMobile": "0", // 是否有固话:1是 0否 ---默认不传
    "isEmail": "0", // 是否有邮箱:1是 0否 ---默认不传
    "page": 1 // 页数,一页50条数据,默认传 1

	// geo_distance查询要加这3个字段
    // "lon": 116.578756, // 经度 小数点控制在后6位数
    // "lat": 39.902787, // 纬度 小数点控制在后6位数
    // "distance": 10, // 距离 单位km 最大 30 km

	// geo_bounding_bo查询要加这4个字段
    // "upLon": 116.41088, // 左上角经度 小数点控制在后6位数
    // "upLat": 39.979349, // 左上角纬度 小数点控制在后6位数
    // "downLon": 116.578756, // 右下角经度 小数点控制在后6位数
    // "downLat": 39.902787, // 右下角纬度 小数点控制在后6位数

}

数据校验,校验调用者传过来的搜索参数

import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.util.List;

@Data
public class CompanyInfoDTO {

    @NotNull(message = "SearchType不能为空")
    @Range(min = 0, max = 3, message = "SearchType只能为0/1/2/3")
    private Integer searchType;

    @NotNull(message = "precision不能为空")
    @Range(min = 0, max = 1, message = "precision只能为0/1")
    private Integer precision;

    private String keyword;

    private List<Integer> province;
    private List<Integer> city;
    private List<Integer> county;

    @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}", message = "time type error")
    private String setupTimeUp;

    @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}", message = "time1 type error")
    private String setupTimeDown;

    @Range(min = 0, max = 100000000, message = "amount type error")
    private double amountUp;
    @Range(min = 0, max = 100000000, message = "amount type error")
    private double amountDown;

    @Pattern(regexp = "(开业|注销|吊销|迁出|停业|撤销|解散|个体转企业|其他)", message = "status error")
    private String status;

    @Pattern(regexp = "[01]", message = "isMobile error")
    private String isMobile;

    @Pattern(regexp = "[01]", message = "isElseMobile error")
    private String isElseMobile;

    @Pattern(regexp = "[01]", message = "isEmail error")
    private String isEmail;

    @Range(min = 0, max = 200, message = "page不能超过200")
    private Integer page;

	// 下面这几个字段是地理搜索用到的

	// geo_distance 用
    private Double lon; // 经度
    private Double lat; // 纬度
    private Integer distance; // 方圆多少距离 km

	// geo_bounding_box 用
    private Double upLon; // 左上角经度
    private Double upLat; // 左上角纬度
    private Double downLon; // 右下角经度
    private Double downLat; // 右下角纬度

}

mapper

package com.xxxx.mapper;
import com.meilianpc.entity.CompanyInfoSearchEs;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface CompanyInfoSearchEsRep extends ElasticsearchRepository<CompanyInfoSearchEs, String> {
}

service

package com.xxxxx.service;
import com.xxxxx.dto.CompanyInfoDTO;
import java.util.HashMap;

public interface CompanyInfoSearchEsSer{
    HashMap<String, Object> searchCompanyInfo(CompanyInfoDTO requestMap);
}

impl 业务核心代码,要完成复杂的搜索关键就在于拆分和组装搜索语句。

package com.xxxxx.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.GeoDistanceSortBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
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.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SourceFilter;
import org.springframework.stereotype.Service;

import com.xxxxx.entity.CompanyInfoSearchEs;
import com.xxxxx.converter.CompanyInfoCon;
import com.xxxxx.dto.CompanyInfoDTO;
import com.xxxxx.service.CompanyInfoSearchEsSer;

import javax.annotation.Resource;
import java.util.*;

@Slf4j
@Service
public class CompanyInfoSearchEsImp implements CompanyInfoSearchEsSer {

    @Resource
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Override
    public HashMap<String, Object> searchCompanyInfo(CompanyInfoDTO requestMap) {

        // 开始构建查询条件
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        // 如果有搜索关键词
        if (requestMap.getKeyword() != null && !requestMap.getKeyword().isEmpty()) {

            // 去除特殊符号
            requestMap.setKeyword(requestMap.getKeyword().replaceAll("[()()#$%^&*_+{}\":;|,.<>'/?~!@¥…—【】、;‘’“”:,。《》?「」『』〔〕\\-=\\]\\[]", ""));

            // searchType = 0 搜索全部字段 这里我搜索了 company, addre, people。可以更具实际情况搜索更多字段
            // field("company", 3) 3是优先级
            if (requestMap.getSearchType() == 0) {
                if (requestMap.getPrecision() == 0) {  // precision=0 模糊搜索
                    QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(requestMap.getKeyword())
                            .defaultOperator(Operator.AND)
                            .field("company", 3).field("addre").field("people").escape(true);
                    boolQueryBuilder.must(queryStringQueryBuilder);

                } else { // 精确搜索 在于 type=phrase
                    QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(requestMap.getKeyword())
                            .defaultOperator(Operator.AND)
                            .type(MultiMatchQueryBuilder.Type.PHRASE)
                            .field("company", 3).field("addre").field("people").escape(true);
                    boolQueryBuilder.must(queryStringQueryBuilder);
                }

            } else {
                // 搜索指定字段
                String searchZd = "company";

                if (requestMap.getSearchType() == 2) {
                    searchZd = "people";

                } else if (requestMap.getSearchType() == 3) {
                    searchZd = "addre";
                }

                if (requestMap.getPrecision() == 0) {  // precision=0 模糊搜索
                    QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(requestMap.getKeyword())
                            .defaultOperator(Operator.AND)
                            .field(searchZd).escape(true);
                    boolQueryBuilder.must(queryStringQueryBuilder);

                } else {
                    // 精确搜索
                    QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(requestMap.getKeyword())
                            .defaultOperator(Operator.AND)
                            .type(MultiMatchQueryBuilder.Type.PHRASE)
                            .field(searchZd).escape(true);
                    boolQueryBuilder.must(queryStringQueryBuilder);
                }
            }
        } else {
            // 如果没有搜索关键词,则搜索全部
            MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
            boolQueryBuilder.must(matchAllQueryBuilder);
        }

        // 省级编码
        if ((requestMap.getProvince() != null && !requestMap.getProvince().isEmpty())) {
            TermsQueryBuilder termsProvince = QueryBuilders.termsQuery("province_id", requestMap.getProvince());
            boolQueryBuilder.must(termsProvince);
        }

        // 市级编码
        if (requestMap.getCity() != null && !requestMap.getCity().isEmpty()) {
            TermsQueryBuilder termsCity = QueryBuilders.termsQuery("city_id", requestMap.getCity());
            boolQueryBuilder.must(termsCity);
        }

        // 县级编码
        if (requestMap.getCounty() != null && !requestMap.getCounty().isEmpty()) {
            TermsQueryBuilder termsCounty = QueryBuilders.termsQuery("county_id", requestMap.getCounty());
            boolQueryBuilder.must(termsCounty);
        }

        // 营业状态
        if (requestMap.getStatus() != null && !requestMap.getStatus().isEmpty()) {
            MatchPhraseQueryBuilder matchPhraseStatus = QueryBuilders.matchPhraseQuery("business_status", requestMap.getStatus());
            boolQueryBuilder.must(matchPhraseStatus);
        }

        // 手机号
        if (requestMap.getIsMobile() != null) {
            MatchPhraseQueryBuilder matchPhraseMobile = QueryBuilders.matchPhraseQuery("is_mobile", requestMap.getIsMobile());
            boolQueryBuilder.must(matchPhraseMobile);
        }

        // 其他号码
        if (requestMap.getIsElseMobile() != null) {
            MatchPhraseQueryBuilder matchPhraseElseMobile = QueryBuilders.matchPhraseQuery("is_else_mobile", requestMap.getIsElseMobile());
            boolQueryBuilder.must(matchPhraseElseMobile);
        }

        // 邮箱
        if (requestMap.getIsEmail() != null) {
            MatchPhraseQueryBuilder matchPhraseEmail = QueryBuilders.matchPhraseQuery("is_email", requestMap.getIsEmail());
            boolQueryBuilder.must(matchPhraseEmail);
        }

        // 成立时间
        if (requestMap.getSetupTimeUp() != null && !requestMap.getSetupTimeUp().isEmpty() && requestMap.getSetupTimeDown() != null
                && !requestMap.getSetupTimeDown().isEmpty()) {
            RangeQueryBuilder rangeTime = QueryBuilders.rangeQuery("setup_time").from(requestMap.getSetupTimeUp()).to(requestMap.getSetupTimeDown());
            boolQueryBuilder.filter(rangeTime);
        }

        // 注册金额
        if (requestMap.getAmountUp() >= 0 && requestMap.getAmountDown() > 1) {
            RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("register").from(requestMap.getAmountUp()).to(requestMap.getAmountDown());
            boolQueryBuilder.filter(rangeQueryBuilder);
        }

        // 页数
        int queryPage = requestMap.getPage();
        if (queryPage == 0) {
            requestMap.setPage(1);
        }
        // 对应es里的 from 和 size
        PageRequest pageRequest = PageRequest.of(queryPage - 1, 50);

        // 显示的字段,对应es _source
        String[] includes = {"id", "company", "intro", "industry", "people", "addre", "business_status", "province_id",
                "province_name", "city_id", "city_name", "county_id", "county_name", "register", "setup_time", "is_mobile",
                "is_else_mobile", "is_email", "img_path", "score_sort", "location"};
        SourceFilter sourceFilter = new FetchSourceFilter(includes, null);
        
        FieldSortBuilder twoSort;
        // 如果有搜索关键词,则根据搜索关键词的搜索得分排序
        if (requestMap.getKeyword() != null && !requestMap.getKeyword().isEmpty()) {
            twoSort = new FieldSortBuilder("_score").order(SortOrder.DESC);
        } else {
            // 如果没有搜索关键词,则根据更新时间排序
            twoSort = new FieldSortBuilder("update_time").order(SortOrder.DESC);
        }
        
        // 如果需要使用es的高亮
        // 自定义高亮html
        // HighlightBuilder highlightBuilder = new HighlightBuilder().preTags("<span style='color:red;'>").postTags("</span>");
        // 高亮字段
        // HighlightBuilder.Field highlightCompany = new HighlightBuilder.Field("company");
        // HighlightBuilder.Field highlightAddress = new HighlightBuilder.Field("address").fragmentSize(1);
        // HighlightBuilder.Field highlightIntro = new HighlightBuilder.Field("intro").fragmentSize(1);
        
        
        // 构建查询
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQueryBuilder)
                .withSourceFilter(sourceFilter)
                // 排序,这里我先自定义排序字段排序,然后再根据第二个排序规则排序
                .withSorts(new FieldSortBuilder("score_sort").order(SortOrder.DESC), twoSort)

                // 如果使用es了的高亮
                // .withHighlightFields(highlightCompany, highlightAddress, highlightIntro) // 高亮字段
                // .withHighlightBuilder(highlightBuilder) // 自定义高亮html
                
                .withPageable(pageRequest)
                .build();

        // 执行查询
        SearchHits<CompanyInfoSearchEs> searchHits = elasticsearchRestTemplate.search(searchQuery, CompanyInfoSearchEs.class);

        List<HashMap<String, Object>> resultList = new ArrayList<>();

        // 处理搜索结果
        for (SearchHit<CompanyInfoSearchEs> searchHit : searchHits) {
            HashMap<String, Object> companyInfo = searchHit.getContent().toHashMap(); // 实体类写的toHashMap方法,转换成HashMap

            // 我没有使用es的高亮,自己处理高亮
            if (requestMap.getKeyword() != null && !requestMap.getKeyword().isEmpty()) {
                if (companyInfo.get("company") != null) {
                    companyInfo.replace("company", CompanyInfoCon.dispHighlight(companyInfo.get("company").toString(), requestMap.getKeyword()));
                }
                if (companyInfo.get("intro") != null) {
                    companyInfo.replace("intro", CompanyInfoCon.dispHighlight(companyInfo.get("intro").toString(), requestMap.getKeyword()));
                }
                if (companyInfo.get("people") != null) {
                    companyInfo.replace("people", CompanyInfoCon.dispHighlight(companyInfo.get("people").toString(), requestMap.getKeyword()));
                }
                if (companyInfo.get("addre") != null) {
                    companyInfo.replace("addre", CompanyInfoCon.dispHighlight(companyInfo.get("addre").toString(), requestMap.getKeyword()));
                }
            }

            // 处理经纬度
            if (searchHit.getContent().getLocation() != null) {
                companyInfo.put("location", new HashMap<String, Double>() {{
                    put("lat", searchHit.getContent().getLocation().getLat());
                    put("lon", searchHit.getContent().getLocation().getLon());
                }});
            }
            resultList.add(companyInfo);
        }

        // 构建返回结果
        HashMap<String, Object> result = new HashMap<>();
        result.put("data", resultList);
        result.put("total", searchHits.getTotalHits());
        return result;
    }


}



  • .escape(true) 表示对查询字符串中的特殊字符进行转义。这样可以避免这些特殊字符被误解析为查询语法的一部分,从而确保查询的准确性。例如,如果你想搜索包含"+“或”-"等符号的文本,如果不使用escape: true,这些符号可能会被解释为查询操作符(如AND、OR等)。而使用escape: true后,这些符号将被当作普通字符进行搜索。

上面一套下来其实执行了es的这个语句,要看es执行的语句,在项目配置文件中加上 logging.level.tracer=TRACE

{
    "from": 0,
    "size": 50,
    "query": {
        "bool": {
            "must": [
                {
                    "query_string": {
                        "query": "电脑",
                        "fields": [
                            "addre^1.0",
                            "company^3.0",
                            "people^1.0"
                        ],
                        "type": "phrase",
                        "default_operator": "and",
                        "escape": true
                    }
                },
                {
                    "terms": {
                        "province_id": [
                            440000,
                            450000
                        ]
                    }
                },
                {
                    "terms": {
                        "city_id": [
                            440100
                        ]
                    }
                },
                {
                    "terms": {
                        "county_id": [
                            440106
                        ]
                    }
                },
                {
                    "match_phrase": {
                        "business_status": {
                            "query": "开业"
                        }
                    }
                },
                {
                    "match_phrase": {
                        "is_mobile": {
                            "query": "1"
                        }
                    }
                },
                {
                    "match_phrase": {
                        "is_else_mobile": {
                            "query": "0"
                        }
                    }
                },
                {
                    "match_phrase": {
                        "is_email": {
                            "query": "0"
                        }
                    }
                }
            ],
            "filter": [
                {
                    "range": {
                        "setup_time": {
                            "from": "2021-06-11",
                            "to": "2024-09-01",
                            "include_lower": true,
                            "include_upper": true,
                            "boost": 1
                        }
                    }
                },
                {
                    "range": {
                        "register": {
                            "from": 1,
                            "to": 10,
                            "include_lower": true,
                            "include_upper": true,
                            "boost": 1
                        }
                    }
                }
            ],
            "adjust_pure_negative": true,
            "boost": 1
        }
    },
    "version": true,
    "explain": false,
    "_source": {
        "includes": [
            "id",
            "company",
            "intro",
            "industry",
            "people",
            "addre",
            "business_status",
            "province_id",
            "province_name",
            "city_id",
            "city_name",
            "county_id",
            "county_name",
            "register",
            "setup_time",
            "is_mobile",
            "is_else_mobile",
            "is_email",
            "img_path",
            "score_sort",
            "location"
        ],
        "excludes": []
    },
    "sort": [
        {
            "score_sort": {
                "order": "desc"
            }
        },
        {
            "_score": {
                "order": "desc"
            }
        }
    ]
}

自定义高亮处理类

import org.springframework.stereotype.Component;

import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.Matcher;


@Component
public class CompanyInfoCon {

    // 处理高亮
    public static String dispHighlight(String higKeyword, String keyword) {
        if (keyword == null || keyword.isEmpty() || higKeyword == null || higKeyword.isEmpty()) {
            return higKeyword;
        }

        char[] keywordArr = keyword.toCharArray();
        StringJoiner charString = new StringJoiner("|");
        for (char c : keywordArr) {
            charString.add(String.valueOf(c));
        }
        Pattern compile = Pattern.compile(charString.toString());
        Matcher matcher = compile.matcher(higKeyword);
        String retHigKeyword = matcher.replaceAll(matchResult -> "<span style='color:red;'>" + matchResult.group() + "</span>");

        retHigKeyword = retHigKeyword.replaceAll("</span><span style='color:red;'>", "");
        return retHigKeyword;
    }

}

自定义处理高亮:比如我的关键词是 “电脑”,处理出来后会变成。
<span style='color:red;'></span><span style='color:red;'></span>
如果用es处理高亮出来也要这样处理
所以有了最后一行代码
retHigKeyword = retHigKeyword.replaceAll("</span><span style='color:red;'>", "");

之所以自己处理高亮处理成单个字高亮在合并高亮,是因为如果模糊搜索,搜索电脑,可能出来的结果就是“电xxxx脑” 或者 “脑xxxx电”。
用整词处理就会有问题。

复杂搜索+地理位置

地理位置搜索我用到了 geo_distancegeo_bounding_box
官网地址
大白话
geo_distance: 在地图上点一个点,然后方圆多少距离内的信息
geo_bounding_box: 在地图上框一个四边形,这个矩形内的信息

结合 geo_distance 查询

调接口时多传几个json参数
在这里插入图片描述
geo_distance 查询

		...
		...
		// 在 上面 impl 业务核心代码文件中, 构建查询前 加入以下代码
   
        if (requestMap.getDistance() > 30) { // 以中心点扩散多少距离,这里我限制了最多30km
            requestMap.setDistance(30);
        }
        boolQueryBuilder.filter(
                new GeoDistanceQueryBuilder("location")
                	.point(requestMap.getLat(), requestMap.getLon()) // 经纬度 注意经纬度前后顺序
                    .distance(requestMap.getDistance(), // 方圆多少距离
                    DistanceUnit.KILOMETERS) // 单位 km
                   		.geoDistance(GeoDistance.ARC) // 模式 arc 更精准,速度慢点。 plane 更快,精准略差,越靠近地球两级精准度越差,越靠近赤道精准度越高
        );

		// 以距离坐标中心点距离排序,单位米。到时候看查询结果sort字段就可以知道距离中心点多远
        GeoDistanceSortBuilder locationSort = new GeoDistanceSortBuilder(
                "location", requestMap.getLat(), requestMap.getLon()
        ).order(SortOrder.ASC).unit(DistanceUnit.METERS).geoDistance(GeoDistance.ARC);

		// 构建查询
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQueryBuilder)
                .withSourceFilter(sourceFilter)
                // 先自定义排序字段排序,然后再根据第二个排序规则排序
                .withSorts(new FieldSortBuilder("score_sort").order(SortOrder.DESC), locationSort)

                // 如果使用es了的高亮
                // .withHighlightFields(highlightCompany, highlightAddress, highlightIntro) // 高亮字段
                // .withHighlightBuilder(highlightBuilder) // 自定义高亮html
                
                .withPageable(pageRequest)
                .build();

        // 执行查询
        SearchHits<CompanyInfoSearchEs> searchHits = elasticsearchRestTemplate.search(searchQuery, CompanyInfoSearchEs.class);
		// 处理查询结果
		...
		...

地理位置查询就是在上面的那些查询中多加一个filter

结合 geo_bounding_box 查询

要多传几个参数,通常精确到小数点后6完全够用了
在这里插入图片描述

和上面结合geo_distance一样,多加一个filter。

		...
		...
		// 在 上面 impl 业务核心代码文件中, 构建查询前 加入以下代码
        GeoBoundingBoxQueryBuilder locationBox = QueryBuilders.geoBoundingBoxQuery("location")
                .setCorners( // 注意经纬度顺序
                        new GeoPoint(requestMap.getUpLat(), requestMap.getUpLon()),
                        new GeoPoint(requestMap.getDownLat(), requestMap.getDownLon())
                );
        boolQueryBuilder.filter(locationBox);

    	FieldSortBuilder twoSort;
        // 如果有搜索关键词,则根据搜索关键词的搜索得分排序
        if (requestMap.getKeyword() != null && !requestMap.getKeyword().isEmpty()) {
            twoSort = new FieldSortBuilder("_score").order(SortOrder.DESC);
        } else {
            // 如果没有搜索关键词,则根据更新时间排序
            twoSort = new FieldSortBuilder("update_time").order(SortOrder.DESC);
        }

		// 构建查询
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQueryBuilder)
                .withSourceFilter(sourceFilter)
                // 先自定义排序字段排序,然后再根据第二个排序规则排序
                .withSorts(new FieldSortBuilder("score_sort").order(SortOrder.DESC), twoSort)

                // 如果使用es了的高亮
                // .withHighlightFields(highlightCompany, highlightAddress, highlightIntro) // 高亮字段
                // .withHighlightBuilder(highlightBuilder) // 自定义高亮html
                
                .withPageable(pageRequest)
                .build();

        // 执行查询
        SearchHits<CompanyInfoSearchEs> searchHits = elasticsearchRestTemplate.search(searchQuery, CompanyInfoSearchEs.class);
		// 处理查询结果
		...
		...

问题

我原以为会顺顺利利,结果一查询,报错了 Elasticsearch exception [type=parsing_exception, reason=failed to parse [geo_bounding_box] query. unexpected field [type]]

Servlet.service() for servlet [dispatcherServlet] in context with path [/apipro] threw exception [Request processing failed; nested exception is RestStatusException{status=400} 
org.springframework.data.elasticsearch.RestStatusException: Elasticsearch exception [type=x_content_parse_exception, reason=[1:598] [bool] failed to parse field [filter]]; nested exception is ElasticsearchStatusException[Elasticsearch exception [type=x_content_parse_exception, reason=[1:598] [bool] failed to parse field [filter]]]; nested: ElasticsearchException[Elasticsearch exception [type=parsing_exception, reason=failed to parse [geo_bounding_box] query. unexpected field [type]]];] with root cause
org.elasticsearch.ElasticsearchException: Elasticsearch exception [type=parsing_exception, reason=failed to parse [geo_bounding_box] query. unexpected field [type]]

原来是type这个字段在es7.14版本的时候就开始废弃了。但是我明明没有设置type,点击QueryBuilders.geoBoundingBoxQuery("location")进去发现不设置type他会给我默认设置一个值。

在这里插入图片描述
网上查了很多质料和问AI之类的都无果,都说版本不兼容之类的。升级到springboot3.x,但那样项目大部分地方都要重写很是费时费力。
研究了一下发现不管怎么样他都会带上这个type字段。在kibana上发现只要把type字段设置为null就可以顺利执行语句。而看了QueryBuilders.geoBoundingBoxQuery源码又发现他根本不接受设置为null。
在这里插入图片描述

解决办法

我的解决办法就是 取代 GeoBoundingBoxQueryBuilder 类,他不支持null就取代他支持null
在这里插入图片描述
他继承了 AbstractQueryBuilder 。我们也写个类继承他。把GeoBoundingBoxQueryBuilder里面的所有东西复制过来,类名改个名字MyGeoBoundingBoxQueryBuilder。然后把这个类里所有返回 GeoBoundingBoxQueryBuilder 的改为 返回MyGeoBoundingBoxQueryBuilder
并把private GeoExecType type = DEFAULT_TYPE; 改为 = null;
在这里插入图片描述

点击上面写好的 QueryBuilders.geoBoundingBoxQuery("location") 中的 geoBoundingBoxQuery
会点到这里 QueryBuilders 文件里的 geoBoundingBoxQuery 方法

public static GeoBoundingBoxQueryBuilder geoBoundingBoxQuery(String name) {
    return new GeoBoundingBoxQueryBuilder(name);
}

把这个方法复制到我们写的 MyGeoBoundingBoxQueryBuilder 类里
改成

public static MyGeoBoundingBoxQueryBuilder geoBoundingBoxQuery(String name) {
    return new MyGeoBoundingBoxQueryBuilder(name);
}

回到 结合 geo_bounding_box 查询

		// 在 上面 impl 业务核心代码文件中, 构建查询前 加入以下代码
        GeoBoundingBoxQueryBuilder locationBox = QueryBuilders.geoBoundingBoxQuery("location")
                .setCorners( // 注意经纬度顺序
                        new GeoPoint(requestMap.getUpLat(), requestMap.getUpLon()),
                        new GeoPoint(requestMap.getDownLat(), requestMap.getDownLon())
                );
        boolQueryBuilder.filter(locationBox);

		把上面这段改为

        MyGeoBoundingBoxQueryBuilder locationBox = MyGeoBoundingBoxQueryBuilder.geoBoundingBoxQuery("location")
                .setCorners(
                        new GeoPoint(requestMap.getUpLat(), requestMap.getUpLon()),
                        new GeoPoint(requestMap.getDownLat(), requestMap.getDownLon())
                );
        locationBox.type();
        boolQueryBuilder.filter(locationBox);

在调接口就可以查询成功了,通过打印出来的日志可以看到type设置为了null
在这里插入图片描述
好了,以上就是我的分享。如果哪里有错误或者你有更好的方法欢迎评论区指正和讨论。

### 回答1: Spring Boot 集成 Elasticsearch 时,如果遇到转义字符查询不出现的问题,可能是因为你的查询语句中使用了转义字符,但是 Elasticsearch 没有正确地处理它们。 解决方法有以下几种: 1. 在查询语句中使用原始字符串,而不是转义字符。 2. 使用 Elasticsearch 的 query_string 查询。 3. 使用 Elasticsearch 提供的特殊字符转义机制。 4. 在 Elasticsearch 的映射中配置字段为 keyword 类型,这样查询时就不会进行分词,避免了转义字符的问题。 5. 使用 Elasticsearch 的 filter 进行过滤,而不是 query 进行查询. ### 回答2: 在Spring Boot集成Elasticsearch时,当遇到一些特殊字符时可能会出现查询不出来的情况,主要的原因是这些字符在查询语句中需要进行转义处理。 在Elasticsearch中,一些特殊字符或具有特殊意义的字符需要进行转义,包括但不限于以下情况: 1. 特殊字符:例如加号(+)、减号(-)、逗号(,)、冒号(:)等,在查询时需要对这些字符进行转义,可以使用反斜杠(\)进行转义。 例如,如果想要查询包含加号(+)的文档,可以使用如下查询语句: ``` GET /index/_search { "query": { "match_phrase": { "field": "\\+" } } } ``` 2. 保留字符:Elasticsearch中有一些保留字符,例如AND、OR、NOT等,这些字符在查询时需要进行转义。可以使用双引号("")将这些字符包裹起来进行转义。 例如,如果想要查询包含"AND"的文档,可以使用如下查询语句: ``` GET /index/_search { "query": { "match_phrase": { "field": "\"AND\"" } } } ``` 需要注意的是,在进行转义时,要根据查询的具体字段类型和实际需求进行适当的转义处理,以确保查询的准确性。另外,还可以根据具体情况使用正则表达式查询或其他查询方式来解决特殊字符查询不出来的问题。 ### 回答3: 在Spring Boot集成Elasticsearch中,有时候会遇到一些特殊字符的查询问题。这些特殊字符经常被称为转义字符,用于在搜索过程中指定特定的匹配规则。 普通数据查询很简单,可以直接使用Elasticsearch查询API进行查询。但是对于包含转义字符的查询,需要特殊处理。 一种常见的情况是查询含有通配符的字符,比如*、?等。Elasticsearch默认会将这些字符视为正则表达式的特殊字符,而不是普通字符。为了正确查询这些字符,可以使用转义字符\来取消其特殊含义,比如将*转义为\*。 另一种情况是查询含有特殊字符的字段,比如包含\、+等字符的字段。这些特殊字符在查询时需要进行转义处理,否则会导致查询失败。可以使用查询的"query_string"查询方法,并在查询字符串中对特殊字符进行转义,比如将\转义为\\。 除了以上情况,还可能会遇到其他特殊字符的查询问题,比如查询包含空格、引号等字符的数据。在这种情况下,可以尝试使用Elasticsearch提供的内置处理方法,比如使用"match_phrase"查询方法进行精确匹配。 总之,当遇到一些转义字符查询无法正常查询的情况时,我们需要对查询字符串进行适当的处理和转义,以确保能够正确匹配到含有特殊字符的数据。这样才能保证在Spring Boot集成Elasticsearch时,能够将包含转义字符的数据正确地查询出来。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值