《Elasticsearch技术解析与实战》Chapter 1.4 Spring Boot整合Elasticsearch

1. spring-boot-starter-data-elasticsearch

1.1 pom.xml和application.yml

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Spring Boot Elasticsearch 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
spring:
data:
elasticsearch:
repositories:
enabled: true
cluster-name: docker-cluster
cluster-nodes: lujiahao.ml:9300

1.2 创建Repository

1
2
3
4
@Repository
public interface PersonEsRepository extends ElasticsearchRepository<Person,Long> {
List<Person> findPersonByName(String name);
}

1.3 文档实体类

1
2
3
4
5
6
7
8
9
10
@Data
@Document(indexName = "person", type = "chinese")
public class Person implements Serializable{
private static final long serialVersionUID = -6804453833406105286L;
@Id
private Long id;
private String name;
private Integer age;
private String address;
}

1.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
@Service
public class EsStarterService {
@Autowired
private PersonEsRepository repository;
/**
* 增
*/
public Person save(Person person) {
return repository.save(person);
}
/**
* 删
*/
public void delete(Person person) {
repository.delete(person);
}
/**
* 改
*/
public Person update(Person person) {
return repository.save(person);
}
/**
* 查
*/
public Iterable<Person> findAll() {
return repository.findAll();
}
}

1.5 单元测试

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
@RunWith(SpringRunner.class)
@SpringBootTest
public class EsStarterServiceTest {
@Autowired
private EsStarterService esStarterService;
@Test
public void save() {
Person person = new Person();
person.setId(new Random().nextLong());
person.setName("lujiahao");
esStarterService.save(person);
}
@Test
public void delete() {
Person person = new Person();
person.setId(-5264182431891613084L);
person.setName("lujiahao123456");
esStarterService.delete(person);
}
@Test
public void update() {
Person person = new Person();
person.setId(542136934419565287L);
person.setName("lujiahao123456");
esStarterService.update(person);
}
@Test
public void findAll() {
Iterable<Person> all = esStarterService.findAll();
all.forEach(System.out::println);
}
}

2. ElasticsearchTemplate

2.1 pom.xml和application.ym

1
2
3
4
5
6
7
8
9
10
11
12
<!--elasticsearch-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
</dependency>
spring:
data:
elasticsearch:
repositories:
enabled: true
cluster-name: docker-cluster
cluster-nodes: lujiahao.ml:9300

2.2 文档实体类

同上

2.3 增删改查

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
@Service
public class ElasticsearchTemplateService {

@Autowired
public ElasticsearchTemplate elasticsearchTemplate;


private static final String INDEX_NAME = "person";
private static final String TYPE_NAME = "chinese";
/**
* 增
*/
public String save(Person person) {
IndexQuery indexQuery = new IndexQueryBuilder()
.withIndexName(INDEX_NAME)
.withType(TYPE_NAME)
.withId(String.valueOf(person.getId()))
.withObject(person)
.build();
String index = elasticsearchTemplate.index(indexQuery);
System.out.println("xxxxxxxxxxxx " + index);
return index;
}

/**
* 删
*/
public void deleteByName(String name) {
DeleteQuery deleteQuery = new DeleteQuery();
deleteQuery.setQuery(QueryBuilders.matchQuery("name", name));
deleteQuery.setIndex(INDEX_NAME);
deleteQuery.setType(TYPE_NAME);
elasticsearchTemplate.delete(deleteQuery);
}

/**
* 改
*/
public UpdateResponse update(Person person) {
try {
UpdateRequest updateRequest = new UpdateRequest()
.index(INDEX_NAME)
.type(TYPE_NAME)
.id(String.valueOf(person.getId()))
.doc(XContentFactory.jsonBuilder()
.startObject()
.field("name", person.getName())
.endObject());
UpdateQuery updateQuery = new UpdateQueryBuilder()
.withIndexName(INDEX_NAME)
.withType(TYPE_NAME)
.withId(String.valueOf(person.getId()))
.withClass(Person.class)
.withUpdateRequest(updateRequest)
.build();
UpdateResponse update = elasticsearchTemplate.update(updateQuery);
return update;
} catch (Exception e) {
return null;
}
}

/**
* 查
*/
public List<Person> getAll() {
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchAllQuery())
.build();
return elasticsearchTemplate.queryForList(searchQuery, Person.class);
}
}

