JavaEE 企业级分布式高级架构师(十七)ElasticSearch全文检索(3)

实操篇

使用Restful接口访问ES

  • ES的接口语法
curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'
  • 其中:
参数解释
VERB适当的 HTTP 方法或谓词 : GET、 POST、 PUT、 HEAD 或者 DELETE。
PROTOCOLhttp 或 https(如果你在 Elasticsearch 前面有一个 https 代理)
HOSTElasticsearch 集群中任意节点的主机名,或者用 localhost 代表本地机器上的节点。
PORT运行 Elasticsearch HTTP 服务的端口号,默认是 9200 。
PATHAPI 的终端路径(例如 _count 将返回集群中文档数量)。Path 可能包含多个组件,例如: _cluster/stats 和 _nodes/stats/jvm
QUERY_STRING任意可选的查询字符串参数 (例如 ?pretty 将格式化地输出 JSON 返回值,使其更容易阅读)
BODY一个 JSON 格式的请求体 (如果请求需要的话)
  • 下面我们通过使用 Kibana 的 DevTools 操作 ES

ES集群信息查看

  • 查看cluster集群状态
GET /_cat/health?v

在这里插入图片描述

  • 查看node状态信息
GET /_nodes/stats/process/?pretty

在这里插入图片描述

  • 查看node的机器使用情况
GET /_cat/nodes?v

在这里插入图片描述

  • 查看集群索引信息
GET /_cat/indices?v
# 只显示状态为黄色的
GET /_cat/indices?v&health=yellow
# 根据文档数量降序排列
GET /_cat/indices?v&s=docs.count:desc
# 显示每个索引占的内存
GET /_cat/indices?v&h=i,tm&s=tm:desc

在这里插入图片描述
在这里插入图片描述

  • node使用的磁盘情况
GET /_cat/allocation?v

在这里插入图片描述

  • 查看文档数
GET /_cat/count?v

在这里插入图片描述

  • fieldData的大小
GET /_cat/fielddata?v
# 查看指定的字段
GET /_cat/fielddata?v&fields=kkb

在这里插入图片描述

  • 主节点信息
GET /_cat/master?v

在这里插入图片描述

  • 查看分片的恢复信息:里边包涵了很多信息,文件信息和translog信息等
GET /_cat/recovery?v

在这里插入图片描述

  • 查看集群信息列表:返回所有的信息查询url
GET /_cat/

在这里插入图片描述

索引库维护

创建索引index和映射mapping
  • 基本语法:
PUT /{index}
{
	# settings
	# mappings
}
  • 例子:
PUT /blog1
{
  "settings": {
    "index": {
      # 分片数量
      "number_of_shards": "5",
      # 副本集数量
      "number_of_replicas": "1"
    }
  },
  "mappings": {
    "properties": {
      "id": {
        "type": "long",
        "store": true,
        "index": true
      },
      "title": {
        "type": "text",
        "store": true,
        "index": true,
        "analyzer": "standard"
      },
      "content": {
        "type": "text",
        "store": true,
        "index": true,
        "analyzer": "standard"
      }
    }
  }
}

在这里插入图片描述

Field的属性
  • type:数据类型
    • 数值类型:integer、long、float等
    • 字符串类型:keyword、text
      • keyword:不分词,可以创建索引。例如身份证号、订单号等。不需要分词,可以创建索引。就会把field中的内容作为一个完整的关键词,创建索引。
      • text:分词的数据类型。可以指定分词器。(稍后会讲)
    • 日期类型等
  • store:是否存储
    • 取决于是否展示给用户看,或者业务需要。
    • 不存储不影响分词,不影响查询,可以节约存储空间。不存储的后果就是无法展示。
  • index:是否索引
    • 如果不分词也可以选择是否创建索引,不创建索引就无法查询此字段的内容。
  • analyzer:定义分词器
    • 默认是标准分词器(standardAnalyzer)
