Elasticsearch

Lucene


Lucene 基于 Java 的全文信息检索工具包,不是一个完整的搜索应用程序,而是为你的应用程序提供索引和搜索功能。

目前有很多应用程序的搜索功能基于 Lucene,比如 Eclipse 的帮助系统的搜索功能。Lucene 能够为文本类型的数据建立索引,只要能把你要索引的数据格式转化为文本,Lucene 就能对你的文档进行索引和搜索。 比如一些 HTML 文档,PDF 文档进行索引,需要把 HTML 文档和 PDF 转化为文本格式。然后把转化后的内容交给 Lucene 索引。最后把创建好的索引文件保存到磁盘或者内存中。根据用户输入的查询条件在索引文件上进行查询。不指定要索引的文档的格式,使得 Lucene 能够几乎适用于所有的搜索应用程序。

索引是现代搜索引擎的核心,建立索引的过程就是把元数据处理成方便查询的索引文件的过程。

Lucene 软件包的发布形式是 jar 包。主要的 java 包有

org.apache.lucene.document,提供了一些为封装要索引的文档所需要的类,比如 Document, Feild 这样,每一个文档最终被封装成了一个 Document 对象。

org.apache.lucene.analysis,对文档进行分词,文档在建立索引之前必须进行分词,这个包的作用可以看成为建立索引做准备工作。

org.apache.lucene.index,这个包提供了一些类来协助创建索引以及对创建好的索引进行更新。这里有两个基础的类:IndexWriter 和 IndexReader,其中 IndexWriter 是用来创建索引,并且添加文档到索引中,IndexReader 用来删除索引中的文档。

org.apache.lucene.search,这个包对建立好的索引上进行搜索,IndexSearcher 和 Hits,IndexSearcher 定义了指定的索引上进行搜索的方法,Hits 保存搜索得到的结果。

建立索引

在电脑的目录中,含有很多个文本文档,我们需要查找哪些文档中有某个关键词。

为了实现这种功能,先利用 Lucene 对这个目录中的文档建立索引,然后再建立好的索引中搜索我们需要查找的文档。

为了对文档进行索引,Lucene 提供了5个基础类,Document,Field,IndexWriter,Analyzer,Directory。

  • Document
    • 描述文档,这里的文档可以指一个 HTML 页面,一封电子邮件,一个文本文件。一个 Document 由多个 Field 对象组成,把一个 Document 对象想象成数据库中的一个记录,每个 Field 对象就是记录的一个字段。
  • Field
    • 描述文档的某个属性,比如一封电子邮件的标题和内容可以用两个 Field 对象分别描述。
  • Analyzer
    • 在一个文档被索引之前,首先需要对文档内容进行分词处理,这部分工作就是由 Analyzer 来做的,Analyzer 类是一个抽象类,有多个实现,针对不同的语言和应用,选择合适的 Analyzer。把分词后的内容交给 IndexWriter 来建立索引。
  • IndexWriter
    • Lucene 创建索引的核心类,把一个个的 Document 对象添加到索引中
  • Directory
    • Lucene 的索引的存储位置,抽象类,FSDirectory,表示存储在文件系统中的索引位置,RAMDirectory 表示一个存储在内存中的索引的位置。

熟悉建立索引需要的这些类后,就开始对某个目录下面的文本文件建立索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package TestLucene; 
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
import java.util.Date;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexWriter;
/**
* This class demonstrate the process of creating index with Lucene
* for text files
*/
public class TxtFileIndexer {
     public static void main(String[] args) throws Exception{
     // 存放索引文件的位置
     File   indexDir = new File("D:\\luceneIndex");
     // 索引对应的目录文件的位置
     File   dataDir  = new File("D:\\luceneData");
     Analyzer luceneAnalyzer = new StandardAnalyzer();
     File[] dataFiles  = dataDir.listFiles();
     IndexWriter indexWriter = new IndexWriter(indexDir,luceneAnalyzer,true);
     long startTime = new Date().getTime();
     for(int i = 0; i < dataFiles.length; i++){
          if(dataFiles[i].isFile() && dataFiles[i].getName().endsWith(".txt")){
               System.out.println("Indexing file " + dataFiles[i].getCanonicalPath());
               Document document = new Document();
               Reader txtReader = new FileReader(dataFiles[i]);
               document.add(Field.Text("path",dataFiles[i].getCanonicalPath()));
               document.add(Field.Text("contents",txtReader));
               indexWriter.addDocument(document);
          }
     }
     indexWriter.optimize();
     indexWriter.close();
     long endTime = new Date().getTime();
         
     System.out.println("It takes " + (endTime - startTime)
         + " milliseconds to create index for the files in directory "
         + dataDir.getPath());       
     }
}

IndexWriter 构造函数需要三个参数,第一个参数指定了创建的索引要存放的位置,可以是 File 对象,也可以是 FSDirectory 或者 RAMDirectory 对象。第二个参数指定了 Analyzer 类的一个实现,指定这个索引是用哪个分词器对文档内容进行分词。第三个参数是一个布尔型的变量,如果为 true 的话就代表创建一个新的索引,为 false 的话就代表在原来索引的基础上进行操作。程序遍历了目录下的所有文本文档,并且为每个文本文档创建了一个 Document 对象。

把文本文档的两个属性:路径和内容加入到两个 Field 对象中,把这个两个对象加入到 Document 对象中,把文档用 IndexWriter 类的 add 方法加入到索引中去,这样就完成了索引的创建。

搜索文档

