Elasticsearch项目实战,商品搜索功能设计与实现!

标签: elasticsearch 项目 商品 | 发表时间:2020-04-14 01:08 | 作者:MacroZheng
出处:https://juejin.im/welcome/backend

SpringBoot实战电商项目mall(30k+star)地址: github.com/macrozheng/…

摘要

上次写了一篇 《Elasticsearch快速入门,掌握这些刚刚好!》,带大家学习了下Elasticsearch的基本用法,这次我们来篇实战教程,以 mall项目中的商品搜索为例,把Elasticsearch用起来!

中文分词器

由于商品搜索会涉及中文搜索,Elasticsearch需要安装插件才可以支持,我们先来了解下中文分词器,这里使用的是IKAnalyzer。在 《Elasticsearch快速入门,掌握这些刚刚好!》中已经讲过其安装方式,这里直接讲解它的用法。

使用IKAnalyzer

  • 使用默认分词器,可以发现默认分词器只是将中文逐词分隔,并不符合我们的需求;
  GET /pms/_analyze
{
  "text": "小米手机性价比很高",
  "tokenizer": "standard"
}
复制代码

  • 使用中文分词器以后,可以将中文文本按语境进行分隔,可以满足我们的需求。
  GET /pms/_analyze
{
  "text": "小米手机性价比很高",
  "tokenizer": "ik_max_word"
}
复制代码

在SpringBoot中使用

在SpringBoot中使用Elasticsearch本文不再赘述,直接参考 《mall整合Elasticsearch实现商品搜索》即可。这里需要提一下,对于需要进行中文分词的字段,我们直接使用@Field注解将analyzer属性设置为 ik_max_word即可。

  /**
 * 搜索中的商品信息
 * Created by macro on 2018/6/19.
 */
@Document(indexName = "pms", type = "product",shards = 1,replicas = 0)
public class EsProduct implements Serializable {
    private static final long serialVersionUID = -1L;
    @Id
    private Long id;
    @Field(analyzer = "ik_max_word",type = FieldType.Text)
    private String name;
    @Field(analyzer = "ik_max_word",type = FieldType.Text)
    private String subTitle;
    @Field(analyzer = "ik_max_word",type = FieldType.Text)
    private String keywords;
    //省略若干代码......
}
复制代码

简单商品搜索

我们先来实现一个最简单的商品搜索,搜索商品名称、副标题、关键词中包含指定关键字的商品。

  • 使用Query DSL调用Elasticsearch的Restful API实现;
  POST /pms/product/_search
{
  "from": 0, 
  "size": 2, 
  "query": {
    "multi_match": {
      "query": "小米",
      "fields": [
        "name",
        "subTitle",
        "keywords"
      ]
    }
  }
}
复制代码

  • 在SpringBoot中实现,使用Elasticsearch Repositories的衍生查询来搜索;
  /**
 * 商品搜索管理Service实现类
 * Created by macro on 2018/6/19.
 */
@Service
public class EsProductServiceImpl implements EsProductService {
    @Override
    public Page search(String keyword, Integer pageNum, Integer pageSize) {
        Pageable pageable = PageRequest.of(pageNum, pageSize);
        return productRepository.findByNameOrSubTitleOrKeywords(keyword, keyword, keyword, pageable);
    }
}
复制代码
  • 衍生查询其实原理很简单,就是将一定规则方法名称的方法转化为Elasticsearch的Query DSL语句,看完下面这张表你就懂了。

综合商品搜索

接下来我们来实现一个复杂的商品搜索,涉及到过滤、不同字段匹配权重不同以及可以进行排序。

  • 首先来说下我们的需求,按输入的关键字搜索商品名称、副标题和关键词,可以按品牌和分类进行筛选,可以有5种排序方式,默认按相关度进行排序,看下接口文档有助于理解;

  • 这里我们有一点特殊的需求,比如商品名称匹配关键字的的商品我们认为与搜索条件更匹配,其次是副标题和关键字,这时就需要用到 function_score查询了;

  • 在Elasticsearch中搜索到文档的相关性由 _score字段来表示的,文档的 _score字段值越高,表示与搜索条件越匹配,而 function_score查询可以通过设置权重来影响 _score字段值,使用它我们就可以实现上面的需求了;

  • 使用Query DSL调用Elasticsearch的Restful API实现,可以发现商品名称权重设置为了10,商品副标题权重设置为了5,商品关键字设置为了2;

  POST /pms/product/_search
{
  "query": {
    "function_score": {
      "query": {
        "bool": {
          "must": [
            {
              "match_all": {}
            }
          ],
          "filter": {
            "bool": {
              "must": [
                {
                  "term": {
                    "brandId": 6
                  }
                },
                {
                  "term": {
                    "productCategoryId": 19
                  }
                }
              ]
            }
          }
        }
      },
      "functions": [
        {
          "filter": {
            "match": {
              "name": "小米"
            }
          },
          "weight": 10
        },
        {
          "filter": {
            "match": {
              "subTitle": "小米"
            }
          },
          "weight": 5
        },
        {
          "filter": {
            "match": {
              "keywords": "小米"
            }
          },
          "weight": 2
        }
      ],
      "score_mode": "sum",
      "min_score": 2
    }
  },
  "sort": [
    {
      "_score": {
        "order": "desc"
      }
    }
  ]
}
复制代码

  • 在SpringBoot中实现,使用Elasticsearch Repositories的search方法来实现,但需要自定义查询条件QueryBuilder;
  /**
 * 商品搜索管理Service实现类
 * Created by macro on 2018/6/19.
 */
