【信息检索】Java简易搜索引擎原理及实现(三)B+树索引和轮排索引结构

文章介绍了如何在Java简易搜索引擎中使用B+树和轮排索引来优化通配符查询。通过轮排索引,将通配符转换到单词尾部,利用B+树进行范围查找,提高查询效率。文章详细讲解了实现思路,包括B+树的构建、轮排索引的创建以及查询处理过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一篇文章 :【信息检索】Java简易搜索引擎原理及实现(二)新增停用词表 + 查询处理,我们在建立好的倒排索引的结构中剔除了停用词,同时引入了AND、OR、ANDNOT操作符,支持三种查询方式。
这篇文章中,我们将在倒排索引的基础上,改进我们的字典结构,使用B+树索引来加快检索速度;同时引入轮排的索引方法,以支持通配符的模糊查询方式。

目标:支持通配符查询

在原有字典的基础上,扩展索引结构,实现支持通配符查询。
本文采用的方式是建立轮排索引(Permuterm Index)和 B+树索引结构

使用B+树的原因

首先我们说一下为什么要使用B+树来作为基础索引结构呢?
在这篇文章之前,我们已经建立好了倒排索引的结构,但各个item项之间,我们是用LinkedList来把它们串连在一起的,对于一个搜索的词汇,直接遍历搜索List明显效率感人。因此,我们考虑使用数据库索引这样的结构来存储item项,即B+树,能有效减少搜索次数。

那么为什么选用B+树而不是B树呢?
关于B+树和B树的对比,我们先来看一张图:

可以看到,B+树的非叶节点没有指针指向实际的数据,非叶节点只做导航作用,而B树在非叶节点上也有指向实际数据的指针。
同时,B+树的叶节点之间相邻节点使用链表相连,便于实现区间查找和遍历。而B树则需要进行每一层的递归遍历,相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。

通配符查询原理

比如查询语句 mon*:找出所有以mon开头的单词。如果采用B+树结构的词典,我们可以很容易的解决,只需要查询范围在mon ≤ w < moo的所有单词就ok了。

但是查询语句 *mon:找出所有以mon结尾的单词就比较困难了。其中一种办法就是我们增加一个额外的B+树来存储所有单词,以从后向前的顺序,然后在这个树上查询范围在nom ≤ w < non的所有单词。

可是如何处理通配符在单词中间的查询呢?

比如query是co*tion的话。我们当然可以分别在B+树查询到co*和*tion的所有单词然后合并这些单词,但是这样开销太大了。

解决办法就是:轮排索引(Permuterm Index),我们把query的通配符转换到结尾处

设置一个标志$表示单词的结尾。

以hello举例,hello可以被转换成hello$, ello$h, llo$he, lo$hel, o$hell。$代表中hello的结束。现在,查询X等于查询X$,查询X等于查询X$,查询X等于查询X$,查询XY等于查询Y$X。对于hel*o来说,X等于hel,Y等于o。

既然我们已经把通配符都弄到了单词尾部,现在我们又可以通过B+树像以前那样查询了。

轮排索引的缺点在于,据统计,会使得词典的大小约变为原始的四倍。

因此,针对这个问题,又有k-gram索引结构,能解决词典扩大的问题。

k-gram:由k个字符组成的序列
2-gram:由2个字符组成的序列,又称二元组(bigram)

例如,对于文本 “April is the cruelest month”
它的2-gram(bigram)就是:
$a, ap, pr, ri, il, l$, $i, is, s$, $t, th, he, e$, $c, cr, ru, ue, el, le, es, st, t$, $m, mo, on, nt, h$
标志$表示单词的结尾

由bigram到词典项的倒排索引关系如图所示:

因此,查询mon*现在被转换为:$m AND mo AND on

具体实现思路

对于每个item项,将它的term值取出,用上面提到的轮排的方式,把该item项对应的所有字符串存为B+树的key,item项存为value,即原本的一个item项此时会对应有轮排出的多个key值。
举个例子来说,一个item项的term值是【互联网】,根据上述轮排的定义,它将对应于
互联网$、联网$互、网$互联
这三个字符串,将每个字符串和item当做一组key、value存入B+树结构中。

构建好B+树和轮排索引结构后,就是对搜索词的查询了。
根据上述在轮排原理处提到的多种不同的含通配符的搜索词格式,我们用不同的映射方式,将用户输入的搜索词映射为实际在B+树中查询的搜索词,然后直接到B+树中进行范围搜索即可,具体范围搜索的方法看下面的代码实现。

代码实现

每部分思路在代码中都有详细的注释,具体可参考代码理解。
B+树提供的功能主要就是数据的插入、精确查询、通配符查询。
用枚举类VagueMode来表示不同的通配符模式。

1.枚举类VagueMode
public enum VagueMode {
   
    X,   //X -> X$
    X_,  //X* -> X*$
    _X,  //*X -> X$*
    _X_, //*X* -> X*
    X_Y  //X*Y -> Y$X*
}
2.B+树构建
import java.util.List;

