Grain Mall 14 Suchdienst

Erstellen Sie die Seitenumgebung

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atlinxi.gulimall</groupId>
    <artifactId>gulimall-search</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-search</name>
    <description>elasticsearch检索服务</description>
    <properties>
        <java.version>1.8</java.version>
        <elasticsearch.version>7.4.2</elasticsearch.version>
        <spring-cloud.version>2020.0.4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.atlinxi.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>7.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
<!--        引入热启动-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>com.alibaba</groupId>-->
<!--            <artifactId>fastjson</artifactId>-->
<!--            <version>1.2.79</version>-->
<!--        </dependency>-->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--        由于SpringCloud Feign高版本不使用Ribbon而是使用spring-cloud-loadbalancer,
            所以需要引用spring-cloud-loadbalancer或者降版本-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Schalten Sie den Thymeleaf-Cache aus

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gulimall-search
server.port=13000

spring.thymeleaf.cache=false

Ersetzen Sie den Inhalt in index.html und kopieren Sie alle statischen Ressourcen nach nginx

href="ersetzen durch href="/static/search/, src="ersetzen durchsrc="/static/search/

Ändern Sie die lokale Windows-Hosts-Datei

192.168.56.10	gulimall.com
192.168.56.10	search.gulimall.com

Ändern Sie gulimall.conf

