分享

hbase源码系列(五)Trie单词查找树

本帖最后由 坎蒂丝_Swan 于 2014-12-29 12:59 编辑

问题导读
1.Trie树的原理是什么?
2.树里面有几种类型的数据结构?分别是什么?







在上一章中提到了编码压缩,讲了一个简单的DataBlockEncoding.PREFIX算法,它用的是前序编码压缩的算法,它搜索到时候,是全扫描的方式搜索的,如此一来,搜索效率实在是不敢恭维,所以在hbase当中单独拿了一个工程出来实现了Trie的数据结果,既达到了压缩编码的效果,亦达到了方便查询的效果,一举两得,设置的方法是在上一章的末尾提了。

下面讲一下这个Trie树的原理吧。

022335546732084.png

树里面有3中类型的数据结构,branch(分支)、leaf(叶子)、nub(节点)
1、branch 分支节点,比如图中的t,以它为结果的词并没有出现过,但它是to、tea等次的分支的地方,单个t的词没有出现过。
2、leaf叶子节点,比如图中的to,它下面没有子节点了,并且出现了7次。
3、nub节点,它是结余两者之间的,比如i,它独立出现了11次。

下面我们就具体说一下在hbase的工程里面它是什么样子的,下面是一个例子:
  1. * Example inputs (numInputs=7):
  2. * 0: AAA
  3. * 1: AAA
  4. * 2: AAB
  5. * 3: AAB
  6. * 4: AAB
  7. * 5: AABQQ
  8. * 6: AABQQ
  9. * <br/><br/>
  10. * Resulting TokenizerNodes:
  11. * AA <- branch, numOccurrences=0, tokenStartOffset=0, token.length=2
  12. * A  <- leaf, numOccurrences=2, tokenStartOffset=2, token.length=1
  13. * B  <- nub, numOccurrences=3, tokenStartOffset=2, token.length=1
  14. * QQ <- leaf, numOccurrences=2, tokenStartOffset=3, token.length=2
复制代码

这里面3个辅助字段,numOccurrences(出现次数)、tokenStartOffset(在原词当中的位置)、token.length(词的长度)。

描述这个数据结构用了两个类Tokenizer和TokenizerNode。

  好,我们先看一下发起点PrefixTreeCodec,这个类是继承自DataBlockEncoder接口的,DataBlockEncoder是专门负责编码压缩的,它里面的有3个重要的方法,encodeKeyValues(编码)、decodeKeyValues(反编码)、createSeeker(创建扫描器)。

因此我们先看PrefixTreeCodec里面的encodeKeyValues方法,这个是我们的入口,我们发现internalEncodeKeyValues是实际编码的地方。
  1. private void internalEncodeKeyValues(DataOutputStream encodedOutputStream,
  2.       ByteBuffer rawKeyValues, boolean includesMvccVersion) throws IOException {
  3.     rawKeyValues.rewind();
  4.     PrefixTreeEncoder builder = EncoderFactory.checkOut(encodedOutputStream, includesMvccVersion);
  5.     try{
  6.       KeyValue kv;
  7.       while ((kv = KeyValueUtil.nextShallowCopy(rawKeyValues, includesMvccVersion)) != null) {
  8.         builder.write(kv);
  9.       }
  10.       builder.flush();
  11.     }finally{
  12.       EncoderFactory.checkIn(builder);
  13.     }
  14. }
复制代码

可以看到从rawKeyValues里面不断读取kv出来,用PrefixTreeEncoder.write方法来进行编码,最后调用flush进行输出。

我们现在就进入PrefixTreeEncoder.write的方法里面吧。

  1. rowTokenizer.addSorted(CellUtil.fillRowRange(cell, rowRange));
  2. addFamilyPart(cell);
  3. addQualifierPart(cell);
  4. addAfterRowFamilyQualifier(cell);
复制代码