2.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
@RunWith(SpringRunner.class)
@SpringBootTest
public class ElasticsearchTemplateServiceTest {
@Autowired
private ElasticsearchTemplateService elasticsearchTemplateService;
@Test
public void save() {
Person person = new Person();
person.setId(new Random().nextLong());
person.setName("haha");
String save = elasticsearchTemplateService.save(person);
System.out.println(save);
}
@Test
public void deleteByName() {
elasticsearchTemplateService.deleteByName("lujiahao");
}
@Test
public void update() {
Person person = new Person();
person.setId(-5264182431891613084L);
person.setName("hahaaaaaaaaa");
UpdateResponse update = elasticsearchTemplateService.update(person);
System.out.println(update);
}
@Test
public void getAll() {
List<Person> all = elasticsearchTemplateService.getAll();
System.out.println(all);
}
}

3. 代码示例

1
https://github.com/lujiahao0708/LearnSeries/tree/master/LearnElasticSerach

Tips

本文同步发表在公众号,欢迎大家关注!😁
后续笔记欢迎关注获取第一时间更新!

《Elasticsearch技术解析与实战》Chapter 1.3 Elasticsearch增删改查

1. 新增文档,建立索引

语法格式:

PUT /index/type/id
{
  "json数据"
}

输入:

PUT /person/chinese/1
{
  "id":12345,
  "name":"lujiahao",
  "age":18
}

输出:

{
  "_index": "person",
  "_type": "chinese",
  "_id": "1",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "created": true
}

es会自动建立index和type,不需要提前创建,而且es默认会对document每个field都建立倒排索引,让其可以被搜索。

2. 检索文档

格式:

GET /index/type/id

输入:

GET /person/chinese/1

输出:

{
  "_index": "person",
  "_type": "chinese",
  "_id": "1",
  "_version": 1,
  "found": true,
  "_source": {
    "id": 12345,
    "name": "lujiahao",
    "age": 18
  }
}

3. 更新文档

3.1 替换方式

格式:

PUT /index/type/id
{
    "json数据"
}

输入:

PUT /person/chinese/1
{
  "name":"lujiahao123"
}

输出:

{
  "_index": "person",
  "_type": "chinese",
  "_id": "1",
  "_version": 2,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "created": false
}

查询:

{
  "_index": "person",
  "_type": "chinese",
  "_id": "1",
  "_version": 2,
  "found": true,
  "_source": {
    "name": "lujiahao123"
  }
}

替换方式更新文档时,必须带上所有的field,才能去进行信息的修改;如果缺少field就会丢失部分数据。其原理时替换,因此需要全部字段。不推荐此种方式更新文档。

3.1 更新方式

格式:

POST /index/type/id/_update
{
    "doc":{
        "json数据"
    }
}

输入:

POST /person/chinese/1/_update
{
  "doc":{
    "name":"lujiahao10010"
  }
}

输出:

{
  "_index": "person",
  "_type": "chinese",
  "_id": "1",
  "_version": 4,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "created": false
}

再次查询:

{
  "_index": "person",
  "_type": "chinese",
  "_id": "1",
  "_version": 6,
  "found": true,
  "_source": {
    "id": 12345,
    "name": "lujiahao10010",
    "age": 18
  }
}

4. 删除文档

格式:

DELETE /index/type/id/_update
{
    "doc":{
        "json数据"
    }
}

输入:

DELETE /person/chinese/1

输出:

{
  "found": true,
  "_index": "person",
  "_type": "chinese",
  "_id": "1",
  "_version": 7,
  "result": "deleted",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}

再次查询:

{
  "_index": "person",
  "_type": "chinese",
  "_id": "1",
  "found": false
}

5. 小结

本文所有操作都是在kibana的Dev tools中进行的,相较于Elasticsearch-Heade插件,kibana中更加方便与美观(个人观点),推荐大家使用。

Tips

本文同步发表在公众号,欢迎大家关注!😁
后续笔记欢迎关注获取第一时间更新!

《Elasticsearch技术解析与实战》Chapter 1.2 Elasticsearch安装

1. 下载安装

1.1 下载

1
2
https://www.elastic.co/downloads/elasticsearch
下载 https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.7.0.tar.gz

1.2 解压

1
tar -zxvf elasticsearch-6.7.0.tar.gz

1.3 运行

1
2
cd elasticsearch-6.7.0
bin/elasticsearch

1.4 检验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
curl http://localhost:9200  或者浏览器访问
{
"name": "q9sdES9",
"cluster_name": "docker-cluster",
"cluster_uuid": "6klEi4d0Q6y0LC3YNYVXTQ",
"version": {
"number": "5.5.0",
"build_hash": "260387d",
"build_date": "2017-06-30T23:16:05.735Z",
"build_snapshot": false,
"lucene_version": "6.6.0"
},
"tagline": "You Know, for Search"
}

2. Docker部署

官方文档 : https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html

2.1 拉取镜像

1
docker pull docker.elastic.co/elasticsearch/elasticsearch:5.5.1

2.2 启动容器

