Learner0x5a's Studio.

ElasticSearch (OpenSearch)

Word count: 3kReading time: 15 min
2021/10/20 Share

ElasticSearch

Elasticsearch 是一个开源的搜索引擎,建立在全文搜索引擎库 Apache Lucene™ 基础之上。 Lucene 是当下最先进、高性能、全功能的搜索引擎库。
Elasticsearch 使用 Java 编写,其内部使用 Lucene 做索引与搜索,但是隐藏了 Lucene 的复杂性,取而代之的一套简单一致的 RESTful API。

存储数据到 Elasticsearch 的行为叫做 索引 ,但在索引一个文档之前,需要确定将文档存储在哪里。 \

一个 Elasticsearch 实例可以 包含多个 索引 ,相应的每个索引可以包含多个 类型 。 这些不同的类型存储着多个 文档 ,每个文档又有 多个 属性 。

ES中的index就像sql中的库,type就像sql中的表,document就像sql中的记录。
但事实上,ElasticSearch实现上“真正用于分隔数据的结构“只有index,而没有type,type实际上作为了一个元数据(类似SQL中的id,作为额外的标识数据)来实现逻辑划分。

TODO: 看一遍文档

集群

目前开发环境只用单节点。

TBD

OpenSearch

基于Elasticsearch 7.10.2 & Kibana 7.10.2集成了一堆插件,尤其是近似KNN搜索插件。

索引

使用 OpenSearch REST API 为数据编制索引。存在两种 API:索引 API 和_bulkAPI。 \

对于新数据以增量方式到达的情况(例如,来自小型企业的客户订单),可以使用 索引 API 在文档到达时单独添加文档。\

1
2
PUT <index>/_doc/<id>
{ "A JSON": "document" }

例如

1
curl -H 'Content-Type: application/json' -XPUT "https://localhost:9200/movies/_doc/1" -d '{ "title": "Spirited Away" }' -u 'admin:admin' --insecure

注意-d后面要用单引号,json内部要用双引号

对于数据流不太频繁的情况(例如,营销网站的每周更新),可以生成一个文件并将其发送到_bulkAPI。\

1
2
3
POST _bulk
{ "index": { "_index": "<index>", "_id": "<id>" } }
{ "A JSON": "document" }

例如

1
curl -H 'Content-Type: application/json' -XPOST "https://localhost:9200/_bulk?pretty" -u 'admin:admin' --insecure --data-binary @data-bulk.json

data-bulk.json:

1
2
3
4
5
6
7
8
9
10
{"index":{ "_index": "books", "_type": "IT", "_id": "1" }}
{"id":"1","title":"Java Book","language":"java","author":"Bruce Eckel","price":70.20,"year":2007,"description":"Java must read", "tel":"15313016138", "d_val":"2018-11-01 12:25:36"}
{"index":{ "_index": "books", "_type": "IT", "_id": "2" }}
{"id":"2","title":"Java perform","language":"java","author":"John Li","price":46.50,"year":2012,"description":"permformance ..", "tel":"13548621254", "d_val":"2018-11-01 08:25:50"}
{"index":{ "_index": "books", "_type": "IT", "_id": "3" }}
{"id":"3","title":"Python compte","language":"python","author":"Tohma Ke","price":81.40,"year":2016,"description":"py ...", "tel":"13245687956", "d_val":"2018-11-01 19:30:20"}
{"index":{ "_index": "books", "_type": "IT", "_id": "4" }}
{"id":"4","title":"Python base","language":"python","author":"Tomash Si","price":54.50,"year": 2014,"description":"py base....", "tel":"aefda1567fdsa13", "d_val":"2018-09-01"}
{"index":{ "_index": "books", "_type": "IT", "_id": "5" }}
{"id":"5","title":"JavaScript high","language":"javascript","author":"Nicholas C.Zakas","price":66.40,"year":2012,"description":"JavaScript.....", "tel":"a14512dfa", "d_val":"2018-08-01"}

对于大量文档,将请求集中在一起并使用_bulkAPI 可提供卓越的性能。
但是,如果文档很大,可能需要单独为它们编制索引。

在 OpenSearch 中,数据的基本单位是 JSON文档。在索引中,OpenSearch 使用唯一 ID 标识每个文档。

