ElasticSearch
1.背景
ElasticSearch的最明显的优势在于其分布式特性,能够扩展到上百台服务器,极大地提高了服务器的容错率。在大数据时代背景下,ElasticSearch与传统的数据库相比较,能够应对大规模的并发搜索请求,同时提供了包括布尔查询、短语查询、模糊匹配、排序等强大便捷的功能。
ElasticSearch是基于lucene实现的,而lucene是一个只限于Java语言的搜索引擎类库,属于Apache公司的1999年开发的项目。而ElasticSearch在lucene的基础上支持分布式,并且提供了Restful接口,减少了整合进项目的复杂度,同时也支持任何语言调用,使得其成为最流行的搜索引擎类库。
总结下来,ElasticSearch就是一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能,同时具有很好地扩展性,且通过RestFulAPI简化了其使用。
相较于MySQL来说,ElasticSearch擅长海量数据的搜索、分析、计算,但在事务操作的安全和一致方面来说不如MySQL,在使用时往往采取二者结合使用,如对安全性要求较高的操作使用MySQL,而对于查询性能要求较高的操作采用ElasticSearch。
2.倒排索引
ElasticSearch之所以能够高效率地实现模糊查询、词组查询等功能,是由于其采用了一种倒排索引的模式。所谓倒排索引其实是相较于MySQL中的正向索引而言的。
sql
select * from student where name like %张三%;
对于以上的SQL来说,由于采用了%前缀模糊查询,因此必定会导致全表扫描,在数据量大的情况下效率会非常低下。为了解决这种问题,ElasticSearch采用的是倒排索引。
了解倒排索引前,需要知道在ElasticSearch中有几个关键的概念,类比于数据库而言,ElasticSearch中有索引(indices)、类型(types)、文档(documents)、字段(fields),对应于数据库中的数据库(database)、表(tables)、行(rows)、列(columns)。其实在ElasticSearch中,面向的是Json文档,其字段就是Json中的键,而文档内容就是其值,另外,在ElasticSearch中的请求语句是Json风格的,被称为DSL。
例如:
json
{
"id":"1",
"姓名":"张大三",
"专业":"计算机软件工程",
"绩点":"4.0"
},
{
"id":"2",
"姓名":"李小四",
"专业":"电子与计算机工程",
"绩点":"3.5"
},
{
"id":"3",
"姓名":"王小五",
"学号":"电子与自动化控制",
"绩点":"3.0"
}
姓名 | 专业 | 绩点 | |
---|---|---|---|
1 | 张大三 | 计算机软件工程 | 4.0 |
2 | 李小四 | 电子与计算机工程 | 3.5 |
3 | 王小五 | 电子与自动化控制 | 3.0 |
不同类型的文档可以组织在一起成为一个索引,如商品索引、学生索引、用户索引等。
倒排索引会对每一个文档的数据利用算法进行分词,得到一个个词条,然后根据词条以及其位置信息创建一个索引。
词条 | 文档id |
---|---|
计算机 | 1,2 |
软件 | 1 |
工程 | 1,2 |
电子 | 2,3 |
自动化控制 | 3 |
这样当用户输入如"计算机软件"进行搜索的时候就可以根据分词查找计算机和软件,去词条列表中查询到文档id,再根据文档id去原索引中查询得到1,2两条数据。这种根据词条找文档的过程就体现了倒排索引的特点。
注意:由倒排索引的原理不难看出,倒排索引提高模糊查询的办法是预先对已有的数据进行了分词处理,然后形成了一个词条的索引,相当于将原本查询所需的时间提前到了增加数据时处理的时候,因此倒排索引不适合高频动态数据,且对于数值型或者范围查询而言更擅长处理文本查询。
3.ELK
ELK是ElasticSearch技术栈的三大开源框架的首字母大写简称,分别是ElasticSearch、Logstash、Kibana,三者也被并称为ElasticStack。
Logstash是中央数据流引擎,主要功能是从各种数据源中收集、转换并将数据传送到目标存储中,比如 Elasticsearch 或者文件系统 。其典型使用场景有日志管理、事件数据处理、数据ETL。
Kibana 是一个开源的数据可视化与探索工具,专为与 Elasticsearch 配合使用而设计。它提供了强大的图形界面,用户可以通过 Kibana 从 Elasticsearch 中查询、分析和可视化数据。Kibana 是 Elastic Stack (ELK Stack) 的重要组成部分,主要用于展示和监控存储在 Elasticsearch 中的数据。
ELK的代表性作用就是日志分析和收集,除此之外也支持数据分析等功能。
4.Mapping
在ES中,Mapping(映射)定义了字段的结构和类型,决定了ES如何存储和索引文档中的数据。
Mapping常见的映射属性有:
tex
1.type
定义字段的类型。常见类型有字符串、数字、布尔值、日期等。
2.index
定义字段是否需要被索引。index:true表示字段可以被搜索,index:false表示该字段不会被索引,不能用于搜索。
3.format
对于日期字段,format属性定义了日期的格式,例如format:"yyyy-MM-dd HH:mm:ss"。
4.analyzer
用于文本字段,定义文本分词器。例如,可以使用标准分词器、简单分词器或者自定义分词器。
5.field name
定义字段的名称。通常ES会更具文档中的字段名称自动生成映射,也可以自定义字段名。
6.muti-fields
定义一个字段可以以多种方式索引。例如,一个文本字段可以既用于全文搜索(分词),又用于精准匹配(不分词)。
Mapping常见的数据类型(type)有:
tex
1.文本类型
text:用于存储需要全文搜索的文本。会进行分词。
keyword:用于存储不分词的精确值,比如标签、状态、URL等。
2.数字类型
integer:整数类型
long:长整型数值
float:单精度浮点数
double:双精度浮点数
3.日期类型
date:存储日期和时间数据,支持多种格式。
4.布尔类型
boolean:true或false。
5.二进制类型
binary:存储二进制数据,通常用于需要存储原始数据但是不用于搜索的情况。
6.对象类型
object:用于嵌套的JSON对象,允许在文档内存储复杂的嵌套结构。
7.数组类型
array:在ElasticSearch中,任何字段都可以是数组,数组的每个元素都具有相同的数据类型。
例如:
json
{
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "standard"
},
"age": {
"type": "integer"
},
"birthdate": {
"type": "date",
"format": "yyyy-MM-dd"
},
"is_active": {
"type": "boolean"
}
}
}
}
5.索引库的CRUD
创建索引及映射
bash
PUT /{my_index}
{
"settings":{
"number_of_shards":3,
"number_of_replicas":2
},
"mappings":{
"properties":{
"name":{
"type":"text"
},
"age":{
"type":"integer"
},
"email":{
"type":"keyword"
}
}
}
}
这里的settings是配置索引的分片数和副本数。比如这里设置了3个主分片和2个副本分片。
查询
bash
GET /{my_index}
修改
索引库一旦创建,就无法修改mapping,因为数据结构改变就需要重新创建倒排索引,耗费巨大。但是可以添加新的字段到mapping中。
bash
PUT /{my_index}/_mapping
{
"properties":{
"new_field":{
"type":"integer"
}
}
}
删除
bash
DELETE /{my_index}
6.文档的CRUD
新增文档
bash
#POST /{my_index}/_doc/{id}
POST /student/_doc/1
查询文档
bash
#GET /{my_index}/_doc/{id}
GET /student/_doc/1
删除文档
bash
#DELETE /{my_index}/_doc/{id}
DELETE /my_index/_doc/1
修改文档
bash
#全量修改:覆盖原来的文档
PUT /{my_index}/_doc/{id}
{
"field1":"value1",
"field2":"value2",
// ...
}
#增量修改:只修改指定id匹配的文档中的部分字段
POST /{my_index}/_update/{id}
{
"doc":{
"field":"new_value"
}
}
7.在Java中封装使用
使用ElasticSearch客户端(Java为例)
依赖:
xml
<dependencies>
<!-- Elasticsearch Client -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.10.2</version>
</dependency>
<!-- Jackson for JSON Parsing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
<!-- Apache HttpComponents for Elasticsearch -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
</dependencies>
封装思路:使用Java操作ES时需要创建ES客户端和索引请求,如果每次执行CRUD操作时都要创建那么会导致代码冗余,且频繁创建和关闭客户端连接会降低性能。所以通常的做法是将客户端的初始化和管理独立出来以确保高效、复用和易维护。这里采用单例模式将客户端封装为一个单例类,确保在整个声明周期中复用同一个客户端实例。
java
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.RestClient;
import org.apache.http.HttpHost;
public class ElasticsearchClientSingleton {
private static RestHighLevelClient client = null;
private ElasticsearchClientSingleton() { }
public static RestHighLevelClient getClient() {
if (client == null) {
synchronized (ElasticsearchClientSingleton.class) {
if (client == null) {
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http")
)
);
}
}
}
return client;
}
public static void closeClient() throws IOException {
if (client != null) {
client.close();
}
}
}
然后将索引创建放在应用启动时执行一次,避免每次CRUD操作都重复创建索引:
java
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.settings.Settings;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class ElasticsearchIndexManager {
public static void createIndexIfNotExists(String indexName) throws IOException {
RestHighLevelClient client = ElasticsearchClientSingleton.getClient();
// 检查索引是否存在
GetIndexRequest getIndexRequest = new GetIndexRequest(indexName);
boolean exists = client.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
if (!exists) {
// 如果索引不存在,则创建索引
CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName);
createIndexRequest.settings(Settings.builder()
.put("index.number_of_shards", 3)
.put("index.number_of_replicas", 2)
);
// 使用 Jackson 构建 mapping
ObjectMapper mapper = new ObjectMapper();
ObjectNode mapping = mapper.createObjectNode();
ObjectNode properties = mapping.putObject("properties");
properties.putObject("name").put("type", "text");
properties.putObject("age").put("type", "integer");
properties.putObject("email").put("type", "keyword");
createIndexRequest.mapping(mapping.toString(), XContentType.JSON);
client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
System.out.println("索引创建成功!");
} else {
System.out.println("索引已存在!");
}
}
}
由于将CRUD操作与索引和客户端管理分离,只需要专注于数据的操作,客户端和索引的创建在应用启动时已经完成:
java
public class ElasticsearchCrudOperations {
// 创建文档
public static void createDocument(String indexName, String documentId, String jsonString) throws IOException {
RestHighLevelClient client = ElasticsearchClientSingleton.getClient();
// 确保索引存在
ElasticsearchIndexManager.createIndexIfNotExists(indexName);
IndexRequest indexRequest = new IndexRequest(indexName)
.id(documentId)
.source(jsonString, XContentType.JSON);
client.index(indexRequest, RequestOptions.DEFAULT);
System.out.println("文档创建成功!");
}
// 读取文档
public static void getDocument(String indexName, String documentId) throws IOException {
RestHighLevelClient client = ElasticsearchClientSingleton.getClient();
// 确保索引存在
ElasticsearchIndexManager.createIndexIfNotExists(indexName);
GetRequest getRequest = new GetRequest(indexName, documentId);
GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
if (getResponse.isExists()) {
String sourceAsString = getResponse.getSourceAsString();
System.out.println("文档内容: " + sourceAsString);
} else {
System.out.println("文档不存在!");
}
}
// 更新文档
public static void updateDocument(String indexName, String documentId, String jsonString) throws IOException {
RestHighLevelClient client = ElasticsearchClientSingleton.getClient();
// 确保索引存在
ElasticsearchIndexManager.createIndexIfNotExists(indexName);
UpdateRequest updateRequest = new UpdateRequest(indexName, documentId)
.doc(jsonString, XContentType.JSON);
client.update(updateRequest, RequestOptions.DEFAULT);
System.out.println("文档更新成功!");
}
// 删除文档
public static void deleteDocument(String indexName, String documentId) throws IOException {
RestHighLevelClient client = ElasticsearchClientSingleton.getClient();
// 确保索引存在
ElasticsearchIndexManager.createIndexIfNotExists(indexName);
DeleteRequest deleteRequest = new DeleteRequest(indexName, documentId);
client.delete(deleteRequest, RequestOptions.DEFAULT);
System.out.println("文档删除成功!");
}
}