Rust 实战丨倒排索引

引言

倒排索引(Inverted Index)是一种索引数据结构,用于存储某个单词(词项)在一组文档中的所有出现情况的映射。它是搜索引擎执行快速全文搜索的核心技术,也广泛用于数据库中进行文本搜索。我们熟知的 ElasticSearch 最核心底层原理便就是倒排索引。

倒排索引的基本原理是将文档中的词汇进行反转,形成倒排列表。 在倒排列表中,每个词汇都对应一个文档标识符的列表,这些标识符指明了该词汇出现在哪些文档中。 通过查询倒排列表,可以快速地找到包含特定词汇的文档。

本文将使用 Rust 语言来实现一个简单的倒排索引,包括倒排索引的构建和搜索过程。在下一篇文章中,笔者会基于《Rust 程序设计(第二版)》并发编程篇章,解读该书作者是如何基于 Rust 通道实现更优秀、更高性能的倒排索引。

可以学到

  1. 倒排索引的原理、优势和使用
  2. 常用 crate:coloredregex
  3. Rust HashMap
  4. Rust 迭代器

开发思路

一个简单的倒排索引开发思路大概如上图所示:

  1. 读取文档
  2. 分词
  3. 构建每个词到每个文档的映射

开发过程

完整源码位于:inverted_index

最终效果

rust 复制代码
fn main() {
    let mut index = InvertedIndex::new();
    index.add(1, "Rust is safe and fast.");
    index.add(2, "Rust is a systems programming language.");
    index.add(3, "Programming in Rust is fun.");

    // query "Rust"
    let results = index.query("Rust");
    for result in results {
        println!("{}", result);
    }

    println!("");

    // query "Programming"
    let results = index.query("Programming");
    for result in results {
        println!("{}", result);
    }
}

执行:

bash 复制代码
cargo run

输出:

版本声明

toml 复制代码
[package]
name = "inverted_index"
version = "0.1.0"
edition = "2021"

[dependencies]
colored = "2.1.0"
regex = "1.10.4"

项目准备

首先我们创建项目:

bash 复制代码
cargo new inverted_index

准备依赖:

bash 复制代码
cargo add regex
cargo add colored
  • colored: 终端高亮,后面我们将实现搜索词的高亮显示,使结果更美观。
  • regex: 正则库,用于实现不区分大小写替换匹配到的搜索词。

实现过程

首先我们定义两个数据结构:

rust 复制代码
struct Document {
    id: usize,
    content: String,
}

struct InvertedIndex {
    indexes: HashMap<String, Vec<usize>>,
    documents: HashMap<usize, Document>,
}

impl InvertedIndex {
    fn new() -> InvertedIndex {
        InvertedIndex {
            indexes: HashMap::new(),
            documents: HashMap::new(),
        }
    }
}
  • Document: 封装原始文档
  • IndexedIndex: 我们将构建的倒排索引

接下来我们要实现 2 个辅助函数,一个是 tokenize,用于将原始的文档信息拆分成独立的词(word/term),另一个是 hightlight,用于将匹配到的文本进行替换,使其在中断可以以紫色输出。

tokenize 实现如下:

rust 复制代码
fn tokenize(text: &str) -> Vec<&str> {
    text.split(|ch: char| !ch.is_alphanumeric())
        .filter(|c| !c.is_empty())
        .collect()
}

#[test]
fn tokenize_test() {
    assert_eq!(
        tokenize("This is\nhedon's tokenize function."),
        vec!["This", "is", "hedon", "s", "tokenize", "function"]
    )
}

highlight 实现如下:

rust 复制代码
fn highlight(term: &str, content: &str) -> String {
    let regex = Regex::new(&format!(r"(?i){}", term)).unwrap();
    let highlighted_content = regex
        .replace_all(content, |caps: &regex::Captures| {
            caps[0].to_string().purple().to_string()
        })
        .to_string();
    highlighted_content
}

#[test]
fn highlight_test() {
    assert_eq!(
        highlight("programming", "I like programming with Rust Programming"),
        "I like \u{1b}[35mprogramming\u{1b}[0m with Rust \u{1b}[35mProgramming\u{1b}[0m"
    );
}

现在我们可以为 InvertedIndex 实现构建索引的方法 add 了,它会接收原始文档,对其进行分词,并将记录每个分词和文档 id 的映射。

rust 复制代码
impl InvertedIndex {
  	fn add(&mut self, doc_id: usize, content: &str) {
        let content_lowercase = content.to_lowercase();
        let words = tokenize(&content_lowercase);
        for word in words {
            self.indexes
                .entry(word.to_string())
                .or_insert(vec![])
                .push(doc_id)
        }

        self.documents.insert(
            doc_id,
            Document {
                id: doc_id,
                content: content.to_string(),
            },
        );
    }
}

然后我们再实现对应的根据分词 term 搜索原始文档的方法:

rust 复制代码
impl InvertedIndex {
  	fn query(&self, term: &str) -> Vec<String> {
        let term_lowercase = term.to_lowercase();
        if let Some(doc_ids) = self.indexes.get(&term_lowercase) {
            doc_ids
                .iter()
                .filter_map(|doc_id| {
                    self.documents
                        .get(doc_id)
                        .map(|doc| highlight(&term_lowercase, &doc.content))
                })
                .collect()
        } else {
            Vec::new()
        }
    }
}

这样一个简单的倒排索引构建和搜索功能就完成了,具体的执行效果你可以回到前面的「最终效果」进行查阅。

总结预告

本文实现的倒排索引虽然非常简单,但是也基本体现了倒排索引的最核心思想和应用方式了。在《Rust 程序设计(第二版)》的并发编程篇章中,该书提出了使用通道 channel 来并发构建倒排索引,同时给出了更加丰富和优雅的实现。在下篇文章中,笔者将阅读这部分的源码,解析并重现当中的实战过程,并进行适当扩展。

peace! enjoy coding~

绘图工具

参考资料

相关推荐
浮江雾8 分钟前
Flutter第十节-----Flutter布局与组件全解析
android·开发语言·前端·学习·flutter·入门
爱喝水的鱼丶8 分钟前
SAP-ABAP:ALV跨版本兼容指南——适配ECC与S4 HANA系统的ALV开发注意事项
开发语言·性能优化·sap·abap·经验交流
布朗克16822 分钟前
Go 入门到精通-27-泛型编程
开发语言·golang·xcode·泛型编程
cui_ruicheng28 分钟前
Python从入门到实战(十五):文件操作与目录管理
开发语言·python
Roy_Sashulin32 分钟前
灵杉Java编程平台第一章:安装
java·开发语言
名字还没想好☜32 分钟前
Go 用 errgroup 管理并发子任务:错误收敛、取消传播与限流
开发语言·后端·golang·go·并发
2zcode1 小时前
基于MATLAB深度卷积特征的多曝光图像自适应融合系统设计与实现
开发语言·matlab·多曝光图像自适应融合系统
拾贰_C1 小时前
【Java | SpringBoot | GlobalExceptionHandler】全局异常拦截器实现思路
java·开发语言·spring boot
星栈独行2 小时前
Node 框架怎么选?Express、Koa、Egg、NestJS 场景化选型指南
后端·程序人生·node.js
你为她披上外套时我正站在窗外2 小时前
拆解 siwi-download:Rust 异步下载器是怎么炼成的
后端