后端开发13.商品搜索模块

概述

简介

商品搜索引擎采用的是elasticsearch

数据库设计

创建商品索引
PUT /goods
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "ik_pinyin": {
          "tokenizer": "ik_smart",
          "filter": "pinyin_filter"
        },
        "tag_pinyin": {
          "tokenizer": "keyword",
          "filter": "pinyin_filter"
        }
      },
      "filter": {
        "pinyin_filter": {
          "type": "pinyin",
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "remove_duplicated_term": true
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "goodsId": {
        "type": "integer",
        "index": true
      },
      "goodsHeadImg": {
        "type": "keyword",
        "index": true
      },
      "goodsName": {
        "type": "text",
        "index": true,
        "analyzer": "ik_pinyin",
        "search_analyzer": "ik_smart"
      },
      "goodsCaption": {
        "type": "text",
        "index": true,
        "analyzer": "ik_pinyin",
        "search_analyzer": "ik_smart"
      },
      "goodsPrice": {
        "type": "text",
        "index": true
      },
      "tags": {
        "type": "completion",
        "analyzer": "tag_pinyin",
        "search_analyzer": "tag_pinyin"
      },
      "goodsBrand": {
        "type": "keyword",
        "index": true
      },
      "goodsType": {
        "type": "keyword",
        "index": true
      },
      "goodsSpecification": {
        "properties": {
          "goodsSpecificationName": {
            "type": "keyword",
            "index": true
          },
          "goodsSpecificationOption": {
            "type": "keyword",
            "index": true
          }
        }
      }
    }
  }
}

实体类

GoodsES
/**
 * 在ES中存储的商品实体类【手动创建索引】
 */
@Document(indexName = "goods", createIndex = false)
@Data
public class GoodsES implements Serializable {
    @Id
    @Field
    private Integer goodsId;
    @Field
    private String goodsHeadImg;//头图
    @Field
    private String goodsName;//商品名
    @Field
    private String goodsCaption;//副标题
    @Field
    private String goodsPrice;//价格

    @CompletionField//标注为自动补全字段
    private List<String> tags; // 关键字
    @Field
    private String goodsBrand; // 品牌名
    @Field
    private List<String> goodsType; // 类型名
    @Field
    private Map<String, List<String>> goodsSpecification; // 规格,键为规格项,值为规格值


}

封装类

GoodsSearchParam
/**
 * 商品搜索条件
 */
@Data
public class GoodsSearchParam implements Serializable {
    private String keyword; // 关键字
    private String brand; // 品牌名
    private Double highPrice; //最高价
    private Double lowPrice; //最低价
    private Map<String, String> specificationOption; // 规格map, 键:规格名,值:规格值
    private String sortFiled; //排序字段 NEW:新品 PRICE:价格
    private String sort; //排序方式 ASC:升序 DESC:降序
    private Integer page; //页码
    private Integer size; //每页条数
}
GoodsSearchResult
/**
 * 商品搜索结果
 */
@Data
public class GoodsSearchResult implements Serializable {
    private Page<GoodsES> goodsPage; // 页面商品信息
    private GoodsSearchParam goodsSearchParam; // 搜索条件回显

    private Set<String> brands; // 和商品有关的品牌列表
    private Set<String> productType; // 和商品有关的类别列表
    // 和商品有关的规格列表,键:规格名,值:规格集合
    private Map<String, Set<String>> specifications;
}

服务层

SearchService
/**
 * 商品es搜索服务
 */
public interface SearchService {
    //向ES同步商品数据
    void syncGoodsToES(GoodsDesc goodsDesc);
    //删除ES中的商品数据
    void delete(Integer id);
    //自动补全关键字
    List<String> autoSuggest(String keyword);
    //搜索商品
    GoodsSearchResult search(GoodsSearchParam goodsSearchParam);

}

SearchServiceImpl
package jkw.service.impl;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jkw.pojo.GoodsES;
import jkw.pojo.GoodsSpecification;
import jkw.pojo.GoodsSpecificationOption;
import jkw.repository.GoodsESRepository;
import jkw.service.SearchService;
import jkw.vo.GoodsDesc;
import jkw.vo.GoodsSearchParam;
import jkw.vo.GoodsSearchResult;
import lombok.SneakyThrows;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.AnalyzeRequest;
import org.elasticsearch.client.indices.AnalyzeResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.SuggestionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
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.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.stream.Collectors;