@Service
public class EsProductServiceImpl implements EsProductService {
    @Override
    public Page search(String keyword, Long brandId, Long productCategoryId, Integer pageNum, Integer pageSize,Integer sort) {
        Pageable pageable = PageRequest.of(pageNum, pageSize);
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        //分页
        nativeSearchQueryBuilder.withPageable(pageable);
        //过滤
        if (brandId != null || productCategoryId != null) {
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            if (brandId != null) {
                boolQueryBuilder.must(QueryBuilders.termQuery("brandId", brandId));
            }
            if (productCategoryId != null) {
                boolQueryBuilder.must(QueryBuilders.termQuery("productCategoryId", productCategoryId));
            }
            nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
        }
        //搜索
        if (StringUtils.isEmpty(keyword)) {
            nativeSearchQueryBuilder.withQuery(QueryBuilders.matchAllQuery());
        } else {
            List filterFunctionBuilders = new ArrayList<>();
            filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("name", keyword),
                    ScoreFunctionBuilders.weightFactorFunction(10)));
            filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("subTitle", keyword),
                    ScoreFunctionBuilders.weightFactorFunction(5)));
            filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("keywords", keyword),
                    ScoreFunctionBuilders.weightFactorFunction(2)));
            FunctionScoreQueryBuilder.FilterFunctionBuilder[] builders = new FunctionScoreQueryBuilder.FilterFunctionBuilder[filterFunctionBuilders.size()];
            filterFunctionBuilders.toArray(builders);
            FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(builders)
                    .scoreMode(FunctionScoreQuery.ScoreMode.SUM)
                    .setMinScore(2);
            nativeSearchQueryBuilder.withQuery(functionScoreQueryBuilder);
        }
        //排序
        if(sort==1){
            //按新品从新到旧
            nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC));
        }else if(sort==2){
            //按销量从高到低
            nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort("sale").order(SortOrder.DESC));
        }else if(sort==3){
            //按价格从低到高
            nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
        }else if(sort==4){
            //按价格从高到低
            nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
        }else{
            //按相关度
            nativeSearchQueryBuilder.withSort(SortBuilders.scoreSort().order(SortOrder.DESC));
        }
        nativeSearchQueryBuilder.withSort(SortBuilders.scoreSort().order(SortOrder.DESC));
        NativeSearchQuery searchQuery = nativeSearchQueryBuilder.build();
        LOGGER.info("DSL:{}", searchQuery.getQuery().toString());
        return productRepository.search(searchQuery);
    }
}
复制代码

相关商品推荐

当我们查看相关商品的时候,一般底部会有一些商品推荐,这里使用Elasticsearch来简单实现下。

  • 首先来说下我们的需求,可以根据指定商品的ID来查找相关商品,看下接口文档有助于理解;

  • 这里我们的实现原理是这样的:首先根据ID获取指定商品信息,然后以指定商品的名称、品牌和分类来搜索商品,并且要过滤掉当前商品,调整搜索条件中的权重以获取最好的匹配度;

  • 使用Query DSL调用Elasticsearch的Restful API实现;

  POST /pms/product/_search
{
  "query": {
    "function_score": {
      "query": {
        "bool": {
          "must": [
            {
              "match_all": {}
            }
          ],
          "filter": {
            "bool": {
              "must_not": {
                "term": {
                  "id": 28
                }
              }
            }
          }
        }
      },
      "functions": [
        {
          "filter": {
            "match": {
              "name": "红米5A"
            }
          },
          "weight": 8
        },
        {
          "filter": {
            "match": {
              "subTitle": "红米5A"
            }
          },
          "weight": 2
        },
        {
          "filter": {
            "match": {
              "keywords": "红米5A"
            }
          },
          "weight": 2
        },
        {
          "filter": {
            "term": {
              "brandId": 6
            }
          },
          "weight": 5
        },
        {
          "filter": {
            "term": {
              "productCategoryId": 19
            }
          },
          "weight": 3
        }
      ],
      "score_mode": "sum",
      "min_score": 2
    }
  }
}
复制代码

  • 在SpringBoot中实现,使用Elasticsearch Repositories的search方法来实现,但需要自定义查询条件QueryBuilder;
  /**
 * 商品搜索管理Service实现类
 * Created by macro on 2018/6/19.
 */