在上一个部分中,已经为一个目录下的文本文档建立好了索引,现在就在索引的基础上搜索包含某个关键字或者短语的文档。Lucene 提供了几个基础的类完成这个过程 IndexSearcher,Term,Query,TermQuery,Hits

  • Query
    • 这是一个抽象类,他有多个实现,比如 TermQuery,BooleanQuery, PrefixQuery,这个类的目的是吧用户输入的查询字符串封装成 Lucene 能够识别的 Query。
  • Term
    • 是搜索的基本单位,一个 Term 对象有两个 String 类型的域组成。生成一个 Term 对象可以 Term term = new Term(“fieldName”, “queryWord”);第一个参数代表要在文档的哪一个 Field 上进行查找,第二个参数代表要查询的关键词。
  • TermQuery
    • TermQuery 是抽象类 Query 的一个子类,同时也是 Lucene 支持的最基本的查询类,生成 TermQeury 对象:TermQuery termQuery = new TermQuery(new Term("fieldName", "queryWord")); 构造函数只接受一个 Term 对象。
  • IndexSearcher
    • 用来在建立好的索引上进行搜索,只能以只读的方式打开一个索引,可以多个 IndexSearcher 的实例在一个索引上进行操作。
  • Hits
    • 保存搜索的结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package TestLucene; 
import java.io.File;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.FSDirectory;
/**
* This class is used to demonstrate the
* process of searching on an existing
* Lucene index
*
*/
public class TxtFileSearcher {
public static void main(String[] args) throws Exception{
String queryStr = "lucene";
//This is the directory that hosts the Lucene index
File indexDir = new File("D:\\luceneIndex");
FSDirectory directory = FSDirectory.getDirectory(indexDir,false);
IndexSearcher searcher = new IndexSearcher(directory);
if(!indexDir.exists()){
System.out.println("The Lucene index is not exist");
return;
}
Term term = new Term("contents",queryStr.toLowerCase());
TermQuery luceneQuery = new TermQuery(term);
Hits hits = searcher.search(luceneQuery);
for(int i = 0; i < hits.length(); i++){
Document document = hits.doc(i);
System.out.println("File: " + document.get("path"));
}
}
}

IndexSearcher 的构造函数接受一个类型为 Directory 的对象,传入两个子类:FSDirectory 和 RAMDirectory 传入一个 FSDirectory 对象作为其参数,代表一个存储的磁盘上的索引的位置。

构造完 IndexSearcher 后,代表以只读的方式打开一个索引,程序构造了一个 Term 对象,通过这个 Term 对象,指定了要在文档的内容中搜索包含关键词 lucene 的文档,利用这个 Term 创建出 TermQuery 对象并且把这个 TermQuery 对象传入到 IndexSearcher 的 search 方法中查询。返回结果保存在 Hits 对象中。

Elasticsearch


基于 Lucene 的搜索服务器,分布式多用户的全文搜索引擎,基于 RESTful web 接口。实时搜索、稳定、快速、安装使用方便。

建立一个网站或者应用程序,并要添加搜索功能,想要完成搜索工作的创建是非常困难的。希望能够简单的使用 Json 通过 Http 来索引数据,希望搜索服务器始终可用,从一台开始扩展到数百台,实时搜索,建立一个云的解决方案,利用 Elasticsearch 解决这些问题及可能出现的更多问题。

6.x 以后,索引内置能有一个类型,后续计划删除类型这个概念,ES6 开始让索引名和类型名一致。

  • 索引 ES将数据存储于一个或者多个索引中,索引是具有类似特性的文档的集合,一个 ES 集群可以按需创建任意数量的索引
  • 映射 Mapping,索引库中的字段名及其数据类型进行定义,类似于 mysql 中的表结构信息。 es 的mapping 比数据库灵活的多,尅动态识别字段,一般不需要指定 mapping,es 会自动根据数据格式识别它的类型,如果需要对某些字段添加特殊属性(定义使用其他分词器、是否分词、是否存储),就必须手动添加 mapping
  • 文档 文档是 lucene 索引和搜索的原子单位,包含了一个或者多个域的容器,基于 json 格式表示,每个域拥有一个名字以及一个或多个值。

ES 的分片(shard) 机制,可以将一个索引内部的数据分布存储于多个节点,通过将一个索引切分为多个底层物理的 lucene 索引完成索引数据的分割存储功能,每一个物理的 lucene 索引成为一个分片(shard),每个分片其内部都是一个全功能且独立的索引,所以可由集群中的任何主机存储,创建索引时,用户可指定分片的数量,默认为5。

shard 有两种类型 primary 和 replica,主副,Replica shard 是 primary shard 的副本。也可以配置多个。 ES 会根据需要自动增加或者减少 Replica shard 的数量。

安装 Elasticsearch


1
2
3
4
brew install elasticsearch
brew install kibana

启动 elasticsearch kibana

分词器


分词器,把文本内容按照标准进行切分,内容转为小写,去掉标点,遇到每个中文字符,都当成一个单词处理。如果要对中文进行分词,安装中文分词器插件 ik

默认分词器

1
2
3
4
5
6
PUT /shop_product

GET /shop_product/_analyze
{
"text": "I am Groot"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"tokens" : [
{
"token" : "i",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "am",
"start_offset" : 2,
"end_offset" : 4,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "groot",
"start_offset" : 5,
"end_offset" : 10,
"type" : "<ALPHANUM>",
"position" : 2
}
]
}

安装 ik 分词器

Elasticsearch 版本 6.8.3 Analysis-ik 6.8.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mingle的个人空间  elastic  正文
Mac OS 安装 Elasticsearch 使用 analysis-ik
mingle mingle 发布于 2018/10/08 17:42 字数 199 阅读 604 收藏 0 点赞 0 评论 0
Mac OS XElasticSearch
系统: Mac OS X
Elasticsearch 版本: 5.6.4 Analysis-ik 5.6.4

# 更新 brew
$ brew update
# 修改formula
$ cd /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
$ git checkout 5874ac8c8fab69f8a714d643581e489bcb176d92 elasticsearch@5.6.rb
# 安装
$ brew install elasticsearch@5.6
# 查看
$ brew info elasticsearch@5.6
# 锁定版本, 防止被误升级
$ brew pin elasticsearch@5.6
# 安装ik
$ cd /usr/local/Cellar/elasticsearch@5.6/5.6.4
$ bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.8.3/elasticsearch-analysis-ik-6.8.3.zip

