介绍

Elasticsearch 是一个基于 Lucene 的搜索服务器。

ES 中的核心组件

  • 索引:Index(数据库的 Database)
  • 类型:type(数据库的 Table)
  • 域:Field(数据库的 column)
  • 映射:mapping(数据库的建表语句)
  • 文档:documents(数据库的数据 rows)

Lucene 是 apache 软件基金会 jakarta 项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文)。Lucene 的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。Lucene 在全文检索领域是一个经典的祖先,现在很多检索引擎都是在其基础上创建的。

Lucene 是根据关健字来搜索的文本搜索工具,只能在某个网站内部搜索文本内容,不能跨网站搜索

Lucene 分词器

在对 Docuemnt 中的内容进行索引之前,需要使用分词器进行分词 ,分词的目的是为了搜索。分词的主要过程就是先分词后过滤

  • 分词:采集到的数据会存储到 document 对象的 Field 域中,分词就是将 Document 中 Field 的 value 值切分成一个一个的词。
  • 过滤:包括去除标点符号过滤、去除停用词过滤(的、是、a、an、the 等)、大写转小写、词的形还原(复数形式转成单数形参、过去式转成现在式。。。)等。

如下是org.apache.lucene.analysis.standard.standardAnalyzer的部分源码

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
@Override
protected TokenStreamComponents createComponents(final String fieldName) {
final Tokenizer src;
if (getVersion().onOrAfter(Version.LUCENE_4_7_0)) {
//创建分词[带有Tokenizer一般都表示创建分词器]
StandardTokenizer t = new StandardTokenizer();
t.setMaxTokenLength(maxTokenLength);
src = t;
} else {
//创建分词[带有Tokenizer一般都表示创建分词器]
StandardTokenizer40 t = new StandardTokenizer40();
t.setMaxTokenLength(maxTokenLength);
src = t;
}
//创建过滤,带有Filter一般表示过滤s
TokenStream tok = new StandardFilter(src);
//大小写过滤
tok = new LowerCaseFilter(tok);
//停用词汇过滤
tok = new StopFilter(tok, stopwords);
return new TokenStreamComponents(src, tok) {
@Override
protected void setReader(final Reader reader) throws IOException {
int m = StandardAnalyzer.this.maxTokenLength;
if (src instanceof StandardTokenizer) {
((StandardTokenizer)src).setMaxTokenLength(m);
} else {
((StandardTokenizer40)src).setMaxTokenLength(m);
}
super.setReader(reader);
}
};
}

分词器的工作流程

1、标准过滤

标准过滤完成的内容是将空格,标点符号,无意义的词语去掉

2、大小写过滤

将大写全部转换为小写

3、停用词过滤

不允许作为搜索条件的词语

4、进行分词

Lucene 全文检索:Field 域

Field 属性

Field 是文档中的域,包括 Field 名和 Field 值两部分,一个文档可以包括多个 Field,Document 只是 Field 的一个承载体,Field 值即为要索引的内容,也是要搜索的内容。

  • 是否分词(tokenized)

是:作分词处理,即将 Field 值进行分词,分词的目的是为了索引。比如:商品名称、商品描述等,这些内容用户要输入关键字搜索,由于搜索的内容格式大、内容多需要分词后将语汇单元建立索引

否:不作分词处理比如:商品 id、订单号、身份证号等

  • 是否索引(indexed)

是:进行索引。将 Field 分词后的词或整个 Field 值进行索引,存储到索引域,索引的目的是为了搜索。比如:商品名称、商品描述分析后进行索引,订单号、身份证号不用分词但也要索引,这些将来都要作为查询条件。

否:不索引。比如:图片路径、文件路径等,不用作为查询条件的不用索引。

  • 是否存储(stored)

是:将 Field 值存储在文档域中,存储在文档域中的 Field 才可以从 Document 中获取。比如:商品名称、订单号,凡是将来要从 Document 中获取的 Field 都要存储。

否:不存储 Field 值。比如:商品描述,内容较大不用存储。如果要向用户展示商品描述可以从系统的关系数据库中获取。

