• Rust 实战丨倒排索引


    引言

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

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

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

    可以学到

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

    开发思路

    倒排索引构建过程

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

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

    开发过程

    完整源码位于:inverted_index

    最终效果

    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);
        }
    }
    

    执行:

    cargo run
    

    输出:

    inverted index 输出示例

    版本声明

    [package]
    name = "inverted_index"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    colored = "2.1.0"
    regex = "1.10.4"
    

    项目准备

    首先我们创建项目:

    cargo new inverted_index
    

    准备依赖:

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

    实现过程

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

    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 实现如下:

    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 实现如下:

    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 的映射。

    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 搜索原始文档的方法:

    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~

    绘图工具

    • https://excalidraw.com/

    参考资料

  • 相关阅读:
    单调栈题目:找出最具竞争力的子序列
    Java注解详解和自定义注解实战,用代码讲解
    python基础语法(四)
    Java分布式ID
    项目实战 | YOLOv5 + Tesseract-OCR 实现车牌号文本识别
    【JS Promise】使用promise一定要注意的几个问题
    【C++】动态内存管理 ④ ( 对象的动态创建和释放引申思考 | 基础数据类型 内存分析 | malloc 分配内存 delete 释放 | new 分配内存 free 释放内存 )
    linux重启centos后常用命令
    vue面试经常会问的那些题
    Mac 上编译 LLVM 7.x
  • 原文地址:https://blog.csdn.net/Hedon954/article/details/139676181