创建索引后设置Mapping
  • 不设置mapping也可以向索引库中添加文档,es会根据文档的数据自动识别field的类型。
  • 推荐先设置好mapping然后再添加数据。因为es识别的数据类型未必是我们想要的数据类型。
  • mapping一旦设置无法修改,只能新增。
  • 设置mapping语法:
POST /{index}/{type}/_mapping
{
	"{type}": {
		"properties": {
			"{field}": {
				# key-value
			}
		}
	}
}
  • 例子:
POST /blog1/_mapping
{
  "properties": {
    "id": {
      "type": "long",
      "store": true,
      "index": true
    },
    "title": {
      "type": "text",
      "store": true,
      "index": true,
      "analyzer": "standard"
    },
    "content": {
      "type": "text",
      "store": true,
      "index": true,
      "analyzer": "standard"
    }
  }
}

在这里插入图片描述

删除索引库
  • 语法:
DELETE /{index}

文档维护

创建文档
  • 基本语法:
PUT /{index}/{type}/{_id}
{
	# 文档内容 key-value
}
  • 例子:
POST /blog1/_doc/1
{
  "id":1,
  "title":"ElasticSearch是一个基于Lucene的搜索服务器",
  "content":"它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。 Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引 擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。"
}

在这里插入图片描述

  • es7中type固定为“_doc”
  • _id:文档的唯一编号和文档中id属性没有任何关系。通常让二者保持一致。
修改文档
  • 在es中所谓的修改其实就是先删除后添加。
  • 例子:
POST /blog1/_doc/1
{
  "id":1,
  "title":"【修改】ElasticSearch是一个基于Lucene的搜索服务器",
  "content":"【修改】它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。 Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引 擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。"
}

在这里插入图片描述

删除文档
  • 语法:
DELETE /{index}/{type}/{_id}
  • 例子:
DELETE /blog1/_doc/1
根据id查询文档
  • 语法:
GET /{index}/{type}/{_id}
  • 例子:
GET /blog1/_doc/1

使用查询表达式(Query DSL)

  • 官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
  • 查询语法
GET /{index}/{type}/_search
{
	"query": {
		"查询方法": "查询条件"
	}
}
  • 查询全部文档
GET /{index}/{type}/_search
{
	"query": {
		"match_all": {}
	}
}
根据关键词(term)查询
  • 需要指定在哪个field上查询,查询哪个关键词。
  • 例子:
GET /blog1/_search
{
  "query": {
    "term": {
      "title": "搜索"
    }
  }
}
使用query_string查询,或者match查询
  • 带分词的查询,先对查询内容进行分词处理,然后基于分词的结果查询。
  • 例子:
GET /blog1/_search
{
  "query": {
    "query_string": {
      "default_field": "title", 
      "query": "搜索服务器"
    }
  }
}

在这里插入图片描述

  • 相关度排序:
    • TF:关键词出现的频率,TF 越高相关度越高,越重要。
    • DF:关键词在多个文档中出现的频率,DF越高越不重要,相关度越低。
  • 可以设置boots值调节field权重。
    在查询时,根据关键词计算每个文档的相关度,给每个文档打分,然后按得分降序排列。
multi_match查询
GET /blog1/_search
{
  "query": {
    "multi_match": {
      "query": "李四",
      "fields": ["name", "address"]
    }
  }
}
bool查询
  • bool (布尔)过滤器:这是个复合过滤器(compound filter) ,它可以接受多个其他过滤器作为参数,并将这些过滤器结合成各式各样的布尔(逻辑)组合。
GET /{index}/_search
{
  "query": {
    "bool": {
      "must": [
        {}
      ],
      "should": [
        {}
      ],
      "must_not": [
        {}
      ],
      "filter": {}
    }
  }
}
  • should:对于一个文档,查询列表中,只要有一个查询匹配,那么这个文档就被看成是匹配的。
  • must_not:对于一个文档,查询列表中的的所有查询都必须都不为真,这个文档才被认为是匹配的。
  • filter:从ES5.0开始过滤条件需要在添加在bool查询中,filter条件中支持查询条件中的所有查询方 法。filter可以配置多个,从而实现多个过滤条件同时生效。
  • 在不需要相关度排序的情况下,推荐使用filter查询,提高查询速度。filter不对文档打分。