Field 常用类型

下边列出了开发中常用的 Filed 类型,注意 Field 的属性,根据需求选择:

Field 类数据类型Analyzed 是否分词Indexed 是否索引Stored 是否存储说明
StringField(FieldName, FieldValue,Store.YES))字符串NYY 或 N这个 Field 用来构建一个字符串 Field,但是不会进行分词,会将整个串存储在索引中,比如(订单号,身份证号等)是否存储在文档中用 Store.YES 或 Store.NO 决定
LongField(FieldName, FieldValue,Store.YES)Long 型YYY 或 N这个 Field 用来构建一个 Long 数字型 Field,进行分词和索引,比如(价格)是否存储在文档中用 Store.YES 或 Store.NO 决定
StoredField(FieldName, FieldValue)重载方法,支持多种类型NNY这个 Field 用来构建不同类型 Field 不分析,不索引,但要 Field 存储在文档中(图片的 url 地址)
TextField(FieldName, FieldValue, Store.NO)或 TextField(FieldName, reader)字符串或流YYY 或 N如果是一个 Reader, lucene 猜测内容比较多,会采用 Unstored 的策略。(商品名字)

基于 ES 的查询实现

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<!--连接客户端-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
<version>7.2.0</version>
</dependency>
<!--操作es的工具包-->
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.2.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

测试

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
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.junit.Test;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Iterator;

public class DemoTest {

/**
* 通过get方式获取数据:只查询一次:查文档域
*
* @throws UnknownHostException
*/
@Test
public void getDemo() throws UnknownHostException {
//构建一个连接对象,不适用es的集群
TransportClient client = new PreBuiltTransportClient(Settings.EMPTY);
//添加连接的ip地址和端口号
client.addTransportAddress(
new TransportAddress(InetAddress.getByName("127.0.0.1"), 9300));
//进行查询
GetResponse getResponse = client.prepareGet("person", "_doc", "Osx4FXsB6WFf174V5EJg").get();
String sourceAsString = getResponse.getSourceAsString();
System.out.println(sourceAsString);
//释放资源
client.close();
}

/**
* 通过search方式获取数据:查询所有的数据---查询一次
*
* @throws UnknownHostException
*/
@Test
public void searchDemo() throws UnknownHostException {
//构建一个连接对象,不适用es的集群
PreBuiltTransportClient client = new PreBuiltTransportClient(Settings.EMPTY);
//添加连接的ip地址和端口号
client.addTransportAddress(
new TransportAddress(InetAddress.getByName("127.0.0.1"), 9300));
//进行查询
SearchResponse searchResponse = client.prepareSearch("person")
.setTypes("_doc")
//设置查询条件
.setQuery(QueryBuilders.matchAllQuery())
//设置查询数据大小,默认10条
.setSize(100)
.get();

//解析数据
SearchHits hits = searchResponse.getHits();
//遍历:迭代器
Iterator<SearchHit> iterator = hits.iterator();
while (iterator.hasNext()) {
SearchHit next = iterator.next();
String sourceAsString = next.getSourceAsString();
System.out.println(sourceAsString);
}
//释放资源
client.close();
}


/**
* 字符串查询
* 通过search方式获取数据:字符串查询2次查询
*
* @throws UnknownHostException
*/
@Test
public void searchDemo1() throws UnknownHostException {
//构建一个连接对象,不适用es的集群
PreBuiltTransportClient client = new PreBuiltTransportClient(Settings.EMPTY);
//添加连接的ip地址和端口号
client.addTransportAddress(new TransportAddress(InetAddress.getByName("127.0.0.1"), 9300));
//进行查询
SearchResponse java0223 = client.prepareSearch("java0223")
.setTypes("_doc")
//设置查询条件,字符查询
.setQuery(QueryBuilders.queryStringQuery("Lucene").field("content"))
//设置查询数据大小,默认10条
.setSize(10)
.get();

//解析数据
SearchHits hits = java0223.getHits();
//遍历:迭代器
Iterator<SearchHit> iterator = hits.iterator();
while (iterator.hasNext()) {
SearchHit next = iterator.next();
String sourceAsString = next.getSourceAsString();
System.out.println(sourceAsString);
}
//释放资源
client.close();
}

/**
* 词条查询
* 通过search方式获取数据:词条查询2次查询
*
* @throws UnknownHostException
*/
@Test
public void searchDemo2() throws UnknownHostException {
//构建一个连接对象,不适用es的集群
PreBuiltTransportClient client = new PreBuiltTransportClient(Settings.EMPTY);
//添加连接的ip地址和端口号
client.addTransportAddress(new TransportAddress(InetAddress.getByName("127.0.0.1"), 9300));
//进行查询
SearchResponse java0223 = client.prepareSearch("java0223")
.setTypes("_doc")
//设置查询条件,词条查询
.setQuery(QueryBuilders.termQuery("content","Lucene"))
//设置查询数据大小,默认10条
.setSize(10)
.get();

//解析数据
SearchHits hits = java0223.getHits();
//遍历:迭代器
Iterator<SearchHit> iterator = hits.iterator();
while (iterator.hasNext()) {
SearchHit next = iterator.next();
String sourceAsString = next.getSourceAsString();
System.out.println(sourceAsString);
}
//释放资源
client.close();
}
}