当将文档添加到尚不存在的索引时,OpenSearch 会自动创建索引。如果未在请求中指定 ID,它还会自动生成一个 ID。
自动 ID 生成有一个明显的缺点:因为索引请求没有指定文档 ID,以后无法轻松更新文档。此外,如果运行此请求 10 次,OpenSearch 会将此文档索引为 10 个具有唯一 ID 的不同文档。
指定一个 ID,如果运行此命令 10 次,仍然只有一个索引的文档,该_version字段增加到 10。

索引命名

OpenSearch 索引具有以下命名限制:

  • 所有字母必须小写。
  • 索引名称不能以下划线 ( _) 或连字符 ( -)开头。
  • 索引名称不能包含空格、逗号或以下字符::, “, *, +, /, , |, ?, #, >, 或<

取数据

索引文档后,可以通过发送 GET 请求取数据,原始文档在返回的json的_source对象中 \

例如

1
curl -H 'Content-Type: application/json' -XGET "https://localhost:9200/movies/_doc/1" -u 'admin:admin' --insecure

更新数据

  • 要更新现有字段或添加新字段,请向_update操作发送 POST 请求,并在doc对象中进行更改

例如

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
curl -H 'Content-Type: application/json' -XPOST "https://localhost:9200/movies/_update/1" -u 'admin:admin' --insecure -d '
{
"doc": {
"title": "Castle in the Sky",
"genre": ["Animation", "Fantasy"]
}
}'

# 得到
{
"_index" : "movies",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "Castle in the Sky",
"genre" : [
"Animation",
"Fantasy"
]
}
}

索引的_version字段代表文档更新的次数。

  • 要完全替换文档,请使用 PUT 请求

    1
    2
    3
    4
    PUT movies/_doc/1
    {
    "title": "Spirited Away"
    }
  • 根据文档是否已经存在有条件地更新

使用upsert对象。在这里,如果文档存在,则其title字段更改为Castle in the Sky。如果不是,OpenSearch 会索引upsert对象中的文档。

1
2
3
4
5
6
7
8
9
10
11
POST movies/_update/2
{
"doc": {
"title": "Castle in the Sky"
},
"upsert": {
"title": "Only Yesterday",
"genre": ["Animation", "Fantasy"],
"date": 1993
}
}

删除数据

要从索引中删除文档,请使用 DELETE 请求:

1
DELETE movies/_doc/1

之后如果再查询,就会得到found=false

1
2
curl -H 'Content-Type: application/json' -XGET "https://localhost:9200/movies/_doc/1" -u 'admin:admin' --insecure
{"_index":"movies","_type":"_doc","_id":"1","found":false}#

索引配置

直接提供数据其实是采用了默认的索引配置,其实可以创建自定义的index,参见近似KNN搜索的index创建

索引别名

如果数据分布在多个索引中,而不是跟踪要查询的索引,可以创建一个别名并对其进行查询。

例如,如果将日志存储到基于月份的索引中,并且经常查询前两个月的日志,可以创建一个last_2_months别名并更新它指向的每个月的索引。

因为可以随时更改别名指向的索引,所以在应用程序中使用别名引用索引可以在不停机的情况下重新索引数据。

TBD

查询

cheatsheet: https://www.jianshu.com/p/3cb205b5354a \

https://elasticsearch-cheatsheet.jolicode.com/

多字段包含关系

1
2
3
4
5
6
7
8
9
10
curl -H 'Content-Type: application/json' -XPOST "https://localhost:9200/books/_search?pretty=true" -u 'admin:admin' --insecure -d '{"query": {
"bool": {
"must": [
{ "match": { "port": "9083" }}, # 这里是包含关系,port字段里只要有9083就是true
{ "match": { "name": "tt_vod_program_play_test" }},
{ "match": { "s_timestamp": "1545719719164" }}
]
}
},
"size": 10}'

查询数字范围

1
2
3
4
5
6
7
8
9
10
-d '{  
"query": {
"range" : {
"price" : {
"gte" : 50,
"lte" : 70
}
}
}
}'

近似KNN搜索

一旦数据集超过数十万个向量,精确KNN开销太大,需要采用近似KNN算法。