@Service
public class EsProductServiceImpl implements EsProductService {
    @Override
    public Page recommend(Long id, Integer pageNum, Integer pageSize) {
        Pageable pageable = PageRequest.of(pageNum, pageSize);
        List esProductList = productDao.getAllEsProductList(id);
        if (esProductList.size() > 0) {
            EsProduct esProduct = esProductList.get(0);
            String keyword = esProduct.getName();
            Long brandId = esProduct.getBrandId();
            Long productCategoryId = esProduct.getProductCategoryId();
            //根据商品标题、品牌、分类进行搜索
            List filterFunctionBuilders = new ArrayList<>();
            filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("name", keyword),
                    ScoreFunctionBuilders.weightFactorFunction(8)));
            filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("subTitle", keyword),
                    ScoreFunctionBuilders.weightFactorFunction(2)));
            filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("keywords", keyword),
                    ScoreFunctionBuilders.weightFactorFunction(2)));
            filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("brandId", brandId),
                    ScoreFunctionBuilders.weightFactorFunction(5)));
            filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("productCategoryId", productCategoryId),
                    ScoreFunctionBuilders.weightFactorFunction(3)));
            FunctionScoreQueryBuilder.FilterFunctionBuilder[] builders = new FunctionScoreQueryBuilder.FilterFunctionBuilder[filterFunctionBuilders.size()];
            filterFunctionBuilders.toArray(builders);
            FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(builders)
                    .scoreMode(FunctionScoreQuery.ScoreMode.SUM)
                    .setMinScore(2);
            //用于过滤掉相同的商品
            BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
            boolQueryBuilder.mustNot(QueryBuilders.termQuery("id",id));
            //构建查询条件
            NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
            builder.withQuery(functionScoreQueryBuilder);
            builder.withFilter(boolQueryBuilder);
            builder.withPageable(pageable);
            NativeSearchQuery searchQuery = builder.build();
            LOGGER.info("DSL:{}", searchQuery.getQuery().toString());
            return productRepository.search(searchQuery);
        }
        return new PageImpl<>(null);
    }
}
复制代码

聚合搜索商品相关信息

