商务服务
12 | 面向对象(下):如何实现一个搜索引擎?
2024-12-31 19:20

承接上文,今天这节课的主要目的是,模拟敏捷开发过程中的迭代开发流程,巩固面向对象的程序设计思想。

我们将从最简单最直接的搜索做起,一步步优化,这其中,不会涉及到过多的超纲算法,但不可避免会介绍一些现代搜索引擎中的基础概念,例如语料(corpus)、倒序索引(inverted index)等。

如果对这方面本身有些了解,自然可以轻松理解;即使之前完全没接触过搜索引擎,也不用过分担心,会力求简洁清晰,降低学习难度。同时,希望把更多的精力放在面向对象的建模思路上。

“高大上”的搜索引擎

引擎一词尤如其名,听起来非常酷炫。搜索引擎,则是新世纪初期互联网发展最重要的入口之一,依托搜索引擎,中国和美国分别诞生了百度、谷歌等巨型公司。

搜索引擎极大地方便了互联网生活,也成为上网必不可少的刚需工具。依托搜索引擎发展起来的互联网广告,则成了硅谷和中国巨头的核心商业模式;而搜索本身,也在持续进步着, Facebook 和微信也一直有意向在自家社交产品架设搜索平台。

关于搜索引擎的价值不必多说了,今天我们主要来看一下搜索引擎的核心构成。

听 Google 的朋友说,他们入职培训的时候,有一门课程叫做 The life of a query,内容是讲用户在浏览器中键入一串文字,按下回车后发生了什么。今天也按照这个思路,来简单介绍下。

我们知道一个搜索引擎由搜索器、索引器、检索器和用户接口四个部分组成。