近似knn算法的benchmark参考:https://github.com/erikbern/ann-benchmarks/

  • 首先需要创建一种索引,其index.knn设置为true,然后添加一个或多个类型为knn_vector的字段
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
curl -H 'Content-Type: application/json'  -u 'admin:admin' --insecure -X PUT "https://localhost:9200/my-knn-index-1" -d '{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 0,
"index": {
"knn": true,
"knn.algo_param.ef_search": 100
}
},
"mappings": {
"properties": {
"my_vector1": {
"type": "knn_vector",
"dimension": 2,
"method": {
"name": "hnsw",
"space_type": "l2",
"engine": "nmslib",
"parameters": {
"ef_construction": 128,
"m": 24
}
}
},
"my_vector2": {
"type": "knn_vector",
"dimension": 4,
"method": {
"name": "hnsw",
"space_type": "cosinesimil",
"engine": "nmslib",
"parameters": {
"ef_construction": 256,
"m": 48
}
}
}
}
}
}'

构造数据示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{ "index": { "_index": "my-knn-index-1", "_id": "1" } }
{ "my_vector1": [1.5, 2.5], "price": 12.2, "hash": "deadbeef" }
{ "index": { "_index": "my-knn-index-1", "_id": "2" } }
{ "my_vector1": [2.5, 3.5], "price": 7.1, "hash": "deadbeee" }
{ "index": { "_index": "my-knn-index-1", "_id": "3" } }
{ "my_vector1": [3.5, 4.5], "price": 12.9, "hash": "deadbeed" }
{ "index": { "_index": "my-knn-index-1", "_id": "4" } }
{ "my_vector1": [5.5, 6.5], "price": 1.2, "hash": "deadbeec" }
{ "index": { "_index": "my-knn-index-1", "_id": "5" } }
{ "my_vector1": [4.5, 5.5], "price": 3.7, "hash": "deadbeeb" }
{ "index": { "_index": "my-knn-index-1", "_id": "6" } }
{ "my_vector2": [1.5, 5.5, 4.5, 6.4], "price": 10.3, "hash": "deadbeea" }
{ "index": { "_index": "my-knn-index-1", "_id": "7" } }
{ "my_vector2": [2.5, 3.5, 5.6, 6.7], "price": 5.5, "hash": "deadbee0" }
{ "index": { "_index": "my-knn-index-1", "_id": "8" } }
{ "my_vector2": [4.5, 5.5, 6.7, 3.7], "price": 4.4, "hash": "deadbedf" }
{ "index": { "_index": "my-knn-index-1", "_id": "9" } }
{ "my_vector2": [1.5, 5.5, 4.5, 6.4], "price": 8.9, "hash": "deadbede" }

然后搜索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET my-knn-index-1/_search
{
"size": 2,
"query": {
"knn": {
"my_vector2": {
"vector": [2, 3, 5, 6],
"k": 2
}
}
},
"post_filter": {
"range": {
"price": {
"gte": 5,
"lte": 10
}
}
}
}
  • 性能优化
  • 在创建索引时禁用刷新间隔

    1
    2
    3
    4
    5
    6
    PUT /<index_name>/_settings
    {
    "index" : {
    "refresh_interval" : "-1"
    }
    }

    索引建完记得重新启用!

  • 增加线程数
    在创建索引时,使用knn.algo_param.index_thread_qty设置要分配的线程数。

  • 增加分片(shard)数,以减小每个分片中的段(section)数,从而增强并行化:在创建索引的时候进行配置:

    1
    2
    3
    4
    "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 1
    },

    注意,Elastic 官方博客文章建议:堆内存和分片的配置比例为1:20,举例:30GB堆内存,最多可有600个分片。
    https://www.elastic.co/guide/en/elasticsearch/reference/7.0/misc-cluster.html#cluster-shard-limit
    https://github.com/elastic/kibana/issues/35529 \

  • 预热索引

    1
    2
    3
    4
    5
    6
    7
    8
    GET /_plugins/_knn/warmup/index1,index2,index3?pretty
    {
    "_shards" : {
    "total" : 6,
    "successful" : 6,
    "failed" : 0
    }
    }
CATALOG
  1. 1. ElasticSearch
    1. 1.1. 集群
  2. 2. OpenSearch
    1. 2.1. 索引
      1. 2.1.1. 索引命名
      2. 2.1.2. 取数据
      3. 2.1.3. 更新数据
      4. 2.1.4. 删除数据
      5. 2.1.5. 索引配置
      6. 2.1.6. 索引别名
    2. 2.2. 查询
      1. 2.2.1. 多字段包含关系
      2. 2.2.2. 查询数字范围
      3. 2.2.3. 近似KNN搜索