使用

1
2
3
4
5
6
7
# 查看状态
$ brew services list
# 启动/重启/停止
$ brew services start elasticsearch
$ brew services start kibana
$ brew services restart elasticsearch
$ brew services stop elasticsearch

分词效果如下: ik_smart 粗粒度分词,ik_max_word 细粒度分词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
GET /shop_product/_analyze
{
"text": "英特尔酷睿i7处理器",
"analyzer": "ik_smart"
}

{
"tokens" : [
{
"token" : "英特尔",
"start_offset" : 0,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "酷",
"start_offset" : 3,
"end_offset" : 4,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "睿",
"start_offset" : 4,
"end_offset" : 5,
"type" : "CN_CHAR",
"position" : 2
},
{
"token" : "i7",
"start_offset" : 5,
"end_offset" : 7,
"type" : "LETTER",
"position" : 3
},
{
"token" : "处理器",
"start_offset" : 7,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 4
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
GET /shop_product/_analyze
{
"text": "英特尔酷睿i7处理器",
"analyzer": "ik_max_word"
}

{
"tokens" : [
{
"token" : "英特尔",
"start_offset" : 0,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "英特",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "尔",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_CHAR",
"position" : 2
},
{
"token" : "酷",
"start_offset" : 3,
"end_offset" : 4,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "睿",
"start_offset" : 4,
"end_offset" : 5,
"type" : "CN_CHAR",
"position" : 4
},
{
"token" : "i7",
"start_offset" : 5,
"end_offset" : 7,
"type" : "LETTER",
"position" : 5
},
{
"token" : "i",
"start_offset" : 5,
"end_offset" : 6,
"type" : "ENGLISH",
"position" : 6
},
{
"token" : "7",
"start_offset" : 6,
"end_offset" : 7,
"type" : "ARABIC",
"position" : 7
},
{
"token" : "处理器",
"start_offset" : 7,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 8
},
{
"token" : "处理",
"start_offset" : 7,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 9
},
{
"token" : "处",
"start_offset" : 7,
"end_offset" : 8,
"type" : "COUNT",
"position" : 10
},
{
"token" : "理",
"start_offset" : 8,
"end_offset" : 9,
"type" : "CN_CHAR",
"position" : 11
},
{
"token" : "器",
"start_offset" : 9,
"end_offset" : 10,
"type" : "CN_CHAR",
"position" : 12
}
]
}

拓展词库

找到 IK 插件中的 config/main.dic 文件,往里面添加新词汇,重启服务器即可。

倒排索引


将文档id 和 文档数据,以分词为索引重新排列,在查找时,根据分词进行查找,找到文档的 id ,然后找出文档内容展示。

以往都是根据文档 id 查询文档内容,而分词的方式,是根据分词查询文档 id,所以叫倒排索引。

基本操作


建立索引,相当于建立数据库

1
2
3
4
5
6
7
8
PUT /索引名
在没有特殊设置的情况下,默认有5个分片,1个备份,也可以通过请求参数的方式来指定
{
"settings": {
"number_of_shards": 5, //设置5个片区
"number_of_replicas": 1 //设置1个备份
}
}

删除索引 DELETE /索引名

映射操作,建立映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
语法:PUT 索引 
{
"mappings": {
类型名: {
"properties": {
字段名: {
"type": 字段类型,
"analyzer": 分词器类型,
"search_analyzer": 分词器类型,
...
}, ...
}
}
}
}
字段类型:double / long / integer / text / keyword / date / binary
注意:text和keyword都是字符串类型,但是只有text类型的数据才能分词,字段的配置一旦确定就不能更改 映射的配置项有很多,我们可以根据需要只配置用得上的属性

查询映射 GET/索引名/_mapping

CRUD 操作


少量的增删改,主要做查询。查询性能很高。启动项目的时候添加数据。添加的数据从其他数据库中导入。不会主动生产数据,数据来源于其他数据库。

增/改 语法

1
2
3
4
5
6
PUT /索引名/类型名/文档ID
{
field1: value1,
field2: value2,
...
}

注意
当索引/类型/映射不存在时,会使用默认设置自动添加
ES中的数据一般是从别的数据库导入的,所以文档的ID 会沿用原数据库中的 ID
索引库中没有该 ID 对应的文档时,则新增,有的时候就替换。

文档内置字段

  • _index 索引
  • _type 类型
  • _id 文档ID
  • _version 乐观锁版本号
  • _source 数据内容

查询

语法:根据 ID 查询 -> GET /索引名/类型/文档ID
查询所有 -> GET /索引名/类型/_search

文档内置字段

  • took 耗时
  • _shards.total 分片总数
  • hits.total 查询到的数量
  • hits.max_score 最大匹配度 可能大于1.0,查询所有是 1.0
  • hits.hits 查询到的结果
  • hits.hits._score 匹配度

删除

语法:DELETE /索引/类型/文档ID

这里的删除不是真正意义上的删除,仅仅是清空文档内容而已,并且标记该文档的状态为删除

高级查询


Elasticsearch 基于 JSON 提供完整的查询 DSL (Domain Specific Language 领域特定语言) 来定义查询

领域特定语言

基本语法 GET /索引/类型/_search
一般都是需要配合查询参数来使用的,配合不同的参数有不同的查询效果。加入如下数据,来体验高级查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
POST /shop_product/shop_product/_bulk
{"create":{"_id": 1}}
{"id":1,"title":"Apple iPhone XR (A2108) 128GB 白色 移动联通电信4G手机 双卡双待","price":5299,"intro":"【iPhoneXR限时特惠!】6.1英寸视网膜显示屏,A12仿生芯片,面容识别,无线充电,支持双卡!选【换修无忧版】获 AppleCare 原厂服务,享只换不修!更有快速换机、保值换新、轻松月付!","brand":"Apple"}
{"create":{"_id": 2}}
{"id":2,"title":"Apple 2019款 Macbook Pro 13.3【带触控栏】八代i7 18G 256G RP645显卡 深空灰 苹果笔记本电脑 轻薄本 MUHN2CH/A","price":15299,"intro":"【八月精选】Pro2019年新品上市送三重好礼,现在购买领满8000减400元优惠神劵,劵后更优惠!","brand":"Apple"}
{"create":{"_id": 3}}
{"id":3,"title":"Apple iPad Air 3 2019年新款平板电脑 10.5英寸(64G WLAN版/A12芯片/Retina显示屏/MUUL2CH/A)金色","price":3788,"intro":"8月尊享好礼!买iPad即送蓝牙耳机!领券立减!多款产品支持手写笔!【新一代iPad,总有一款适合你】选【换修无忧版】获 AppleCare 原厂服务,享只换不修!更有快速换机、保值换新、轻松月付!","brand":"Apple"}
{"create":{"_id": 4}}
{"id":4,"title":"华为HUAWEI MateBook X Pro 2019款 英特尔酷睿i5 13.9英寸全面屏轻薄笔记本电脑(i5 8G 512G 3K 触控) 灰","price":7999,"intro":"3K全面屏开启无界视野;轻薄设计灵动有型,HuaweiShare一碰传","brand":"华为"}
{"create":{"_id": 5}}
{"id":5,"title":"华为 HUAWEI Mate20 X (5G) 7nm工艺5G旗舰芯片全面屏超大广角徕卡三摄8GB+256GB翡冷翠5G双模全网通手机","price":6199,"intro":"【5G双模,支持SA/NSA网络,7.2英寸全景巨屏,石墨烯液冷散热】5G先驱,极速体验。","brand":"华为"}
{"create":{"_id": 6}}
{"id":6,"title":"华为平板 M6 10.8英寸麒麟980影音娱乐平板电脑4GB+64GB WiFi(香槟金)","price":2299,"intro":"【华为暑期购】8月2日-4日,M5青春版指定爆款型号优惠100元,AI语音控制","brand":"华为"}
{"create":{"_id": 7}}
{"id":7,"title":"荣耀20 PRO DXOMARK全球第二高分 4800万四摄 双光学防抖 麒麟980 全网通4G 8GB+128GB 蓝水翡翠 拍照手机","price":3199,"intro":"白条6期免息!麒麟980,4800万全焦段AI四摄!荣耀20系列2699起,4800万超广角AI四摄!","brand":"荣耀"}
{"create":{"_id": 8}}
{"id":8,"title":"荣耀MagicBook Pro 16.1英寸全面屏轻薄性能笔记本电脑(酷睿i7 8G 512G MX250 IPS FHD 指纹解锁)冰河银","price":6199,"intro":"16.1英寸无界全面屏金属轻薄本,100%sRGB色域,全高清IPS防眩光护眼屏,14小时长续航,指纹一健开机登录,魔法一碰传高速传输。","brand":"荣耀"}
{"create":{"_id": 9}}
{"id":9,"title":"荣耀平板5 麒麟8核芯片 GT游戏加速 4G+128G 10.1英寸全高清屏影音平板电脑 WiFi版 冰川蓝","price":1549,"intro":"【爆款平板推荐】哈曼卡顿专业调音,10.1英寸全高清大屏,双喇叭立体环绕音,配置多重护眼,值得拥有!","brand":"荣耀"}
{"create":{"_id": 10}}
{"id":10,"title":"小米9 4800万超广角三摄 6GB+128GB全息幻彩蓝 骁龙855 全网通4G 双卡双待 水滴全面屏拍照智能游戏手机","price":2799,"intro":"限时优惠200,成交价2799!索尼4800万广角微距三摄,屏下指纹解锁!","brand":"小米"}
{"create":{"_id": 11}}
{"id":11,"title":"小米(MI)Pro 2019款 15.6英寸金属轻薄笔记本(第八代英特尔酷睿i7-8550U 16G 512GSSD MX250 2G独显) 深空灰","price":6899,"intro":"【PCIE固态硬盘、72%NTSC高色域全高清屏】B面康宁玻璃覆盖、16G双通道大内存、第八代酷睿I7处理器、专业级调校MX150","brand":"小米"}
{"create":{"_id": 12}}
{"id":12,"title":"联想(Lenovo)拯救者Y7000P 2019英特尔酷睿i7 15.6英寸游戏笔记本电脑(i7 9750H 16G 1T SSD GTX1660Ti 144Hz)","price":9299,"intro":"超大1T固态,升级双通道16G内存一步到位,GTX1660Ti电竞级独显,英特尔9代i7H高性能处理器,144Hz电竞屏窄边框!","brand":"联想"}

结果排序

1
2
3
4
5
6
7
参数格式:
{
"sort" : [
{field: 排序规则},
...
]
}

排序规则:asc 表示升序,desc 表示降序。 没有配置排序的情况下,默认按照评分降序排列。

分页查询

1
2
3
4
5
参数格式:
{
"from" : start,
"size" : pageSize
}

需求1:查询所有文档按照价格降序排列

1
2
3
4
5
6
7
8
9
10
11
# 需求1:查询所有文档按照价格降序排列
GET /shop_product/shop_product/_search
{
"sort": [
{
"price": {
"order": "desc"
}
}
]
}

需求2:分页查询文档按照价格降序排列,显示第2页,每页显示3个

1
2
3
4
5
6
7
8
9
10
11
12
GET /shop_product/shop_product/_search
{
"sort": [
{
"price": {
"order": "desc"
}
}
],
"from": 1,
"size": 3
}

检索查询

1
2
3
4
5
6
参数格式:
{
"query" : {
检索方式: {field: value}
}
}

检索方式:

  • term 表示精确匹配,value 值不会被分词器拆分,按照倒排索引匹配
  • match 表示全文检索,value 值会被分词器拆分,然后去倒排索引中匹配
  • range 表示范围检索,其 value 值是一个对象,如{ “range”: {field: {比较规则: value, …}} },比较规则有 gt/gte/lt/lte 等

注意 term 和 match 都能用在数值和字符上,range 多用在数值上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 需求1:查询商品标题中符合"游戏 手机"的字样的商品
GET /shop_product/shop_product/_search
{
"query": {
"match": {
"title": "游戏 手机"
}
}
}

# 需求2:查询商品价格等于15299的商品
GET /shop_product/shop_product/_search
{
"query": {
"term": {
"price": 15299
}
}
}

# 需求3:查询商品价格在5000~10000之间商品,按照价格升序排列
GET /shop_product/shop_product/_search
{
"query": {
"range": {
"price": {
"gt": 5000,
"lt": 10000
}
}
}
}

关键字查询

1
2
3
4
5
6
7
8
9
参数格式:
{
"query" : {
"multi_match" : {
"query": value,
"fields": [field1, field2, ...]
}
}
}

multi_match 表示在多个字段间做检索,只要其中一个字段满足条件就能查询出来,多用在字符上。

1
2
3
4
5
6
7
8
9
10
# 需求1:查询商品标题或简介中符合"蓝牙 指纹 双卡"的字样的商品
GET /shop_product/shop_product/_search
{
"query": {
"multi_match": {
"query": "蓝牙 指纹 双卡",
"fields": ["title","intro"]
}
}
}

高亮显示

1
2
3
4
5
6
7
8
9
10
11
12
13
参数格式:
{
"query" : {...},
"highlight" : {
"fields" : {
field1 : {},
field2 : {},
...
}
},
"pre_tags": 开始标签,
"post_tags": 结束标签
}

highlight: 表示高亮显示,需要在 fields 中配置哪些字段中检索到该内容需要高亮显示,必须配合检索(term/match) 一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 需求1:查询商品标题或简介中符合"蓝牙 指纹 双卡"的字样的商品,并且高亮显示
GET /shop_product/shop_product/_search
{
"query": {
"multi_match": {
"query": "蓝牙 指纹",
"fields": ["title","intro"]
}
},
"highlight": {
"fields": {
"title": {},
"intro": {}
},
"pre_tags": "<span style='color:red'>",
"post_tags": "</span>"
}
}

逻辑查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
参数格式:
{
"query": {
"bool": {
逻辑规则: [
{
检索方式: {
field: value
},
...
}
],
...
}
}
}

逻辑规则:must/should/must_not, 相当于 and/or/not

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 需求1:查询商品标题中符合"i7"的字样并且价格大于7000的商品
GET /shop_product/shop_product/_search
{
"query": {
"bool": {
"must": [
{"range": {
"price": {
"gte": 7000
}
}},
{
"match": {
"title": "i7"
}
}
]
}
}
}

# 需求2:查询商品标题中符合"pro"的字样或者价格在1000~3000的商品
GET /shop_product/shop_product/_search
{
"query": {
"bool": {
"should": [
{"range": {
"price": {
"gt": 1000,
"lt": 3000
}
}},
{
"match":{
"title" : "pro"
}
}
]
}
}
}

过滤查询

1
2
3
4
5
6
7
8
9
10
11
参数格式:
{
"query": {
"bool": {
"filter": [
{检索方式: {field: value}},
...
]
}
}
}

从效果上,过滤查询和检索查询一样,区别是过滤查询不评分,结果可以缓存。检索查询要评分,结果不缓存,一般不会直接使用过滤查询。都是在检索了一定数据的基础上采用。

query 和 filter 的区别

分组查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
参数格式:
{
"size": 0,
"aggs": {
自定义分组字段名: {
"terms": { // 按照什么来分组
"field": 分组字段,
"order": {自定义统计字段: 排序规则},
"size": 10 // 默认显示10组
},
"aggs": { // 分组后的统计查询,相当于 mysql 分组查询函数
自定义统计字段: {
分组运算: {
"field": 统计字段
}
}
}
}
}
}

分组运算 avg/sum/min/max/value_count/stats(执行以上所有功能),size=0目的是为了不显示 hit 内容,专注点放在分组上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# 需求1:按照品牌分组,统计各品牌的数量
GET /shop_product/shop_product/_search
{
"size": 0,
"aggs": {
"groupByBrand": {
"terms": {
"field": "brand.keyword"
}
}
}
}

{
"took" : 6,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 12,
"max_score" : 0.0,
"hits" : [ ]
},
"aggregations" : {
"groupByBrand" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "Apple",
"doc_count" : 3
},
{
"key" : "华为",
"doc_count" : 3
},
{
"key" : "荣耀",
"doc_count" : 3
},
{
"key" : "小米",
"doc_count" : 2
},
{
"key" : "联想",
"doc_count" : 1
}
]
}
}
}






