【文档搜索引擎】在内存中构造出索引结构(上)

文章目录

主要思路

通过 Index 类,在内存中构造出索引结构。这个类要提供的方法:

  1. 给定一个 docId,在正排索引中,查询文档的详细信息
  2. 给定一个词,在倒排索引中,查询哪些文档和这个词关联
  3. 往索引中新增一个文档
  4. 把内存中的索引结构保存到磁盘中
  5. 把磁盘中的索引数据加载到内存中

正排索引和倒排索引的表示

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
  • 最后在类中加上 getset 方法就可以了
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. 新增文档

新增一个文档,需要同时给正排索引和倒排索引里面增加信息

正排索引

构建正排索引是很简单的,只需要将 titleurlcontent 包装成一个 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 这个数组最后面。这个新的 docInfodocId 就是之前的 forwardIndex 的长度

  • 初始情况下,forwardIndex 的长度为 0,新的 docInfodocId 就是 0,也就放在 0 号下标上
  • 这个时候 forwardIndex 长度是 1,再来一个 docInfodocId 就是 1,也放在 1 号下标上

倒排索引

倒排索引的关键就是:词到文档 id 之间的映射关系。首先就需要先知道当前这个文档里面都有哪些词。因此就需要针对当前文档进行分词

  • 针对标题
  • 针对正文
    然后就可以结合这个分词的结果。就知道当前这个文档的 id 应该要加入那个倒排索引的 key 里面了。

倒排索引是一个键值对结构(HashMap),key 是分词结果(term),value 是一组和这个分词结果相关的文档 id 列表。因此就可以先针对当前文档进行分词,然后根据每个分词结果,去倒排索引中去找到对应的 value,然后把当前文档 id 给加入到对应的 value 列表中即可


value 里面的 id 信息好确定,但是如何来确定权重值 weight

  • 这个 weight 值描述了词和文档之间的"相关性"
  • 此处我们就单纯的通过词出现的次数,来表示相关性
  • 标题更短,包含的词更少,这里的词更能表达文档的核心意思
  • 正文更长,包含的词更多,这里的词更不能表达文档的核心意思
    所以标题的权重更高一些,我们就分开统计标题和正文里 面词出现的次数,最后进行汇总

在真实的搜索引擎中,相关性往往是一个专门的算法团队来进行负责。根据文档中提取的特征,训练模型,最终借助机器学习的方法来衡量相关性

主要的步骤:

  1. 针对文档标题进行分词
  2. 遍历分词结果,统计每个词出现的次数
  3. 针对正文页进行分词
  4. 遍历分词结果,统计每个词出现的次数
  5. 把上面的结果汇总到一个 HashMap 里面
    最终文档的权重,就设定成 标题中出现的次数 * 10 + 正文中出现的次数
  6. 遍历刚才这个 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++;  
}

如果出现 ArrayListarraylist 这样的情况,应该怎么算次数?是同一个词出现两次,还是不同的词各出现一次?

  • 我们在主流的搜索引擎中观察可以发现,里面并没有区分大小写
  • 我们在使用分词的操作 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 快速地找到 valueSet 这里存的是一个把键值对打包在一起的类,成为 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);  
    }  
}
相关推荐
邓熙榆几秒前
Logo语言的网络编程
开发语言·后端·golang
S-X-S1 小时前
项目集成ELK
java·开发语言·elk
Johaden2 小时前
EXCEL+Python搞定数据处理(第一部分:Python入门-第2章:开发环境)
开发语言·vscode·python·conda·excel
ByteBlossom6666 小时前
MDX语言的语法糖
开发语言·后端·golang
肖田变强不变秃7 小时前
C++实现矩阵Matrix类 实现基本运算
开发语言·c++·matlab·矩阵·有限元·ansys
沈霁晨7 小时前
Ruby语言的Web开发
开发语言·后端·golang
小兜全糖(xdqt)7 小时前
python中单例模式
开发语言·python·单例模式
DanceDonkey7 小时前
@RabbitListener处理重试机制完成后的异常捕获
开发语言·后端·ruby
Python数据分析与机器学习7 小时前
python高级加密算法AES对信息进行加密和解密
开发语言·python
军训猫猫头7 小时前
52.this.DataContext = new UserViewModel(); C#例子 WPF例子
开发语言·c#·wpf