文章目录

  • 前言
  • 一、反向索引
  • 二、实现代码


前言

数据库文章全文搜索中我们很少使用 like 查询,因为假设使用包含查询,我们需要对每个检索词每一篇文章的每个单词进行遍历,检索的时间复杂度则会达到 o(n三次方)。通常,我们会直接使用 elasticsearch 组件来实现全文检索,但我们很少了解全文检索的原理,今天我们尝试使用 redis 来构建一个具有基本检索功能的全文索引组件,将时间复杂度降低到 o(n * log2n)。
这里补充 redislabs 提供的全文检索插件:https://docs.redislabs.com/latest/modules/redisearch/


一、反向索引

在实现这个全文索引组件前,我们需要了解反向索引的数据结构,因为接下来我们将使用反向索引来构建全文检索。
相比正向索引(代表从文档查询到词),而反向索引与之相反则代表从词查询到文档。
下面举个例子:我们使用不同数字代表不同的文档,以下这就是一个正向索引:

0:“hello world”
 1:“hello men”
 2:“better world”

而反向索引则使用单词作为索引,而文档的数字作为被索引的元素,以下这就是一个反向索引:

“hello” :{0,1}
 “world” :{0,2}
 “men” :{1}
 “better” :{2}


如果要检索"hello men",那么就可以对 {0,1},{1} 取交集,最终得到文档索引 1 。

二、实现代码

注:以下代码使用 Java 实现,但实际上没有用到 Java 语言的特殊实现,主要依靠于 redis 的 SET、ZSET 以及对应的取交集命令 SINTER、SINTERSTORE,如果使用其他语言可做参考

首先使用 spring-boot-starter-data-redis 简化配置,让我们专注于业务。实现代码如下:

package com.ch.demo.redis.search;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.*;
import java.util.stream.Collectors;

@SpringBootApplication
public class Application {

    private static ConfigurableApplicationContext context;
    private static StringRedisTemplate redisTemplate;

    private static Map<String, String> articles = new HashMap<>();

    static {
        // 初始化文章
        articles.put("article_id_1", "there are moments in life when you miss someone so much that you just want to pick them from your dreams and hug them for real");
        articles.put("article_id_2", "when you were born,you were crying and everyone around you was smiling.Live your life so that when you die,you're the one who is smiling and everyone around you is crying");
    }

    public static void main(String[] args) {
        context = SpringApplication.run(Application.class, args);
        redisTemplate = context.getBean(StringRedisTemplate.class);
        // 建立反向索引
        buildIndex();
        System.out.println(search("you were crying"));
        System.out.println(search("you"));
    }

    private static Set<String> search(String txt) {
        // 将搜索语句分词
        List<String> keys = Arrays.asList(txtSplit(txt));
        keys = keys.stream().map(o -> generateKey(o)).collect(Collectors.toList());
        // 对这些分词下的集合取交集
        Set<String> articleIds = redisTemplate.opsForSet().intersect(keys);
        return articleIds;
    }

    /**
     * 构建全文索引
     * <p>这里采用反向索引</p>
     */
    private static void buildIndex() {
        // 将所有文章分词,将每个分词在 redis 上建立 key 为分词值为文章id的集合
        articles.forEach((id, txt) -> {
            String[] keys = txtSplit(txt);
            for (String key : keys) {
                // 如果需要排序则可使用 zset 进行存储
                redisTemplate.opsForSet().add(generateKey(key), id);
            }
        });
    }

    /**
     * 分词器
     *
     * @return String[]
     */
    private static String[] txtSplit(String txt) {
        return txt.split(" ");
    }

    /**
     * redis key 拼接
     *
     * @return String
     */
    private static String generateKey(String key) {
        return "SEARCH:" + key;
    }

}

以上代码运行结果:

[article_id_2]
[article_id_2, article_id_1]

代表 you were crying 查询到文章 article_id_2 ,而 you 则查询到文章 article_id_2、article_id_1。