全文检索的极致之选:Elasticsearch完全指南
1、倒排索引相关
1.) 倒排索引的原理以及它是用来解决哪些问题
倒序索引也被称为“反向索引”或“反向文件”,是一种索引数据结构。倒序索引在“内容”和存放内容的“位置”之间的映射,其目的在于快速全文索引和使用最小处理代价将新文件添加进数据库。通过倒序索引,可以快速根据“内容”查到包含它的文件。这种数据结构被广泛使用在搜索引擎中,倒排索引有两种不同的索引形式:
- 一种是给定一个词语,查找出所有包含这个词语的文档
- 另外一种是给定一个词语,不仅查找出所包含词语的文档,还能查找出这个词语在这篇文章中的位置
为什么会从以这个为出发点来优化索引的问题呢?以 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 数组。
2.) 倒序索引的更新策略是什么?
更新策略主要有以下 4 种:完全重建策略、再合并策略、原地更新策略、混合策略
-
完全重建策略:新文档并不会立即解析加入到索引中,而是先进行“文档暂存”,待文档暂存区中的文档达到一定数量后,将这些新旧文档混在一起,对所有文档进行重新索引,替换旧索引。这种方式代价是极高的,但是现在主流的商业搜索引擎基本都会采用这种方式来更新索引
-
再合并策略:新文件会立即解析,但解析结果并不会立刻加入到旧索引中,而是进行“索引暂存”。索引暂存其实也是一个建立索引的过程。待索引暂存区达到一定数量后,暂存区中的索引和旧索引进行合并
-
原地更新策略:新文档被立即解析,解析结果立刻被加到旧索引中。这种方法有较好的时效性,在理论上是种比较优秀的策略。为了加快索引速度,索引内部一般都有一个“调优”机制,例如,移动某些文件在磁盘上的位置,使索引过程中磁头移动距离尽可能小,磁盘等待时间尽量少。如果新文档立刻进入旧索引,那么,索引内部就会不停地执行“调优”过程,有时反而会使性能降低
-
混合策略:其思想是混合使用以上几种策略,以期用这种方式,达到最好的性能
-
3.)倒排索引底层数据结构
逆向思考一下,既然我们这里要讲倒序索引,这里就追问一下,你知道正排索引是什么吗? “正排索引”又称为“前向索引”。它是创建倒序索引的基础,通过文档到关键词(doc->word)的映射,具有以下字段:
正排索引是一个文本搜索引擎中的关键组件之一,用于存储文档的详细信息和内容。正排索引通常包含以下字段:
-
LocalId(局部文档 ID):每个文档都有一个唯一的标识符,称为全局文档 ID。而 LocalId 则是在每个分片或者节点内的文档编号。
-
WordId(单词 ID):文本检索时要根据查询词来匹配文档中的单词,WordId 就是将单词映射为数字 ID,以便进行快速匹配。
-
NHits(命中次数):NHits 表示查询词在文档中出现的次数。
-
Hitlist(命中列表):HitList 记录了查询词在文档中出现的具体位置,以便实现高亮显示等功能。
以这四个字段为例,可以解释如何使用它们来构建正排索引。假设有一个文档集合,其中包含多篇文档,机器对这些文档进行分析,提取出其中的单词,并将每个单词分配一个唯一的数字 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。
需要注意的是,文档矩阵可能非常庞大,因此一般会使用稀疏矩阵来存储,以节省存储空间和计算资源。稀疏矩阵只存储非零元素,将零值的单元格从矩阵中删除。
倒排索引是搜索引擎中的一个重要组成部分,用于快速查找文档中包含指定单词的位置。倒排索引的数据结构通常包括以下三个主要部分:
-
单词词项表(Term Dictionary):单词词项表存储了所有文档中出现过的单词以及它们在倒排索引数组中的位置信息。每个单词都有一个对应的指针,指向该单词在倒排索引数组中的起始位置。
-
倒排列表(Posting List):每个单词在倒排索引中都有一个对应的倒排列表,用于记录包含该单词的所有文档编号和位置信息。倒排列表可以是按照文档编号排序的数组,也可以是使用链表等其他数据结构来实现。
-
位置信息(Position Information):位置信息记录了单词在文档中的具体位置。对于某些应用场景,例如短语匹配、高亮显示等,需要知道单词在文档中的精确位置信息,因此需要将位置信息存储在倒排列表中。
倒排索引的建立过程包括两个主要步骤:分析和索引。分析阶段主要是将文本进行分词处理,得到单词序列;索引阶段则是将文档中出现的单词按照上述数据结构组织起来,并构建倒排索引。
需要注意的是,倒排索引可以占用大量的存储空间,因此在实际应用中可能需要使用压缩技术来减小索引的大小。常见的压缩方法包括前缀编码、变长编码、霍夫曼编码等。
-
4.)倒排表的压缩算法(底层算法)
-
- FOR(Frame Of Reference)
FOR 算法的核心思想是用减法来削减数值大小,从而达到降低空间存储。假设 V(n)表示数组中第 n 个字段的值,那么经过 FOR 算法压缩的数值 V(n)=V(n)-V(n-1)。也就是说存储的最后一位减去前一位的差值。存储是也不再按照 int 来计算了,而是看这个数组的最大值需要占用多少 bit 来计算。
Frame Of Reference(FOR)算法是一种用于数据压缩和存储的算法,它可以大幅度减少数据存储的空间占用,并在不降低数据质量的情况下提高查询效率。该算法适用于各种类型的数据,例如数值型、文本型、时间型等。
以下是 FOR 算法的主要概念:
- a. FOR 块
FOR 算法将数据分成若干个大小相等的块(Block),称之为 FOR 块。每个 FOR 块包含一个参考点(Reference Point),它是该块中所有元素与参考点之间差值的最大值。这样做可以将数据压缩到很小的范围内,从而节约存储空间。
- b. 块编码
在进行块编码时,FOR 算法会对每个 FOR 块中的所有元素进行差分编码(Delta Encoding)。具体来说,它会将当前元素与参考点之间的差值编码为一个整数,然后将该整数存储到磁盘上。这样做不仅可以减少数据存储的空间占用,还可以加速查询操作。
- c. 变化数组
变化数组(Variation Array)是 FOR 算法中的关键数据结构,它记录了每个 FOR 块中的参考点和元素个数。具体来说,变化数组包括两个部分:参考点数组和偏移量数组。参考点数组记录了每个 FOR 块的参考点值,而偏移量数组记录了每个 FOR 块中第一个元素的位置。
- d. FOR 压缩
在进行 FOR 压缩时,FOR 算法会将原始数据划分为若干个 FOR 块,并对每个 FOR 块进行差分编码。接下来,它会使用变化数组来记录每个 FOR 块的参考点和偏移量信息,并将编码后的数据存储到磁盘上。这样做可以大幅度减少数据存储的空间占用,并在查询操作中快速定位所需的数据。
以上就是 FOR 算法的概念,总结一下:
-
(1)数组元素值为与前一位的差值 V(n)=V(n)-V(n-1),n=2,3,4…
-
(2)计算数组中最大值所需占用的大小
-
(3)计算数组是否需要拆分,计算拆分后每组的最大值所需占用的大小并记录
-
- RBM(RoaringBitmap) FOR 算法当然也有缺陷,FOR 算法的核心是用减法来缩减数值大小,但是减法一定能缩减大小吗?当数值大小很大时,减法能够达到的效果是不明显的,比如数值本身很大,即使缩减值是个大数,但是相减后值仍然很大,所以就有了 RBM 数值。RBM 的核心就是通过除法来缩减数值大小,但也并非直接的粗暴相除,而是位运算同时低位补 0。
RBM 算法的核心步骤如下:
-
(1)数组中每个数除以 2^16,以商,余数的形式表示出来
-
(2)将相同商的归在一个 Container,如果 Contaniner 中数值容量超过 4096 使用 bitmap 的形式来存储一个 Container 中的数,如果没有超过那就使用 short[]来存储,如果是连续数组那就使用 RunContainer 来存储
-
5.)Trie 字典树(Prefix Trees)原理
Trie 字典树,也被称为前缀树(Prefix Tree),是一种用于字符串匹配的数据结构。与其他基于比较的数据结构不同,Trie 使用键本身来构建树形结构,从而实现高效的字符串查找和插入操作。
Trie 树的核心思想是将相同前缀的字符串合并到一起,形成一个公共节点,从而减少存储空间和提高查询效率。每个节点包含一个字符和指向子节点的指针,根据字符串中每个字符的顺序确定树的层级结构。最终,在 Trie 树中,每个单词都对应着一条从根节点到叶子节点的路径,同时每个节点都代表了一段前缀。
Trie 树具有以下一些重要特点:
- Trie 树可以支持高效的查找和插入操作,时间复杂度为 O(m),其中 m 为字符串的长度;
- Trie 树可以存储大量的字符串,并且空间利用率较高;
- Trie 树可以通过前缀搜索,快速匹配所有以给定前缀开头的字符串。
总之,Trie 树是一种非常实用的数据结构,主要用于处理字符串相关问题,例如单词查找、模式匹配、拼写纠错等。除了 Trie 树之外,B-Trees、B+Trees、红黑树等数据结构也经常用于处理各种类型的字符串和键值对。
-
6.)FST 原理
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(有限状态机)的一种,具有以下特征:
-
- 有穷状态集合:FSA 基于一组有限状态集合,它们描述了系统可能的状态。
-
- 转移函数:FSA 通过转移函数定义状态之间的迁移,该函数描述从一个状态到另一个状态的转换。
-
- 输入字母表:在 FSA 中,输入是基于字母表的,该字母表可以是任何类型的,例如整数、字符或二进制值。
-
- 初始状态:FSA 具有一个初始状态,即开始状态。
-
- 接受状态:FSA 也具有一组接受状态,当 FSA 处于其中一个接受状态时,它表示输入被接受。
-
- 确定性:FSA 是确定性的,因为对于给定的输入和当前状态,只有一个转移可用。
-
- 非确定性:除了确定性 FSA 之外,还存在非确定性 FSA,其中多个转移可能与给定的输入和状态相关联。
(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 的一个完整的符号串,并且可以被接受。
在自动机理论和语言理论中,前缀是一个重要的概念,通常用于描述自动机能识别哪些字符串或语言。
-
7.)索引文件的内部结构(.tip 和.tim 文件内部数据结构)
在 Lucene 中,索引文件包含多个文件,其中两个文件的后缀名分别为.tip 和.tim,它们分别对应着词典(Term Dictionary)和文档编号(Document Number)的信息。下面分别介绍这两个文件的内部结构:
-
.tip 文件:该文件是 Lucene 索引文件中的一个关键组成部分,用于存储所有单词及其在倒排索引中的位置信息。具体而言,.tip 文件由两部分组成:
(1) Term Dictionary:以二进制格式存储了所有单词及其在倒排索引数组中的位置信息,每一项占用固定长度的字节数,通常为 8 个字节。每个单词都有一个指针,指向该单词在倒排索引数组中的起始位置。
(2) Term Index:以二进制格式存储了所有单词及其在词典中的位置信息,每一项也占用固定长度的字节数,通常为 8 个字节。Term Index 中记录着若干个单词的首字母位置,根据首字母在 Term Index 中的位置可以快速定位到相应的单词在 Term Dictionary 中的位置。
-
.tim 文件:该文件存储了所有的文档编号信息,以及每个文档编号对应的偏移量和长度信息。具体而言,.tim 文件由三部分组成:
(1) Document Number:以二进制格式存储了所有文档的编号信息,每一项占用固定长度的字节数,通常为 4 个字节。在 Lucene 中,文档编号是按照插入顺序依次生成的。
(2) Index Offset:以二进制格式存储了每个文档编号在.fdx 文件中的偏移量信息,每一项占用固定长度的字节数,通常为 8 个字节。
(3) Index Length:以二进制格式存储了每个文档编号在.fdx 文件中的长度信息,每一项占用固定长度的字节数,通常为 8 个字节。Index Offset 和 Index Length 两部分信息共同描述了每个文档编号所对应的文档在.fdx 文件中的范围。
需要注意的是,.tip 和.tim 文件都是 Lucene 索引文件中的关键组成部分,它们的内部结构和具体的实现方式可能会随着 Lucene 版本的更新而变化。
-
8.)FST 在 Lucene 的读写过程
FST(Finate State Transducer)是一种有限状态转换器,用于高效地存储和检索大量的字符串。在 Lucene 中,FST 被广泛应用于自动补全、拼写纠错、同义词替换等功能的实现。
下面简要介绍 FST 在 Lucene 中的读写过程:
-
写入过程:
(1) 构建 FST:首先,需要将所有需要存储的字符串构建成一个 FST。在构建过程中,可以通过预设的比较器对字符串进行排序,从而提高查询效率。
(2) 序列化:将构建好的 FST 序列化成二进制格式,并写入到磁盘文件中。在序列化过程中,会根据节点类型和输出值等信息来压缩每个节点的数据,从而减小存储空间。
-
读取过程:
(1) 反序列化:首先需要从磁盘文件中读取存储的 FST 二进制数据,并反序列化成可操作的内存对象。反序列化过程中,会根据压缩方式和节点类型等信息还原每个节点的数据。
(2) 查找:在读取完成后,就可以使用 FST 进行查找操作了。Lucene 中提供了多个 FST 查询类,例如 BytesRefFSTEnum、IntSequenceOutputs 以及 PairOutputs 等,可以根据具体的查询需求选择合适的查询类进行操作。
在 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()); }
2、Elasticsearch 的写入原理
Elasticsearch 的写入原理包括以下几个步骤:
-
文档数据的分析:在写入文档之前,Elasticsearch 首先需要对文档进行分析,将其转换成倒排索引所需的格式。具体而言,它会将文本数据进行分词、过滤、归一化等处理,得到一系列的词项(term)和其出现的位置信息。
-
索引数据的生成:在对文档进行分析后,Elasticsearch 会根据文档 ID、分析结果等信息生成相应的索引数据,并将其存储在内存中的缓冲区中。此时,索引数据还没有被持久化到磁盘上,只存在于内存中。
-
文档数据的批量提交:为了提高写入效率和减少磁盘 I/O 的次数,Elasticsearch 采用了批量提交的方式将多个文档的索引数据一起写入到磁盘上。一般来说,当缓冲区达到一定大小或者一定时间间隔之后,就会触发一次批量提交操作。
-
索引数据的持久化:在批量提交的过程中,Elasticsearch 会将缓冲区中的索引数据写入到磁盘上,同时更新与之相关的元数据信息。其中,索引数据会被写入到一个或多个分片(shard)中,每个分片对应着磁盘上的一个目录。
-
索引数据的刷新:为了确保新写入的索引数据能够立即对外可见,Elasticsearch 会触发一次索引数据的刷新(refresh)操作。在这个过程中,它会将写入的索引数据合并到主存储(MMapDirectory)中,并更新相关的文件指针和元数据信息。此时,新写入的文档才可以被搜索到。
3、读写性能调优
- Elasticsearch 的写入原理:
在 Elasticsearch 中,写入数据的过程主要可以分为以下几个步骤:文档数据的分析、索引数据的生成、文档数据的批量提交、索引数据的持久化以及索引数据的刷新。其中,缓冲区的设置、批量提交的策略、内存和磁盘的使用等因素都会影响写入性能。
为了提高写入性能,可以考虑采取以下措施:
- 提升硬件配置:Elasticsearch 的写入性能受到硬件配置的限制,可以通过增加 CPU 核数、内存容量、磁盘 IOPS 等方式来提升写入性能;
- 调整缓冲区大小:Elasticsearch 使用缓冲区来暂存待写入的索引数据,可以适当调整缓冲区的大小来平衡内存和磁盘的使用效率,从而提高写入性能;
# 在elasticsearch.yml中添加以下配置项
indices.memory.index_buffer_size: 30%
- 优化批量提交策略:Elasticsearch 的批量提交操作是提高写入性能的关键,可以适当调整批量提交的时间间隔、批量大小等参数,以寻求最优的性能表现;
// 使用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);
- 优化插入数据的格式:在进行写入操作时,可以优化插入数据的格式,例如尽可能合并多个请求、使用 bulk API 等方式,以减少网络传输和请求的次数,从而提高写入性能。
- Elasticsearch 的读写性能调优:
除了针对写入性能进行优化之外,还可以通过以下措施来提高 Elasticsearch 的读写性能:
- 使用 SSD 磁盘:因为 Elasticsearch 的搜索和索引操作都需要频繁地读取和写入磁盘数据,因此使用 SSD 等快速磁盘可以显著提升读写性能;
- 调整分片数量和副本数量:Elasticsearch 的文档数据被分散存储在多个分片中,可以适当调整分片数量和副本数量,以平衡性能和可用性的要求;
# 创建新index时,可以指定其分片数量和副本数量
PUT /my_index
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
}
}
- 优化查询方式:Elasticsearch 提供了多种查询方式,可以根据实际需求选择合适的查询方式,例如 bool 查询、match 查询、term 查询等;
// 使用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 内部缓存了一些常用的搜索结果、聚合结果等信息,可以适当调整缓存策略,以提高读写性能;
# 在elasticsearch.yml中添加以下配置项
indices.queries.cache.size: 10%
该配置项可以设置缓存的大小,单位是 JVM 堆内存的百分比,默认值是 0%,通过增加该值,可以提高缓存命中率,从而减少查询操作的响应时间。
- 系统参数调优:可以适当调整 JVM 参数、文件句柄数等系统参数,以提高 Elasticsearch 的读写性能。
总之,Elasticsearch 的读写性能调优需要考虑多方面的因素,包括硬件配置、软件参数、数据格式等问题。需要根据实际应用场景和使用情况进行调整,以获得最优的性能表现。
- 写入性能调优
-
增加 flush 时间间隔,目的是减小数据写入磁盘的频率,减小磁盘 IO
-
增加 refresh_interval 的参数值,目的是减少 segment 文件的创建,减少 segment 的 merge 次数,merge 是发生在 jvm 中的,有可能导致 full GC,增加 refresh 会降低搜索的实时性。
-
增加 Buffer 大小,本质也是减小 refresh 的时间间隔,因为导致 segment 文件创建的原因不仅有时间阈值,还有 buffer 空间大小,写满了也会创建。 默认最小值 48MB< 默认值 堆空间的 10% < 默认最大无限制
-
大批量的数据写入尽量控制在低检索请求的时间段,大批量的写入请求越集中越好。
- 第一是减小读写之间的资源抢占,读写分离
- 第二,当检索请求数量很少的时候,可以减少甚至完全删除副本分片,关闭 segment 的自动创建以达到高效利用内存的目的,因为副本的存在会导致主从之间频繁的进行数据同步,大大增加服务器的资源占用。
-
Lucene 的数据的 fsync 是发生在 OS cache 的,要给 OS cache 预留足够的内从大小。
-
通用最小化算法,能用更小的字段类型就用更小的,keyword 类型比 int 更快,
-
ignore_above:字段保留的长度,越小越好
-
调整_source 字段,通过 include 和 exclude 过滤
-
store:开辟另一块存储空间,可以节省带宽
在 Elasticsearch 中,store 属性是字段的一个设置,用于控制是否将该字段的原始值保存到磁盘上。该属性可以在创建索引时指定,也可以在 mapping API 中进行修改。涉及到 update、update_by_query、reindex、mapping 等操作时,也需要注意与 store 属性相关的问题。
1. update、update_by_query、reindex、mapping 等操作
-
- update 操作
在执行 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 操作
在执行 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 操作
在执行 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 操作
在 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 属性相关的问题,以避免对数据造成意外影响。
2. 高亮失效
在 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 属性的字段的原始值。
3. reindex 失效,原本可以修改的 mapping 部分参数将无法修改,并且无法升级索引
在 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 会根据目标索引中定义的字段来映射源索引中的数据,以确保数据能够正确地复制。
需要注意的是,当禁止动态映射时,如果源索引中包含未定义的字段,则会被忽略。因此,在进行数据转移之前,需要确保源索引和目标索引中的字段定义是一致的。
4. 无法查看元数据和聚合搜索
在 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 占用率,不过在实际场景中很难确定业务是否会用到这些信息,除非是在一开始就非常确定用不到,否则不建议删除
3、搜索速度调优
-
禁用 swap
-
使用 filter 代替 query
-
避免深度分页,避免单页数据过大,常用的解决方案有这两种解决方案:
scroll search
和search after
-
注意关于 index type 的使用
-
避免使用稀疏数据
-
避免单索引业务重耦合
-
命名规范
-
冷热分离的架构设计
-
fielddata:搜索时正排索引,doc_value 为 index time 正排索引。
-
enabled:是否创建倒排索引
-
doc_values:正排索引,对于不需要聚合的字段,关闭正排索引可节省资源,提高查询速度
-
开启自适应副本选择(ARS),6.1 版本支持,7.0 默认开启,
4、ES 的节点类型
- master:候选节点
- data:数据节点
- data_content:数据内容节点
- data_hot:热节点
- data_warm:索引不再定期更新,但仍可查询
- data_code:冷节点,只读索引
- Ingest:预处理节点,作用类似于 Logstash 中的 Filter
- ml:机器学习节点
- remote_cluster_client:候选客户端节点
- transform:转换节点
- voting_only:仅投票节点
5、Mater 选举过程设计思路?
所有分布式系统都需要解决数据的一致性问题,处理这类问题一般采取两种策略,避免数据不一致情况的发生,定义数据不一致后的处理策略,主从模式和无主模式,ES 为什么使用主从模式?
- 在相对稳定的对等网络中节,点的数量远小于单个节点可以维护的节点数,并且网络环境不必经常处理节点的加入和离开。
ES 的选举算法
- Bully 和 Paxos
脑裂是什么以及如何避免
在 Elasticsearch 集群中,脑裂(split brain)指的是由于网络故障或其他不可预见的问题导致集群中的两个或多个节点无法通信,从而形成两个或多个独立的子集群。这种情况下,每个子集群都认为自己是“主”节点,并尝试继续服务客户端请求。这可能会导致数据的不一致性、丢失、冲突等问题。
为了避免发生脑裂,可以采取以下措施:
- 配置 Zen Discovery
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
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 集群的容错能力和可靠性。
6、Elasticsearch 调优
-
通用法则
- 通用最小化算法:对于搜索引擎级的大数据检索,每个 bit 尤为珍贵。
- 业务分离:聚合和搜索分离
-
硬件优化
es 的默认配置是一个非常合理的默认配置,绝大多数情况下是不需要修改的,如果不理解某项配置的含义,没有经过验证就贸然修改默认配置,可能造成严重的后果。比如 max_result_window 这个设置,默认值是 1W,这个设置是分页数据每页最大返回的数据量,冒然修改为较大值会导致 OOM。ES 没有银弹,不可能通过修改某个配置从而大幅提升 ES 的性能,通常出厂配置里大部分设置已经是最优配置,只有少数和具体的业务相关的设置,事先无法给出最好的默认配置,这些可能是需要我们手动去设置的。关于配置文件,如果你做不到彻底明白配置的含义,不要随意修改。
jvm heap 分配:7.6 版本默认 1GB,这个值太小,很容易导致 OOM。Jvm heap 大小不要超过物理内存的 50%,最大也不要超过 32GB(compressed oop),它可用于其内部缓存的内存就越多,但可供操作系统用于文件系统缓存的内存就越少,heap 过大会导致 GC 时间过长
-
节点:
根据业务量不同,内存的需求也不同,一般生产建议不要少于 16G。ES 是比较依赖内存的,并且对内存的消耗也很大,内存对 ES 的重要性甚至是高于 CPU 的,所以即使是数据量不大的业务,为了保证服务的稳定性,在满足业务需求的前提下,我们仍需考虑留有不少于 20%的冗余性能。一般来说,按照百万级、千万级、亿级数据的索引,我们为每个节点分配的内存为 16G/32G/64G 就足够了,太大的内存,性价比就不是那么高了。
-
内存:
根据业务量不同,内存的需求也不同,一般生产建议不要少于 16G。ES 是比较依赖内存的,并且对内存的消耗也很大,内存对 ES 的重要性甚至是高于 CPU 的,所以即使是数据量不大的业务,为了保证服务的稳定性,在满足业务需求的前提下,我们仍需考虑留有不少于 20%的冗余性能。一般来说,按照百万级、千万级、亿级数据的索引,我们为每个节点分配的内存为 16G/32G/64G 就足够了,太大的内存,性价比就不是那么高了。
-
磁盘:
对于 ES 来说,磁盘可能是最重要的了,因为数据都是存储在磁盘上的,当然这里说的磁盘指的是磁盘的性能。磁盘性能往往是硬件性能的瓶颈,木桶效应中的最短板。ES 应用可能要面临不间断的大量的数据读取和写入。生产环境可以考虑把节点冷热分离,“热节点”使用 SSD 做存储,可以大幅提高系统性能;冷数据存储在机械硬盘中,降低成本。另外,关于磁盘阵列,可以使用 raid 0。
-
CPU:
CPU 对计算机而言可谓是最重要的硬件,但对于 ES 来说,可能不是他最依赖的配置,因为提升 CPU 配置可能不会像提升磁盘或者内存配置带来的性能收益更直接、显著。当然也不是说 CPU 的性能就不重要,只不过是说,在硬件成本预算一定的前提下,应该把更多的预算花在磁盘以及内存上面。通常来说单节点 cpu 4 核起步,不同角色的节点对 CPU 的要求也不同。服务器的 CPU 不需要太高的单核性能,更多的核心数和线程数意味着更高的并发处理能力。现在 PC 的配置 8 核都已经普及了,更不用说服务器了。
-
网络:
ES 是天生自带分布式属性的,并且 ES 的分布式系统是基于对等网络的,节点与节点之间的通信十分的频繁,延迟对于 ES 的用户体验是致命的,所以对于 ES 来说,低延迟的网络是非常有必要的。因此,使用扩地域的多个数据中心的方案是非常不可取的,ES 可以容忍集群夸多个机房,可以有多个内网环境,支持跨 AZ 部署,但是不能接受多个机房跨地域构建集群,一旦发生了网络故障,集群可能直接 GG,即使能够保证服务正常运行,维护这样(跨地域单个集群)的集群带来的额外成本可能远小于它带来的额外收益。
-
集群规划:没有最好的配置,只有最合适的配置。
-
在集群搭建之前,首先你要搞清楚,你 ES cluster 的使用目的是什么?主要应用于哪些场景,比如是用来存储事务日志,或者是站内搜索,或者是用于数据的聚合分析。针对不同的应用场景,应该指定不同的优化方案。
-
集群需要多少种配置(内存型/IO 型/运算型),每种配置需要多少数量,通常需要和产品运营和运维测试商定,是业务量和服务器的承载能力而定,并留有一定的余量。
-
一个合理的 ES 集群配置应不少于 5 台服务器,避免脑裂时无法选举出新的 Master 节点的情况,另外可能还需要一些其他的单独的节点,比如 ELK 系统中的 Kibana、Logstash 等。
-
-
架构优化
-
合理的分配角色和每个节点的配置,在部署集群的时候,应该根据多方面的情况去评估集群需要多大规模去支撑业务。这个是需要根据在你当前的硬件环境下测试数据的写入和搜索性能,然后根据你目前的业务参数来动态评估的,比如
- 业务数据的总量、每天的增量
- 查询的并发以及 QPS
- 峰值的请求量
-
节点并非越多越好,会增加主节点的压力
-
分片并非越多越好,从 deep pageing 的角度来说,分片越多,JVM 开销越大,负载均衡(协调)节点的转发压力也越大,查询速度也越慢。单个分片也并非越大越好,一般来说单个分片大小控制在 30-50GB
-
Mpping 优化
-
优化字段的类型,关闭对业务无用的字段
-
尽量不要使用 dynamic mapping 分片大小
-
Developer 调优:修炼内功,提升修养
7、索引备份还原
snapshot,
8、数据同步方案
-
数据一致性问题
-
基于 Canal+binlog 同步 MySql
-
基于 packetbeat 监听 9200 端口
9、搜索引擎和 ES
-
概念:大数据检索(区分搜索)、大数据分析、大数据存储
-
性能:PB 级数据秒查(NRT Near Real Time)
- 高效的压缩算法
- 快速的编码和解码算法
- 合理的数据结构
- 通用最小化算法
-
场景:搜索引擎、垂直搜索、BI、GIthub、ELKB
10、ES 容灾问题
11、分片是啥
10、深度分页问题
11、深度优先和广度优先算法
12、向量空间模型
13、 如何在 golang 项目中使用 ElasticSearch
官方有个名叫客户端的库,叫做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
创建强有力的搜索引擎。