public class BplusTree {
   

    /** 根节点 */
    protected Node root;

    /** 阶数,M值 */
    protected int order;

    /** 叶子节点的链表头*/
    protected Node head;

    public BplusTree(int order){
   
        if (order < 3) {
   
            System.out.print("order must be greater than 2");
            System.exit(0);
        }
        this.order = order;
        root = new Node(true, true);
        head = root;
    }

    public Node getHead() {
   
        return head;
    }

    public void setHead(Node head) {
   
        this.head = head;
    }

    public Node getRoot() {
   
        return root;
    }

    public void setRoot(Node root) {
   
        this.root = root;
    }

    public int getOrder() {
   
        return order;
    }

    public void setOrder(int order) {
   
        this.order = order;
    }

    public Item get(String key) {
   
        return root.get(key);
    }

    public List<Item> rangeFind(String key, VagueMode mode) {
   
        return root.rangeFind(key, mode);
    }

    public void insertOrUpdate(String key, Item obj) {
   
        root.insertOrUpdate(key, obj, this);
    }

}
3.Node节点
import java.text.Collator;
import java.util.*;

public class Node {
   
    /** 是否为叶子节点 */
    protected boolean isLeaf;

    /** 是否为根节点*/
    protected boolean isRoot;

    /** 父节点 */
    protected Node parent;

    /** 叶节点的前节点*/
    protected Node previous;

    /** 叶节点的后节点*/
    protected Node next;

    /** 节点的关键字 */
    protected List<Map.Entry<String, Item>> entries;

    /** 子节点 */
    protected List<Node> children;

    static Comparator<Object> comparator = Collator.getInstance(Locale.CHINA);

    public Node(boolean isLeaf) {
   
        this.isLeaf = isLeaf;
        entries = new ArrayList<>();

        if (!isLeaf) {
   
            children = new ArrayList<>();
        }
    }

    public Node(boolean isLeaf, boolean isRoot) {
   
        this(isLeaf);
        this.isRoot = isRoot;
    }

    //X*格式的通配符查询 -> X*$
    //key为已去除末尾的*的X
    private List<Item> modeX_(String key) {
   
        //如果是叶子节点
        if (isLeaf) {
   
            Node node = this;
            int firstLargerPos = 0;
            int flag = 0; //用于控制循环的标志位,确保循环最多执行2次
            while (flag < 2) {
   
                firstLargerPos = 0;
                //找到第一个大于等于key的元素的下标
                for (; firstLargerPos < node.entries.size(); firstLargerPos++) {
   
                    if (comparator.compare(node.entries.get(firstLargerPos).getKey(), key) >= 0) {
   
                        flag = 2;
                        break;
                    }
                }
                node = node.next;
                flag++;
            }
            node = node.previous;

            if (firstLargerPos < node.entries.size()) {
   
                List<Item> list = new ArrayList<>();
                while (node.entries.get(firstLargerPos).getKey().startsWith(key)
                        && node.entries.get(firstLargerPos).getKey().endsWith("$")) {
   
                    list.add(node.entries.get(firstLargerPos).getValue());
                    firstLargerPos++;
                    if (firstLargerPos >= node.entries.size()) {
   
                        node = node.next;
                        firstLargerPos = 0;
                    }
                }
                return list;
            }
            //未找到所要查询的对象
            return null;

        //如果不是叶子节点
        } else {
   
            //如果key小于等于节点最左边的key,沿第一个子节点继续搜索
            if (comparator.compare(key, entries.get(0).getKey()) <= 0) {
   
                return children.get(0).modeX_(key);
                //如果key大于节点最右边的key,沿最后一个子节点继续搜索
            } else if (comparator.compare(key, entries.get(entries.size()-1).getKey()) >= 0) {
   
                return children.get(children.size()-1).modeX_(key);
                //否则沿比key大的前一个子节点继续搜索
            } else {
   
                for (int i = 0; i < entries.size(); i++) {
   
                    if (comparator.compare(entries.get(i).getKey(), key) <= 0
                            && comparator.compare(entries.get(i+1).getKey(), key) > 0) {
   
                        return children.get(i).modeX_(key);
                    }
                }
            }
        }

        return null;
    }

    //*X格式的通配符查询 -> X$*
    //key = X$
    private List<Item> mode_X(String key) {
   
        //如果是叶子节点
        if (isLeaf) {
   
            Node node = this;
            int firstLargerPos = 0;
            int flag = 0; //用于控制循环的标志位,确保循环最多执行2次
            while (flag < 2) {
   
                firstLargerPos = 0;
                //找到第一个大于等于key的元素的下标
                for (; firstLargerPos < node.entries.size(); firstLargerPos++) {
   
                    if (comparator.compare(node.entries.get(firstLargerPos).getKey(), key) >= 0) {
   
                        flag = 2;
                        break;
                    }
                }
                node = node.next;
                flag++;
            }
            node = node.previous;

            if (firstLargerPos < node.entries.size())<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值