1
docker run -p 9200:9200 9300:9300 -e "http.host=0.0.0.0" -e “transport.host=0.0.0.0" --name elasticsearch_5.5.0 -d docker.elastic.co/elasticsearch/elasticsearch:5.5.0

2.3 修改配置

1
2
3
4
5
进入到容器中 : docker exec -it elasticsearch_5.5.0 /bin/bash
修改jvm配置 : vi /config/jvm.options
-Xms2g —> -Xms512m
-Xmx2g —> -Xmx512m
修改小一些,服务器内存有限😂

2.4 重启容器,查看是否成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
http://服务器ip:9200
{
"name": "q9sdES9",
"cluster_name": "docker-cluster",
"cluster_uuid": "6klEi4d0Q6y0LC3YNYVXTQ",
"version": {
"number": "5.5.0",
"build_hash": "260387d",
"build_date": "2017-06-30T23:16:05.735Z",
"build_snapshot": false,
"lucene_version": "6.6.0"
},
"tagline": "You Know, for Search"
}
此处集群的名称为 docker-cluster, 可以自行修改 : vi /config/elasticsearch.yml

Tips

本文同步发表在公众号,欢迎大家关注!😁
后续笔记欢迎关注获取第一时间更新!

《Elasticsearch技术解析与实战》Chapter 1.1:Elasticsearch入门和倒排索引

1. 简介

Elasticsearch是一个机遇Lucene构建的开源、分布式、RESTful接口全文搜索引擎。同时,Elasticsearch还是一个分布式文档数据库,能够扩展至数百个服务器存储以处理PB级数据,通常作为复杂搜索场景的首选利器。

Elasticsearch的优点:

  1. 横向可扩展性:只需要增加一台服务器,配置完毕即可加入集群。
  2. 分片机制提供更好的分布性:同一个索引分成多个分片,类似于HDFS的块机制,分而治之的方式提升处理效率。
  3. 高可用:提供复制机制,一个分片可以设置多个副本,在某台服务器宕机情况下,集群依旧可以工作,并在宕机服务器重启后恢复数据。
  4. 使用简单:开箱即用,快速搭建搜索服务。

Elasticsearch wiki:https://zh.wikipedia.org/wiki/Elasticsearch

2. 数据库搜索

在数据量少的情况下可以当做搜索服务来使用,然而数据库归根结底是做持久化存储。如果数据量大就需要做搜索服务,底层数据还是关系数据库。我司老系统中有一个订单表,数据量已经高达两亿,客服等后台系统通常带有范围或批量条件等查询,这时数据库基本上就无法响应了,报警根本停不下来。因此,用数据库来实现搜索,性能差,可用性不高。

3. Lucene

Lucene是一个开源的全文搜索引擎工具包,其目的是为开发者提供一个简单工具包,以快速实现全文检索的功能。

Lucene wiki:https://zh.wikipedia.org/wiki/Lucene

4. 倒排索引

倒排索引中的索引对象是文档或者文档集合中的单词等,用来存储这些单词在一个文档或者一组文档中的存储位置,是对文档或者文档集合的一种最常用的索引机制。搜索引擎的关键步骤就是建立倒排索引,下面介绍Lucene是如何建立倒排索引和相应的生成算法。

假设有两篇文章:
文章1:Tom lives in Guangzhou, I live in Guangzhou too.
文章2:He once lived in Shanghai.

4.1 取得关键词

Lucene是基于关键词索引和查询的,首先要进行关键词提取:

  • 分词:英文单词由空格分隔,较好处理;中文词语由于是连在一起的,需要进行特殊的分词处理(后面会介绍分词器相关知识)。

  • 过滤无概念词语:英文中“in”“once”“too”等词没有实际意义;中文中“的”“是”等也无实际意义,这些无概念词语可以过滤掉。

  • 统一大小写:“he”和“HE”表示的含义一样,所以单词需要统一大小写。

  • 语义还原:通常用户查询“live”时希望能将“lives”和“lived”也查询出来,所以需要将“lives”和“lived”还原成“live”。

  • 过滤标点符号

    经过以上过滤,得到如下结果:
    文章1关键词:tom live guangzhou i live guangzhou
    文章2关键词:he live shanghai

4.2 建立倒排索引

关键词建立完成后,就可以进行倒排索引建立了。过滤后的关系是:“文章号“对”文章中所有关键词“,倒排索引把这个关系倒过来变成:”关键词“对”拥有关键词的所有文章号“。

通常仅知道关键词在哪些文章中出现还不够,还需要知道关键词在文章中出现的次数和位置,通常有两种位置:

  1. 字符位置,即记录该词是文章中第几个字符(优点是显示并定位关键词快)。
  2. 关键词位置,即记录该词是文章中的第几个关键词(优点是节约索引空间、词组查询快),Lucene中记录的就是这种位置。