server {
    
    
    listen       80;
    server_name  gulimall.com *.gulimall.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location /static {
    
    
        root   /usr/share/nginx/html;
    }

    location / {
    
    
       proxy_set_header Host $host;
       proxy_pass http://gulimall;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
    
    
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    
    
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    
    
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    
    
    #    deny  all;
    #}
}

Fügen Sie Routing am Ende des Gateway-Moduls hinzu

- id: gulimall_search_route
  uri: lb://gulimall-search
  predicates:
   - Host=search.gulimall.com

index.html des Produktmoduls

//  href="/static/#" 去掉
<a href="/static/#" ><img src="/static/index/img/img_09.png" onclick="search()" /></a>


window.location.href="/static/http://search.gulimall.com/search.html?keyword="+keyword;
// 将上面的改为
window.location.href="http://search.gulimall.com/list.html?keyword="+keyword;

此时,就可以访问search.gulimall.com

Mall-Suche – Analyse der Suchbedingungen

Fügen Sie hier eine Bildbeschreibung ein

Nginx

gulimall.conf

server {
    
    
    listen       80;
    server_name  gulimall.com *.gulimall.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location /static {
    
    
        root   /usr/share/nginx/html;
    }

    location / {
    
    
       proxy_set_header Host $host;
       proxy_pass http://gulimall;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
    
    
        root   /usr/share/nginx/html;
    }

es dsl-Anweisung

Der vorherige ES-Index sollte Produkt sein. Wenn wir eine Zuordnung durchführen, schauen wir uns nur die Felder wie brandImg und brandName an und führen keine Aggregation und Abfrage durch, sodass diese Felder zu diesem Zeitpunkt alle falsch sind und wir dies tun index、doc_valuesmüssen Führen Sie eine Indexdatenmigration durch (es kann die Zuordnung nicht direkt ändern).

# 更新映射
# 映射更新之后,之前的数据映射还是不会改变的
# 所以我们建一个新的索引用来迁移
PUT gulimall_product
{
    
    
    "mappings": {
    
    
      "properties": {
    
    
        "attrs": {
    
    
          "type": "nested",
          "properties": {
    
    
            "attrId": {
    
    
              "type": "long"
            },
            "attrName": {
    
    
              "type": "keyword"
            },
            "attrValue": {
    
    
              "type": "keyword"
            }
          }
        },
        "brandId": {
    
    
          "type": "long"
        },
        "brandImg": {
    
    
          "type": "keyword"
        },
        "brandName": {
    
    
          "type": "keyword"
        },
        "catalogId": {
    
    
          "type": "long"
        },
        "catalogName": {
    
    
          "type": "keyword"
        },
        "hasStock": {
    
    
          "type": "boolean"
        },
        "hotScore": {
    
    
          "type": "long"
        },
        "saleCount": {
    
    
          "type": "long"
        },
        "skuId": {
    
    
          "type": "long"
        },
        "skuImg": {
    
    
          "type": "keyword"
        },
        "skuPrice": {
    
    
          "type": "keyword"
        },
        "skuTitle": {
    
    
          "type": "text",
          "analyzer": "ik_smart"
        },
        "spuId": {
    
    
          "type": "keyword"
        }
      }
    }
  }



# 将product的数据迁移到gulimall_product
POST _reindex
{
    
    
  "source": {
    
    
    "index": "product"
  },
  "dest": {
    
    "index": "gulimall_product"}
}




# 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析

# 如果是嵌入式的属性,查询,聚合,分析都应该用嵌入式的
# dsl完整查询语句
{
    
    
  "query": {
    
    
    "bool": {
    
    
      "must": [
        {
    
    
          "match": {
    
    
            "skuTitle": "华为"
          }
        }
      ],
      "filter": [
        {
    
    
          "term": {
    
    
            "catalogId": "225"
          }
        },
        {
    
    
          "terms": {
    
    
            "brandId": [
              "5",
              "6",
              "7"
            ]
          }
        },
        {
    
    
          "nested": {
    
    
            "path": "attrs",
            "query": {
    
    
              "bool": {
    
    
                "must": [
                  {
    
    
                    "term": {
    
    
                      "attrs.attrId": {
    
    
                        "value": "1"
                      }
                    }
                  },
                  {
    
    
                    "terms": {
    
    
                      "attrs.attrValue": [
                        "CET-AL00",
                        "balabala"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
    
    
          "term": {
    
    
            "hasStock": {
    
    
              "value": "false"
            }
          }
        },
        {
    
    
          "range": {
    
    
            "skuPrice": {
    
    
              "gte": 0,
              "lte": 6000
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
    
    
      "skuPrice": {
    
    
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 2,
  "highlight": {
    
    
    "fields": {
    
    
      "skuTitle": {
    
    }
    },
    "pre_tags": "<b style='color:yellow'>",
    "post_tags": "</b>"
  },
  "aggs": {
    
    
    "brand_agg": {
    
    
      "terms": {
    
    
        "field": "brandId",
        "size": 100
      },
      "aggs": {
    
    
        "brand_name_agg": {
    
    
          "terms": {
    
    
            "field": "brandName",
            "size": 10
          }
        },
        "brand_img_agg": {
    
    
          "terms": {
    
    
            "field": "brandImg",
            "size": 10
          }
        }
      }
    },
    "catalog_agg": {
    
    
      "terms": {
    
    
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
    
    
        "catalog_name_agg": {
    
    
          "terms": {
    
    
            "field": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attr_agg": {
    
    
      "nested": {
    
    
        "path": "attrs"
      },
      "aggs": {
    
    
        "attr_id": {
    
    
          "terms": {
    
    
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
    
    
            "attr_name_agg": {
    
    
              "terms": {
    
    
                "field": "attrs.attrName",
                "size": 10
              }
            },
            "attr_value_agg": {
    
    
              "terms": {
    
    
                "field": "attrs.attrValue",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

Commons-R-Klassen ändern

/**
 * Copyright (c) 2016-2019 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */

package com.atlinxi.common.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.apache.http.HttpStatus;

import java.util.HashMap;
import java.util.Map;

/**
 * 返回数据
 *
 * @author Mark [email protected]
 *
 *
 * 老师是用泛型的方式封装的data,
 * 在feign接口中返回泛型类时,由于java的泛型机制,在实例化之前无法得到具体的类型 ,
 * 因此,虽然服务提供方返回的是具体实例的数据,但是在客户端decode时,无法转化为具体的类。
 *
 * 上面的话看不太懂,翻译成人话就是,feign在被远程调用返回结果的时候,泛型是null
 *
 * 因为R继承了HashMap,我们写的所有私有属性都没用,只能存键值对,具体原因未知,
 *
 * public class R<T> extends HashMap<String, Object> {
 * 	private static final long serialVersionUID = 1L;
 *
 * 	private T data;
 *
 * 	public T getData() {
 * 		return this.data;
 *        }
 *
 *
 * 	public void setData(T data) {
 * 		this.data = data;
 *    }
 */
public class R extends HashMap<String, Object> {
    
    
	private static final long serialVersionUID = 1L;


	public R setData(Object data){
    
    
		put("data",data);
		return this;
	}



	public <T> T getData(TypeReference<T> typeReference) {
    
    
		Object data = get("data");	//默认是map
		String jsonString = JSON.toJSONString(data);
		T t = JSON.parseObject(jsonString, typeReference);
		return t;
	}

	//利用fastjson进行反序列化
	public <T> T getData(String key,TypeReference<T> typeReference) {
    
    
		Object data = get(key);	//默认是map
		String jsonString = JSON.toJSONString(data);
		T t = JSON.parseObject(jsonString, typeReference);
		return t;
	}

	public R() {
    
    
		put("code", 0);
		put("msg", "success");
	}
	
	public static R error() {
    
    
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
	}
	
	public static R error(String msg) {
    
    
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
	}
	
	public static R error(int code, String msg) {
    
    
		R r = new R();
		r.put("code", code);
		r.put("msg", msg);
		return r;
	}

	public static R ok(String msg) {
    
    
		R r = new R();
		r.put("msg", msg);
		return r;
	}
	
	public static R ok(Map<String, Object> map) {
    
    
		R r = new R();
		r.putAll(map);
		return r;
	}
	
	public static R ok() {
    
    
		return new R();
	}

	public R put(String key, Object value) {
    
    
		super.put(key, value);
		return this;
	}

	public int getCode(){
    
    

		return (Integer) this.get("code");

	}
}

Alle Entitätsklassen, Konstanten usw.

package com.atlinxi.gulimall.search.constant;

public class EsConstant {
    
    

	// 这儿写错了,常量名应该都是大写
    public static final String Product_INDEX = "gulimall_product"; // sku数据在es中的索引
    public static final Integer Product_PAGESIZE = 2; // 前期为了方便测试,分页只两个
}

// 查询条件实体类
package com.atlinxi.gulimall.search.vo;

import lombok.Data;

import java.util.List;

/**


 * 封装页面所有可能传递过来的查询条件
 *
 * catalog3Id=225&keyword=小米&sort=saleCount_asc&hasStock=0/1&brandId=1&brandId=2
 * &attrs=1_5寸:8寸&attrs=2_16G:8G
 */
@Data
public class SearchParam {
    
    

    private String keyword; // 页面传递过来的全文匹配关键字
    private Long catalog3Id; // 页面传递过来的三级分类id
    /**
     * sort=saleCount_asc/desc
     * sort=skuPrice_asc/desc
     * sort=hotScore_asc/desc
     */
    private String sort; // 排序条件

    /**
     * 好多的过滤条件
     * hasStock(是否有货)、skuPrice(区间)、brandId、catalog3Id、attrs
     *
     * hasStock 0/1
     * skuPrice 1_500/_500/500_
     * brandId=1
     * attrs=1_其他:安卓&attrs=2_5寸:6寸(_前面代表属性,后面代表值,多个值之间用:分割)
     */

    private Integer hasStock; // 是否只显示有货 0(无库存) 1(有库存)
    private String skuPrice; // 价格区间查询
    private List<Long> brandId; // 按照品牌进行查询,可以多选
    private List<String> attrs; // 按照属性进行筛选
    private Integer pageNum = 1; // 页码


    private String _queryString; // url原生的所有查询条件


}

// 返回结果实体类
package com.atlinxi.gulimall.search.vo;

import com.atlinxi.common.to.es.SkuEsModel;
import lombok.Data;

import java.util.ArrayList;
import java.util.List;

@Data
public class SearchResult {
    
    

    // 查询到的所有商品信息
    private List<SkuEsModel> products;

    /**
     * 分页信息
     */
    private Integer pageNum; // 当前页码
    private Long total; // 总记录数
    private Integer totalPages; // 总页码
    private List<Integer> pageNavs;


    private List<BrandVo> brands; // 当前查询到的结果所有涉及到的品牌
    private List<AttrVo> attrs; // 当前查询到的结果所有涉及到的属性
    private List<CatalogVo> catalogs; // 当前查询到的结果所有涉及到的属性

    // ==========================以上是返回给页面的所有信息=======================

    // 面包屑导航数据
    private List<NavVo> navs = new ArrayList<>();
    private List<Long> attrIds = new ArrayList<>();

    @Data
    public static class NavVo{
    
    
        private String navName;
        private String navValue;
        private String link;
    }

    @Data
    public static class BrandVo{
    
    
        private Long brandId;
        private String brandName;
        private String brandImg;

    }

    @Data
    public static class AttrVo{
    
    
        private Long attrId;
        private String attrName;
        private List<String> attrValue;
    }


    @Data
    public static class CatalogVo{
    
    
        private Long catalogId;
        private String catalogName;
    }
}









package com.atlinxi.gulimall.search.vo;

import lombok.Data;

@Data
public class AttrResponseVo {
    
    

    // 所属分类名字
    private String catelogName;

    // 所属分组名字
    private String groupName;

    // 三级分类路径
    private Long[] catelogPath;


    /**
     * 属性id
     */
    private Long attrId;
    /**
     * 属性名
     */
    private String attrName;
    /**
     * 是否需要检索[0-不需要,1-需要]
     */
    private Integer searchType;
    /**
     * 属性图标
     */
    private String icon;
    /**
     * 可选值列表[用逗号分隔]
     */
    private String valueSelect;
    /**
     * 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
     */
    private Integer attrType;
    /**
     * 启用状态[0 - 禁用,1 - 启用]
     */
    private Long enable;
    /**
     * 所属分类
     */
    private Long catelogId;
    /**
     * 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整
     */
    private Integer showDesc;

    private Long attrGroupId;
}












package com.atlinxi.gulimall.search.vo;

import lombok.Data;

@Data
public class BrandVo {
    
    

    private Long brandId;
    private String brandName;
}


einen Fernanruf vortäuschen

package com.atlinxi.gulimall.search.feign;

import com.atlinxi.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@FeignClient("gulimall-product")
public interface ProductFeignService {
    
    

    // 在写feign接口的时候,请求路径必须是完整的,函数命可以与原服务的函数名不一样
    @RequestMapping("/product/attr/info/{attrId}")
    R attrInfo(@PathVariable("attrId") Long attrId);


    @GetMapping("/product/brand//infos")
    R BrandsInfo(@RequestParam("brandIds") List<Long> brandIds);
}








/**
     *
     * @param attrId
     * @return
     */
    // search远程调用的时候耗时太长,我们把返回的结果放进缓存
    @Cacheable(value = "attr",key = "'attrInfo:' + #root.args[0]")
    @Override
    public AttrResVo getAttrInfo(Long attrId) {
    
    

        AttrResVo attrResVo = new AttrResVo();
        AttrEntity attrEntity = this.getById(attrId);
        BeanUtils.copyProperties(attrEntity,attrResVo);

        if (attrEntity.getAttrType()==ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
    
    
            // 1.设置分组信息
            AttrAttrgroupRelationEntity attrgroupRelation = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));

            if (attrgroupRelation!=null){
    
    
                attrResVo.setAttrGroupId(attrgroupRelation.getAttrGroupId());
                AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupRelation.getAttrGroupId());

                if (attrGroupEntity!=null){
    
    
                    attrResVo.setGroupName(attrGroupEntity.getAttrGroupName());
                }


            }
        }



        // 2. 设置分类信息
        Long catelogId = attrEntity.getCatelogId();

        Long[] catelogPath = categoryService.findCatelogPath(catelogId);

        CategoryEntity categoryEntity = categoryDao.selectById(catelogId);

        if (categoryEntity!=null){
    
    
            attrResVo.setCatelogPath(catelogPath);
            attrResVo.setCatelogName(categoryEntity.getName());
        }



        return attrResVo;
    }

Regler

package com.atlinxi.gulimall.search.controller;

import com.atlinxi.gulimall.search.service.MallSearchService;
import com.atlinxi.gulimall.search.vo.SearchParam;
import com.atlinxi.gulimall.search.vo.SearchResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;

@Controller
public class SearchController {
    
    

    @Autowired
    MallSearchService mallSearchService;

    /**
     * springMVC 自动将页面提交过来的所有请求查询参数封装成指定的对象
     * @param param
     * @return
     */
    @GetMapping("/list.html")
    public String listPage(SearchParam param, Model model, HttpServletRequest request){
    
    

        String queryString = request.getQueryString();

        param.set_queryString(queryString);

        // 1. 根据传递来的页面的查询参数,去es中检索商品
        SearchResult result = mallSearchService.search(param);

        model.addAttribute("result",result);
        // 我们整合了thymeleaf,所以不用谢templates和.html
        return "list";
    }
}

Startup-Klasse

package com.atlinxi.gulimall.search;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableFeignClients(basePackages = "com.atlinxi.gulimall.search.feign")
public class GulimallSearchApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(GulimallSearchApplication.class, args);
    }

}

Service

package com.atlinxi.gulimall.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atlinxi.common.to.es.SkuEsModel;
import com.atlinxi.common.utils.R;
import com.atlinxi.gulimall.search.config.GulimallElasticSearchConfig;
import com.atlinxi.gulimall.search.constant.EsConstant;
import com.atlinxi.gulimall.search.feign.ProductFeignService;
import com.atlinxi.gulimall.search.service.MallSearchService;
import com.atlinxi.gulimall.search.vo.AttrResponseVo;
import com.atlinxi.gulimall.search.vo.BrandVo;
import com.atlinxi.gulimall.search.vo.SearchParam;
import com.atlinxi.gulimall.search.vo.SearchResult;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class MallSearchServiceImpl implements MallSearchService {
    
    

    @Autowired
    private RestHighLevelClient client;


    @Autowired
    private ProductFeignService productFeignService;

    // 去es中进行检索
    @Override
    public SearchResult search(SearchParam param) {
    
    
        // 1. 动态构建出查询需要的DSL语句
        SearchResult result = null;

        // 1. 准备检索请求
        SearchRequest searchRequest = buildSearchRequest(param);
        ;


        try {
    
    
            // 2. 执行检索请求
            SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

            // 3. 分析响应数据封装成我们需要的格式
            result = buildSearchResult(response, param);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }

        return result;
    }


    /**
     * 构建结果数据
     *
     * @param response
     * @return
     */
    private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
    
    

        SearchResult result = new SearchResult();

        // 1. 返回的所有查询到的商品
        SearchHits hits = response.getHits();
        List<SkuEsModel> esModels = new ArrayList<>();
        if (hits.getHits() != null && hits.getHits().length > 0) {
    
    
            for (SearchHit hit : hits.getHits()) {
    
    
                String sourceAsString = hit.getSourceAsString();
                SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);


                if (!StringUtils.isEmpty(param.getKeyword())) {
    
    
                    HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                    String s = skuTitle.getFragments()[0].toString();
                    esModel.setSkuTitle(s);
                }


                esModels.add(esModel);

            }
        }

        result.setProducts(esModels);


        // 2. 当前所有商品涉及到的所有属性信息
        List<SearchResult.AttrVo> attrVos = new ArrayList<>();

        ParsedNested attr_agg = response.getAggregations().get("attr_agg");
        ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");

        for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
    
    
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo();

            // 得到属性的id
            long attrId = bucket.getKeyAsNumber().longValue();

            // 得到属性的名字
            String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();


            // 得到属性的所有制值
            List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(
                    item -> {
    
    
                        String keyAsString = ((Terms.Bucket) item).getKeyAsString();
                        return keyAsString;
                    }).collect(Collectors.toList());

            attrVo.setAttrId(attrId);
            attrVo.setAttrName(attrName);
            attrVo.setAttrValue(attrValues);


            attrVos.add(attrVo);
        }

        result.setAttrs(attrVos);

        // 3. 当前所有商品涉及到的所有品牌信息
        List<SearchResult.BrandVo> brandVos = new ArrayList<>();

        ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
        for (Terms.Bucket bucket : brand_agg.getBuckets()) {
    
    
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();

            // 得到品牌的id
            long brandId = bucket.getKeyAsNumber().longValue();

            // 得到品牌的名字
            String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();

            // 得到品牌的图片
            String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();

            brandVo.setBrandId(brandId);
            brandVo.setBrandName(brandName);
            brandVo.setBrandImg(brandImg);

            brandVos.add(brandVo);

        }


        result.setBrands(brandVos);

        // 4. 当前所有商品涉及到的所有分类信息
        ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
        List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets();
        for (Terms.Bucket bucket : buckets) {
    
    
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            // 得到分类id
            String keyAsString = bucket.getKeyAsString();
            catalogVo.setCatalogId(Long.parseLong(keyAsString));

            // 得到分类名
            ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
            String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();
            catalogVo.setCatalogName(catalog_name);
            catalogVos.add(catalogVo);
        }
        result.setCatalogs(catalogVos);

        // 5. 分页信息 - 页码
        result.setPageNum(param.getPageNum());
        // 分页信息 - 总记录数
        long total = hits.getTotalHits().value;
        result.setTotal(total);
        // 分页信息 - 总页码 - 计算得到
        int totalPages = (int) total % EsConstant.Product_PAGESIZE == 0 ? (int) total / EsConstant.Product_PAGESIZE : (int) total / EsConstant.Product_PAGESIZE + 1;
        result.setTotalPages(totalPages);

        List<Integer> pageNavs = new ArrayList<>();
        for (int i = 1; i <= totalPages; i++) {
    
    
            pageNavs.add(i);
        }
        result.setPageNavs(pageNavs);


        // 6. 构建面包屑导航功能

        // 面包屑导航只限于属性,不包括分类和keyword(检索条件)
        // 因为我们如果去掉分类或者检索条件的话,属性则无意义

        if (param.getAttrs() != null && param.getAttrs().size() > 0) {
    
    

            List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
    
    

                SearchResult.NavVo navVo = new SearchResult.NavVo();
                // 1. 分析每个attrs传过来的查询参数值
                // attrs=1_其他:安卓&attrs=2_5寸:6寸
                String[] s = attr.split("_");
                navVo.setNavValue(s[1]);

                R r = productFeignService.attrInfo(Long.parseLong(s[0]));
                result.getAttrIds().add(Long.parseLong(s[0]));
                if (r.getCode() == 0) {
    
    
                    AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
    
    

                    });
                    navVo.setNavName(data.getAttrName());
                } else {
    
    
                    navVo.setNavName(s[0]);
                }

                // 2. 取消了面包屑以后,我们要跳转到哪个地方,将请求地址的url置空
                // 拿到所有的查询条件,去掉当前属性
                String replace = replaceQueryString(param, attr, "attrs");
                navVo.setLink("http://search.gulimall.com/list.html?" + replace);
                return navVo;
            }).collect(Collectors.toList());


            // todo 分类,不需要导航取消

            result.setNavs(collect);


        }


        // 品牌,分类
        if (param.getBrandId() != null && param.getBrandId().size() > 0) {
    
    
            List<SearchResult.NavVo> navs = result.getNavs();
            SearchResult.NavVo navVo = new SearchResult.NavVo();

            navVo.setNavName("品牌");
            R r = productFeignService.BrandsInfo(param.getBrandId());

            if (r.getCode() == 0) {
    
    
                List<BrandVo> brand = r.getData("brand", new TypeReference<List<BrandVo>>() {
    
    
                });

                StringBuffer buffer = new StringBuffer();
                String replace = "";
                for (BrandVo brandVo : brand) {
    
    
                    buffer.append(brandVo.getBrandName() + ";");
                    replace = replaceQueryString(param, brandVo.getBrandId() + "", "brandId");
                }

                navVo.setNavValue(buffer.toString());
                navVo.setLink(replace);
            }

            navs.add(navVo);
        }


        return result;
    }

    private String replaceQueryString(SearchParam param, String value, String key) {
    
    
        String encode = null;
        try {
    
    
            // 中文需要编码
            encode = URLEncoder.encode(value, "UTF-8");
            encode = encode.replace("+", "%20"); // 浏览器对空格编码和java不一样,浏览器是%20,java是+
        } catch (UnsupportedEncodingException e) {
    
    
            e.printStackTrace();
        }

        String replace = param.get_queryString().replace("&" + key + "=" + encode, "");
        return replace;
    }

    /**
     * 准备检索请求
     * <p>
     * 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析
     *
     * @return
     */
    private SearchRequest buildSearchRequest(SearchParam param) {
    
    

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); // 构建DSL语句

        /**
         * 查询:模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
         */

        // 1. 构建bool query
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        // 1.1 must 模糊匹配
        if (!StringUtils.isEmpty(param.getKeyword())) {
    
    

            boolQuery.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
        }

        // 1.2 bool filter 按照三级分类id查询
        if (param.getCatalog3Id() != null) {
    
    

            boolQuery.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
        }
        // 1.2 bool filter 按照品牌id查询
        if (param.getBrandId() != null && param.getBrandId().size() > 0) {
    
    

            boolQuery.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
        }
        // 1.2 bool filter 按照所有指定的属性进行查询
        if (param.getAttrs() != null && param.getAttrs().size() > 0) {
    
    


            for (String attrStr : param.getAttrs()) {
    
    
                // attrs=1_5寸:8寸&attrs=2_16G:8G
                BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();
                // attrs=1_5寸:8寸&
                String[] s = attrStr.split("_");
                String attrId = s[0]; // 检索的属性id
                String[] attrValue = s[1].split(":"); // 这个属性检索用的值
                nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
                nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValue));

                // param3 聚合的这些结果以什么方式参与评分
                // 在这儿我们先不让它参与评分

                // 每一个必须都得生成一个nested查询
                NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);

                boolQuery.filter(nestedQuery);
            }


        }

        // 1.2 bool filter 按照是否有库存进行查询
        // 字段是用0,1代表,es是用bool值代表
        if (param.getHasStock() != null) {
    
    
            boolQuery.filter(QueryBuilders.termsQuery("hasStock", param.getHasStock() == 1));
        }


        // 1.2 bool filter 按照价格区间 skuPrice 1_500/_500/500_
        if (!StringUtils.isEmpty(param.getSkuPrice())) {
    
    

            /**
             *         {
             *           "range":{
             *             "skuPrice":{
             *               "gte":0,
             *               "lte":6000
             *             }
             *           }
             *         }
             */
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
            String[] s = param.getSkuPrice().split("_");
            if (s.length == 2) {
    
    
                // 区间
                rangeQuery.gte(s[0]).lt(s[1]);
            } else if (s.length == 1) {
    
    
                if (param.getSkuPrice().startsWith("_")) {
    
    
                    rangeQuery.lte(s[0]);
                }

                if (param.getSkuPrice().endsWith("_")) {
    
    
                    rangeQuery.gte(s[0]);
                }
            }
            boolQuery.filter(rangeQuery);
        }


        sourceBuilder.query(boolQuery);


        /**
         * 排序,分页,高亮,
         */
        // 2.1 排序
        if (!StringUtils.isEmpty(param.getSort())) {
    
    
            String sort = param.getSort();
            // sort=saleCount_asc/desc
            String[] s = sort.split("_");

            SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
            sourceBuilder.sort(s[0], order);
        }

        // 2.2 分页
        // pageNum:1 from:0 size:2
        // pageNum:2 from:2 size:2
        // from = (pageNum-1)*size
        sourceBuilder.from((param.getPageNum() - 1) * EsConstant.Product_PAGESIZE);
        sourceBuilder.size(EsConstant.Product_PAGESIZE);

        // 2.3 高亮
        if (!StringUtils.isEmpty(param.getKeyword())) {
    
    

            HighlightBuilder builder = new HighlightBuilder();
            builder.field("skuTitle");
            builder.preTags("<b style='color:red'>");
            builder.postTags("</b>");
            sourceBuilder.highlighter(builder);

        }

        /**
         * 聚合分析
         */

        // 1. 品牌聚合
        TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg");
        brand_agg.field("brandId").size(50);
        // 品牌聚合的子聚合
        brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
        brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
        sourceBuilder.aggregation(brand_agg);
        // 2. 分类聚合
        TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").field("catalogId").size(2);
        catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
        sourceBuilder.aggregation(catalog_agg);
        // 3. 属性聚合
        NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
        // 聚合出当前所有的attrId
        TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
        // 聚合分析出当前attr_id对应的名字
        attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
        // 聚合分析出当前attr_id对应的所有可能的属性值attrValue
        attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
        attr_agg.subAggregation(attr_id_agg);
        sourceBuilder.aggregation(attr_agg);


        String s = sourceBuilder.toString();
        System.out.println("构建的DSL" + s);


        SearchRequest searchRequest = new SearchRequest(new String[]{
    
    EsConstant.Product_INDEX}, sourceBuilder);

        return searchRequest;
    }
}

Liu Yiting weiß, dass der größte Vorteil eines Kindes darin besteht, dass niemand ihre Worte ernst nimmt. Sie kann prahlen, ihr Wort brechen und sogar lügen. Es ist auch ein reflexiver Selbstschutz von Erwachsenen, denn was Kinder zuerst sagen, ist oft die Wahrheit, und Erwachsene müssen sich trösten, was Kinder wissen. Unter Rückschlägen entwickeln sich Kinder von Kindern, die die Wahrheit sagen, zu Kindern, die sich dafür entscheiden können, die Wahrheit zu sagen. In der Demokratie des Diskurses wachsen Kinder zu Erwachsenen heran.

Fang Siqis erstes Liebesparadies
Yihan Lam

Guess you like

Origin blog.csdn.net/weixin_44431371/article/details/128824160