搜索器,通俗来讲就是我们常提到的爬虫(scrawler,它能在互联网上大量爬取各类网站的内容,送给索引器。索引器拿到网页和内容后,会对内容进行处理,形成索引(index,存储于内部的数据库等待检索。

最后的用户接口很好理解,是指网页和 App 前端界面,例如百度和谷歌的搜索页面。用户通过用户接口,向搜索引擎发出询问(query,询问解析后送达检索器;检索器高效检索后,再将结果返回给用户。

爬虫知识不是我们今天学习的重点,这里就不做深入介绍了。我们假设搜索样本存在于本地磁盘上。

为了方便,我们只提供五个文件的检索,内容放在了下面这段代码中

 

我们先来定义 SearchEnginebase 基类。这里先给出了具体的代码,不必着急操作,还是那句话,跟着节奏慢慢学,再难的东西也可以啃得下来。

 

SearchEnginebase 可以被继承,继承的类分别代表不同的算法引擎。每一个引擎都应该实现 process_corpus() 和 search() 两个函数,对应我们刚刚提到的索引器和检索器。main() 函数提供搜索器和用户接口,于是一个简单的包装界面就有了。

具体来看这段代码,其中

add_corpus() 函数负责读取文件内容,将文件路径作为 ID,连同内容一起送到 process_corpus 中。

process_corpus 需要对内容进行处理,然后文件路径为 ID ,将处理后的内容存下来。处理后的内容,就叫做索引(index)。

search 则给定一个询问,处理询问,再通过索引检索,然后返回。

,理解这些概念后,接下来,我们实现一个最基本的可以工作的搜索引擎,代码如下

 

你可能很惊讶,只需要短短十来行代码居然就可以了吗

没错,正是如此,这段代码我们拆开来看一下

SimpleEngine 实现了一个继承 SearchEnginebase 的子类,继承并实现了 process_corpus 和 search 接口,同时,也顺手继承了 add_corpus 函数(当然你想重写也是可行的,因此我们可以在 main() 函数中直接调取。

在我们新的构造函数中,self.__id_to_texts = {} 初始化了自己的私有变量,也就是这个用来存储文件名到文件内容的字典。

process_corpus() 函数则非常直白地将文件内容插入到字典中。这里注意,ID 需要是唯一的,不然相同 ID 的新内容会覆盖掉旧的内容。

search 直接枚举字典,从中找到要搜索的字符串。如果能够找到,则将 ID 放到结果列表中,最后返回。

你看,是不是非常简单呢?这个过程始终贯穿着面向对象的思想,这里梳理成了几个问题,可以自己思考一下,当成是一个小复习。

现在对父类子类的构造函数调用顺序和方法应该更清楚了吧

继承的时候,函数是如何重写的

基类是如何充当接口作用的(可以自行删掉子类中的重写函数,抑或是修改一下函数的参数,看一下会报什么错

方法和变量之间又如何衔接起来的呢

我们重新回到搜索引擎这个话题。

相信你也能看得出来,这种实现方式简单,但显然是一种很低效的方式:每次索引后需要占用大量空间,因为索引函数并没有做任何事情;每次检索需要占用大量时间,因为所有索引库的文件都要被重新搜索一遍。如果把语料的信息量视为 n,那么这里的时间复杂度和空间复杂度都应该是 O(n) 级别的。

而且,还有一个问题:这里的 query 只能是一个词,或者是连起来的几个词。如果你想要搜索多个词,它们又分散在文章的不同位置,我们的简单引擎就无能为力了。

这时应该怎么优化呢

最直接的一个想法,就是把语料分词,看成一个个的词汇,这样就只需要对每篇文章存储它所有词汇的 set 即可。根据齐夫定律(Zipf’s law,在自然语言的语料库里,一个单词出现的频率与它在频率表里的排名成反比,呈现幂律分布。因此,语料分词的做法可以大大提升我们的存储和搜索效率。

那具体该如何实现呢

Bag of Words 和 Inverted Index

我们先来实现一个名叫 Bag of Words 的搜索模型。请看下面的代码

 

你应该发现,代码开始变得稍微复杂些了。

这里我们先来理解一个概念,BOW Model,即,中文叫做词袋模型。这是 NLP 领域最常见最简单的模型之一。

假设一个文本,不考虑语法、句法、段落,也不考虑词汇出现的顺序,只将这个文本看成这些词汇的集合。于是相应的,我们把 id_to_texts 替换成 id_to_words,这样就只需要存这些单词,而不是全部文章,也不需要考虑顺序。

其中,process_corpus() 函数调用类静态函数 parse_text_to_words,将文章打碎形成词袋,放入 set 之后再放到字典中。

search() 函数则稍微复杂一些。这里我们假设,想得到的结果,是所有的搜索关键词都要出现在同一篇文章中。那么,我们需要同样打碎 query 得到一个 set,然后把 set 中的每一个词,和我们的索引中每一篇文章进行核对,看一下要找的词是否在其中。而这个过程由静态函数 query_match 负责。

可以回顾一下上节课学到的静态函数,我们看到,这两个函数都是没有状态的,它们不涉及对象的私有变量(没有 self 作为参数,相同的输入能够得到完全相同的输出结果。因此设置为静态,可以方便其他的类来使用。

可是,即使这样做,每次查询时依然需要遍历所有 ID,虽然比起 Simple 模型已经节约了大量时间,但是互联网上有上亿个页面,每次都全部遍历的代价还是太大了。到这时,又该如何优化呢

你可能想到了,我们每次查询的 query 的单词量不会很多,一般也就几个、最多十几个的样子。那可不可以从这里下手呢

再有,词袋模型并不考虑单词间的顺序,但有些人希望单词按顺序出现,或者希望搜索的单词在文中离得近一些,这种情况下词袋模型现任就无能为力了。

针对这两点,我们还能做得更好吗?显然是可以的,请看接下来的这段代码。

 

首先要强调一下,这次的算法并不需要你完全理解,这里的实现有一些超出了本章知识点。但希望你不要因此退缩,这个例子会告诉你,面向对象编程是如何把算法复杂性隔离开来,而保留接口和其他的代码不变。

我们接着来看这段代码。你可以看到,新模型继续使用之前的接口,仍然只在 __init__()、process_corpus()和search()三个函数进行修改。

这其实也是大公司里团队协作的一种方式在合理的分层设计后,每一层的逻辑只需要处理好分内的事情即可。在迭代升级我们的搜索引擎内核时, main 函数、用户接口没有任何改变。当然,如果公司招了新的前端工程师,要对用户接口部分进行修改,新人也不需要过分担心后台的事情,只要做好数据交互就可以了。

继续看代码,可能注意到了开头的 Inverted Index。Inverted Index Model,即倒序索引,是非常有名的搜索引擎方法,接下来简单介绍一下。

倒序索引,一如其名,也就是说这次反过来,我们保留的是 word -> id 的字典。于是情况就豁然开朗了,在 search 时,我们只需要把想要的 query_word 的几个倒序索引单独拎出来,然后从这几个列表中找共有的元素,那些共有的元素,即 ID,就是我们想要的查询结果。这样,我们就避免了将所有的 index 过一遍的尴尬。

process_corpus 建立倒序索引。注意,这里的代码都是非常精简的。在工业界领域,需要一个 unique ID 生成器,来对每一篇文章标记上不同的 ID,倒序索引也应该按照这个 unique_id 来进行排序。

至于 search() 函数,你大概了解它做的事情即可。它会根据 query_words 拿到所有的倒序索引,如果拿不到,就表示有的 query word 不存在于任何文章中,直接返回空;拿到之后,运行一个“合并 K 个有序数组”的算法,从中拿到我们想要的 ID,并返回。

注意,这里用到的算法并不是最优的,最优的写法需要用最小堆来存储 index。这是一道有名的 leetcode hard 题,有兴趣请参考

遍历的问题解决了,那第二个问题,如果我们想要实现搜索单词按顺序出现,或者希望搜索的单词在文中离得近一些呢

我们需要在 Inverted Index 上,对于每篇文章也保留单词的位置信息,这样一来,在合并操作的时候处理一下就可以了。

倒序索引就介绍到这里了,如果感兴趣可以自行查阅资料。还是那句话,我们的重点是面向对象的抽象,别忘了体会这一思想。

LRU 和多重继承

到这一步,终于,你的搜索引擎上线了,有了越来越多的访问量(QPS)。欣喜骄傲的同时,你却发现服务器有些“不堪重负”了。经过一段时间的调研,你发现大量重复性搜索占据了 90% 以上的流量,于是,你想到了一个大杀器——给搜索引擎加一个缓存。

所以,最后这部分,就来讲讲缓存和多重继承的内容。

 

它的代码很简单,LRUCache 定义了一个缓存类,你可以通过继承这个类来调用其方法。LRU 缓存是一种很经典的缓存(同时,LRU 的实现也是硅谷大厂常考的算法面试题,这里为了简单,直接使用 pylru 这个包,它符合自然界的局部性原理,可以保留最近使用过的对象,而逐渐淘汰掉很久没有被用过的对象。

因此,这里的缓存使用起来也很简单,调用 has() 函数判断是否在缓存中,如果在,调用 get 函数直接返回结果;如果不在,送入后台计算结果,然后再塞入缓存。

我们可以看到,BOWInvertedIndexEngineWithCache 类,多重继承了两个类。首先,你需要注意的是构造函数(上节课的思考题,你思考了吗)。多重继承有两种初始化方法,我们分别来看一下。

第一种方法,用下面这行代码,直接初始化该类的第一个父类

 

不过使用这种方法时,要求继承链的最顶层父类必须要继承 object。

第二种方法,对于多重继承,如果有多个构造函数需要调用, 我们必须用传统的方法LRUCache.__init__(self) 。

其次,应该注意,search() 函数被子类 BOWInvertedIndexEngineWithCache 再次重载,但是我还需要调用 BOWInvertedIndexEngine 的 search() 函数,这时该怎么办呢?请看下面这行代码

 

我们可以强行调用被覆盖的父类的函数。

这样一来,我们就简洁地实现了缓存,而且还是在不影响 BOWInvertedIndexEngine 代码的情况下。这部分内容希望多读几遍,自己揣摩清楚,通过这个例子多多体会继承的优势。

总结

今天这节课是面向对象的实战应用,相比起前面的理论知识,内容其实不那么友好。不过,若能静下心来,仔细学习,理清楚整个过程的要点,对理解面向对象必将有所裨益。比如,可以根据下面两个问题,来检验今天这节课的收获。

能把这节课所有的类的属性和函数抽取出来,自己在纸上画一遍继承关系吗

迭代开发流程是怎样的

思考题

    以上就是本篇文章【12 | 面向对象(下):如何实现一个搜索引擎?】的全部内容了,欢迎阅览 ! 文章地址:https://sicmodule.kub2b.com/news/13916.html
     栏目首页      相关文章      动态      同类文章      热门文章      网站地图      返回首页 企库往资讯移动站 https://sicmodule.kub2b.com/mobile/ , 查看更多   
最新文章
手机贴膜硬核科普,一分钟搞懂8种手机膜的区别手机钢化膜「手机贴膜硬核科普,一分钟搞懂8种手机膜的区别」
创作立场声明:文中列举商品仅为示范作用,与品牌无关。说起手机贴膜,想必大家并不陌生,很多人拿到手机后的第一件事,就是贴膜
你以为它死了,其实它复活了,诺基亚手机回归带来十个疑问高颜值手机「你以为它死了,其实它复活了,诺基亚手机回归带来十个疑问」
  2008年1月16日,德国波鸿,在一次员工示威期间,一位诺基亚公司的女员工落泪。你以为它死了,其实它复活了,是的,说的就是
华为折叠手机2023新款价格 华为最新款手机折叠华为新款手机「华为折叠手机2023新款价格 华为最新款手机折叠」
折叠手机是智能手机的一种造型,柔性AMOLED屏幕是折叠手机的突破关键。寰宇舷窗,探索未来独创寰宇舷窗设计,以探索之姿洞见未⁠
139手机邮箱注册(139手机号邮箱注册)
  关于《139手机邮箱注册》的文章  在当今信息化社会,电子邮件已成为人们日常生活和工作中不可或缺的一部分。而手机邮箱因
信息门户手机信息「信息门户」
我校信息门户于2019年1月上线,与南京大学APP互为移动端服务补充,为师生提供在线服务、消息提醒、推文宣传等服务功能。 微信搜
手机能一直开着录音吗 手机一直开着录音行吗【详解】手机录音「手机能一直开着录音吗 手机一直开着录音行吗【详解】」
  能一直开着录音,但是要保证电量和储存空间的充足。一旦录音的储存空间被占满,录音就会停止,保证电量充足,可以边充边录音
张蔷属于昨天,更属于“明天”(音乐节)v i v o 手机「张蔷属于昨天,更属于“明天”(音乐节)」
张蔷,中国内地流行音乐代表人物,传奇天才女歌手,80年代中国流行文化偶像符号,21世纪迪斯科回潮的新女皇。 从小深受从事音乐
2k14手机(2k14手机版中文版下载)
  《2K14手机》:超越视觉的极致体验  在当今科技飞速发展的时代,手机已经成为了我们生活中不可或缺的一部分。而《2K14手机
适合情侣玩的手机游戏前五名 有适合两个人玩的游戏吗情侣手机「适合情侣玩的手机游戏前五名 有适合两个人玩的游戏吗」
游戏还是两个人一起玩有意思,特别是情侣之间,不但能娱乐,还能增进俩人之间的亲密感情。还有异地恋的情侣们,每天只能依靠煲电
创新之城,非凡园区!星海红领巾访园区展示中心v i v o 手机「创新之城,非凡园区!星海红领巾访园区展示中心」
创新之城 非凡园区红领巾寻访苏州工业园区展示中心 这里的街道宽敞整洁,很少见到密如蛛网的电线和凌乱的街边小店; 这里的马路