以上就是Lucene索引结构中最核心的部分,关键字是按字符顺序排列的(Lucene没有使用B树结构),因此Lucene可以使用二元搜索算法快速定位关键词。

4.3 实现

Lucene将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)、位置文件(positions)保存。其中词典文件不仅保存了每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。

Lucene中使用了field的概念,用于表达信息所在的位置(如标题中、文章中、url中),在建索引中,该field信息也记录在词典文件中,每个关键词都有一个field信息,因为每个关键字一定属于一个或多个field。

4.4 压缩算法

为了减小索引文件的大小,Lucene对索引还是用了压缩技术。
首先,对词典文件中的关键词进行压缩,关键词压缩为<前缀长度,后缀>,例如:当前词为”阿拉伯语“,上一个词为”阿拉伯“,那么”阿拉伯语“压缩为<3,语>。
其次大量用到的是对数字的压缩,数字只保存与上一个值的差值(这样可以减少数字的长度,进而减少保存该数字需要的字节数)。例如当前文章号是16389(不压缩要用3个字节),上一文章号是16382,压缩后保存7(只用一个字节)。

压缩算法推荐阅读:https://www.cnblogs.com/dreamroute/p/8484457.html

4.5 实战

查询单词”live“,Lucene先对词典二元查找,找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果。词典通常非常小,可以达到毫秒级返回。而用普通的顺序匹配算法,不建立索引,而是对所有文章的内容进行字符串匹配,过程是很缓慢的,当数据量很大时,耗时更加严重。

5. 基础概念

5.1 索引词(term)

Elasticsearch中能够被索引的精确值。foo、Foo、FOO几个单词是不同的索引词。索引词可以通过term查询进行准确的搜索。

5.2 文本(text)

文本会被拆分成一个个索引词存储在索引库中,为后续搜索提供支持。

5.3 分析(analysis)

分析是将文本转换为索引词的过程,其结果依赖于分词器。

5.4 集群(cluster)

集群由一个或多个节点组成,对外提供服务。Elasticsearch节点如果有相同的集群名称会自动加入到同一个集群,因此如果你拥有多个独立集群,每个集群都要设置不同的名称。

5.5 节点(node)

节点是一个逻辑上独立的服务,是集群的一部分,可以存储数据,并参与集群的索引和搜索功能。

5.6 路由(routing)

文档存储时是通过散列值进行计算,最终选择存储在主分片中,这个值默认是由文档的ID生成。

5.7 分片(shard)

分片是单个Lucene实例,是Elasticsearch管理的比较底层的功能。当索引占用空间很大超过一个节点的物理存储,Elasticsearch将索引切分成多个分片,分散在不同的物理节点上,以解决单物理节点存储空间有限的问题。

5.8 主分片(primary shard)

每个文档都存储在一个分片中,存储文档时系统会首先存储在主分片中,然后复制到不同的副本中。默认情况下一个索引拥有5个主分片,分片一旦建立,主分片数量就无法修改。

5.9 副本分片(replica shard)

每个主分片有零个或多个副本,是主分片的复制,其主要目的是:

  1. 增加高可用性:当主分片失败时,某一副本分片提升为主分片
  2. 提高性能:副本分片数量可以动态配置,可以为主分片分担查询压力。
  3. 允许水平分割扩展数据
  4. 允许分配和并行操作,从而提高性能和吞吐量。

5.10 复制(replica)

主分片的数据会复制到副本分片中,这样避免了单点问题,当某个节点发生故障,复制可以对故障进行转移,保证系统的高可用。

5.11 索引(index)

索引是具有相同结构的文档合集。

5.12 类型(type)

一个索引可以定义一个或多个类型,类型是索引的逻辑分区。

5.13 文档(document)

文档是存储在Elasticsearch中的一个JSON格式的字符串,就像关系数据库中表的一行记录。

5.14 映射(mapping)

映射像关系数据库中的表结构,每个索引都有一个映射,它定义了索引中的每一个字段类型。映射可以事先被定义,也可以在第一次存储文档时被自动识别。

5.15 字段(field)

文档中包含零个或多个字段,字段可以是一个简单的值,也可以是一个数组或对象的嵌套结构。字段类似于关系数据库中表的列,每个字段都对应一个字段类型。

5.16 来源字段(source field)

默认情况下源文档将被存储在_source字段中,查询时返回该字段。

5.17 主键(ID)

ID是文件的唯一标识,如果未指定,系统会自动生成一个ID,文档的index/type/id必须是唯一的。

5.18 Elasticsearch核心概念 vs. 数据库核心概念

Elasticsearch 数据库
Document row 行
Type table 表
Index database 库

Tips

本文同步发表在公众号,欢迎大家关注!😁
后续笔记欢迎关注获取第一时间更新!