其它查询
  • terms:跟term有点类似,但可以同时指定多个条件,相当于union all的作用,汇聚所有查询的值
{
   "query": {
       "term": {"date": ["2018-09-01","2018-10-03"]}
   }
}
  • range:范围查询,指定一个范围查询
{
   "query": {
       "range":{
           "age":{   //查询age字段
               "gte":60, //大于等于60
               "lt":70 //小于70
           }
       }
   }
}
  • exists:所有值不为空,相当于 is not null
  • missing:所有值为空,相当于 is null
{
   "query": {
       "missing":{
           "field":"age"  // age字段为空的所有数据
       }
   }
}
  • prefix:前缀搜索
{
   "query": {
       "prefix":{
           "_id":1  // _id 以1开头的数据,不适合值为中文
       }
   }
}
  • phrase_match:篇幅匹配,不拆词匹配,寻找邻近的几个单词,我理解为精确短语匹配,即查找的短语不会被分词查找
{
   "query": {
       "match_phrase":{
           "content":"china reference"  // content中包含china reference而不是chian 或 reference
       }
   }
}
  • 其他方法
search_type,match_phrase,fuzzy,and,or,not,limit,size,from,to,gt,gte,lt,lte,field,fields,aggs,count,sum,min,max,avg

高级使用

Mapping映射
  • 如果不添加,生成时系统会默认指定mapping(映射)结构,检索时系统会猜测你想要的类型,如果对系统反馈的不满意,我们就可以手动设置
  • 添加
PUT /kkb1
{
  "mappings": {
    "properties": {
      "age": {"type": "long"}
    }
  }
}
// 注意——在ES 7.0.0之前,mappings定义需要一个类型名,如:
{
  "mappings": {
    "java": {
      "properties": {
        "age": {"type": "long"}
      }
    }
  }
}
  • 获取
GET /kkb1/_mapping

