1. 借鉴
如果要学习es,推荐一篇博客:铭毅天下
如果要学习es,推荐一部教学:极客时间 阮一鸣老师的Elasticsearch核心技术与实战
有关倒排索引,也可以参看以下文章:
什么是倒排索引?
官网:倒排索引
官网:索引员工文档
Elasticsearch中的相似度模型(原文:Similarity in Elasticsearch)
TF-IDF及其算法
Elasticsearch中的倒排索引
Elasticsearch中的倒排索引
ES:倒排索引、分词详解
Lucene倒排索引原理探秘(1)
Elasticsearch 6.x 倒排索引与分词
聊聊 Elasticsearch 的倒排索引
时间序列数据库的秘密 (2)——索引
七大查找算法
字典树
数据结构——从英文字典树到中文字典树
2. 开始
写一些自己的心得和体会。如果有错误或者不同见解,还望看官不吝指教,多谢。
2.1索引
理解索引需要一步步来,把大象放冰箱需要三步。
2.1.1文档
文档就是保存在es中的一条记录,可以想象是数据库表里面的一条记录(打个比喻:一个文档就是一只小狗)。在es中就是一个json对象,而每一个对象都有一个唯一的ID
{
"id": 1,
"name": "srk",
"age": 18,
"sex": "male"
}
2.2.2索引
了解文档了,则索引可以想象为文档的集合(打个比喻:一个索引就是一群小狗)。注:这里的索引是名词
索引一个文档:就是将文档加入到es中,对文档建立倒排索引。注:这里的索引是动词
在说倒排索引之前,先说一下正排索引(或者叫正向索引,积极的索引之类的)->_->
正排索引
【表格1】
野兽id | name | age | sex | description |
---|---|---|---|---|
1 | 狮子 | 1 | 公 | 叫声很好听,毛发旺盛 |
2 | 老虎 | 1 | 公 | 能游泳的大型猫科动物 |
3 | 豹子 | 1 | 公 | 奔跑时速可达80公里 |
以上是三只1岁的男性野兽,如果
我要找description包含“奔跑时速”的野兽
,我是不是要从第一个开始,首先找到这行,然后看看description是不是包含"奔跑时速",如果不是则继续往后找
,好吧如果你要说能一下从3个中一眼看出来,我也表示认同,但是如果是100行呢,很显然需要一次次的查询。
这种给定一个关键字a(“奔跑时速”),每行记录通过指定的字段(“description”),依次查找字段中的文本是否包含关键字a的过程就是正排索引(非官方解释,理解为主)
倒排索引
了解了正排索引,现在来看看倒排索引。如果说正排索引是通过文档来找关键词,从而判断文档是否匹配(如果包含关键字则判定匹配了),那倒排索引就是通过关键字即可直接判断文档是否匹配了。
【表格2】
野兽id | name | age | sex | description |
---|---|---|---|---|
1 | 狮子 | 1 | 公 | 叫声很好听,毛发旺盛 |
2 | 老虎 | 1 | 公 | 能游泳的大型猫科动物 |
3 | 豹子 | 1 | 公 | 奔跑时速可达80公里 |
4 | 驴 | 1 | 公 | 奔跑时速可达1公里唉 |
为了理解倒排索引,我们加了一种野兽,首先我们对description提取关键字。那啥叫关键字,就是你觉得是关键的字。。。每个人都不相同,从我的角度我可以这么划分,因为我们只分析description,其他的无关属性(sex之类的)我就不关心了。。。
【表格3】
野兽id | description |
---|---|
1 | [叫声] [很好听] [毛发旺盛] |
2 | [能游泳] [的] [大型] [猫科动物] |
3 | [奔跑] [时速] [奔跑时速] [可达] [80] [公里] |
4 | [奔跑] [时速] [奔跑时速] [可达] [1] [公里] [唉] |
好了,上面就是我的关键字了(每一个[]是一个关键字),你可能会问,【奔跑时速】为啥能拆出来三个(奔跑,时速,奔跑时速),因为他们都能单独成词啊,当然你也可以不厌其烦的将每一个都拆出来或者将整个一句话当做关键字(如:奔跑时速可达1公里这句话,你爱咋分咋分,比如:[奔] [跑] [奔跑] [时] [速] [时速] [奔跑时速] [可] [达] [可达] [1] [公] [里] [公里] [唉])等等等。
接下来,我们就以我的关键字为例子来说明一个简单的倒排索引
【表格4】
关键词id | 关键词 | 野兽id |
---|---|---|
1 | 叫声 | 1 |
2 | 很好听 | 1 |
3 | 毛发旺盛 | 1 |
4 | 能游泳 | 2 |
5 | 的 | 2 |
6 | 大型 | 2 |
7 | 猫科动物 | 2 |
8 | 奔跑 | 3,4 |
9 | 时速 | 3,4 |
10 | 奔跑时速 | 3,4 |
11 | 可达 | 3,4 |
12 | 80 | 3 |
13 | 公里 | 3,4 |
14 | 1 | 4 |
15 | 唉 | 4 |
如上倒排索引就可以直接通过关键词"奔跑时速"直接找到野兽id为3,4这两种野兽了
这种通过关键词("奔跑时速")直接查找文档(3,4)的索引方式就是倒排索引
当然es里面的倒排索引没有如此简单,为啥呢?你想啊,有两个文档都包含“奔跑时速”,那这两个那个排在前面尼,总得有个先后吧?
这就涉及到TF-IDF,BM25等算法,以及es里面倒排索引的结构了。
先来看一下TF-IDF算法的概念:[以下语录摘自TF-IDF算法原理及其使用详解]
TF-IDF(Term Frequency-inverse Document Frequency)是一种针对关键词的统计分析方法,用于评估一个词对一个文件集或者一个语料库的重要程度。一个词的重要程度跟它在文章中出现的次数成正比,跟它在语料库出现的次数成反比。这种计算方式能有效避免常用词对关键词的影响,提高了关键词与文章之间的相关性。
其中TF指的是某词在文章中出现的总次数,该指标通常会被归一化定义为TF=(某词在文档中出现的次数/文档的总词量),这样可以防止结果偏向过长的文档(同一个词语在长文档里通常会具有比短文档更高的词频)。IDF逆向文档频率,包含某词语的文档越少,IDF值越大,说明该词语具有很强的区分能力,IDF=loge(语料库中文档总数/包含该词的文档数+1),+1的原因是避免分母为0。TFIDF=TFxIDF,TFIDF值越大表示该特征词对这个文本的重要性越大。
总结一下就是:
- TF:某一个关键字在待搜索列中出现的次数
- IDF:某一个关键字在整个文档中出现的频率
- 词频归一化:会除上待搜索列所有词的数量
如果概念不是很好理解,我们再来看一下这个例子:以下语录摘自百度百科tf-idf
词频 (TF) 是一词语出现的次数除以该文件的总词语数。假如一篇文件的总词语数是100个,而词语“母牛”出现了3次,那么“母牛”一词在该文件中的词频就是3/100=0.03。一个计算文件频率 (IDF) 的方法是文件集里包含的文件总数除以测定有多少份文件出现过“母牛”一词。所以,如果“母牛”一词在1,000份文件出现过,而文件总数是10,000,000份的话,其逆向文件频率就是 lg(10,000,000 / 1,000)=4。最后的TF-IDF的分数为0.03 * 4=0.12。
了解了算法,下面来看看es中倒排索引的数据结构
倒排索引主要由单词词典(Term Dictionary)和倒排列表(Posting List)及倒排文件(Inverted File)组成。
现在逐一简单解释一下这三个东西:
- 单词词典:就是关键词的集合,
单词词典是由文档集合中出现过的所有关键词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向倒排列表的指针
我们用下面这个东西来解释一下上面的这个文本,其中*表示指针
或许我们把它旋转一下更好理解
- 倒排列表:倒排列表记载了出现过某个关键词的所有文档的文档列表及关键词在该文档中出现的位置信息,每条记录称为一个倒排项。根据倒排列表,即可获知哪些文档包含某个关键词【此段语录摘自什么是倒排索引?】
辣么倒排列表又长啥样子呢?上面我们说了,单词词典里面的每一个关键词都有一个指向倒排列表的指针,所以【表格4】中的每个词都有一个下面的这个列表,我们还是以”奔跑时速“这个关键词来分析吧。
【表格5】
关键词ID | 野兽id | 文档频率 | 词频(TF) | 位置 | 偏移 |
---|---|---|---|---|---|
10 | 3 | 2 | 1 | 2 | <[0,4)> |
10 | 4 | 2 | 1 | 2 | <[0,4)> |
来解释一下各个列的含义,部分摘自Elasticsearch 6.x 倒排索引与分词
【表格6】
列 | 解释 |
---|---|
关键词ID | 就是关键词的ID了【见表4】 |
野兽ID | 就是文档的ID了【见表2】 |
文档频率 | 代表文档集合中有多少个文档包含某个单词 |
单词频率(TF,Term Frequency) | 记录该单词在该文档中出现的次数,用于后续相关性算分 |
位置(Posting) | 记录单词在文档中的分词位置(多个),用于做词语搜索(Phrase Query) |
偏移(Offset) | 记录单词在文档的开始和结束位置,用于高亮显示 |
说完了单词词典和倒排列表,我们来深究一下它们在内存里面的存储结构吧。如果要问为什么有这么一小节,那我们就先想一想这么一个问题:我们这篇文章中的例子关键词为15个,好像比较少,如果是是几万个野兽进行分词,那整个单词词典就会非常大,如果我们顺序遍历,那这是搜索引擎万万不能的。
为了提高某个单词在词典中的查询效率,我们有很多优化的算法:可以看下文档开头的引用”七大查找算法“,倒排索引用二分查找[为什么用二分查找待我想想][使用二分查找的前提是有序,所以需要对整个单词词典进行排序],时间复杂度为O(log2n),在有限次【次数我不太确定,我再查一下资料】磁盘IO能够得到目标,但是磁盘的随机读还是很慢的,所以为了减少磁盘IO,所以会把一部分数据放到内存中,但是整个单词词典依旧很大,所以有了”单词索引“(term index),单词索引是一颗字典树,有关于字典树,可以查看文档开头的引用”字典树“
为了便于理解,我们先来构建一颗简单的模拟的英文字典树来看下,有以下英文单词:i,who,where,are
对于中文的字典树实现起来需要做一些处理,有关于中文字典树,设计方案有很多种,可以查看文档开头的引用”数据结构——从英文字典树到中文字典树“,我们以"这些年,这些数据集,这种测试,关键,富有,挑战,典型,测试,数据集,算法"来做测试,下面展示的是一颗模拟的简单的中文字典树,我们就以”数据结构——从英文字典树到中文字典树“里面的设计思路[对于每个节点都有map(或hash_map),键为前缀汉字(单个),值为指向后续汉字节点的指针]来构建好了
这里我先用hash拉链实现一下,等后续再补充用其他方式实现【主要是看到很多博客说B树,B+树,我也确实不知道到底是哪种,等看看源码再写出来吧】
先看一下它的图
参考”数据结构——从英文字典树到中文字典树“的java实现
import java.util.*;
public class ChineseTerm {
static class TrieNode {
int count = 0;
Map child = new HashMap<>();
}
static class Trie {
private TrieNode root = new TrieNode();;
/*
* @Author 【孙瑞锴】
* @Description 插入字段
* @Date 12:28 上午 2020/3/26
* @Param [str]
* @return void
**/
void insert(String str) {
TrieNode current = root;
int len = str.length();
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
TrieNode trieNode = current.child.get(c);
if (trieNode == null) {
TrieNode newNode = new TrieNode();
current.child.put(c, newNode);
current = newNode;
} else {
current = trieNode;
}
}
current.count++;
}
/*
* @Author 【孙瑞锴】
* @Description 获取前缀为target的字符串
* @Date 12:25 上午 2020/3/26
* @Param [target]
* @return java.util.List
**/
List search(String target) {
List ret = new ArrayList<>();
TrieNode node = getPrefix(target);
if (node != null) {
add2Container(node, target, ret);
}
return ret;
}
/*
* @Author 【孙瑞锴】
* @Description 将搜索结果添加到容器
* @Date 12:08 上午 2020/3/26
* @Param [node, target, ret]
* @return void
**/
private void add2Container(TrieNode node, String target, List ret) {
Map child = node.child;
Iterator> iterator = child.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry next = iterator.next();
add2Container(next.getValue(), target + next.getKey(), ret);
}
if (node.count != 0) {
ret.add(target);
}
}
/*
* @Author 【孙瑞锴】
* @Description 查询包含target的所有字符串
* @Date 12:25 上午 2020/3/26
* @Param [target]
* @return ChineseTerm.TrieNode
**/
private TrieNode getPrefix(String target) {
TrieNode current = root;
int len = target.length();
int i = 0;
for (; i < len; i++) {
char c = target.charAt(i);
TrieNode trieNode = current.child.get(c);
if (trieNode == null) return null;
current = trieNode;
}
if (i == len) return current;
return null;
}
}
public static void main(String[] args) {
Trie trie = new Trie();
// trie.insert("我是一个中国人");
// trie.insert("我很自豪");
// trie.insert("我很开心");
// trie.insert("开心");
// System.out.println(trie);
trie.insert("这些年");
trie.insert("这些数据集");
trie.insert("这种测试");
trie.insert("关键");
trie.insert("富有");
trie.insert("挑战");
trie.insert("典型");
trie.insert("测试");
trie.insert("数据集");
trie.insert("算法");
List list = trie.search("这");
System.out.println(list);
}
}
- 倒排文件:所有关键词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件即被称之为倒排文件,倒排文件是存储倒排索引的物理文件【此段语录摘自什么是倒排索引?】
3. 大功告成
这个系列会逐步更新,直到完结。此系列主要是自己工作中对es逐步深入使用,以及对阮一鸣老师的课程进行个人总结和实验,希望对es逐渐有更深层次的理解。