这里就跳到Tokenizer.addSorted方法里面
  1. public void addSorted(final ByteRange bytes) {
  2.     ++numArraysAdded;
  3.     //先检查最大长度,如果它是最大,改变最大长度
  4.     if (bytes.getLength() > maxElementLength) {
  5.       maxElementLength = bytes.getLength();
  6.     }
  7.     if (root == null) {
  8.       // 根节点
  9.       root = addNode(null, 1, 0, bytes, 0);
  10.     } else {
  11.       root.addSorted(bytes);
  12.     }
  13.   }
复制代码

如果root节点为空,就new一个root节点出来,有了根节点之后,就把节点添加到root节点的孩子队列里面。

下面贴一下addSorted的代码吧

  1. public void addSorted(final ByteRange bytes) {// recursively build the tree
  2.     /*
  3.      * 前缀完全匹配,子节点也不为空,取出最后一个节点,和最后一个节点也部分匹配
  4.      * 就添加到最后一个节点的子节点当中
  5.      */
  6.     if (matchesToken(bytes) && CollectionUtils.notEmpty(children)) {
  7.       TokenizerNode lastChild = CollectionUtils.getLast(children);
  8.       //和最后一个节点前缀部分匹配
  9.       if (lastChild.partiallyMatchesToken(bytes)) {
  10.         lastChild.addSorted(bytes);
  11.         return;
  12.       }
  13.     }
  14. //匹配长度
  15.     int numIdenticalTokenBytes = numIdenticalBytes(bytes);// should be <= token.length
  16.     //当前token的起始长度是不变的了,剩余的尾巴的其实位置
  17.     int tailOffset = tokenStartOffset + numIdenticalTokenBytes;
  18.     //尾巴的长度
  19.     int tailLength = bytes.getLength() - tailOffset;
  20.     if (numIdenticalTokenBytes == token.getLength()) {
  21.       //和该节点完全匹配
  22.       if (tailLength == 0) {// identical to this node (case 1)
  23.         incrementNumOccurrences(1);
  24.       } else {
  25.         // 加到节点的下面,作为孩子
  26.         int childNodeDepth = nodeDepth + 1;
  27.         int childTokenStartOffset = tokenStartOffset + numIdenticalTokenBytes;
  28.         TokenizerNode newChildNode = builder.addNode(this, childNodeDepth, childTokenStartOffset, bytes, tailOffset);
  29.         addChild(newChildNode);
  30.       }
  31.     } else {
  32.       split(numIdenticalTokenBytes, bytes);
  33.     }
  34.   }
复制代码

1、我们先添加一个AAA进去,它是根节点,parent是null,深度为1,在原词中起始位置为0。

031505561118771.png

2、添加一个AAA,它首先和之前的AAA相比,完全一致,走的是incrementNumOccurrences(1),出现次数(numOccurrences)变成2。

3、添加AAB,它和AAA相比,匹配的长度为2,尾巴长度为1,那么它走的是这条路split(numIdenticalTokenBytes, bytes)这条路径

  1. protected void split(int numTokenBytesToRetain, final ByteRange bytes) {
  2.     int childNodeDepth = nodeDepth;
  3.     int childTokenStartOffset = tokenStartOffset + numTokenBytesToRetain;
  4.     //create leaf AA 先创建左边的节点
  5.     TokenizerNode firstChild = builder.addNode(this, childNodeDepth, childTokenStartOffset,
  6.       token, numTokenBytesToRetain);
  7.     firstChild.setNumOccurrences(numOccurrences);// do before clearing this node's numOccurrences
  8.     //这一步很重要,更改原节点的长度,node节点记录的数据不是一个简单的byte[]
  9.     token.setLength(numTokenBytesToRetain);//shorten current token from BAA to B
  10.     numOccurrences = 0;//current node is now a branch
  11.     moveChildrenToDifferentParent(firstChild);//point the new leaf (AA) to the new branch (B)
  12.     addChild(firstChild);//add the new leaf (AA) to the branch's (B's) children
  13.     //create leaf 再创建右边的节点
  14.     TokenizerNode secondChild = builder.addNode(this, childNodeDepth, childTokenStartOffset,
  15.       bytes, tokenStartOffset + numTokenBytesToRetain);
  16.     addChild(secondChild);//add the new leaf (00) to the branch's (B's) children
  17.     // 递归增加左右子树的深度
  18.     firstChild.incrementNodeDepthRecursively();
  19.     secondChild.incrementNodeDepthRecursively();
  20.   }