@Service
public class SearchServiceImpl implements SearchService {
    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private GoodsESRepository goodsESRepository;

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    /**
     * 分词
     *
     * @param text     文本
     * @param analyzer 分词器
     * @return 分词结果
     */
    @SneakyThrows // 抛出已检查异常(lombok里面的)
    public List<String> analyze(String text, String analyzer) {
        // 分词请求
        AnalyzeRequest request = AnalyzeRequest.withIndexAnalyzer("goods", analyzer, text);
        // 分词响应
        AnalyzeResponse response = restHighLevelClient.indices().analyze(request, RequestOptions.DEFAULT);
        // 分词结果集合
        List<String> words = new ArrayList<>();
        // 处理响应
        List<AnalyzeResponse.AnalyzeToken> tokens = response.getTokens();
        for (AnalyzeResponse.AnalyzeToken token : tokens) {
            String term = token.getTerm(); // 分出的词
            words.add(term);
        }
        return words;
    }

    /**
     * 构造搜索条件
     *
     * @param goodsSearchParam 查询条件对象
     * @return 搜索条件对象
     */
    public NativeSearchQuery buildQuery(GoodsSearchParam goodsSearchParam) {
        // 1.创建复杂查询条件对象
        BoolQueryBuilder builder = QueryBuilders.boolQuery();
        // 2.如果查询条件有关键词,关键词可以匹配商品名、副标题、品牌字段;否则查询所有商品
        if (!StringUtils.hasText(goodsSearchParam.getKeyword())) {
            MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
            builder.must(matchAllQueryBuilder);
        } else {
            String keyword = goodsSearchParam.getKeyword();
            MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keyword, "goodsName", "caption", "brand");
            builder.must(multiMatchQueryBuilder);
        }
        // 3.如果查询条件有品牌,则精准匹配品牌
        String brand = goodsSearchParam.getBrand();
        if (StringUtils.hasText(brand)) {
            TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("goodsBrand", brand);
            builder.must(termQueryBuilder);
        }
        // 4.如果查询条件有价格,则匹配价格
        Double highPrice = goodsSearchParam.getHighPrice();
        Double lowPrice = goodsSearchParam.getLowPrice();
        if (highPrice != null && highPrice != 0) {
            RangeQueryBuilder lte = QueryBuilders.rangeQuery("goodsPrice").lte(highPrice);
            builder.must(lte);
        }
        if (lowPrice != null && lowPrice != 0) {
            RangeQueryBuilder gte = QueryBuilders.rangeQuery("goodsPrice").gte(lowPrice);
            builder.must(gte);
        }
        // 5.如果查询条件有规格项,则精准匹配规格项
        Map<String, String> specificationOptions = goodsSearchParam.getSpecificationOption();
        if (specificationOptions != null && specificationOptions.size() > 0) {
            Set<Map.Entry<String, String>> entries = specificationOptions.entrySet();
            for (Map.Entry<String, String> entry : entries) {
                String key = entry.getKey();
                String value = entry.getValue();
                if (StringUtils.hasText(key)) {
                    TermQueryBuilder termQuery = QueryBuilders.termQuery("goodsSpecification." + key + ".keyword", value);
                    builder.must(termQuery);
                }
            }
        }
        // 6.添加分页条件
        PageRequest pageable = PageRequest.of(goodsSearchParam.getPage() - 1, goodsSearchParam.getSize());
        // 查询构造器,添加条件和分页
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        nativeSearchQueryBuilder.withQuery(builder).withPageable(pageable);
        // 7.如果查询条件有排序,则添加排序条件
        String sortFiled = goodsSearchParam.getSortFiled();
        String sort = goodsSearchParam.getSort();
        SortBuilder sortBuilder = null;
        if (StringUtils.hasText(sort) && StringUtils.hasText(sortFiled)) {
            // 新品的正序是id的倒序
            if (sortFiled.equals("NEW")) {
                sortBuilder = SortBuilders.fieldSort("goodsId");
                if (sort.equals("ASC")) {
                    sortBuilder.order(SortOrder.DESC);
                }
                if (sort.equals("DESC")) {
                    sortBuilder.order(SortOrder.ASC);
                }
            }
            if (sortFiled.equals("PRICE")) {
                sortBuilder = SortBuilders.fieldSort("goodsPrice");
                if (sort.equals("ASC")) {
                    sortBuilder.order(SortOrder.ASC);
                }
                if (sort.equals("DESC")) {
                    sortBuilder.order(SortOrder.DESC);
                }
            }
            nativeSearchQueryBuilder.withSorts(sortBuilder);
        }

