概述
简介
商品搜索引擎采用的是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);
}
}
数据测试
同步数据测试
自动补全测试
搜索测试