在这里插入图片描述

  • 配置 mapping 是对索引字段的映射配置,可以直接对索引文档字段设置。
{
	// 数据类型:一般文本使用text,表示可分词进行模糊查询;keyword表示无法被分词(不需要执行分词器),用于精确查找
	"type": "text",
	// 分词器:一般使用最大分词 ik_max_word
	"analyzer": "ik_max_word",
	// 字段标准化规则:如把所有字符转为小写
	"normalizer": "normalizer_name",
	// 字段权重:用于查询时评分,关键字段的权重就会高一些,默认都是1;另外查询时可临时指定权重
	"boost": 1.5,
	// 清理脏数据:字符串会被强制转换为整数,浮点数被强制转换为整数,默认为true
	"coerce": true,
	// 自定_all字段:指定某几个字段拼接成自定义
	"copy_to": "field_name",
	// 加快排序、聚合操作,但需要额外存储空间,默认true,对于确定不需要排序和聚合的字段可false
	"doc_values": true,
	// 新字段动态添加 true-无限制 false-数据可写入但该字段不保留,strict-无法写入抛异常
	"dynamic": true,
	// 是否会被索引,但都会存储;可以针对一整个_doc
	"enabled": true,
	// 针对text字段加快排序和聚合(doc_values对text无效);此项官网建议不开启,非常消耗内存
	"fielddata": false,
	// 是否开启全局预加载,加快查询;此参数只支持text和keyword,keyword默认可用,而text需要设置fielddata属性
	"eager_global_ordinals": true,
	// 格式化 此参数代表可接受的时间格式 3种都接受
	"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis",
	// 指定字段索引和存储的长度最大值,超过最大值的会被忽略
	"ignore_above": 100,
	// 插入文档时是否忽略类型,默认是false,类型不一致无法插入
	"ignore_malformed": false,
	// 4个可选参数:分词字段默认是positions,其他的默认是docs
	// docs: 索引文档号
	// freqs: 文档号+词频
	// positions: 文档号+词频+位置,通常用来做距离查询
	// offsets: 文档号+词频+位置+偏移量,通常被使用在高亮字段
	"index_options": "docs",
	// 该字段是否会被索引和可查询,默认 true
	"index": true,
	// 可以对一个字段提供多种索引模式,使用text类型做全文检索,也可使用keyword类型做聚合和排序
	"fields": {"raw": {"type": "keyword"}},
	// 用于标准化文档,以便查询时计算文档的相关性,建议不开启
	"norms": true,
	// 可以让值为null的字段显式得可索引、可搜索
	"null_value": "NULL",
	// 词组查询时可以跨词查询,既可变为分词查询,默认100
	"position_increment_gap": 0,
	// 嵌套属性,例如该字段是音乐,音乐还有歌词,类型,歌手等属性
	"properties": {},
	// 查询分词器,一般情况和analyzer对应
	"search_analyzer": "ik_max_word",
	// 用于指定文档评分模型,参数有三个
	// BM25:ES和Lucene默认的评分模型
	// classic:TF/IDF评分
	// boolean:布尔模型评分
	"similarity": "BM25",
	// 默认情况false,其实并不是真没有存储,_source字段里会保存一份原始文档
	// 在某些情况下,store参数有意义,比如一个文档里面有title、date和超大的content字段,如果只想获取title和date
	"store": true,
	// 默认不存储向量信息
	// 支持参数yES(term存储),with_positions(term + 位置),with_offsets(term + 偏移量),with_positions_offsets(term + 位置 + 偏移量)
	// 对快速高亮fast vector highlighter能提升性能,但开启又会加大索引体积,不适合大数据量用
	"term_vector": "no"
}
索引模板
介绍
  • 当索引和文档变多后,我们不能每一个都是手动的设置,这就用到了模板。
  • 模板包涵了settings和mappings信息,通过模式匹配的方式使得多个索引重用一个模板。
    • settings:索引的相关配置信息,例如分片数、副本数、tranlog、refresh等,一般使用 elasticsearch.yml 中配置。
    • mappings:说明信息,索引是否需要检索,字段类型等等,就是映射的内容设置。
使用
  • 创建模板:
POST /_template/tkkb1?include_type_name=true
{
  "index_patterns": ["kkbt*"],
  "settings": {
    "index.number_of_shards": 2,
    "number_of_replicas": 1
  },
  "mappings": {
    "java": {
      "properties": {
        "age": {
          "type": "long"
        },
        "info": {
          "search_analyzer": "ik_max_word",
          "analyzer": "ik_max_word",
          "type": "text"
        }
      }
    }
  }
}
  • 创建索引:
PUT /kkbt1/java/1
{
  "age": "18",
  "info": "我是程序员"
}
  • 查看索引信息:
GET /kkbt1/_settings
GET /kkbt1/_mapping
  • 删除模板:
DELETE /_template/tkkb1
别名
介绍
  • 类似于数据库的视图,单索引视图可以直接操作,多索引的只能查。
单索引
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "kkb1",
        "alias": "kkb"
      }
    }
  ]
}
  • 别名查询:
GET /kkb1/_alias
多索引
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "kkb1",
        "alias": "kkb"
      }
    },
    {
      "add": {
        "index": "kkbt1",
        "alias": "kkbt"
      }
    }
  ]
}
  • 别名查询:
GET /kkb1/_alias
GET /kkbt1/_alias
  • 删除别名:
POST /_aliases
{
  "actions": [
    {
      "remove": {
        "index": "kkb1",
        "alias": "kkb"
      }
    }
  ]
}
  • 需要一个一个删除,更新的话先删除后添加。
缓存管理
  • ES是先写入缓存然后再到磁盘,使用过程中会用到一些缓存类的操作。
  • 强制刷新:虽然ES设置是1秒写一次内存segment,但是可能出现延时。
// 单个
POST /kkb1/_refresh
// 全量
POST /_refresh
  • 清理缓存:缓存过大会引起效率低等情况。