# 需求2:按照品牌分组,统计各品牌的平均价格
GET /shop_product/shop_product/_search
{
"size": 0,
"aggs": {
"groupByBrand": {
"terms": {
"field": "brand.keyword",
"order": {"avgPrice": "desc"}
},
"aggs": {
"avgPrice": {
"avg": {
"field": "price"
}
}
}
}
}
}

{
"took" : 24,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 12,
"max_score" : 0.0,
"hits" : [ ]
},
"aggregations" : {
"groupByBrand" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "联想",
"doc_count" : 1,
"avgPrice" : {
"value" : 9299.0
}
},
{
"key" : "Apple",
"doc_count" : 3,
"avgPrice" : {
"value" : 8128.666666666667
}
},
{
"key" : "华为",
"doc_count" : 3,
"avgPrice" : {
"value" : 5499.0
}
},
{
"key" : "小米",
"doc_count" : 2,
"avgPrice" : {
"value" : 4849.0
}
},
{
"key" : "荣耀",
"doc_count" : 3,
"avgPrice" : {
"value" : 3649.0
}
}
]
}
}
}


# 需求3:按照品牌分组,统计各品牌的价格数据
GET /shop_product/shop_product/_search
{
"size": 0,
"aggs": {
"groupByBrand": {
"terms": {
"field": "brand.keyword"
},
"aggs": {
"avgPrice": {
"stats": {
"field": "price"
}
}
}
}
}
}