        // 8.返回查询条件对象
        NativeSearchQuery query = nativeSearchQueryBuilder.build();
        return query;
    }

    /**
     * 封装查询面板,即根据查询条件,找到查询结果关联度前20名的商品进行封装
     *
     * @param goodsSearchParam
     * @param goodsSearchResult
     */
    public void buildSearchPanel(GoodsSearchParam goodsSearchParam, GoodsSearchResult goodsSearchResult) {
        // 1.构造搜索条件
        goodsSearchParam.setPage(1);
        goodsSearchParam.setSize(20);
        goodsSearchParam.setSort(null);
        goodsSearchParam.setSortFiled(null);
        NativeSearchQuery query = buildQuery(goodsSearchParam);
        // 2.搜索
        SearchHits<GoodsES> search = elasticsearchRestTemplate.search(query, GoodsES.class);
        // 3.将结果封装为List对象
        List<GoodsES> content = new ArrayList();
        for (SearchHit<GoodsES> goodsESSearchHit : search) {
            GoodsES goodsES = goodsESSearchHit.getContent();
            content.add(goodsES);
        }
        // 4.遍历集合,封装查询面板
        // 商品相关的品牌列表
        Set<String> brands = new HashSet();
        // 商品相关的类型列表
        Set<String> productTypes = new HashSet();
        // 商品相关的规格列表
        Map<String, Set<String>> specifications = new HashMap();
        for (GoodsES goodsES : content) {
            // 获取品牌
            brands.add(goodsES.getGoodsBrand());
            // 获取类型
            List<String> productType = goodsES.getGoodsType();
            productTypes.addAll(productType);
            // 获取规格
            Map<String, List<String>> specification = goodsES.getGoodsSpecification();
            Set<Map.Entry<String, List<String>>> entries = specification.entrySet();
            for (Map.Entry<String, List<String>> entry : entries) {
                // 规格名
                String key = entry.getKey();
                // 规格值
                List<String> value = entry.getValue();
                // 如果没有遍历出该规格,新增键值对,如果已经遍历出该规格,则向规格中添加规格项
                if (!specifications.containsKey(key)) {
                    specifications.put(key, new HashSet(value));
                } else {
                    specifications.get(key).addAll(value);
                }
            }
        }
        goodsSearchResult.setBrands(brands);
        goodsSearchResult.setProductType(productTypes);
        goodsSearchResult.setSpecifications(specifications);
    }

    public void syncGoodsToES(GoodsDesc goodsDesc) {
        GoodsES goodsES = new GoodsES();
        goodsES.setGoodsId(goodsDesc.getGoodsId());
        goodsES.setGoodsHeadImg(goodsDesc.getGoodsHeadImg());
        goodsES.setGoodsName(goodsDesc.getGoodsName());
        goodsES.setGoodsCaption(goodsDesc.getGoodsCaption());
        goodsES.setGoodsPrice(goodsDesc.getGoodsPrice());
        //品牌
        goodsES.setGoodsBrand(goodsDesc.getGoodsBrand().getGoodsBrandName());
        //类型
        List<String> goodsTypeList = new ArrayList<>();
        goodsTypeList.add(goodsDesc.getGoodsType1().getGoodsTypeName());
        goodsTypeList.add(goodsDesc.getGoodsType2().getGoodsTypeName());
        goodsTypeList.add(goodsDesc.getGoodsType3().getGoodsTypeName());
        goodsES.setGoodsType(goodsTypeList);
        //规格
        Map<String, List<String>> specificationMap = new HashMap();
        List<GoodsSpecification> specifications = goodsDesc.getSpecifications();
        for (GoodsSpecification goodsSpecification : specifications) {
            List<GoodsSpecificationOption> goodsSpecificationOptions = goodsSpecification.getGoodsSpecificationOptions();
            List<String> options = new ArrayList<>();
            for (GoodsSpecificationOption goodsSpecificationOption : goodsSpecificationOptions) {
                options.add(goodsSpecificationOption.getGoodsSpecificationOptionName());
            }
            specificationMap.put(goodsSpecification.getGoodsSpecificationName(), options);
        }
        goodsES.setGoodsSpecification(specificationMap);
        //关键字
        List<String> tags = new ArrayList();
        tags.add(goodsDesc.getGoodsBrand().getGoodsBrandName()); //品牌名是关键字
        tags.addAll(analyze(goodsDesc.getGoodsName(), "ik_smart"));//商品名分词后为关键词
        tags.addAll(analyze(goodsDesc.getGoodsCaption(), "ik_smart"));//副标题分词后为关键词
        goodsES.setTags(tags);
        goodsESRepository.save(goodsES);

    }

    @Override
    public void delete(Integer id) {
        goodsESRepository.deleteById(id);
    }


    @Override
    public List<String> autoSuggest(String keyword) {
        // 1.创建补全条件
        SuggestBuilder suggestBuilder = new SuggestBuilder();
        SuggestionBuilder suggestionBuilder = SuggestBuilders
                .completionSuggestion("tags")
                .prefix(keyword)
                .skipDuplicates(true)
                .size(10);
        suggestBuilder.addSuggestion("prefix_suggestion", suggestionBuilder);

        // 2.发送请求
        SearchResponse response = elasticsearchRestTemplate.suggest(suggestBuilder, IndexCoordinates.of("goods"));

        // 3.处理结果
        List<String> result = response
                .getSuggest()
                .getSuggestion("prefix_suggestion")
                .getEntries()
                .get(0)
                .getOptions()
                .stream().map(Suggest.Suggestion.Entry.Option::getText)
                .map(Text::toString)
                .collect(Collectors.toList());
        return result;
    }

    @Override
    public GoodsSearchResult search(GoodsSearchParam goodsSearchParam) {
        // 1.构造ES搜索条件
        NativeSearchQuery query = buildQuery(goodsSearchParam);
        // 2.搜索
        SearchHits<GoodsES> search = elasticsearchRestTemplate.search(query, GoodsES.class);
        // 3.将查询结果封装为Page对象
        // 3.1 将SearchHits转为List
        List<GoodsES> content = new ArrayList();
        for (SearchHit<GoodsES> goodsESSearchHit : search) {
            GoodsES goodsES = goodsESSearchHit.getContent();
            content.add(goodsES);
        }
        // 3.2 将List转为MP的Page对象
        Page<GoodsES> page = new Page();
        page.setCurrent(goodsSearchParam.getPage()) // 当前页
                .setSize(goodsSearchParam.getSize()) // 每页条数
                .setTotal(search.getTotalHits()) // 总条数
                .setRecords(content); // 结果集


        // 4.封装结果对象
        // 4.1 查询结果
        GoodsSearchResult result = new GoodsSearchResult();
        result.setGoodsPage(page);
        // 4.2 查询参数
        result.setGoodsSearchParam(goodsSearchParam);
        // 4.3 查询面板
        buildSearchPanel(goodsSearchParam, result);
        return result;
    }


}