// 单个
POST /kkb1/_cache/clear
// 全量
POST /_cache/clear
分词
  • 使用:在创建mapping的时候对指定的字段添加分词器。
自带分词器
  • ES 有自带的分词器,但是对中文的支持很不友好,下面简单说下自带的几个分词器,可以通过analyzer 字段来选择分词器。
  • standard:默认的分词器,对词汇转换成小写切割,去掉标点和助词(a/an/the),支持中文但是是根据文字单个切分。结果是中文直接按照每个字拆分。
POST /_analyze
{
  "analyzer": "standard",
  "text": "我是一个程序员,咿呀咿呀哟!"
}
  • simple:通过非字母字符来切割,会过滤掉数字。这个分词粒度比较大,如果不需要太细的搜索,力度大了效率会高,具体看实际场景来使用
POST /_analyze
{
  "analyzer": "simple",
  "text": "我是一个程序员,咿呀咿呀哟!"
}

在这里插入图片描述

analysis-ik
  • 官方地址:https://github.com/medcl/elasticsearch-analysis-ik
  • 使用:ik_smart,ik_max_word是ik 的两种分词器
    • ik_max_word:会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合。
    • ik_smart:会做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”。
POST /_analyze
{
  "analyzer": "ik_smart",
  "text": "我是一个程序员,咿呀咿呀哟!"
}
  • 创建ik的mapping 对kkb索引下的text字段设置分词器
POST /kkb/java/_mapping?include_type_name=true
{
  "properties": {
    "info": {
      "type": "text",
      "analyzer": "ik_max_word",
      "search_analyzer": "ik_max_word"
    }
  }
}
  • 存放多个文档值:
POST /kkb/java/1
{
  "info": "我是一个程序员,咿呀咿呀呀!"
}
POST /kkb/java/2
{
  "info": "我是一个产品,咿呀咿呀呀!"
}
POST /kkb/java/3
{
  "info": "程序员要揍产品!"
}
  • 查询:
POST /kkb/fulltext/_search
{
  "query": {
    "match": {
      "info": "程序"
    }
  }
}
  • 结果会根据词库去匹配去匹配,词库配置在IKAnalyzer.cfg.xml 位置
{conf}/analysis-ik/config/IKAnalyzer.cfg.xml 
或者jar里 {plugins}/elasticsearch-analysis-ik- */config/IKAnalyzer.cfg.xml
  • 文件内容:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM
"http://java.sun.com/dtd/properties.dtd">
<properties>
	<comment>IK Analyzer 扩展配置</comment> 
	<!-- 用户可以在这里配置自己的扩展字典  --> 
	<entry key="ext_dict">custom/mydict.dic;custom/single_word_low_freq.dic</entry>
	<!-- 用户可以在这里配置自己的扩展停止词字典 --> 
	<entry key="ext_stopwords">custom/ext_stopword.dic</entry> 
	<!-- 用户可以在这里配置远程扩展字典  -->
	<entry key="remote_ext_dict">location</entry> 
	<!-- 用户可以在这里配置远程扩展停止词字典 -->
	<entry key="remote_ext_stopwords">http://xxx.com/xxx.dic</entry>
</properties>
  • 可能遇到的错误:java.net.SocketPermission 网络访问权限问题 修改jdk/lib/java.policy文件
permission java.net.SocketPermission "*", "connect,resolve";
# 重启
高亮查询highlight
  • ES有三种高亮:
    • plain highlight(默认)
    • posting highlight(性能) 对磁盘消耗少,切割的片大
    • fast vector highlight(文件) 对大文件检索快
POST /kkb/fulltext/_search
{
  "query": {
    "match": {
      "info": "程序"
    }
  },
  "highlight": {
    "pre_tags": ["<tag1>", "<tag2>"],
    "post_tags": ["</tag1>", "</tag2>"],
    "fields": {
      "info": {}
    }
  }
}

SpringBoot中使用ES

  • 利用ES搜索商品信息

创建项目

  • 定义一个SpringBoot工程 elasticsearch-demo。
  • 引入依赖:
<dependencies>
  <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <!--spring-es-->
  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-elasticsearch</artifactId>
  </dependency>

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

代码实现

  • 启动类:
@SpringBootApplication
public class SearchServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(SearchServiceApplication.class, args);
    }
}
  • yml配置文件:
server:
  port: 8000
spring:
  application:
    name: es-demo
  data:
    # 配置ES集群地址
    elasticsearch:
      cluster-name: yw-es
      cluster-nodes: 192.168.254.120:9300,192.168.254.102:9300,192.168.254.104:9300
  • repository和pojo:
// 商品信息
@Data
@Accessors(chain = true)
@Document(indexName = "supergo", type = "goods")
public class Goods {
    @Id
    @Field(type = FieldType.Long, store = true)
    private long id;
    @JsonProperty(value = "goods_name")
    @Field(type = FieldType.Text, store = true, analyzer = "ik_max_word")
    private String goodsName;
    @JsonProperty(value = "seller_id")
    @Field(type = FieldType.Keyword, store = true)
    private String sellerId;
    @JsonProperty(value = "nick_name")
    @Field(type = FieldType.Keyword, store = true)
    private String nickName;
    @JsonProperty(value = "brand_id")
    @Field(type = FieldType.Long, store = true)
    private long brandId;
    @JsonProperty(value = "brand_name")
    @Field(type = FieldType.Keyword, store = true)
    private String brandName;
    @JsonProperty(value = "goods_name")
    @Field(type = FieldType.Long, store = true)
    private long category1Id;
    @JsonProperty(value = "goods_name")
    @Field(type = FieldType.Keyword, store = true)
    private String cname1;
    @JsonProperty(value = "category2_id")
    @Field(type = FieldType.Long, store = true)
    private long category2Id;
    @Field(type = FieldType.Keyword, store = true)
    private String cname2;
    @JsonProperty(value = "category3_id")
    @Field(type = FieldType.Long, store = true)
    private long category3Id;
    @Field(type = FieldType.Keyword, store = true)
    private String cname3;
    @JsonProperty(value = "small_pic")
    @Field(type = FieldType.Keyword, store = true, index = false)
    private String smallPic;
    @Field(type = FieldType.Float, store = true)
    private double price;
}
// GoodsRepository
public interface GoodsRepository extends ElasticsearchCrudRepository<Goods, Long>{

}
  • 控制器:
// 搜索结果
@Data
@Accessors(chain = true)
public class SearchResult {
    private List<Goods> goodsList;
    private List<?> aggs;
    private Integer page;
    private Integer totalPage;
}
// api接口
@RestController
public class SearchController {
    @Resource
    private SearchService searchService;

    @GetMapping("/search")
    public SearchResult search(@RequestParam(required = true) String keyword,
                               @RequestParam(required = false) String ev,
                               @RequestParam(defaultValue = "1") int page,
                               @RequestParam(defaultValue = "50") int rows) {
        // 将ev转换成List类型
        List<Map> filters = null;
        if (StringUtils.isNotBlank(ev)) {
            String[] strings = ev.split("\\|");
            filters = Stream.of(strings).map(e -> {
                Map<String, String> fs = new HashMap<>(2);
                fs.put("key", e.split("-")[0]);
                fs.put("value", e.split("-")[1]);
                return fs;
            }).collect(Collectors.toList());
        }
        return searchService.search(keyword, filters, page, rows);
    }
}
  • service层:
@Service
public class SearchService {
    @Resource
    private ElasticsearchTemplate elasticsearchTemplate;

