注:本文基于Lucene 8.2.0 版本。

说到搜索我们的第一反应应该就是百度、Google这样的搜索巨头,然后一些开发者可能还会想到Solr和ElasticSearch(后文简称ES)这样的开源的全文检索(full-text search)引擎。特别是ES,现在发展的可谓是如火如荼,前段时间还和阿里、腾讯达成战略合作,开始在中国市场发力。不论是百度搜索、Google搜索、Solr或者ES,它们都是为了解决一个问题:信息检索(Information Retrieval,IR)。这是一门非常古老且复杂的学科,现在的人工智能,特别是NLP、数据挖掘领域更是和IR有着千丝万缕的关系。但这些都不是本文的重点,本文要讨论的是一个轻松的话题,那就是Solr和ES背后所使用的全文检索库Lucene,顺便介绍一下简易版的搜索引擎背后的流程。提到Lucene,大多数人应该还对他比较陌生,要么是没听过,要么就是久闻其名,但也说不清这是个什么东西。下面让我来介绍一下。

有趣的故事

1997年的后半年,有一个叫Doug Cutting的人,对手头的工作不是很感兴趣,他觉得自己应该做点自己感兴趣的事情。而当时呢,Java非常的火,他觉得他得找个理由学习一下这门编程语言。作为一个对搜索技术已经很擅长的软件开发者,他决定就拿Java来开发一个搜索软件吧。然后就这样,一个牛逼至今,而且应该还会继续牛逼下去的搜索库Lucene就这样诞生了。当然除了Lucene,Doug Cutting还有很多其他非常厉害的“作品”,比如大家熟知的Hadoop也是他写的。这里再来一点小插曲:Lucene诞生后不久,有一个叫Shay Banon的开发者失业了,他的新婚妻子打算去伦敦学厨师,他也就跟着去了。反正失业也没事做,他就想着为妻子开发一款方便菜谱搜索的应用。于是他就使用Lucene开发了一款应用,名叫“Compass”,也就是ElasticSearch的前身。没错,Shay Banon就是如今非常火的ElasticSearch的创始人,elastic公司的创始人兼CEO。牛人们就是这样,在平凡中孕育不平凡。

好了,现在你已经知道Lucene出自大师之手,那它究竟是什么呢?Lucene是一个高性能的、可扩展的信息检索库。起初只支持Java,现在已经支持很多常见的编程语言了,比如C++、Python等。为什么说这个库牛逼呢,因为信息检索太复杂了,从0写一个搜索软件对于非IR大师来说,基本上是不可能的。而Lucene就是Doug Cutting这个IR领域的大师兼顶级程序员帮我们写的一个库,他把复杂的算法、理论封装到库里面,提供给使用者的是一些简单易懂且不失灵活性的API。Doug Cutting本人也是开源软件的支持者,所以他的作品基本都是开源的,这样也让更多的开发者可以为这些优秀的软件贡献力量。故事先讲到这里,下面我们来看一下全文检索背后的大概工作流程。

全文检索背后的流程

先来看一下下面这张图(此图摘自“Lucene In Action(2rd edition)”一书):

seach engine

图的两侧是一些管理和分析功能,此处可以暂时忽略。中间从上往下是前台用户查询流程,从下往上是后台索引构建流程。我们以百度搜索为例介绍一下每个步骤都做了什么事情。

用户查询流程:

  • 这个比较简单,我们就是用户,即图中的"Users"。
  • "Search User Interface"就是搜索引擎提供给用户的搜索入口,一般就是一个搜索框。
  • 用户输入关键字后,百度后台服务根据关键字构建查询(Build Query),然后执行查询(Run Query)。执行查询的时候会去索引库(Index)里面检索。
  • 查到结果后,返回给用户(Render Results),也就是我们看到的搜索出来的网页。

后台索引构建流程:

  • 原始数据(Raw Content)就是互联网上面的各种各样的数据,然后通过爬虫(百度蜘蛛)等技术收集这些数据(Acquire Content)到百度后台的服务器中。
  • 得到这些数据之后就开始构建文档(Build Document),这一步一般是将从网络上爬取的各种格式的数据转化为文本格式,并去掉一些不必要的部分,比如去掉HTML格式数据中的HTML标签。对于互联网搜索引擎来说,一个文档(Document)一般指一个HTML网页。
  • 接着是分析文档(Analyze Document),这一步的工作比较多。核心的工作是分词,比如"My name is Ni Yanchun"这一句话,经过Lucene的标准分词器分词后的结果是:["my", "name", "is", "ni", "yanchun"]。可以看到,原来的一句话被分成了单个的词,并且全部转化为了小写。分词后的一个个词(也可能是词组)称之为tokens.分词这一步是比较复杂的,不同的分词器得到的结果也不一样。比如”我是中国人“这句话使用ik分词器(一个非常流行的中文分词器)的"ik_max_word"模式分词的结果为:[”我“,”是“,“中国人”,“中国”,“国人”],用"ik_smart"模式分词的结果为:[”我“,“是”,“中国人”]。
  • 分完词,就是索引文档了(Index Document)。注意,这里索引(Index)是动词。索引的主要过程就是根据分词的结果形成一种称之为倒排索引(Inverted Index)的数据结构存储起来。存储的结果称之为索引(Index),注意,这里的索引是名词。倒排索引是全文检索中非常重要的一个概念,这种数据结构的好处在于可以非常高效的查找出一个词在包含在哪些文档里面。当然也有正排索引(Forward Index)。举个例子,一本书的目录类似于正排索引,我们可以很高效的知道某个主题在哪一页,而倒排索引类似于书后面的关键字附录,可以快速知道某个关键字在书的哪些页出现过(书的页码可以理解为文档的唯一标识ID)。

整个流程大概就是这个样子。当然,这里仅是以互联网搜索引擎为例。但搜索引擎只是IR的一种场景,还有很多其他的场景。比如Windows操作系统的资源管理器(对应于Mac OS的Finder)里面也可以进行全文搜索,再比如几乎所有的邮件客户端都支持邮件搜索等等,都属于IR,也符合上面的流程,只是概念对应上有些差异。比如对于邮件客户端来说,一个Document指一封邮件。

Lucene作为一个Java开发的IR库,并非能完成上述的所有流程,它只帮助我们实现了整个流程中的一些非常核心的流程,即图中颜色较深的那些步骤:Build QueryRun QueryRender ResultsAnalyze DocumentIndex DocumentIndex(存储索引)。另外,需要注意Lucene只是一个库,并不是一个开箱可用的产品。你必须根据你自己的场景对其做二次开发才可以成为一个独立可用的产品。比如ElasticSearch、Solr就是基于Lucene开发的开箱即用的全文检索产品。本文就先介绍到这里,下篇文章我会使用Lucene实现一个索引(此处为动词)和查询的例子,来加深对本文中提到的一些概念的理解,同时也会引入一些新的概念。

References

  • Lucene In Action, Second Edtion