文章目录
安装
https://www.elastic.co/downloads/past-releases/elasticsearch-7-17-0
下载完成启动bin/elasticsearch
服务,可以在Postman
调试各种请求。
基本语法
索引
"索引"一词被用来专指用于存储数据的地方,你可以把它比作是传统数据库中的"数据库"概念。
Elasticsearch
将索引分成了多个分片,这些分片被分布在一个或多个节点上,用以保证数据的分布式存储和高可用。分片可以进一步分为两类:
- 主分片(
Primary Shards
):主分片的数量在索引创建时指定,并且在索引的生命周期内不能改变。 - 复制分片(
Replica Shards
):复制分片是主分片的副本,用于提供数据的冗余副本,以防硬件故障造成数据丢失,同时也可以通过增加复制分片来提高查询性能。
当一个文档被索引(即添加或更新)时,基于特定的哈希算法(通常是文档ID的哈希)决定这个文档应该存放在哪个主分片上,一般会针对分片数量取余。这保证了每个文档都被均匀地分配到所有的主分片上。一个文档不会被分割到多个主分片上,它总是完整地存储在单个主分片上。
假如有一个索引设定为有3个主分片(P1, P2, P3)和每个主分片有2个复制分片(R1, R2),那么:
- 每个文档仅存储在一个主分片中。
- 该主分片的数据会被复制到它的所有复制分片中。
总共会有9个分片(包含主分片和复制分片),但对于给定的文档,它只存在于其中的3个分片上(1个主分片和它的2个复制分片)。
创建索引
PUT: http://127.0.0.1:9200/shopping
Request Body:
{
"settings": {
// 所有的文档数据将在这3个主分片之间分布存储。
"number_of_shards": 3,
// 表示每个主分片将有1个复制分片。
// 复制分片是为了提高数据的可用性和容错性,以及增加读取(查询)操作的能力。
"number_of_replicas": 1
}
Response:
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "shopping"
}
多次创建同一个索引时,会报错,说明创建请求是幂等性的(同一个操作执行多次和执行一次效果相同,不会因为多次执行而产生不同的效果)。所以应该用PUT
而非POST
,POST
每次都会创建一条新的记录,对于同一个请求,并不会由于索引重复而报错。
修改索引设置
PUT: http://127.0.0.1:9200/shopping/_settings
Request Body:
{
"number_of_replicas": 2
}
查看索引
GET: http://127.0.0.1:9200/shopping
包含了每个索引的健康状况、状态、名称、唯一ID(UUID)、分片数(primary和replica)、包含的文档数、删除的文档数、以及存储大小所有主分片占用的物理磁盘空间总。
删除索引
DELETE: http://127.0.0.1:9200/shopping
倒排索引
倒排索引(Inverted Index)
是一种数据库索引设计,它允许我们非常快速地从大量数据中找到包含某个词的所有文档。倒排索引主要是为了解决全文搜索的需求提出的。在没有倒排索引的早期系统中,进行全文搜索可能需要遍历数据库中的每一条记录,并检查每条记录是否包含给定的搜索词,这在数据量大时效率极低。
工作原理
倒排索引的工作原理是将文档中的每个词与出现该词的所有文档的列表相关联。具体来说,倒排索引由两个主要部分组成:词汇表和倒排文件。
-
词汇表(Vocabulary):包含了所有文档中出现过的词(或称为词项、Tokens),通常以排序的形式存储以便快速查找。
-
倒排文件(Postings List):对于词汇表中的每一个词,都有一个对应的倒排列表(也称为Posting List),列出了包含该词的所有文档的标识符(通常是文档ID)。这些列表可能还会包括词在文档中出现的位置和频率等附加信息,以支持更复杂的查询,如邻近搜索和权重计算。
工作过程
当进行全文搜索时:
- 搜索引擎首先将查询分解为单独的词;
- 然后在词汇表中查找每一个词;
- 接着收集每个词的倒排列表;
- 最后,综合这些列表来找出包含所有或部分查询词的文档,并根据相关度评分等进行排名。
倒排索引被设计为不可变(Immutable)的。这意味着一旦倒排索引被创建和写入,就不能更改。这种设计有几个重要的好处:
- 提高性能:不可变的数据结构可以更高效地被多线程访问,因为不需要复杂的同步机制来防止数据被并发修改。
- 缓存友好:不可变性使得操作系统和硬件能够更有效地缓存数据,因为数据不会被修改,所以一旦被加载到缓存中就可以重用。
- 简化数据恢复:在系统故障的情况下,不可变索引更容易恢复,因为没有复杂的中间状态。
- 支持时间旅行:不可变索引可以通过在不同时间点的快照来支持数据的历
尽管倒排索引本身是不可变的,但Elasticsearch等系统仍然支持更新和删除操作。这是通过创建新的索引段来实现的,新的数据会被写入新的不可变索引段中。
补充索引(Supplemental Indexing)
当需要更新或删除已经索引的文档时,系统会将这些更新或删除标记为一个新的操作,并写入一个新的索引段中。查询时,系统会查阅所有的索引段,包括原始的和新增的,然后基于最新的更新来呈现查询结果。
删除与合并
- 标记删除:对于要删除的文档,搜索引擎通常采用“标记删除”的方式,在新的索引段中记录哪些文档被删除,并不立即从物理存储中移除这些文档。
- 后台合并:搜索引擎会定期在后台进行索引合并操作,把多个小的索引段合并成较大的索引段,并在此过程中移除被删除的文档,优化查询性能。
虽然单个倒排索引段是不可变的,但通过合并索引段和添加新的索引段,Elasticsearch等系统能够有效地支持更新和删除操作,同时保持不可变索引的所有优点。补充索引(或者更准确地说是补充索引段)和段合并是这些系统管理不可变索引并保持高效性能的关键机制。
文档
创建文档
POST: http://127.0.0.1:9200/shopping/_doc`或者`POST: http://127.0.0.1:9200/shopping/_create
Request body:
{
"name": "xiaomi su7",
"type": 1,
"price": 29.99
}
Response:
{
"_index": "shopping",
"_type": "_doc",
"_id": "bhajbY8BDgHLWJjh9Xp7",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
如果觉得id
不好记忆,可以指定id
创建文档:POST/PUT: http://127.0.0.1:9200/shopping/_doc/1001
,重复调用会更新文档,如果明确用_create
,重复调用会有冲突。
更新文档
POST: http://127.0.0.1:9200/shopping/_update/1001
Request body:
{
"doc": {
"compony": "xiaomi "
}
}
Response:
{
"_index": "shopping",
"_type": "_doc",
"_id": "1001",
"_version": 9,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 10,
"_primary_term": 1
}
如果加上doc
就是增量更新,否则为全亮更新。
文档查询过程
Elasticsearch
在读取文档数据时采取了一系列措施来实现负载均衡和高效的数据访问。下面是读取过程的大致流程和实现负载均衡的机制:
文档数据的读取过程
-
接收查询请求:当
Elasticsearch
收到一个查询请求时,该请求首先被发往集群中的一个节点。这个节点称为协调节点(coordinating node)。 -
确定目标分片:协调节点根据查询内容确定此次查询涉及哪些主分片和复制分片。因为主分片和其对应的复制分片之间的数据是一样的,所以查询可以在主分片或其任意一个复制分片上执行。
-
分发查询请求:协调节点将查询请求分发到涉及的所有分片上。为了实现负载均衡,协调节点会尽量均匀地选择主分片和复制分片处理查询请求。例如,如果两个查询请求同时到来,针对同一分片的查询,第一个查询可以在主分片上执行,第二个查询则可以在该主分片的一个复制分片上执行。
-
收集响应:每个目标分片独立执行查询,并将结果返回给协调节点。协调节点收到所有分片的响应后,会合并这些结果,然后返回给客户端。
实现负载均衡的机制
-
利用复制分片:正如以上所述,查询可以在主分片或其任意复制分片上执行。这样,即使某个主分片的负载较高,查询请求也可以被重定向到相对空闲的复制分片上。
-
分片分布:
Elasticsearch
会尽量将分片均匀分布在集群的所有节点上,这样可以确保每个节点都参与处理查询和索引请求,从而避免某些节点成为瓶颈。 -
查询缓存:
Elasticsearch
拥有查询缓存机制,能够缓存查询结果的一部分。当相似的查询再次发生时,可以直接从缓存中获取结果,从而减轻后端分片的查询压力。 -
自适应副本选择:从版本5.3开始,Elasticsearch引入了自适应副本选择(adaptive replica selection,ARS)算法。此算法在选择进行查询的分片复制时,会考虑到节点的当前负载、响应时间等因素,从而更智能地选择最佳的节点处理查询请求,进一步优化读取操作的性能和负载均衡。
通过上述机制,Elasticsearch
能够在处理读取请求时实现有效的负载均衡,进而提升整体的查询性能和集群的稳定性。
匹配查询
GET: http://127.0.0.1:9200/shopping/_search
Request Body:
{
"query": {
"match_all": {
}
/**
"match": {
"compony": "xiaomi"
}
*/
"from": 0,
"size": 2,
"_source": ["type", "price"],
"sort": {
"price": {
"order": "desc"
}
}
}
Response:
{
"took": 8,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "shopping",
"_type": "_doc",
"_id": "1003",
"_score": null,
"_source": {
"price": 39.99,
"type": 1
},
"sort": [
39.99
]
},
{
"_index": "shopping",
"_type": "_doc",
"_id": "bhajbY8BDgHLWJjh9Xp7",
"_score": null,
"_source": {
"price": 29.99,
"type": 1
},
"sort": [
29.99
]
}
]
}
}
多条件查询
GET: http://127.0.0.1:9200/shopping/_search
Request Body:
{
"query": {
"bool": {
// 数组里的条件同时满足
"must": [
{
"match": {
"name": "xiaomi su7"
}
},
{
"match": {
"type": 1
}
}
],
// 数组里的条件满足一个即可
"should": [
{
"match": {
"price": 39.99
}
}
],
// 对结果进行范围过滤
"filter": {
"range": {
"price": {
"gt": 30
}
}
}
}
}
}
Response:
{
"took": 9,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 2.2391143,
"hits": [
{
"_index": "shopping",
"_type": "_doc",
"_id": "1003",
"_score": 2.2391143,
"_source": {
"name": "xiaomi su7 pro",
"type": 1,
"price": 39.99
}
}
]
}
}
match
是分词检索,doc
在存储的时候是倒排索引,会把里面的字段按空格(实验结果)拆成一些词,每个词都会对应一条id
的数据。默认查询时用的并不是全词匹配,比如:
{
"query": {
"match": {
"name": "su7"
}
}
}
能匹配到name = "xiaomi su7" / "xiaomi su7 pro"
的数据。
聚合查询
GET: http://127.0.0.1:9200/shopping/_search
Request Body:
{
"aggs": {
"price_group": {
"terms": { // 分组
"field": "price"
}
}
},
"size": 0
}
Response:
{
"took": 15,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"price_group": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": 29.989999771118164,
"doc_count": 2
},
{
"key": 39.9900016784668,
"doc_count": 1
}
]
}
}
}
映射
可以为某个索引设置一些映射的字段,包括字段是否能用分词查询(否则全亮匹配)、是否能用来查询。
PUT: http://127.0.0.1:9200/user/_mapping
Request Body:
{
"properties": {
"name": {
"type": "text", // 可分词查询
"index": true. // 可作为查询字段
},
"sex": {
"type": "keyword", // 只能全亮匹配
"index": true
},
"tel": {
"type": "keyword",
"index": false // 不可作为查询字段
}
}
}
文档冲突
文档冲突通常指的是在并发环境下,多个操作尝试同时修改同一文档时发生的问题。这在分布式数据库、全文搜索引擎(如Elasticsearch)、版本控制系统(如Git)等多用户或多进程操作的系统中尤为常见。文档冲突通常分为两大类:乐观并发控制(Optimistic Concurrency Control, OCC)和悲观并发控制(Pessimistic Concurrency Control, PCC)产生的冲突。
乐观并发控制(OCC)
在乐观并发控制中,系统假设多个事务在大多数情况下不会互相冲突。它允许多个事务几乎同时进行,只在提交操作的时候检查是否存在冲突。如果发现冲突,比如其他事务已经修改了同一个文档,系统通常会拒绝当前事务的提交。
举例:Elasticsearch就是使用乐观并发控制来处理文档更新的冲突。它通过版本号来实现这一机制。每个文档在创建时都会被赋予一个版本号,每当文档被成功更新,其版本号增加。当更新文档时,可以指定期望的文档版本号,如果该版本号与服务器上当前文档的版本号不一致(表示文档已经被其他操作更新),则更新将会失败,以此来防止冲突。
悲观并发控制(PCC)
悲观并发控制是基于这样一种假设:冲突在多个事务中是常见的,因此在访问文档之前需要先获取锁。只有持有锁的事务才能读取或写入文档,其他事务必须等待直到锁被释放。这种方式可以有效预防冲突,但可能导致事务等待时间较长。
举例:传统的关系型数据库管理系统(RDBMS)如MySQL或PostgreSQL在处理事务时,常见的悲观锁策略包括行锁和表锁,用于控制对特定数据行或表的并发访问。
解决和避免文档冲突
- 重试机制:在操作失败时,可以根据具体的错误类型(如版本冲突异常)自动或手动进行重试。
- 事务日志:某些系统提供事务日志记录,可以在冲突发生时回溯并了解导致冲突的操作。
- 应用程序逻辑:在应用级别处理冲突,例如通过用户界面提醒用户冲突发生,并提供选择如何解决冲突的选项。
- 分布式锁:在分布式系统中,通过分布式锁管理机制来强制序列化对特定资源的访问,避免冲突。
文档冲突的处理和解决是实现高可用、高并发系统的关键之一,正确选择和实现并发控制机制对于系统的稳定性和性能至关重要。
if_seq_no
和if_primary_term
是Elasticsearch中用于实现乐观并发控制(Optimistic Concurrency Control, OCC)的两个参数,主要用于在更新、删除或使用索引操作对文档进行操作时防止文档冲突。它们允许您仅在特定条件满足时执行这些操作,确保当您更新或删除一个文档时,该文档没有被其他进程修改过。
在Elasticsearch中,每个文档都包括几个元数据字段,其中_seq_no
(序列号)和_primary_term
(主分片期数)对于实现乐观并发控制尤其重要:
_seq_no
(序列号):这是一个递增的数字,每当文档发生变化时(例如更新或删除),这个数字就会增加。它可以帮助跟踪文档的变更历史。_primary_term
(主分片期数):每当分片的主副本因任何原因改变时(比如因为故障转移),这个数字就会增加。主分片期数提供了一种方式来识别文档的特定版本是否仍然有效。
当你希望基于文档的当前状态执行更新或删除操作时,可以在请求中包含 if_seq_no
和if_primary_term
参数。Elasticsearch将会检查这些参数与存储在索引中的当前文档版本是否匹配。只有当提供的_seq_no
和_primary_term
与索引中文档的当前值一致时,操作才会成功执行;否则,操作将被拒绝,防止了潜在的文档版本冲突。
分词器
分词器(Tokenizer)
在全文搜索中发挥着至关重要的作用,它负责将文本拆分成一系列易于索引的单词或词语。这对于构建搜索引擎的倒排索引特别重要。分词器通常会考虑语言的特点,如词干提取、停用词过滤等,以提高搜索的准确性和效率。在不同的全文搜索系统中,都提供了分词器的配置和自定义功能。
搜索引擎提供了多种标准分词器,如简单分词器(Whitespace Tokenizer)、英文分词器(English Tokenizer)等,以满足不同的需求。但在某些情况下,这些标准分词器可能无法完全满足特定应用的需求,如需要处理专业术语、缩写或特定领域内的词汇。这时,就需要自定义分词器来实现。
自定义分词器
自定义分词器通常包含以下几个组件:
- 字符过滤器(Character Filters):对原始文本进行预处理,如去除HTML标签、替换指定字符等。
- 分词器(Tokenizer):基础的文本拆分规则,如按空格、标点等拆分。
- 词项过滤器(Token Filters):对拆分后的词项进行处理,如小写转换、停用词过滤、同义词处理等。
以Elasticsearch为例,自定义分词器的步骤大致如下:
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"my_char_filter": {
"type": "mapping",
"mappings": [
"&=> and"
]
}
},
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": ["the", "is"]
}
},
"tokenizer": {
"my_tokenizer": {
"type": "pattern",
"pattern": "\\s+"
}
},
"analyzer": {
"my_custom_analyzer": {
"type": "custom",
"char_filter": [
"my_char_filter"
],
"tokenizer": "my_tokenizer",
"filter": [
"lowercase",
"my_stopwords"
]
}
}
}
}
}
在这个例子中,我们自定义了一个名为my_custom_analyzer
的分词器,它包含一个字符过滤器my_char_filter
用于将&
替换为and
,一个基于正则表达式的分词器my_tokenizer
,以及两个词项过滤器lowercase
将词项转换为小写、my_stopwords
用于过滤停用词。
添加自定义词
对于添加自定义词、短语或术语等特定需求,你可以通过词项过滤器来实现,特别是同义词过滤器(Synonym Filter)
。例如,在上述设置中,可以添加一个同义词过滤器来引入自定义词:
"filter": {
"my_synonyms": {
"type": "synonym",
"synonyms": [
"NLP, 自然语言处理",
"IR, 信息检索"
]
}
}
这样就能让Elasticsearch在分词时认识并处理这些自定义词或术语。
总之,通过对分词器的精细配置和自定义,可以显著提升搜索引擎处理文本的能力,以更好地适应特定应用场景的需求。