在搜索商品时,经常会有一个筛选界面来帮助我们找到想要的商品,这里使用Elasticsearch来简单实现下。

  • 首先来说下我们的需求,可以根据搜索关键字获取到与关键字匹配商品相关的分类、品牌以及属性,下面这张图有助于理解;

  • 这里我们可以使用Elasticsearch的聚合来实现,搜索出相关商品,聚合出商品的品牌、商品的分类以及商品的属性,只要出现次数最多的前十个即可;

  • 使用Query DSL调用Elasticsearch的Restful API实现;

  POST /pms/product/_search
{
  "query": {
    "multi_match": {
      "query": "小米",
      "fields": [
        "name",
        "subTitle",
        "keywords"
      ]
    }
  },
  "size": 0,
  "aggs": {
    "brandNames": {
      "terms": {
        "field": "brandName",
        "size": 10
      }
    },
    "productCategoryNames": {
      "terms": {
        "field": "productCategoryName",
        "size": 10
      }
    },
    "allAttrValues": {
      "nested": {
        "path": "attrValueList"
      },
      "aggs": {
        "productAttrs": {
          "filter": {
            "term": {
              "attrValueList.type": 1
            }
          },
          "aggs": {
            "attrIds": {
              "terms": {
                "field": "attrValueList.productAttributeId",
                "size": 10
              },
              "aggs": {
                "attrValues": {
                  "terms": {
                    "field": "attrValueList.value",
                    "size": 10
                  }
                },
                "attrNames": {
                  "terms": {
                    "field": "attrValueList.name",
                    "size": 10
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
复制代码
  • 比如我们搜索 小米这个关键字的时候,聚合出了下面的分类和品牌信息;

  • 聚合出了 屏幕尺寸5.05.8的筛选属性信息;

  • 在SpringBoot中实现,聚合操作比较复杂,已经超出了Elasticsearch Repositories的使用范围,需要直接使用ElasticsearchTemplate来实现;
  /**
 * 商品搜索管理Service实现类
 * Created by macro on 2018/6/19.
 */
@Service
public class EsProductServiceImpl implements EsProductService {
    @Override
    public EsProductRelatedInfo searchRelatedInfo(String keyword) {
        NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
        //搜索条件
        if(StringUtils.isEmpty(keyword)){
            builder.withQuery(QueryBuilders.matchAllQuery());
        }else{
            builder.withQuery(QueryBuilders.multiMatchQuery(keyword,"name","subTitle","keywords"));
        }
        //聚合搜索品牌名称
        builder.addAggregation(AggregationBuilders.terms("brandNames").field("brandName"));
        //集合搜索分类名称
        builder.addAggregation(AggregationBuilders.terms("productCategoryNames").field("productCategoryName"));
        //聚合搜索商品属性,去除type=1的属性
        AbstractAggregationBuilder aggregationBuilder = AggregationBuilders.nested("allAttrValues","attrValueList")
                .subAggregation(AggregationBuilders.filter("productAttrs",QueryBuilders.termQuery("attrValueList.type",1))
                .subAggregation(AggregationBuilders.terms("attrIds")
                        .field("attrValueList.productAttributeId")
                        .subAggregation(AggregationBuilders.terms("attrValues")
                                .field("attrValueList.value"))
                        .subAggregation(AggregationBuilders.terms("attrNames")
                                .field("attrValueList.name"))));
        builder.addAggregation(aggregationBuilder);
        NativeSearchQuery searchQuery = builder.build();
        return elasticsearchTemplate.query(searchQuery, response -> {
            LOGGER.info("DSL:{}",searchQuery.getQuery().toString());
            return convertProductRelatedInfo(response);
        });
    }

    /**
     * 将返回结果转换为对象
     */
    private EsProductRelatedInfo convertProductRelatedInfo(SearchResponse response) {
        EsProductRelatedInfo productRelatedInfo = new EsProductRelatedInfo();
        Map aggregationMap = response.getAggregations().getAsMap();
        //设置品牌
        Aggregation brandNames = aggregationMap.get("brandNames");
        List brandNameList = new ArrayList<>();
        for(int i = 0; i<((Terms) brandNames).getBuckets().size(); i++){
            brandNameList.add(((Terms) brandNames).getBuckets().get(i).getKeyAsString());
        }
        productRelatedInfo.setBrandNames(brandNameList);
        //设置分类
        Aggregation productCategoryNames = aggregationMap.get("productCategoryNames");
        List productCategoryNameList = new ArrayList<>();
        for(int i=0;i<((Terms) productCategoryNames).getBuckets().size();i++){
            productCategoryNameList.add(((Terms) productCategoryNames).getBuckets().get(i).getKeyAsString());
        }
        productRelatedInfo.setProductCategoryNames(productCategoryNameList);
        //设置参数
        Aggregation productAttrs = aggregationMap.get("allAttrValues");
        List attrIds = ((LongTerms) ((InternalFilter) ((InternalNested) productAttrs).getProperty("productAttrs")).getProperty("attrIds")).getBuckets();
        List attrList = new ArrayList<>();
        for (Terms.Bucket attrId : attrIds) {
            EsProductRelatedInfo.ProductAttr attr = new EsProductRelatedInfo.ProductAttr();
            attr.setAttrId((Long) attrId.getKey());
            List attrValueList = new ArrayList<>();
            List attrValues = ((StringTerms) attrId.getAggregations().get("attrValues")).getBuckets();
            List attrNames = ((StringTerms) attrId.getAggregations().get("attrNames")).getBuckets();
            for (Terms.Bucket attrValue : attrValues) {
                attrValueList.add(attrValue.getKeyAsString());
            }
            attr.setAttrValues(attrValueList);
            if(!CollectionUtils.isEmpty(attrNames)){
                String attrName = attrNames.get(0).getKeyAsString();
                attr.setAttrName(attrName);
            }
            attrList.add(attr);
        }
        productRelatedInfo.setProductAttrs(attrList);
        return productRelatedInfo;
    }
}
复制代码

参考资料

关于Spring Data Elasticsearch的具体使用可以参考官方文档。

docs.spring.io/spring-data…

项目地址

github.com/macrozheng/…

公众号

mall项目全套学习教程连载中, 关注公众号第一时间获取。

公众号图片

相关 [elasticsearch 项目 商品] 推荐:

Elasticsearch项目实战,商品搜索功能设计与实现!

- - 掘金后端
SpringBoot实战电商项目mall(30k+star)地址: github.com/macrozheng/…. 上次写了一篇 《Elasticsearch快速入门,掌握这些刚刚好. 》,带大家学习了下Elasticsearch的基本用法,这次我们来篇实战教程,以 mall项目中的商品搜索为例,把Elasticsearch用起来.

[译]elasticsearch mapping

- - an74520的专栏
es的mapping设置很关键,mapping设置不到位可能导致索引重建. 请看下面各个类型介绍^_^. 每一个JSON字段可以被映射到一个特定的核心类型. JSON本身已经为我们提供了一些输入,支持 string,  integer/ long,  float/ double,  boolean, and  null..

Elasticsearch as Database - taowen - SegmentFault

- -
【北京上地】滴滴出行基础平台部招聘 Elasticsearch 与 Mysql binlog databus 开发工程师. 内推简历投递给: [email protected]. 推销Elasticsearch. 时间序列数据库的秘密(1)—— 介绍. 时间序列数据库的秘密(2)——索引.

ElasticSearch 2 的节点调优(ElasticSearch性能)

- - 行业应用 - ITeye博客
一个ElasticSearch集群需要多少个节点很难用一种明确的方式回答,但是,我们可以将问题细化成一下几个,以便帮助我们更好的了解,如何去设计ElasticSearch节点的数目:. 打算建立多少索引,支持多少应用. elasticsearch版本: elasticsearch-2.x. 需要回答的问题远不止以上这些,但是第五个问题往往是容易被我们忽视的,因为单个ElasticSearch集群有能力支持多索引,也就能支持多个不同应用的使用.

Elasticsearch:使用 Elasticsearch 进行语义搜索

- - 掘金 后端
在数字时代,搜索引擎在通过浏览互联网上的大量可用信息来检索数据方面发挥着重要作用. 此方法涉及用户在搜索栏中输入特定术语或短语,期望搜索引擎返回与这些确切关键字匹配的结果. 虽然关键字搜索对于简化信息检索非常有价值,但它也有其局限性. 主要缺点之一在于它对词汇匹配的依赖. 关键字搜索将查询中的每个单词视为独立的实体,通常会导致结果可能与用户的意图不完全一致.

elasticsearch的javaAPI之query

- - CSDN博客云计算推荐文章
elasticsearch的javaAPI之query API. the Search API允许执行一个搜索查询,返回一个与查询匹配的结果(hits). 它可以在跨一个或多个index上执行, 或者一个或多个types. 查询可以使用提供的 query Java API 或filter Java API.

Elasticsearch基础教程

- - 开源软件 - ITeye博客
转自:http://blog.csdn.net/cnweike/article/details/33736429.     Elasticsearch有几个核心概念. 从一开始理解这些概念会对整个学习过程有莫大的帮助.     接近实时(NRT).         Elasticsearch是一个接近实时的搜索平台.

ElasticSearch索引优化

- - 行业应用 - ITeye博客
ES索引的过程到相对Lucene的索引过程多了分布式数据的扩展,而这ES主要是用tranlog进行各节点之间的数据平衡. 所以从上我可以通过索引的settings进行第一优化:. 这两个参数第一是到tranlog数据达到多少条进行平衡,默认为5000,而这个过程相对而言是比较浪费时间和资源的. 所以我们可以将这个值调大一些还是设为-1关闭,进而手动进行tranlog平衡.

elasticsearch集群搭建

- - zzm
之前对于CDN的日志处理模型是从 . logstash agent==>>redis==>>logstash index==>>elasticsearch==>>kibana3,对于elasticsearch集群搭建,可以把索引进行分片存储,一个索引可以分成若干个片,分别存储到集群里面,而对于集群里面的负载均衡,副本分配,索引动态均衡(根据节点的增加或者减少)都是elasticsearch自己内部完成的,一有情况就会重新进行分配.

Elasticsearch集群入门

- - 编程语言 - ITeye博客
欢迎来到Elasticsearch的奇妙世界,它是优秀的全文检索和分析引擎. 不管你对Elasticsearch和全文检索有没有经验,都不要紧. 我们希望你可以通过这本书,学习并扩展Elasticsearch的知识. 由于这本书也是为初学者准备的,我们决定先简单介绍一般性的全文检索概念,接着再简要概述Elasticsearch.