文章目录
- 主要思路
- 正排索引和倒排索引的表示
- [1. 正排索引查询文档详细信息](#1. 正排索引查询文档详细信息)
- [2. 倒排索引中查找关联词](#2. 倒排索引中查找关联词)
- [3. 新增文档](#3. 新增文档)
主要思路
通过 Index
类,在内存中构造出索引结构。这个类要提供的方法:
- 给定一个
docId
,在正排索引中,查询文档的详细信息 - 给定一个词,在倒排索引中,查询哪些文档和这个词关联
- 往索引中新增一个文档
- 把内存中的索引结构保存到磁盘中
- 把磁盘中的索引数据加载到内存中
正排索引和倒排索引的表示
java
// 正排索引
private ArrayList<DocInfo> forwardIndex = new ArrayList<>();
// 倒排索引
private HashMap<String, ArrayList<Weight>> invertedIndex = new HashMap<>();
- 使用数组下标表示
docId
- 使用一个哈希表来表示倒排索引
key
------词词value
------一组和这个词相关的文章,放在ArrayList
里面
1. 正排索引查询文档详细信息
创建一个相关方法 getDocInfo
。期望其能查询到相关文档信息,所以返回值为 DocInfo
DocInfo
方法中的信息主要包含:
title
url
content
- 最后在类中加上
get
和set
方法就可以了
java
// 1. 给定一个 docId,在正排索引中,查询文档的详细信息
public DocInfo getDocInfo(int docId){
return forwardIndex.get(docId);
}
java
public class DocInfo {
private int id;
private String title;
private String url;
private String content;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
2. 倒排索引中查找关联词
因为含有这个词的文档可能会有很多,所以我们用 List
进行存储。但是文档和搜索词之间的关联性是有区别的,是有权重区分的。所以我们要另建一个类,来对文档 id 和词之间的"相关性"进行判断。
最终倒排索引中查找关键词的方法 getInverted
的返回值就是"相关性" Weight
java
// 2. 给定一个词,在倒排索引中,查询哪些文档和这个词关联
public List<Integer> getInverted(String term){
return invertedIndex.get(term);
}
java
// 这个类就是把 文档id 和 文档与词的相关性 权重 进行一个包裹
public class Weight {
private int id;
// 这个 weight 就表示 文档 和 词 之间的 "相关性"
// 这个值越大,救人位相关性越强。weight 值具体怎么算,后面再说
private int weight;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
}
3. 新增文档
新增一个文档,需要同时给正排索引和倒排索引里面增加信息
正排索引
构建正排索引是很简单的,只需要将 title
、url
、content
包装成一个 docInfo
,再把它加到正排索引的 ArrayList<DocInfo>
里面就可以了
java
private DocInfo buildForward(String title, String url, String content) {
DocInfo docInfo = new DocInfo();
docInfo.setId(forwardIndex.size());
docInfo.setTitle(title);
docInfo.setUrl(url);
docInfo.setContent(content);
forwardIndex.add(docInfo);
return docInfo;
}
新加入的 docId
要放到 forwardIndex
这个数组最后面。这个新的 docInfo
的 docId
就是之前的 forwardIndex
的长度
- 初始情况下,
forwardIndex
的长度为0
,新的docInfo
的docId
就是0
,也就放在0
号下标上 - 这个时候
forwardIndex
长度是1
,再来一个docInfo
,docId
就是1
,也放在1
号下标上
倒排索引
倒排索引的关键就是:词到文档 id
之间的映射关系。首先就需要先知道当前这个文档里面都有哪些词。因此就需要针对当前文档进行分词
- 针对标题
- 针对正文
然后就可以结合这个分词的结果。就知道当前这个文档的 id 应该要加入那个倒排索引的key
里面了。
倒排索引是一个键值对结构(HashMap
),key
是分词结果(term
),value
是一组和这个分词结果相关的文档 id
列表。因此就可以先针对当前文档进行分词,然后根据每个分词结果,去倒排索引中去找到对应的 value
,然后把当前文档 id
给加入到对应的 value
列表中即可
value
里面的 id
信息好确定,但是如何来确定权重值 weight
?
- 这个
weight
值描述了词和文档之间的"相关性" - 此处我们就单纯的通过词出现的次数,来表示相关性
- 标题更短,包含的词更少,这里的词更能表达文档的核心意思
- 正文更长,包含的词更多,这里的词更不能表达文档的核心意思
所以标题的权重更高一些,我们就分开统计标题和正文里 面词出现的次数,最后进行汇总
在真实的搜索引擎中,相关性往往是一个专门的算法团队来进行负责。根据文档中提取的特征,训练模型,最终借助机器学习的方法来衡量相关性
主要的步骤:
- 针对文档标题进行分词
- 遍历分词结果,统计每个词出现的次数
- 针对正文页进行分词
- 遍历分词结果,统计每个词出现的次数
- 把上面的结果汇总到一个
HashMap
里面
最终文档的权重,就设定成标题中出现的次数 * 10 + 正文中出现的次数
- 遍历刚才这个
HashMap
,依次来更新倒排索引中的结构
实现词频统计
我们为了方便,就把标题的次数和正文的次数装到一个类里面了,而不是弄两个 HashMap
。如果搞两个 HashMap
,后面遍历的时候就很麻烦,就得查两次 HashMap
。现在打包在一起就只需要查一次 HashMap
就可以了
java
class WordCnt {
public int titleCount;
public int contentCount;
}
// 这个数据结构用来统计词频
HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();
第一步:针对文档的标题进行分词操作
我们只需要结合前面说到的分词操作,对这里的标题进行分词即可
java
List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
- 仍然是用一个
List
来接收分出的每一个term
第二步:遍历分词的结果,统计每个词出现的次数
首先我们要在刚刚收集了分词结果的 List
中取出 term
。遍历一下 terms
,对里面的 term
出现次数进行统计
- 如果
term
没有出现过,则创建一个新的键值对,titleCount
设为1
- 如果
term
出现过,就找到之前的值,然后把对应的titleCount + 1
java
for(Term term : terms){
// 先判断一下 term 是否存在
String word = term.getName();
WordCnt wordCnt = wordCntHashMap.get(word);
if(wordCnt == null) {
// 如果不存在,就创建一个新的键值对,插入进去,titleCount 设为 1
WordCnt newWordCnt = new WordCnt();
newWordCnt.titleCount = 1;
newWordCnt.contentCount = 0;
wordCntHashMap.put(word, newWordCnt);
}
// 如果存在,就找到之前的值,然后把对应的 titleCount + 1
wordCnt.titleCount++;
}
如果出现
ArrayList
和arraylist
这样的情况,应该怎么算次数?是同一个词出现两次,还是不同的词各出现一次?
- 我们在主流的搜索引擎中观察可以发现,里面并没有区分大小写
- 我们在使用分词的操作
ToAnalysis
的时候,就已经把所有的字母都变成小写了 - 所以我们就不用再转换了。若未转,可以用
toLowerCase
转换成全小写
第三步:针对正文页进行分词
java
terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
第四步:遍历分词的结果,统计每个词出现的次数
java
for(Term term : terms) {
String word = term.getName();
WordCnt wordCnt = wordCntHashMap.get(word);
if(wordCnt == null) {
WordCnt newWordCnt = new WordCnt();
newWordCnt.titleCount = 0;
newWordCnt.contentCount = 1;
wordCntHashMap.put(word, newWordCnt);
}else{
wordCnt.contentCount++;
}
}
第五步:把上面的结果汇总到一个 HashMap 里面
- 最终文档的权重,就设定成标题中出现的次数 * 10 + 正文中出现的次数
第六步:遍历刚才这个 HashMap,依次来更新倒排索引中的结构
java
for(Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {
}
在这个代码中,最终是使用 for-each
来遍历的,什么样的对象能用 for-each
来遍历呢?
- 必须要求这个对象是"可迭代的"/实现了
Iterable
接口
但是 Map
并没有实现 Interable
接口(Map
存在的意义,本就不是为了遍历,主要还是为了能够进行根据 key
查找 value
)
Set
实现了 Interable
,就可以把 Map
转换成 Set
。本来 Map
存的是键值对,可以根据 key
快速地找到 value
,Set
这里存的是一个把键值对打包在一起的类,成为 Entry
(入口/条目)
- 转换成
Set
之后,也就失去了通过key
快速查找到value
的能力,但是换来的就是可以进行遍历了 - 通过这个遍历,就可以获取到每一个键值对了
- 倒排索引的结构
- 第一步就是先根据这里的值,去倒排索引中查一查,把 ArrayList 获取到
- 但也不一定可以获取到,所以要进行判断
java
// 将 Map 转换成 Set 进行遍历
for(Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {
// 先根据这里的值,去倒排索引中查一查
// invertedList 是倒排索引中的一个值------------倒排拉链
List<Weight> invertedList = invertedIndex.get(entry.getKey());
// 判断拉链是不是存在的(空的),key 可能不存在
if(invertedList == null) {
// 如果为空,就插入一个新的键值对
ArrayList<Weight> newInvertedList = new ArrayList<>();
// 把新的文档(当前的 DocInfo)构造成 Weight 对象,插入进来
Weight weight = new Weight();
weight.setDocId(docInfo.getDocId());
// 权重计算公式:标题中出现的次数 * 10 + 正文中出现的次数
weight.setWeight(entry.getValue().titleCount * 10
+ entry.getValue().contentCount);
newInvertedList.add(weight);
// 将键值对 key-ArrayList 一并加到倒排索引 invertedIndex 中
invertedIndex.put(entry.getKey(), newInvertedList);
}else{
// 如果非空,就把当前这个文档,构造出一个 Weight 对象,插入到倒排拉链的后面
Weight weight = new Weight();
weight.setDocId(docInfo.getDocId());
// 权重计算公式:标题中出现的次数 * 10 + 正文中出现的次数
weight.setWeight(entry.getValue().titleCount * 10
+ entry.getValue().contentCount);
invertedList.add(weight);
}
}