批处理

需要集中的批量处理文档,如果使用单个 API,会浪费大量资源,ES 为提高性能,专门提供了批处理 API

mget 批量查询

1
2
3
4
5
6
7
8
语法:
GET /索引名/类型/_mget
{
"docs": [
{"_id": 文档ID},
...
]
}

bulk 批量增删改

1
2
3
4
5
6
7
8
9
10
语法
POST /索引名/类型/_bulk
{
动作: {"_id": 文档ID}
}
{...}
{
动作: {"_id": 文档ID}
}
{...}

动作:create/update/delete,其中 delete 只有1行 json,其他都是2行 json,并且 json 不能格式化,如果是 update 动作,它的数据需要加个 key:doc。
{"update": {"_id": xx}}
{"doc": {"xx": xx, "xx": xx}}

Spring Data Elasticsearch


准备工作 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
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.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.lizhaoloveit</groupId>
<artifactId>elasticsearch</artifactId>
<version>1.0</version>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 用于高亮显示 -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

application.properties

1
2
3
4
# 配置集群名称,名称写错会连接不上服务器,默认 elasticsearch
spring.data.elasticsearch.cluster-name=elasticsearch
# 配置集群节点
spring.data.elasticsearch.cluster-nodes=localhost:9300

编写 domain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @Document:配置操作哪个索引下的哪个类型
* @Id:标记文档ID字段
* @Field:配置映射信息,如:分词器
*/
@NoArgsConstructor
@Setter
@Getter
@Document(indexName = "shop_product", type = "shop_product")
public class product {
@Id // 哪个属性时文档的主键
private Long id;
@Field(type = FieldType.Keyword, analyzer = "ik_smart", searchAnalyzer = "ik_smart")
private String title;

@Field(type = FieldType.Long)
private Integer price;

@Field(type = FieldType.Keyword, analyzer = "ik_smart", searchAnalyzer = "ik_smart")
private String intro;
}

