开发者学堂课程【高校精品课-上海交通大学-企业级应用体系架构:Searching】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/75/detail/15838
Searching(一)
内容简介
一. Lucene 建立索引的方式
二. 反向索引
三. 搜索
四. 如何建索引
五. 运行
六. 四种域的区别
七. 四种域的实例
八. 如何实现更新
九. 置顶与排序问题
一.Lucene 建立索引的方式
我们来讲解一下 Lucene 建立索引的方式是什么。
按顺序扫描每个文件这种方法由于缺陷很大而且非常耗时间,所以 Lucene 为了使搜索速度变快,先把这些文本做一个索引,不需要去做全文扫描。
Lucene 将索引建成如图所示的样子,当客户抽取了这个文档里的内容之后,客户告诉 Lucene 在这里面我需要设置一些什么样的域,这些域就会去建立一个索引, Lucene 再统计每一个域里不同的值,比如在记录 author 这个域时,我这一堆的文档里面就出现有 author 这样的一个表述,它里面就会有我定义好的什么样的人,有两个不同的人,他们各在一个文档里出现过, author 它有两个不同的取值,各在一个文档里出现过,那这个文档是谁紧跟着的?
比如说这里(如图),叫做 contents 的里面有一个叫 junk 的东西出现过两次,那它在下面的表格里就会告诉你,上面的 action 出现过三次,它也会在下面的表格里告诉你。 unie 出现在两个文档里,然后会在下面的表格里告诉你是哪两个文档,即5和6文档,然后在这两个文档里各出现过一次和两次,那这里一次和两次分别是指,1次是在这个文档5里的第9位置,2次是在这个文档6里的位置1和位置3。这个索引的意思就是,如果未来我要找一个 offer 的名字,那通过这个索引就能看到,它出现过一次,然后到下面的表里看它在哪个文档里出现过,再找它出现的位置,以及在这个文档里出现了几次,根据它的位置,就能找到它在哪儿,这就它的索引。当然这只是一个概念,它里面比这个稍微复杂一点。
二.反向索引
它为什么叫反向索引。跟反向索引对应的自然是正向索引,我们见到的索引的一般概念可能会说,在一个文档里它包含了哪些词,比如词1出现了两次,词2出现了三次等,然后还有一个表,记录都有哪些文档,比如有文档一、文档二,文档一包含哪些词。
这是我们想要的一个正向索引。正向索引为什么用于查找都比较慢,比如用户想找包含 Tom 的这种人有多少,那就必须先去找有多少文件,在每个文件里面去找 Tom 这个词有没有被统计到,如果有,他出现了多少次,然后把这些次数全部加起来,才能知道 Tom 在当前的这一组文件里出现了多少次,效率很低。如果反过来写,先把所有的文档里的内容拿出来,比如 Tom 出现了,出现在十个文档里,这十个文档分别是哪些文档,在每一个文档里它都出现了几次,每一次出现在这个文档里的第几个词。就是和正向索引反过来,所以所谓的反向索引就是在和正向索引的方向相反,它不是按照文档这种结构去建索引,而是反过来,拿词里面出现的内容建索引,指向出现在哪一些文档的什么位置上。但是建好这种索引之后,它就适合于我们做全文搜索,你确实就是,在整个这一个目里所有的文件里,去找这一个词出现在哪里,这就是 Lucene 建立这个反向索引的原因。那么,反向索引就意味着它和正向索引不同,几乎所有的全文搜索的引擎都是用反向索引这种方式建立索引。但是有的不建任何索引,一样能实现这个功能,比如关系性数据库,没有索引也能实现,直接扫描就可以。建立反向索引的目的就是要让客户在对数据进行操作的时候,速度更快。正是因为它建立了这样的反向索引,所以客户在里面按内容去搜索的时候,比如要搜某个词出现多少次的时候,它就会比较快。这是概念上的一个解释,实际上比这个要复杂,所以我们后面要稍微展开去讲它这个复杂程度指的是什么。
三.搜索
我们现在可以看到,有了这样的索引,现在搜索,就可以只在刚才的这个索引去找这些词,它就会告诉你这些词出现在哪一些文档里面,它出现的位置是什么。
如此,通过反向索引就可以使搜索的速度变快,总的来说,它就在加快速度。但是在加快的时候到底能快到什么程度,不是只有一个索引就能完善的,还与文件数量有关,文件数量多到一定程度可能会影响搜索速度,当然相对于全文扫描要快的多,还有就是与搜索内容有关,现在是找一个词会比较简单,当条件比较复杂时比如 Tom 和 Jerry 同时出现,或者出现 Tom ,但是没有和 Jerry 一起出现的概率是多少,就是类似这种复杂的查询、短语的查询、通配符查询它是不是都支持。
那我们找一个例子,就是在讲 Lucene 的时候,既然要去做全部搜索它包含两个部分,一个是做索引,一个是做搜索,那么这两件事分别是怎么做的。为了去讲清楚这件事情,这个例子没有直接说,对一个文件操作,而是对一个目录里所有文件进行操作,这样去做这个事情。
那我们来看一下,先讲一下例子,然后讲解代码。先建索引而且导入Lucene 的包,然后在执行时用命令行传递两个参数,一个是建好索引后放在哪里,第二个是对哪个目录里所有的文件进行操作。它里面核心的函数是这个“ index(indexDir,dataDir)”,就是我们要去建索引,前后只是记录一下建索引用了多少时间。
四.如何建索引
1.前期准备
首先检查一下要处理的目录和建好的索引在不在,建好索引要注意,前面的目录(第二张图片),它不是一个文件而是若干个文件,要放到一个目录里,所以建好的索引要放到一个目录里面。然后建索引针对的那些文件在一个目录,两个目录先做了一下防御性的编程,就是在看这两个目录本身是否存在,接着是 Lucene 的代码,红颜色的是跟 Lucene 相关的代码。用 Lucene index writer 这个对象向里面写东西。
2.Analyzer 和 optimize
实际上写的内容进来之后需要做解析,解析之后需要通过分析器去分析,即把里面关键词抽出来,然后去记住这个词在哪些位置出现、在哪些文章里出现等等,主要是靠分析器来做处理,然后写入索引的目录里,写成几个文件。
所以在建立索引的时候,除了要用一个 writer 把索引写进去,还需要有一个 Analyzer 。因为在这个例子里面,我们放入的是常规的内容,所以用的是 Standard Analyzer ,如果有一些特殊的需求,可以自己实现一个 Analyzer,在这个例子中我们用标准 Analyzer 来做分析、产生索引,然后我们在这里面写了一个关键的建立索引的内容,那么这个过程就在建立索引。建立好索引之后,在这个 writer 里面把文档的数量写进去,然后一定要调一下optimize ,optimize 它才真正把这个内容写入到文件,并且把它写到硬盘上去,这时候这个才能用索引。否则,如果没有 optimize ,那么索引可能只在内存里,其他用户是看不到。
建索引的过程用的是递归的过程,就是它要取里面的每一个文件,对每一个文件操作,所以先去检查一下,我先把这个目录里所有的子目录和文件全部装载到 files 数组里,然后遍历这个数组查看它是不是一个子目录,如果是子目录继续递归调用,如果不是子目录已经是个具体文件了,那就用 indexFile 这个函数传递进来 writer 去把 file 处理一下。
3.indexFile 是如何实现的
后面我们这里写的这个函数,这里面红颜色的是跟 Lucene 相关的东西,前面是他的一些防御性编程,要建立一个 document ,在 Lucene 看来,数据进来之后全部都是一个一个的 document ,只需要对这些 document 做索引,因此要将它组装成 document 。进来的一个只要是单个文件就让它处理,那么进来一个文件, Lucene 就建立一个新的 document ,把这个文件里面的内容用 FileReader 读出来,读出来以后要塞到这个 document 里面去,这个 document 就像一个二维表,也有很多的字段,或者说,可以认为它像个 Json 一样有很多的域,其中有一个叫 text , text的意思是 text 这个域里的内容,将来在建索引的时候就是建立索引的依据,并且它要直接存在索引。我们把内容写到这个 text 里面,这个里面本身也可以是一代若干个不同的属性,我们现在给它定义一个属性叫 contents ,也就是说,它有 text 类型或者其他类型, text 里面又分为各种类型比如 contents ,但是这个 contents 是我们自己自定义的。也就是说,在 document 里增加了一个 text 类型的名字叫 contents 的一个属性,它的值可以从文件里读出来。然后在这个 document 里面又增加了一个叫做 keyword 类型的 document 。keyword 也会被建立索引,也会被写入到索引里面去,但是他本身不参与这个索引。可以理解为在这个里面还有叫 keyword 类型的属性,我们对它起的名字叫做 filename ,就是把这个 f 去掉它带路径的那个名字之后,把剩下的这部分写进去,前面就是在组装这个 document 。
现在我们是把要建立索引的那个文件里的内容拿出来放到了 document 里面,还把文件名放到里面,但是它们两个的域的类型不一样。然后我们讲 writer ,writer 就是往索引里面写东西,告诉 Lucene 现在多了一个 document, 即现在创建的这个document ,你将它 at 进去。
4.递归调用的过程
这样的话,我们可以看到这个递归调用的过程:如果是子目录它就一级一级往下,如果是文件,它直接写。也就是说这个 writer 里面,未来就添加了一系列的 document ,这个方法执行完之后到这里( writer.optimize),write 只是往里面添加了一系列的 document ,如果不做 optimize ,索引就无法生成,所以一定要调一下 optimize ,索引生成之后把它关闭掉,那这个索引就能被其他人使用,这是建索引的过程。
五.运行
运行一下刚才这个代码。然后传递给 Lucene 这个目录,比如指定一个目录去跑,它就会告诉你,已经把这里面所有内容全跑了一遍。这个目录里可以带子目录,它就把这目录里所有的内容全部读出来去建立一个反向索引。
反向索引建好后,客户就可以去搜索。如何去搜索呢?客户给它一个参数,即要搜什么、索引在哪里,前面是一样的要做一个防范性编程,然后它就开始搜索,搜索的时候就在指定的索引目录里去搜。
搜的时候,首先要打开这个索引 searcher,然后创建一个搜索的字符串,这个字符串即包含要搜什么、要在哪里搜。刚才在这里添加了一个名为 contents 的域,所以要说清楚要在哪里去搜,contents 就将两个关联起来。其次要按照什么样的 Analyzer 去搜,因为我们建索引的时候用的就是 StandardAnalyzer ,所以在这里搜的时候仍然用了它,这样做完之后,我们就在 searcher 对象上面去执行 query ,那它就会找到满足这个 query 的结果,这些结果就放到了一个叫 hits 的对象里 ,hits 对象实际上是个数组,所以我们可以遍历它。它里面就放进去了所有满足这个查询条件,就是 contents 里面包含了满足查询条件的 documents ,所以在里面去遍历的时候就可以取出一个一个的 documents ,那去把 documents 里面的 Filename ,即 keywords 里面的 filename 这一个值取出来,那它就可以输出所有包含了要去查找的文件的这个文件名,它就会产生这样的结果。
在这里举一个例子,给大家演示一下 soft 。这是 Lucene 的例子,那这个例子和你们现在看到的稍微有点差异的是我稍微改了一点,因为目录的问题稍微改了一点,就是这里建了一个resource目录,里面放了两个 Lucene 的文件,这两个文件是啥呢,就是 Lucene 的 license 文件,和一个说明文件。在这边 index file 文件的时候,我会让它去处理这个 resource 目录里面的东西,其他的跟刚才写的差不多。
它在执行的时候就会去建这个索引,这时我指定让它去建 resources 目录里面的东西, resources 目录里面就会有一个隐藏文件,还有两个是我刚才放进的 Lucene1 和 Lucene2 。这个索引建完之后,我们就可以进行搜索,搜索代码跟前面基本上一样。为了在上面能够自己搜输入,我稍微做了一下调整,比如我们去搜Apache ,它就会搜到在 Lucene1 和 Lucene2 中都有,我可以看一下 Lucene1 的 Apache 在这里面的位置,反正他要在这儿有一个 Apache,他就会说在这两个文件里面都出现了 Apache ,它的运行方式就是这样。对于 Lucene 的运行方式,它不需要自己去取一个所谓的 Lucene 服务器,它没有这个概念,你只要在你的包里面打进去 Lucene 相应的包就可以。
在这里面,我们看到的有一个 Lucene 的 call ,如果你用pop 文件来做这个,用 main 来开发的话,就要把它打进 Lucene 。这个 index 就是它刚才跑完之后产生的索引,我们让它把索引写到了如图所示的位置, index 实际上写了好几个文件,共同构成了
index 文件。
打开一个就能看到它生成的格式,它的格式是什么东西我们不知道,反正就是按照它自己的格式去写进去。
如图所示为它生成的 index ,我们通过 Lucene 知道了大概的过程,即它是如何生成反向索引的、如何跑代码的。但实际上,如果大家上 Lucene 的网站就会发现和 Lucene 的网站并列的 Soft 这个工具是在 Lucene 的基础上做了一次封装产生的,就是为了让它更好用,因此 Lucene 本身的原生的 Lucene 的编码,我们就不需要详细探讨。然后我们来看 Lucene 里面的原理,因为 so 其实是在 Lucene 的基础上开发出来的。在进行操作的时候,在里面写的东西有 keywords 、有 text ,其实它还有两种东西, unindexed 和 unstored ,就是往 Lucene 的 document 里面去写 field ,即域的时候,它有四种不同类型的域。也就是在这边我们看到有 keywords、unindexed、unstored、text ,这个document 里面有四种不同的域,刚才我们是遇到了 text 和 index ,用这四种不同的域添加到 writer 里面,去做索引的时候,它处理方式是不一样的。