总结

判断查询是否会进行分词,通过设置需要查询值为大写进行查询,因为如果进行了分词,会将大写转小写,这样才能和索引域匹配到

  • 字符串查询 matchQuery:对于输入的条件,会进行分词
  • 字符串查询 queryStringQuery:对于输入的条件,会进行分词
  • 词条查询 termQuery:对于输入的条件,不会进行分词
  • 模糊查询 wildcardQuery:对于输入的条件,不会进行分词
  • 相似度查询 fuzzyQuery:对于输入的条件,不会进行分词

高亮查询的实现

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
public static void main(String[] args) throws UnknownHostException {
//构建一个连接对象,不适用es的集群
PreBuiltTransportClient client = new PreBuiltTransportClient(Settings.EMPTY);
//添加连接的ip地址和端口号
client.addTransportAddress(new TransportAddress(InetAddress.getByName("127.0.0.1"), 9300));

//设置高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
//指定对哪个域进行高亮
highlightBuilder.field("content");
//对于查出来的结果使用标签括起来
highlightBuilder.preTags("<font style='color:red'>");
highlightBuilder.postTags("</font>");

//进行查询
SearchResponse java0223 = client.prepareSearch("java0223")
.setTypes("_doc")
//设置查询条件,词条查询
.setQuery(QueryBuilders.matchQuery("content", "Lucene"))
//设置查询数据大小,默认10条
.setSize(10)
.highlighter(highlightBuilder)
.get();

//解析数据
SearchHits hits = java0223.getHits();
//遍历:迭代器
Iterator<SearchHit> iterator = hits.iterator();
while (iterator.hasNext()) {
SearchHit next = iterator.next();
//这里取出来的只是原数据
String sourceAsString = next.getSourceAsString();
//对原数据进行反序列化
Article article = JSONObject.parseObject(sourceAsString, Article.class);
//获取指定域的高亮数据
HighlightField highlightField = next.getHighlightFields().get("content");
//判断获取到的高亮对象是否为空
if (highlightField != null) {
//拿到高亮数据
Text[] fragments = highlightField.getFragments();
if (fragments != null && fragments.length > 0) {
//定义一个String类型变量用于存放高亮数据
String content = "";
//遍历获取高亮数据,放到String类型的变量中
for (Text fragment : fragments) {
content += fragment;
}
article.setContent(content);
}
}
System.out.println(article);
}
//释放资源
client.close();
}

这里建立了一个实体类用来反序列化,实体类的字段和索引相同

1
2
3
4
5
6
7
8
public class Article implements Serializable {

private Long id;
private String title;
private String content;

//....省略get/set/toString/构造方法
}