组件介绍

  • ElasticsearchTemplate:框架封装的用于便捷操作Elasticsearch的模板类
  • ElasticsearchRepository:框架封装的用于便捷完成常用操作的工具接口
  • NativeSearchQueryBuilder:用于生成查询条件的构建器,需要去封装各种查询条件
  • QueryBuilder:该接口表示一个查询条件,其对象可以通过 QueryBuilders 工具类中的方法快速生成各种条件
    • boolQuery() 生成 bool 条件,相当于 "bool": {}
    • matchQuery() 生成 match 条件,相当于 "match": {}
    • rangeQuery() 生成 range 条件,相当于 "range": {}
  • AbstractAggregationBuilder:用于生成分组查询的构建器,其对象通过 AggregationBuilders 工具类生成
  • PageAble: 表示分页参数,对象通过 PageRequest.of(页数, 容量)获取
  • SortBuilder 排序构建器,对象通过 SortBuilders.fieldSort(字段).order(规则)获取

ElasticsearchTemplate

模板类,封装了便捷操作 Elasticsearch 的模板方法,包裹索引/映射/CRUD 等底层操作和高级操作,该对象用起来复杂写,尤其对于查询,需要把查询到的结果自己封装对象。

1
2
3
// 该对象已经由 SpringBoot 完成自动配置,直接注入即可
@Autowired
private ElasticsearchTemplate template;

ElasticsearchRepository

接口是框架封装用于操作 Elasticearch 的高级接口,我们自己写个接口去继承该接口就能直接对 Elasticsearch 进行 CRUD 操作。

1
2
3
4
5
6
7
8
9
/**
* 泛型1 domain的类型
* 泛型2 文档主键类型
* 该接口直接该给 Spring,底层会使用 JDK 代理的方式创建对象,交给容器管理
*/
@Repository
public interface ProductESRepository extends ElasticsearchRepository<User, Long> {
// 符合Spring data 规范的高级查询方法
}

一般情况下, ElasticsearchTemplate 和 ElasticsearchRepository 是分工合作的,ElasticsearchRepository 已经能完成绝大部分的功能,遇到复杂的查询使用 ElasticsearchTmplate,如多字段分组、高亮显示等等。

实例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Autowired
private ProductESRepository repository;

@Test
public void saveOrUpdate() {
// 索引库中有该 ID 对应的文档就是更新操作,否则就是新增操作
Product product = new Product();
product.setId(13l);
product.setTitle("11");
product.setPrice(19l);
product.setIntro("22");
repository.save(product);
}

@Test
public void delete() {
repository.deleteById(13l);
}

@Test
public void delete() {
Optional<Product> optional = repository.findById(13l);
}

@Test
public void delete() {
// 两个参数,Sort sort 和 Pageable pageable 分页器
Iterable<Product> all = repository.findAll();
}

高级查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
@Autowired
private ProductESRepository repository;

/**
* 按照价格降序排列
*/
@Test
public void testQuery1() {
Iterable<Product> all = repository.findAll(Sort.by(Sort.Direction.DESC, "price"));
all.forEach(System.out::println);
}

/**
* 分页查询文档按照价格排序,显示第二页,每页显示3个
*/
@Test
public void testQuery2() {
Sort sort = Sort.by(Sort.Direction.DESC, "price");
PageRequest of = PageRequest.of(1, 3, sort);
Iterable<Product> all = repository.findAll(of);
all.forEach(System.out::println);
}

/**
* 分页查询文档按照价格排序,显示第二页,每页显示3个
*/
@Test
public void testQuery3() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
// 设置分页信息
builder.withPageable(PageRequest.of(1, 3));
// 设置排序信息
builder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));

Page<Product> page = repository.search(builder.build());
System.out.println(page.getTotalElements()); // 总记录数
System.out.println(page.getTotalPages()); // 总页数
page.forEach(System.out::println);
}