    public SearchResult search(String queryString, List<Map> filters, int page, int rows) {
        //创建一个查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.queryStringQuery(queryString).defaultField("goods_name"))
                //设置分页条件
                .withPageable(PageRequest.of(page, rows))
                //设置聚合条件
                .addAggregation(AggregationBuilders.terms("brand_aggs").field("brand_name"))
                .addAggregation(AggregationBuilders.terms("category_aggs").field("cname3"))
                //设置高亮显示
                .withHighlightFields(new HighlightBuilder.Field("goods_name").preTags("<em>").postTags("</em>"));
        //设置过滤条件
        if (filters != null && !filters.isEmpty()) {
            for (Map filer : filters) {
                queryBuilder.withFilter(QueryBuilders.termQuery((String) filer.get("key"), filer.get("value")));
            }
        }
        //创建查询对象
        NativeSearchQuery query = queryBuilder.build();
        //-----------------
        //执行查询
        AggregatedPage<Goods> aggregatedPage = elasticsearchTemplate.queryForPage(query, Goods.class, new SearchResultMapper() {
            @Override
            public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> clazz, Pageable pageable) {
                List<Goods> goodsList = new ArrayList<>();
                SearchHits searchHits = searchResponse.getHits();
                searchHits.forEach(hits -> {
                    Goods goodsEntity = new Goods();
                    goodsEntity.setId((Long) hits.getSourceAsMap().get("id"))
                            .setGoodsName((String) hits.getSourceAsMap().get("goods_name"))
                            .setSellerId((String) hits.getSourceAsMap().get("seller_id"))
                            .setNickName((String) hits.getSourceAsMap().get("nick_name"))
                            .setBrandId((Integer) hits.getSourceAsMap().get("brand_id"))
                            .setBrandName((String) hits.getSourceAsMap().get("brand_name"))
                            .setCategory1Id((Integer) hits.getSourceAsMap().get("category1_id"))
                            .setCname1((String) hits.getSourceAsMap().get("cname1"))
                            .setCategory2Id((Integer) hits.getSourceAsMap().get("category2_id"))
                            .setCname2((String) hits.getSourceAsMap().get("cname2"))
                            .setCategory3Id((Integer) hits.getSourceAsMap().get("category3_id"))
                            .setCname3((String) hits.getSourceAsMap().get("cname3"))
                            .setSmallPic((String) hits.getSourceAsMap().get("small_pic"))
                            .setPrice((Double) hits.getSourceAsMap().get("price"));
                    //取高亮结果
                    HighlightField highlightField = hits.getHighlightFields().get("goods_name");
                    if (highlightField != null) {
                        String hl = highlightField.getFragments()[0].string();
                        goodsEntity.setGoodsName(hl);
                    }
                    goodsList.add(goodsEntity);
                });
                return new AggregatedPageImpl<>((List<T>) goodsList, pageable, searchHits.totalHits, searchResponse.getAggregations());
            }
        });
        //取商品列表
        List<Goods> content = aggregatedPage.getContent();
        //取聚合结果
        Terms termBrand = (Terms) aggregatedPage.getAggregation("brand_aggs");
        List<String> brandAggsList = termBrand.getBuckets().stream().map(MultiBucketsAggregation.Bucket::getKeyAsString).collect(Collectors.toList());
        Map brandMap = new HashMap(4);
        brandMap.put("name", "品牌");
        brandMap.put("content", brandAggsList);
        brandMap.put("filed", "brand_name");
        Terms termCategory = (Terms) aggregatedPage.getAggregation("category_aggs");
        List<String> categoryAggsList = termCategory.getBuckets().stream().map(MultiBucketsAggregation.Bucket::getKeyAsString).collect(Collectors.toList());
        Map catMap = new HashMap(4);
        catMap.put("name", "分类");
        catMap.put("content", categoryAggsList);
        catMap.put("filed", "cname3");
        List<Map> aggsList = new ArrayList<>();
        aggsList.add(brandMap);
        aggsList.add(catMap);
        //取总记录数
        int totalPages = aggregatedPage.getTotalPages();
        //封装成一个SearchResult对象
        SearchResult searchResult = new SearchResult();
        searchResult.setGoodsList(content);
        searchResult.setAggs(aggsList);
        searchResult.setPage(page);
        searchResult.setTotalPage(totalPages);
        //返回结果
        return searchResult;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

讲文明的喜羊羊拒绝pua

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

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

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

打赏作者

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

抵扣说明:

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

余额充值