倒序索引也被称为“反向索引”或“反向文件”,是一种索引数据结构。倒序索引在“内容”和存放内容的“位置”之间的映射,其目的在于快速全文索引和使用最小处理代价将新文件添加进数据库。通过倒序索引,可以快速根据“内容”查到包含它的文件。这种数据结构被广泛使用在搜索引擎中,倒排索引有两种不同的索引形式:
为什么会从以这个为出发点来优化索引的问题呢?以 mysql 来举例,我们知道 mysql 的数据库中数据条目超过千万条就会出现数据瓶颈,即使你把数据采用各种主从模式进行部署,对于涉及到的有关数据的汇总需求的业务部分,也会因为不同机房的数据同步机制,以及数据的汇总逻辑,而让处理高度复杂化。同时,mysql 默认会与从磁盘读取数据,读取的数据 size 为 16kb,底层实现采用 b+树的原因就在于这样可以降低树的高度,虽然 b+树的非叶子节点上并不存储数据,只存储索引,但是如果针对的全是长文本,那么,这个 size 的数据,也会导致叶子叶点很快被填满,这样通过降低树高,从而提高数据检索效率的设计就发挥不出这种设计的初衷了,可见用 mysql 中广泛使用的 B+树结构来做长文本的数据检索是不适合这种文本匹配的场景,而且对于文本匹配,普通使用的都是%xx
这种方式,根据 mysql 的最左前缀匹配原则,会导致索引失效,所以走了全表扫描,再加上数据条目的极其庞大,性能可想而知会慢到何种程度,那针对这种情况,像Elasticsearch
是利用何种方式来解决这个问题呢?
Elasticsearch
是Lucence
这个搜索引擎框架的组成部分,但是单独部署异常庞大,对系统配置要求特别高,对于只需要优化查询功能的普通业务来说,可以说是得不偿失,所以Elasticsearch
被独立出来,可以单独进行部署,它是如何解决 mysql 没有解决的问题的?
Lucene
会把所有的目标域(field)进行分词操作,就是把表的组成字段切分成若干个词项(Term),针对于不同语言,做分词的效果是大相径庭的。比如说英文天然空格就可以做拆分,但是中文如果没有语义支撑,无法做有效的分词,对于有效筛选词无法精确匹配到,那做出的搜索功能想必用户体验就会极差了,nlp
领域比较有名的分词工具,比如说结巴分词,如果你使用过,一定知道我在讲什么。分好的词,如何来使用呢?Lucene
会在Index time
把索引字段的所有词项切分计算出来,并按照字典序生成一个词项字典(Term Dictionary)
,此项字段存储的是去重了之后的所有词项。查询时有效组成的部分包括term dictionary(最终生成的词项词典)
和倒排表(Posting List)
,它保存的就是包含所有当前词项的元数据的 id 的有序 int 数组。
更新策略主要有以下 4 种:完全重建策略、再合并策略、原地更新策略、混合策略
逆向思考一下,既然我们这里要讲倒序索引,这里就追问一下,你知道正排索引是什么吗? “正排索引”又称为“前向索引”。它是创建倒序索引的基础,通过文档到关键词(doc->word)的映射,具有以下字段:
正排索引是一个文本搜索引擎中的关键组件之一,用于存储文档的详细信息和内容。正排索引通常包含以下字段:
以这四个字段为例,可以解释如何使用它们来构建正排索引。假设有一个文档集合,其中包含多篇文档,机器对这些文档进行分析,提取出其中的单词,并将每个单词分配一个唯一的数字 ID,即 WordId。对于每个文档,都会生成一个 LocalId 作为该文档在该分片内的本地编号。当用户输入查询词时,系统会根据查询词的 WordId 在索引中查找匹配的文档,并返回 NHits 和 Hitlist 信息。
举个例子,如果用户输入了一个查询词"apple",系统会在正排索引中找到所有含有单词"apple"的文档。对于每个匹配的文档,系统会返回该文档的 LocalId、NHits 和 HitList 信息,以便进行后续处理,如文本摘要、高亮显示等。
总之,正排索引是实现文本搜索的关键组件之一,它存储了文档的详细信息和内容,以帮助搜索引擎更加快速地查找并返回相关的搜索结果,但是我们有使用过搜索引擎的经验,我们都知道,网页检索等场景都是用关键字来找文档,因此需要把正排序转换成为倒排索引,才能满足相应需求,也就是我们实际要讲的主题,倒排索引。
文档矩阵是用来表示文本集合中的文档与单词之间的关系的一种数据结构。文档矩阵通常采用二维矩阵来表示,其中行表示文档,列表示单词,矩阵中的每个元素表示该单词在该文档中是否出现。
下面是一个简单的例子,通过图表形式展示了词汇和文档之间的关系:
Doc1 | Doc2 | Doc3 | |
---|---|---|---|
apple | 1 | 0 | 1 |
orange | 0 | 1 | 0 |
banana | 1 | 1 | 1 |
在上述矩阵中,我们可以看到有三个文档(Doc1、Doc2 和 Doc3)和三个单词(apple、orange 和 banana)。矩阵中的每个元素表示对应文档中对应单词是否出现,如果出现则为 1,否则为 0。例如,在文档"Doc1"中,单词"apple"和单词"banana"都出现了,因此对应位置的值为 1,而单词"orange"没有出现,因此对应位置的值为 0。
需要注意的是,文档矩阵可能非常庞大,因此一般会使用稀疏矩阵来存储,以节省存储空间和计算资源。稀疏矩阵只存储非零元素,将零值的单元格从矩阵中删除。
倒排索引是搜索引擎中的一个重要组成部分,用于快速查找文档中包含指定单词的位置。倒排索引的数据结构通常包括以下三个主要部分:
倒排索引的建立过程包括两个主要步骤:分析和索引。分析阶段主要是将文本进行分词处理,得到单词序列;索引阶段则是将文档中出现的单词按照上述数据结构组织起来,并构建倒排索引。
需要注意的是,倒排索引可以占用大量的存储空间,因此在实际应用中可能需要使用压缩技术来减小索引的大小。常见的压缩方法包括前缀编码、变长编码、霍夫曼编码等。
FOR 算法的核心思想是用减法来削减数值大小,从而达到降低空间存储。假设 V(n)表示数组中第 n 个字段的值,那么经过 FOR 算法压缩的数值 V(n)=V(n)-V(n-1)。也就是说存储的最后一位减去前一位的差值。存储是也不再按照 int 来计算了,而是看这个数组的最大值需要占用多少 bit 来计算。
Frame Of Reference(FOR)算法是一种用于数据压缩和存储的算法,它可以大幅度减少数据存储的空间占用,并在不降低数据质量的情况下提高查询效率。该算法适用于各种类型的数据,例如数值型、文本型、时间型等。
以下是 FOR 算法的主要概念:
FOR 算法将数据分成若干个大小相等的块(Block),称之为 FOR 块。每个 FOR 块包含一个参考点(Reference Point),它是该块中所有元素与参考点之间差值的最大值。这样做可以将数据压缩到很小的范围内,从而节约存储空间。
在进行块编码时,FOR 算法会对每个 FOR 块中的所有元素进行差分编码(Delta Encoding)。具体来说,它会将当前元素与参考点之间的差值编码为一个整数,然后将该整数存储到磁盘上。这样做不仅可以减少数据存储的空间占用,还可以加速查询操作。
变化数组(Variation Array)是 FOR 算法中的关键数据结构,它记录了每个 FOR 块中的参考点和元素个数。具体来说,变化数组包括两个部分:参考点数组和偏移量数组。参考点数组记录了每个 FOR 块的参考点值,而偏移量数组记录了每个 FOR 块中第一个元素的位置。
在进行 FOR 压缩时,FOR 算法会将原始数据划分为若干个 FOR 块,并对每个 FOR 块进行差分编码。接下来,它会使用变化数组来记录每个 FOR 块的参考点和偏移量信息,并将编码后的数据存储到磁盘上。这样做可以大幅度减少数据存储的空间占用,并在查询操作中快速定位所需的数据。
以上就是 FOR 算法的概念,总结一下:
RBM 算法的核心步骤如下:
Trie 字典树,也被称为前缀树(Prefix Tree),是一种用于字符串匹配的数据结构。与其他基于比较的数据结构不同,Trie 使用键本身来构建树形结构,从而实现高效的字符串查找和插入操作。
Trie 树的核心思想是将相同前缀的字符串合并到一起,形成一个公共节点,从而减少存储空间和提高查询效率。每个节点包含一个字符和指向子节点的指针,根据字符串中每个字符的顺序确定树的层级结构。最终,在 Trie 树中,每个单词都对应着一条从根节点到叶子节点的路径,同时每个节点都代表了一段前缀。
Trie 树具有以下一些重要特点:
总之,Trie 树是一种非常实用的数据结构,主要用于处理字符串相关问题,例如单词查找、模式匹配、拼写纠错等。除了 Trie 树之外,B-Trees、B+Trees、红黑树等数据结构也经常用于处理各种类型的字符串和键值对。
Lucene 采用了一种称为 FST(Finite State Transducer)的结构来构建词典,这个结构保证了时间和空间复杂度的均衡,它是 Lucene 的核心功能之一。FST 类似于一种TRIE
树,它使用FSM(Finite State Machines)有限状态机
作为数据结构,它表示有限个状态(State)集合以及这些状态之间转移和动作的数学模型。其中一个状态被标记为开始状态,0 个或更多的状态被标记为 final 状态。一个 FSM 同一时间只处理 1 个状态。
Ordered Sets
它是一个有序集合。通常一个有序集合可以用二叉树、B 树实现。无序集合使用 hash table 来实现,这里我们用了一个确定无环有限状态接收机(Deterministric acyclic finite state acceptor,FSA)
来实现。FSA 是一个 FSM(有限状态机)的一种,具有以下特征:
(start) ---'j'---> (q1) ---'u'---> (q2) ---'l'---> (q3: accept)
#在这个FSA中,(start)表示起始状态。从这个状态开始,输入字母 'j' 将使FSA进入另一个状态 (q1),然后输入 'u' 将使FSA进入 (q2),最后,输入字母 'l' 将使FSA到达接受状态 (q3: accept)。这表示我们已经找到了一个完整的键为 "jul" 的元素。
在这个 FSA 中,(start)表示起始状态。从这个状态开始,输入字母 ‘j’ 将使 FSA 进入另一个状态 (q1),然后输入 ‘u’ 将使 FSA 进入 (q2),最后,输入字母 ’l’ 将使 FSA 到达接受状态 (q3: accept)。这表示我们已经找到了一个完整的键为 “jul” 的元素。
在 FSA 中,一个前缀是指任何从起始状态到达某个状态的路径上的所有字符。换句话说,一个前缀就是输入字符串的子集,它以起始状态为开始并在某个状态结束。
例如,假设我们有以下 FSA:
(start) ---'c'---> (q1) ---'a'---> (q2) ---'t'---> (q3: accept)
| |
+---'h'----> (q4: accept)
如果我们输入了字符串 “cat”,则 “c”、“ca” 和 “cat” 都是该字符串的前缀。因为这三个前缀都对应于从起始状态到接受状态 (q3: accept)
的不同路径。
同样,如果我们输入字符串 “chat”,则 “c”、“ch” 和 “cha” 是该字符串的前缀。此外,由于状态 (q4: accept)
也是一个接受状态,因此 “chat” 也是该 FSA 的一个完整的符号串,并且可以被接受。
在自动机理论和语言理论中,前缀是一个重要的概念,通常用于描述自动机能识别哪些字符串或语言。
在 Lucene 中,索引文件包含多个文件,其中两个文件的后缀名分别为.tip 和.tim,它们分别对应着词典(Term Dictionary)和文档编号(Document Number)的信息。下面分别介绍这两个文件的内部结构:
需要注意的是,.tip 和.tim 文件都是 Lucene 索引文件中的关键组成部分,它们的内部结构和具体的实现方式可能会随着 Lucene 版本的更新而变化。
FST(Finate State Transducer)是一种有限状态转换器,用于高效地存储和检索大量的字符串。在 Lucene 中,FST 被广泛应用于自动补全、拼写纠错、同义词替换等功能的实现。
下面简要介绍 FST 在 Lucene 中的读写过程:
在 Lucene 中,FST 的具体实现主要依赖于 org.apache.lucene.util.fst 包中的多个类。通过这些类的协作,FST 可以高效地存储和检索大量的字符串信息,从而实现各种文本相关的搜索和匹配功能。
// 构建FST
PositiveIntOutputs outputs = PositiveIntOutputs.getSingleton();
Builder<IntsRef> builder = new Builder<>(FST.INPUT_TYPE.BYTE1, outputs);
for (String term : terms) {
builder.add(Util.toUTF32(term, new IntsRefBuilder()), i++);
}
FST<IntsRef> fst = builder.finish();
// 将FST序列化成二进制格式,并写入到磁盘文件中
File file = new File("path/to/fst.bin");
OutputStream stream = Files.newOutputStream(file.toPath());
fst.save(stream);
stream.close();
// 从磁盘文件中读取存储的FST二进制数据,并反序列化成可操作的内存对象
File file = new File("path/to/fst.bin");
InputStream stream = Files.newInputStream(file.toPath());
FST<IntsRef> fst = new FST<>(stream, PositiveIntOutputs.getSingleton());
stream.close();
// 使用FST进行查找操作
BytesRefFSTEnum<IntsRef> fstEnum = new BytesRefFSTEnum<>(fst);
BytesRefFSTEnum.InputOutput<IntsRef> result;
while ((result = fstEnum.next()) != null) {
System.out.println(Util.toUtf8(result.input.utf8ToString()) + ": " + result.output.intValue());
}
Elasticsearch 的写入原理包括以下几个步骤:
在 Elasticsearch 中,写入数据的过程主要可以分为以下几个步骤:文档数据的分析、索引数据的生成、文档数据的批量提交、索引数据的持久化以及索引数据的刷新。其中,缓冲区的设置、批量提交的策略、内存和磁盘的使用等因素都会影响写入性能。
为了提高写入性能,可以考虑采取以下措施:
# 在elasticsearch.yml中添加以下配置项
indices.memory.index_buffer_size: 30%
// 使用bulk API进行批量提交
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.add(new IndexRequest("index", "doc", "1").source("field", "value"));
bulkRequest.add(new IndexRequest("index", "doc", "2").source("field", "value"));
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
除了针对写入性能进行优化之外,还可以通过以下措施来提高 Elasticsearch 的读写性能:
# 创建新index时,可以指定其分片数量和副本数量
PUT /my_index
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
}
}
// 使用bool查询进行复合查询
SearchRequest searchRequest = new SearchRequest("index");
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.must(QueryBuilders.termQuery("field1", "value1"));
boolQuery.should(QueryBuilders.termQuery("field2", "value2"));
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(boolQuery);
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
# 在elasticsearch.yml中添加以下配置项
indices.queries.cache.size: 10%
该配置项可以设置缓存的大小,单位是 JVM 堆内存的百分比,默认值是 0%,通过增加该值,可以提高缓存命中率,从而减少查询操作的响应时间。
总之,Elasticsearch 的读写性能调优需要考虑多方面的因素,包括硬件配置、软件参数、数据格式等问题。需要根据实际应用场景和使用情况进行调整,以获得最优的性能表现。
在执行 update 操作时,可以通过 doc 参数来更新文档中的某些字段:
UpdateRequest request = new UpdateRequest("my_index", "1");
String jsonString = "{\"field\": \"value\"}";
request.doc(jsonString, XContentType.JSON);
UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
如果对应的字段已经开启了 store 属性,则执行 update 操作时不会影响该字段的原始值。但如果该字段的 store 属性为 false,则执行 update 操作后,该字段的原始值将被清空。
在执行 update_by_query 操作时,可以使用 script 脚本来更新文档中的某些字段:
UpdateByQueryRequest request = new UpdateByQueryRequest("my_index");
Script script = new Script(ScriptType.INLINE, "painless", "ctx._source.field += params.count", Collections.singletonMap("count", 1));
request.setScript(script);
BulkByScrollResponse response = client.updateByQuery(request, RequestOptions.DEFAULT);
与 update 操作类似,在执行 update_by_query 操作时也需要注意与 store 属性相关的问题。如果要更新的字段的 store 属性为 true,则执行 update_by_query 操作时该字段的原始值不会被影响;如果该字段的 store 属性为 false,则执行 update_by_query 操作后,该字段的原始值将被清空。
在执行 reindex 操作时,可以使用 scripts 参数来对文档进行转换和过滤:
ReindexRequest request = new ReindexRequest();
request.setSourceIndices("my_index");
request.setDestIndex("new_index");
request.setScript(new Script(ScriptType.INLINE, "painless", "ctx._source.new_field = ctx._source.old_field", Collections.emptyMap()));
BulkByScrollResponse response = client.reindex(request, RequestOptions.DEFAULT);
在执行 reindex 操作时,如果要保留原始字段的值,则需要使用 store 属性来指定。例如,在新索引创建时可以使用以下方式来设置字段的 store 属性:
PUT /new_index
{
"mappings": {
"properties": {
"field_name": {"type": "text", "store": true}
}
}
}
在 mapping 操作中,可以使用 store 参数来指定字段的 store 属性:
PutMappingRequest request = new PutMappingRequest("my_index");
String mapping = "{\n" +
" \"properties\": {\n" +
" \"field_name\": {\n" +
" \"type\": \"keyword\",\n" +
" \"store\": true\n" +
" }\n" +
" }\n" +
"}";
request.source(mapping, XContentType.JSON);
AcknowledgedResponse response = client.indices().putMapping(request, RequestOptions.DEFAULT);
通过设置 store 参数为 true,可以将字段的原始值保存到磁盘上。
需要注意的是,开启 store 属性会增加磁盘空间的占用,因此需要根据实际需求来进行选择。在执行 update、update_by_query、reindex、mapping 等操作时,都需要注意与 store 属性相关的问题,以避免对数据造成意外影响。
在 Elasticsearch 中,设置 store 属性为 true 会将字段的原始值保存到磁盘上。当对这些字段进行搜索时,如果使用了高亮功能,则需要在查询中指定 stored_fields 参数,以便让 Elasticsearch 知道要从哪些字段中获取原始值。
以下是一个示例代码片段,展示了如何在查询中指定 stored_fields 参数:
SearchRequest searchRequest = new SearchRequest("my_index");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("field", "value"));
searchSourceBuilder.highlighter(new HighlightBuilder().field("field"));
String[] includeFields = new String[]{"field"};
searchSourceBuilder.storedFields(includeFields);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
在该代码片段中,通过调用 searchSourceBuilder.storedFields 方法来指定 stored_fields 参数,参数值包含要获取原始值的字段名数组。这样,在执行搜索操作时,Elasticsearch 会同时返回检索结果和指定字段的原始值,并且可以正确地应用高亮功能。
需要注意的是,在使用 stored_fields 参数时,需要确保查询中涉及到的所有字段都已经开启了 store 属性。否则,即使指定了 stored_fields 参数,也无法获取缺少 store 属性的字段的原始值。
在 Elasticsearch 中,有一些情况下会导致索引失效,进而影响 reindex 操作的执行。
当源索引中包含目标索引未定义的字段时,执行 reindex 操作可能会失败。在这种情况下,需要先使用 mapping API 创建目标索引,并在其中定义所有字段及其属性。然后,再执行 reindex 操作。
以下是一个示例代码片段,展示了如何在 mapping API 中定义索引映射:
CreateIndexRequest createIndexRequest = new CreateIndexRequest("my_index");
createIndexRequest.mapping(new MappingBuilder()
.startObject()
.startObject("properties")
.startObject("field1")
.field("type", "keyword")
.field("store", true)
.endObject()
.startObject("field2")
.field("type", "text")
.field("store", true)
.endObject()
.endObject()
.endObject());
client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
在该代码片段中,通过调用 MappingBuilder 的 startObject 方法来创建索引,并在其中定义相应的字段及其属性。其中,设置 store 属性为 true,以便将字段的原始值保存到磁盘上。在执行 reindex 操作时,Elasticsearch 会从源索引中获取数据,并将其复制到目标索引中,同时保留原始字段的值。
当禁止动态映射时,如果源索引中包含未定义的字段,或者类型与目标索引中定义的字段不匹配时,执行 reindex 操作可能会失败。
在这种情况下,需要先使用 mapping API 创建目标索引,并在其中定义所有字段及其属性。然后,再使用 reindex API 执行显示映射的操作,以确保源索引中的数据可以正确地映射到目标索引中。
以下是一个示例代码片段,展示了如何在 mapping API 中禁止动态映射:
CreateIndexRequest createIndexRequest = new CreateIndexRequest("my_index");
createIndexRequest.mapping(new MappingBuilder()
.startObject()
.field("dynamic", "false")
.startObject("properties")
.startObject("field1")
.field("type", "keyword")
.field("store", true)
.endObject()
.startObject("field2")
.field("type", "text")
.field("store", true)
.endObject()
.endObject()
.endObject());
client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
在该代码片段中,通过将 dynamic 属性设置为 false 来禁止动态映射。这样,在执行 reindex 操作时,Elasticsearch 会根据目标索引中定义的字段来映射源索引中的数据,以确保数据能够正确地复制。
需要注意的是,当禁止动态映射时,如果源索引中包含未定义的字段,则会被忽略。因此,在进行数据转移之前,需要确保源索引和目标索引中的字段定义是一致的。
在 Elasticsearch 中,设置 store 属性为 false 会使得该字段的原始值不被保存到磁盘上。当对这些字段进行元数据查看和聚合搜索时,由于缺少原始值,可能会导致结果不准确。
在执行元数据查看操作时(如_get、_source、_field_stats 等),如果使用了 store 属性为 false 的字段,则无法获取该字段的原始值。例如,在使用_source API 获取文档时,如果源索引中某个字段的 store 属性为 false,则返回的结果中将不包含该字段的原始值。
以下是一个示例代码片段,展示了如何使用_source API 获取文档:
GetRequest getRequest = new GetRequest("my_index", "1");
GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
Map<String, Object> sourceAsMap = getResponse.getSourceAsMap();
在该代码片段中,调用 getResponse.getSourceAsMap 方法可以获取文档的源数据。如果在创建索引时禁用了某个字段的 store 属性,则在获取文档时无法获取该字段的原始值。
在执行聚合搜索操作时,如果使用了 store 属性为 false 的字段,则无法对该字段进行聚合计算。例如,在执行 terms 聚合时,如果要对某个字段进行分组统计,就需要保证该字段的 store 属性为 true。
以下是一个示例代码片段,展示了如何使用 terms 聚合对某个字段进行分组统计:
SearchRequest searchRequest = new SearchRequest("my_index");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.aggregation(AggregationBuilders.terms("group_by_field").field("field"));
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
Aggregations aggregations = searchResponse.getAggregations();
在该代码片段中,调用 searchSourceBuilder.aggregation 方法可以指定聚合操作,其中使用了 field 参数来指定要统计的字段名称。如果在创建索引时禁用了某个字段的 store 属性,则无法对该字段进行聚合计算。
因此,在创建索引时需要认真考虑是否开启某个字段的 store 属性,以确保在元数据查看和聚合搜索等操作中能够正确地获取原始值。
Elasticsearch 的 store 属性用于控制是否将原始字段值存储到磁盘上。当 store 属性为 true 时,Elasticsearch 会将原始值保存到磁盘上以供检索和聚合搜索使用。这样做虽然能提高搜索请求的响应速度,但同时也影响了索引的容灾能力。
具体来说,以下是一些可能导致索引容灾能力受影响的场景:
开启 store 属性会增加索引的存储空间占用。如果索引中包含大量的字段,并且这些字段的 store 属性都被设置为 true,那么索引的存储空间需求将会非常大。这样,一旦出现硬件故障或者其他不可预见的情况导致数据丢失,恢复索引的时间和成本都会变得更高。
当开启 store 属性时,在进行数据同步操作时需要考虑如何保证数据的完整性和一致性。例如,在使用 reindex 操作将源索引中的数据复制到目标索引时,需要在两个索引中都开启 store 属性,以便复制原始值。如果只在一个索引中开启 store 属性,则可能会导致目标索引中缺少某些字段的原始值,从而影响搜索和聚合操作的准确性。
在开启 store 属性的情况下,Elasticsearch 需要将原始值存储到磁盘上。这样做会增加 I/O 操作的负担,并可能导致索引性能下降。如果索引的写入速度无法满足业务需求,则可能会出现数据积压和查询响应延迟等问题。
因此,在设置 Elasticsearch 的 store 属性时,需要根据实际需求来进行选择。为了提高索引的容灾能力,可以考虑禁止某些字段的 store 属性,以减少索引的存储空间占用。同时,在进行数据同步和索引性能优化时,也需要仔细考虑 store 属性的设置方式,以确保数据的完整性和一致性。
禁用_all 字段:_all 字段的包含所有字段分词后的 Term,作用是可以在搜索时不指定特定字段,从所有字段中检索,ES 6.0 之前需要手动关闭
关闭 Norms 字段:计算评分用的,如果你确定当前字段将来不需要计算评分,设置 false 可以节省大量的磁盘空间,有助于提升性能。常见的比如 filter 和 agg 字段,都可以设为关闭。
关闭 index_options(谨慎使用,高端操作):词设置用于在 index time 过程中哪些内容会被添加到倒排索引的文件中,例如 TF,docCount、postion、offsets 等,减少 option 的选项可以减少在创建索引时的 CPU 占用率,不过在实际场景中很难确定业务是否会用到这些信息,除非是在一开始就非常确定用不到,否则不建议删除
scroll search
和search after
所有分布式系统都需要解决数据的一致性问题,处理这类问题一般采取两种策略,避免数据不一致情况的发生,定义数据不一致后的处理策略,主从模式和无主模式,ES 为什么使用主从模式?
在 Elasticsearch 集群中,脑裂(split brain)指的是由于网络故障或其他不可预见的问题导致集群中的两个或多个节点无法通信,从而形成两个或多个独立的子集群。这种情况下,每个子集群都认为自己是“主”节点,并尝试继续服务客户端请求。这可能会导致数据的不一致性、丢失、冲突等问题。
为了避免发生脑裂,可以采取以下措施:
Zen Discovery 是 Elasticsearch 的一种自动化节点发现机制,它使用 ping/pong 协议来检测和发现新的节点,并在节点加入或离开集群时更新集群状态。通过配置 Zen Discovery,可以确保所有节点都知道彼此的存在,并及时响应节点变更事件。
以下是一个示例配置文件,展示了如何在 Elasticsearch 中启用 Zen Discovery:
discovery.zen.ping.unicast.hosts: ["node1", "node2", "node3"]
在该配置文件中,将 discovery.zen.ping.unicast.hosts 设置为要加入集群的所有节点的 IP 地址或主机名。这样,在启动每个节点时,它们可以使用 Zen Discovery 来发现彼此,并加入同一集群。
Minimum Master Nodes 是 Elasticsearch 的一种安全机制,它用于防止脑裂发生。通过设置 Minimum Master Nodes,只有当集群中的足够数量的节点参与选举时,才能选出新的主节点。这样做可以确保只有一个子集群被选为主节点,并避免数据不一致性等问题。
以下是一个示例配置文件,展示了如何在 Elasticsearch 中设置 Minimum Master Nodes:
discovery.zen.minimum_master_nodes: 2
在该配置文件中,将 discovery.zen.minimum_master_nodes 设置为一个整数值,该值表示集群中必须至少有多少个节点参与选举。假设集群中有 5 个节点,则可以将此值设置为 3,以确保只有一个子集群被选为主节点。
定期监控 Elasticsearch 集群的状态和性能,及时发现和解决故障和瓶颈问题,可以帮助降低发生脑裂的风险。例如,可以使用 Elasticsearch 的监控工具(如 X-Pack)来收集关键指标和日志信息,并进行告警和自动化操作。
同时,需要注意及时升级 Elasticsearch 版本,修复任何已知的漏洞和问题,以提高集群的稳定性和可靠性。
总之,避免脑裂需要多方面的措施,包括配置 Zen Discovery、设置 Minimum Master Nodes、监控和管理等。只有综合应用这些措施,才能提高 Elasticsearch 集群的容错能力和可靠性。
snapshot,
官方有个名叫客户端的库,叫做elastic
,这个库提供了与Elasticsearch
交互便捷且丰富的功能,包括索引、搜索、同时更新文档,也可以执行更复杂的操作,类似于聚合和地理位置查询等。
以下是一个用elastic
库与Elasticsearch
索引文件的的例子:
package main
import (
"context"
"fmt"
"github.com/olivere/elastic"
)
func main() {
// Connect to Elasticsearch
client, err := elastic.NewClient(elastic.SetURL("http://localhost:9200"))
if err != nil {
panic(err)
}
// Create a new document
doc := struct {
Title string `json:"title"`
Body string `json:"body"`
}{
Title: "My first Elasticsearch document",
Body: "Hello, Elasticsearch!",
}
// Index the document
_, err = client.Index().
Index("myindex").
Type("mytype").
BodyJson(doc).
Do(context.Background())
if err != nil {
panic(err)
}
fmt.Println("Document indexed!")
}
这个例子展示了如何用elastic
库创建一个Elasticsearch
客户端的例子,创建新文档,然后在Elasticsearch
中做索引。你也可以用这个库执行其它操作,比如查询和更新文档,也可以做更高级的操作,比如聚合操作、地理位置查询等,如果你用过 mongodb 的话,想必你对于聚合查询和地理位置查询并不陌生。
再举一个使用Elasticsearch
和Golang
的高级事例,是创建一个实时的数据管道,让它以近乎实时的方式摄取、处理和分析数据,包括以下一些步骤:
Elasticsearch
中:可以通过批量接口把数据添加到Elasticsearch
中,这允许在单独一个请求中索引和更新多个文档Elasticsearch
处理数据:当数据被索引到Elasticsearch
中以后,它可以使用Elasticsearch Query DSL
处理数据,这允许执行强大的搜索和聚合操作,比如过滤和通过相应字段进行分组,计算统计数量和度量等等Kibana
做分析:Kibana
是一个可以用来与Elasticsearch
结合,来创建交互面板和图表的可视化的工具。它允许数据以实时的方式可视和分析,提供对数据趋势、模式、和异常的观察下面是个 Go 程序,它是个把JSON
数据写入Elasticsearch
的例子,用Elasticsearch Query DSL
处理,然后再Kibana
中做可视化:
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/olivere/elastic"
)
func main() {
// Connect to Elasticsearch
client, err := elastic.NewClient(elastic.SetURL("http://localhost:9200"))
if err != nil {
panic(err)
}
// Ingest data into Elasticsearch
go func() {
for {
resp, err := http.Get("http://your-data-source")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
var docs []interface{}
json.Unmarshal(body, &docs)
bulk := client.Bulk()
for _, doc := range docs {
req := elastic.NewBulkIndexRequest().Index("myindex").Type("mytype").Doc(doc)
bulk.Add(req)
}
_, err = bulk.Do(context.Background())
if err != nil {
panic(err)
}
fmt.Println("Data ingested into Elasticsearch!")
time.Sleep(10 * time.Second)
}
}()
// Process data using Elasticsearch
go func() {
for {
// Perform search and aggregation
res, err := client.Search().
Index("myindex").
Query(elastic.NewMatchAllQuery()).
Aggregation("myagg", elastic.NewTermsAggregation().Field("field1")).
Do(context.Background())
if err != nil {
panic(err)
}
fmt.Println("Data processed using Elasticsearch!")
// Visualize data in Kibana
// ...
time.Sleep(60 * time.Second)
}
总结一下,Elasticsearch
和Golang
这种组合,是个创建强有力且高效搜索和分析系统的组合。这个弹性库提供了一种与Elasticsearch
交互便利且有效的 API,使它更容易用Golang
创建强有力的搜索引擎。