/**
* 查询商品标题中符合 "游戏 手机" 的字样的商品
*/
@Test
public void testQuery4() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
/**
* {
* query: {
* match: {
* title: "游戏 手机"
* }
* }
* }
*/
builder.withQuery(QueryBuilders.matchQuery("title", "游戏 手机"));
Page<Product> page = repository.search(builder.build());
page.forEach(System.out::println);
}

/**
* 查询商品价格等于15299 的商品
*/
@Test
public void testQuery5() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
/**
* {
* query: {
* term: {
* price: 15299
* }
* }
* }
*/
builder.withQuery(QueryBuilders.termQuery("price", 15299));
Page<Product> page = repository.search(builder.build());
page.forEach(System.out::println);
}

/**
* 查询商品价格在5000-9000之间的商品,按照价格升序排列
*/
@Test
public void testQuery6() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
/**
* {
* query: {
* range: {
* price: {
* gte:5000,
* lte:9000
* }
* }
* }
* }
*/
builder.withQuery(
QueryBuilders.rangeQuery("price").gte(5000).lte(9000)
);
builder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
Page<Product> page = repository.search(builder.build());
page.forEach(System.out::println);
}

/**
* 查询商品标题或简介中符合 "蓝牙 指纹 双卡"的字样的商品
*/
@Test
public void testQuery7() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
/**
* {
* query: {
* multi_match: {
* query: "蓝牙 指纹 双卡",
* fieleds: ["title", "intro"]
* }
* }
* }
*/
builder.withQuery(
QueryBuilders.multiMatchQuery("蓝牙 指纹 双卡", "title", "intro")
);
Page<Product> page = repository.search(builder.build());
page.forEach(System.out::println);
}

/**
* 查询商品标题中符合 "i7" 的字样并且价格大于7000 的商品
*/
@Test
public void testQuery8() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
/**
* {
* query: {
* bool: {
* must: [
* {
* match: {
* title: i7
* }
* },
* {
* range: {
* price: {gt: 7000}
* }
* }
* ]
* }
* }
* }
*/
builder.withQuery(
QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("title", "i7"))
.must(QueryBuilders.rangeQuery("price").gt(7000))
);
Page<Product> page = repository.search(builder.build());
page.forEach(System.out::println);
}

/**
* 查询商品标题中符合 "pro" 的字样或者价格在 1000-3000 的商品
*/
@Test
public void testQuery9() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
/**
* {
* query: {
* bool: {
* should: [
* {
* match: {
* title: pro
* }
* },
* {
* range: {
* price: {gte: 1000, lte: 3000}
* }
* }
* ]
* }
* }
* }
*/
builder.withQuery(
QueryBuilders.boolQuery()
.should(QueryBuilders.matchQuery("title", "pro"))
.should(QueryBuilders.rangeQuery("price").gte(1000).lte(3000))
);
Page<Product> page = repository.search(builder.build());
page.forEach(System.out::println);
}

/**
* 按照品牌分组,统计各品牌的数量
*/
@Test
public void testQuery10() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
/**
* {
* "size": 0,
* "aggs": {
* "groupByBrand": {
* terms: {
* field: "brand.keyword"
* }
* }
* }
* }
*/
builder.addAggregation(
AggregationBuilders.terms("groupByBrand").field("brand.keyword")
);
AggregatedPage<Product> page = (AggregatedPage<Product>) repository.search(builder.build());
StringTerms groupByBrand = (StringTerms) page.getAggregation("groupByBrand");
List<StringTerms.Bucket> buckets = groupByBrand.getBuckets();
for (StringTerms.Bucket bucket : buckets) {
System.out.println(bucket.getKey());
System.out.println(bucket.getDocCount());
}
}

// 按照品牌分组,统计各个品牌的平均价格
@Test
public void testQuery11() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
/**
* {
* "size": 0,
* "aggs": {
* "groupByBrand": {
* terms: {
* field: "brand.keyword"
* },
* aggs: {
* avgPrice: {
* avg: {
* field: price
* }
* }
* }
* }
* }
* }
*/
builder.addAggregation(
AggregationBuilders.terms("groupByBrand").field("brand.keyword")
.subAggregation(AggregationBuilders.avg("avgPrice").field("price"))
);
AggregatedPage<Product> page = (AggregatedPage<Product>) repository.search(builder.build());
StringTerms groupByBrand = (StringTerms) page.getAggregation("groupByBrand");
List<StringTerms.Bucket> buckets = groupByBrand.getBuckets();
for (StringTerms.Bucket bucket : buckets) {
System.out.println(bucket.getKey());
System.out.println(bucket.getDocCount());
InternalAvg avgPrice = bucket.getAggregations().get("avgPrice");
System.out.println(avgPrice.getValue());
}
}

// 按照品牌分组,统计各个品牌的价格
@Test
public void testQuery12() {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
/**
* {
* "size": 0,
* "aggs": {
* "groupByBrand": {
* terms: {
* field: "brand.keyword"
* },
* aggs: {
* avgPrice: {
* stats: {
* field: price
* }
* }
* }
* }
* }
* }
*/
builder.addAggregation(
AggregationBuilders.terms("groupByBrand").field("brand.keyword")
.subAggregation(AggregationBuilders.stats("statsPrice").field("price"))
);
AggregatedPage<Product> page = (AggregatedPage<Product>) repository.search(builder.build());
StringTerms groupByBrand = (StringTerms) page.getAggregation("groupByBrand");
List<StringTerms.Bucket> buckets = groupByBrand.getBuckets();
for (StringTerms.Bucket bucket : buckets) {
System.out.println(bucket.getKey());
System.out.println(bucket.getDocCount());
InternalStats statsPrice = bucket.getAggregations().get("statsPrice");
System.out.println(statsPrice.getAvg());
System.out.println(statsPrice.getMax());
System.out.println(statsPrice.getMin());
System.out.println(statsPrice.getSum());
}
}

高亮显示