控制层

@RequestMapping("/front/search")
@RestController
@CrossOrigin
public class FrontSearchCon {
    @Autowired
    private GoodsService goodsService;
    @Autowired
    private SearchService searchService;

    /***
     * 同步商品数据到es中
     * @return
     */
    @GetMapping("/syncGoodsToES")
    public BaseResult syncGoodsToES(){
        List<Goods> goodsList = goodsService.findAll();
        for (Goods goods : goodsList) {
            GoodsDesc goodsDesc = goodsService.findDescById(goods.getGoodsId());
            searchService.syncGoodsToES(goodsDesc);
        }
        return BaseResult.ok();
    }

    /**
     * 自动补齐关键字
     *
     * @param keyword 被补齐的词
     * @return 补齐的关键词集合
     */
    @GetMapping("/autoSuggest")
    public BaseResult<List<String>> autoSuggest(String keyword) {
        List<String> keywords = searchService.autoSuggest(keyword);
        return BaseResult.ok(keywords);
    }

    /**
     * 搜索商品
     *
     * @param goodsSearchParam 搜索条件
     * @return 搜索结果
     */
    @PostMapping ("/search")
    public BaseResult<GoodsSearchResult> search(@RequestBody GoodsSearchParam goodsSearchParam) {
        GoodsSearchResult result = searchService.search(goodsSearchParam);
        return BaseResult.ok(result);
    }
}

数据测试

同步数据测试

 

自动补全测试

 

 

搜索测试

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

月木@

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

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

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

打赏作者

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

抵扣说明:

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

余额充值