复制代码

split完成的效果:

031505571897201.png

1) 子节点的tokenStartOffset 等于父节点的tokenStartOffset 加上匹配的长度,这里是0+2=2
2)创建左孩子,token为A,深度为父节点一致,出现次数和父亲一样2次
3)父节点的token长度变为匹配长度2,即(AA),出现次数置为0
4)把原来节点的子节点指向左孩子
5)把左孩子的父节点指向当前节点
6)创建右孩子,token为B,深度为父节点一致
7)把右孩子的父节点指向当前节点
8)把左右孩子的深度递归增加。

4、 添加AAB,和AA完全匹配,最后一个孩子节点AAB也匹配,调用AAB节点的addSorted(bytes),因为是完全匹配,所以和第二步一样,B的出现次数加1

031505580958643.png

5、添加AABQQ,和AA完全匹配,最后一个孩子节点AAB也匹配,调用AAB节点的addSorted(bytes), 成为AAB的孩子
先走的这段代码,走进递归
  1. if (matchesToken(bytes) && CollectionUtils.notEmpty(children)) {
  2.       TokenizerNode lastChild = CollectionUtils.getLast(children);
  3.       //和最后一个节点前缀部分匹配
  4.       if (lastChild.partiallyMatchesToken(bytes)) {
  5.         lastChild.addSorted(bytes);
  6.         return;
  7.       }
  8. }
复制代码

然后再走的这段代码
  1. int childNodeDepth = nodeDepth + 1;  
  2. int childTokenStartOffset = tokenStartOffset + numIdenticalTokenBytes;
  3. TokenizerNode newChildNode = builder.addNode(this, childNodeDepth, childTokenStartOffset,
  4.           bytes, tailOffset);
  5. addChild(newChildNode);
复制代码

031505592055316.png

6、添加AABQQ,和之前的一样,这里就不重复了,增加QQ的出现次数

031506003616003.png
构建玩Trie树之后,在flush的时候还做了很多操作,为这棵树构建索引信息,方便查询,这块博主真的无能为力了,不知道怎么才能把这块讲好。




hbase源码系列(二)HTable 探秘hbase源码系列(三)Client如何找到正确的Region Server
hbase源码系列(四)数据模型-表定义和列族定义的具体含义
hbase源码系列(五)Trie单词查找树
hbase源码系列(六)HMaster启动过程
hbase源码系列(七)Snapshot的过程
hbase源码系列(八)从Snapshot恢复表
hbase源码系列(九)StoreFile存储格式
hbase源码系列(十)HLog与日志恢复
hbase源码系列(十一)Put、Delete在服务端是如何处理?
hbase源码系列(十二)Get、Scan在服务端是如何处理?
hbase源码系列(十三)缓存机制MemStore与Block Cache
hbase源码之如何查询出来下一个KeyValue
hbase源码Compact和Split

欢迎加入about云群90371779322273151432264021 ,云计算爱好者群,亦可关注about云腾讯认证空间||关注本站微信

已有(2)人评论

跳转到指定楼层
kanwei163 发表于 2014-12-26 22:13:08
能说说,hbase中的lsm树和这个tire树分别干啥用的吗?
回复

使用道具 举报

kanwei163 发表于 2014-12-26 22:13:56
能说说,hbase中的lsm树和这个Trie树分别干啥用的吗?
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条