需求,查询商品标题或者简介中符合 “蓝牙 指纹 双卡” 的字样的商品,并且高亮显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 查询商品标题或简介中符合 "蓝牙 指纹 双卡" 的字样的商品,并且高亮显示
*/
@Test
public void testQueryHighlight() {
String preTag = "<span style='color:red'>";
String postTag = "</span>";

NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(
QueryBuilders.multiMatchQuery("蓝牙 指纹 双卡", "title", "intro")
);
builder.withHighlightFields(
new HighlightBuilder.Field("title")
.preTags(preTag).postTags(postTag),
new HighlightBuilder.Field("intro")
.preTags(preTag).postTags(postTag)
);

AggregatedPage<Product> products = template.queryForPage(builder.build(), Product.class);
products.forEach(System.out::println);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 需求1:查询商品标题或简介中符合"蓝牙 指纹 双卡"的字样的商品,并且高亮显示
GET /shop_product/shop_product/_search
{
"query": {
"multi_match": {
"query": "蓝牙 指纹",
"fields": ["title","intro"]
}
},
"highlight": {
"fields": {
"title": {},
"intro": {}
},
"pre_tags": "<span style='color:red'>",
"post_tags": "</span>"
}
}

"hits" : {
"total" : 6,
"max_score" : 2.8370674,
"hits" : [
{
"_index" : "shop_product",
"_type" : "shop_product",
"_id" : "8",
"_score" : 2.8370674,
"_source" : {
"id" : 8,
"title" : "荣耀MagicBook Pro 16.1英寸全面屏轻薄性能笔记本电脑(酷睿i7 8G 512G MX250 IPS FHD 指纹解锁)冰河银",
"price" : 6199,
"intro" : "16.1英寸无界全面屏金属轻薄本,100%sRGB色域,全高清IPS防眩光护眼屏,14小时长续航,指纹一健开机登录,魔法一碰传高速传输。",
"brand" : "荣耀"
},
"highlight" : {
"intro" : [
"16.1英寸无界全面屏金属轻薄本,100%sRGB色域,全高清IPS防眩光护眼屏,14小时长续航,<span style='color:red'>指</span><span style='color:red'>纹</span>一健开机登录,魔法一碰传高速传输。"
],
"title" : [
"荣耀MagicBook Pro 16.1英寸全面屏轻薄性能笔记本电脑(酷睿i7 8G 512G MX250 IPS FHD <span style='color:red'>指</span><span style='color:red'>纹</span>解锁)冰河银"
]
}
},
...
]
}

上述代码并不能满足需求,原因是在执行 template.queryForPage 的,在对结果封装的时候,默认的封装模式,会在结果中直接找到 hits 中的 hits,然后从 hits 中继续找到 _source 中的内容根据传入的 class 进行结果集的封装。
所以如果得到的 intro 或者 title 中直接含有高亮的字段的代码,需要自己解析封装结果。template.queryForPage 方法的第三个参数,匿名构造器,中写入对结果集的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 查询商品标题或简介中符合 "蓝牙 指纹 双卡" 的字样的商品,并且高亮显示
*/
@Test
public void testQueryHighlight() {
// jackson 转换工具
ObjectMapper mapper = new ObjectMapper();

String preTag = "<span style='color:red'>";
String postTag = "</span>";

NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(
QueryBuilders.multiMatchQuery("蓝牙 指纹 双卡", "title", "intro")
);
builder.withHighlightFields(
new HighlightBuilder.Field("title")
.preTags(preTag).postTags(postTag),
new HighlightBuilder.Field("intro")
.preTags(preTag).postTags(postTag)
);

AggregatedPage<Product> products = template.queryForPage(builder.build(), Product.class, new SearchResultMapper() {
@Override
// searchResponse 表示最外面的{},
public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
List<T> content = new ArrayList<>();
// 处理响应效果
for (SearchHit hit : searchResponse.getHits().getHits()) {
try {
T t = mapper.readValue(hit.getSourceAsString(), aClass);
content.add(t);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return new AggregatedPageImpl<>(
content,
pageable,
searchResponse.getHits().getTotalHits()
);
}
});
products.forEach(System.out::println);
}

上面是没有高亮的结果。也是默认的结果集的封装代码。 需要在添加到 content 之前,将 highlight 中的数据替换到 bean 中对应的属性上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 查询商品标题或简介中符合 "蓝牙 指纹 双卡" 的字样的商品,并且高亮显示
*/
@Test
public void testQueryHighlight() {
// jackson 转换工具
ObjectMapper mapper = new ObjectMapper();

String preTag = "<span style='color:red'>";
String postTag = "</span>";

NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(
QueryBuilders.multiMatchQuery("蓝牙 指纹 双卡", "title", "intro")
);
builder.withHighlightFields(
new HighlightBuilder.Field("title")
.preTags(preTag).postTags(postTag),
new HighlightBuilder.Field("intro")
.preTags(preTag).postTags(postTag)
);

AggregatedPage<Product> products = template.queryForPage(builder.build(), Product.class, new SearchResultMapper() {
@Override
// searchResponse 表示最外面的{},
public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
List<T> content = new ArrayList<>();
// 处理响应效果
for (SearchHit hit : searchResponse.getHits().getHits()) {
try {
T t = mapper.readValue(hit.getSourceAsString(), aClass);
// 取出带有高亮的属性替换对应的属性
for (HighlightField field : hit.getHighlightFields().values()) {
String name = field.getName(); // 属性名
Object value = field.getFragments()[0]; // 获取具有高亮显示的属性值
// 给 bean 的 property 设置 value
BeanUtils.setProperty(t, name, value);
}
content.add(t);
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return new AggregatedPageImpl<>(
content,
pageable,
searchResponse.getHits().getTotalHits()
);
}
});
products.forEach(System.out::println);
}
文章作者: Ammar
文章链接: http://lizhaoloveit.cn/2019/10/08/Elasticsearch/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ammar's Blog
打赏
  • 微信
  • 支付宝

评论