ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 引子 如何能实现全文检索的功能呢? 可以使用SQL的like 数据量小的时候Like还凑合,但是数据量大了,效率就非常低了 那么可以使用inverted index ## Inverted Index Inverted Index被翻译成倒排索引 比如有两篇文档: * 文档1:A computer is a device that can execute operations * 文档2:Early computers are big devices 把这两篇文章中的单词都抽取出来,并且记录下这个单词出现在哪个文档中。 ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821095752.png?picgo) 通过这个 “倒排索引”,只要给出一个单词,就可以迅速地定位到它在哪个文档中。 用来做全文搜素非常合适。 这个倒排索引有些粗糙,可以精化一下 1. 对于 a, is , to ,that ,can ,the,are 这些词对搜索来说没什么意义,用户几乎不会用他们搜索,可以过滤掉。 2. 用户搜索的时候虽然输入了 computer,但是也希望搜出 computers 出来,所以需要把复数单词,过去式单词等做还原。 3. 用户虽然输入了 device , 但是也希望搜索出 Device 出来,所以需要把大写都改成小写。 经过这番转换,文档 1 和文档 2 的关键字变成了这样: 文档 1:[computer] [device] [execute] [operation] 文档 2:[early] [computer] [big] [device] ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821095957.png?picgo) 为了更好的统计和高亮显示搜索结果,还可以记录关键词在文章中出现的次数和位置(例如:是第几个单词) ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821100043.png?picgo) ## 架构 只要把那些书籍的标题,介绍,作者等信息给提取出来,形成 inverted index,不就可以做关键字检索了吗? 这个需求应该是个通用的需求,不仅是图书馆有,很多互联网应用,例如网上商城也需要, 我完全可以设计一个类库出来 ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821100249.png?picgo) 不管你是什么类型的文档,HTML, PDF, Word,甚至是数据库的数据,只要能从中抽取出文本,就可以当作我的数据源,对文本做了分析以后,就可以存入索引库。 用户可以发出各种查询,不仅仅是单个关键字,还支持关键字的组合: keyword1 AND keyword2 等,对索引库进行搜索以后,把结果返回给用户。 ## 抽象 接下来就是API设计了。 软件设计就是一个不断抽象的过程。既然有 HTML,PDF, Word 文档, 很自然,你可以做一个 Document 的抽象 但是如果数据源是数据库,怎么变成 Document,比如一行数据可以映射到一个Document,当然,这个转化必须得有程序员来完成。程序员决定什么东西是 Document Document有什么东西? Document 可以有很多属性,每个属性都有名称和值, 我可以把属性叫做 Field。 比如一个 HTML 文档,可能有 path, title,content 等多个 Field,其中 path 可以唯一地标志这个文档, title 和 content 需要做进行分词,形成倒排索引,让用户搜索 ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821100658.png?picgo) 用户创建了 Document 和 Field 以后,就可以进行分析了,原始的内容划分成一个个 Term,这样,我可以定义一个 Analyzer 的抽象类,让别的程序可以扩展它 分析的结果可以加入索引库,这个操作可以让一个单独的类,比如 IndexWriter 类来完成。 可是 IndexWriter 如何知道索引应该存在什么地方? 内存? 文件系统 那就再来一个抽象的概念:Directory,表示索引文件的存储 ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821101105.png?picgo) 对于对于用户搜索来说,肯定得有个叫做 Query 的东西,用来表达用户的搜索要求。当然,这个 Query 也应该允许扩展。 ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821101128.png?picgo) 但是这些类使用起来还是比较麻烦, 最好是支持用户输入字符串来表达搜索的意图:(computer or phone) and price 所以必须得有个解析器来完成从字符串到 Query 的转化,然后我们让 IndexSearcher 来接收 Query,就可以实现搜索的功能了 ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821101246.png?picgo) 有个叫做 Lucene 的开源系统,和我们的设计很像啊,功能更加强大 ## 三年之后 人们发现Lucene用起来太低级了,比如想搜索一下我的商品描述,现在还得理解什么 “Directory”,"Analyzer","Query",实在是太复杂了! 还有我们的数据越来越多,索引也越来占的空间也越来越大,怎么才能实现分布式的存储啊 是时候对 Lucene 做一层封装,提供一个简单的 API 了...... ## Lucene的问题 * Lucene做搜索很强大,但是**API用起来太低级**,想搜索一下我的产品描述,现在还得理解什么 “Directory”,"Analyzer","IndexWriter" * 互联网的数据是海量的,仅仅是单机存储索引是远远不够的。 俗话说:软件开发中遇到的所有问题,都可以通过增加一层抽象而得以解决 ## Java API -> Web API Web 开发面对的都是领域模型,比如 User,Product, Blog,Account 之类。用户想做的无非就是搜索产品的描述, 搜索 Blog 的标题,内容等等 如果能围绕领域模型的概念进行搜索的各种操作就好了。 Web 时代中,程序员都喜欢采用 RESTful 的方式来描述一个 Web 资源, 对于搜索而言,完全可以借鉴一下 /coolspace/blog/1001 : 表示一个编号为 1001 的博客 /coolspace/user/u3876:表示一个 ID 为 u3876 的用户 * /coolspace 表示一个 “索引库” * /blog ,/user 表示 “索引的类型”(可以理解为编程中的领域模型)。 1001, u3876 表示数据的 ID,格式是 /<index>/<type>/<id> 如果和关系数据库做个类比的话: > 索引库 <---> 数据库 > 索引的类型 <---> 数据库的表 用户看到的就是领域模型, 当用户想去操作时候,用 HTTP 的 GET, PUT 等操作就好了,交互的数据可以使用 JSON 这个简单的格式。 ## 基本操作 * 把文档加入索引库 把一个 blog 的 “文档” 加入索引库,这个文档的数据是 JSON 格式,其中的每个字段将来都可以被搜索: ~~~ PUT /coolspace/blog/1001 { "title" : "xxxxxxx", "content" : "xxxxxxxxxxxx", "author" : "xxxxx", "create_date": "xxxxxx" ... } ~~~ * 把一个blog文档删除 ~~~ DELETE /coolspace/blog/1001 ~~~ * 用户搜索 用户想搜索的时候也很简单,发起一个这样的请求就行: ~~~ GET /coolspace/blog/_search ~~~ 但是如何表达查询的具体需求呢,这时候必须得定义一个规范了,例如:想查询一个内容字段(content) 包含 “java" 的 blog。 ~~~ GET /coolspace/blog/_search { "query" : { "match" : { "content" : "java" } } } ~~~ 也就是query可以增加更加复杂的条件,表示用户的查询需求,因为是JSON格式的,可以非常的灵活,返回值也是JSON ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821140336.png?picgo) 通过这样一个抽象层, Lucene 那些复杂的 API 全部被隐藏了 ## 分布式 如果索引太大,我们把它切割一下,分成一片一片的,存储到各个机器上不就得了 ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821140440.png?picgo) 那么怎么要去保存索引和搜索索引呢? 首先要保存每个分片(shard)和机器之间的对应关系, ~~~ 分片 1 :node1 分片 2 :node2 分片 3 :node3 ~~~ 可以用余数算法来确定一个‘文档’到底保存在哪个 shard 中。 **shard 编号 = hash(文档的 ID) % shard 总数** 这样对于任意一个文档,对它的 ID 做 hash 计算,然后对总分片数量求余, 就可以得到 shard 的编号,然后就可以找到对应的机器了 但是如果用户想增加shard数的时候余数算法就不能用了。 比如原来 shard 总数是 3, hash 值是 100, shard 编号 = 100 % 3 = 1 假设用户又增加了两台机器,shard 总数变成了 5, 此时 shard 编号 = 100 % 5 = 0 , 如果去 0 号机器上去找索引,肯定是找不到的。 要不采用分布式一致性算法, 嗯,它会减少出错的情况,还是无法避免,这该怎办 我们可以立下一个规矩: 用户在创建索引库的时候,必须要指定 shard 数量,并且一旦指定,就不能更改了 ~~~ PUT /coolspace { "settings" : { "number_of_shards" : 3 } } ~~~ 虽然对用户来说有点不爽, 但余数算法的高效和简单确实太吸引人了 索引数据分布了,如果某个节点坏掉了,数据就会丢失,我们得做个备份才行 可以用新的node来做replica,也可以为了节省空间,复用现有的node来做replica。 之前的分片叫做主分片,primary shard ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821140804.png?picgo) ~~~ PUT /coolspace/_settings { "number_of_replicas" : 2 } ~~~ 虽然主分片的数目在创建 “索引库” 的时候已经确定,但是副本的数目是可以任意增减的,这依赖于硬件的情况:性能和数量。 “现在每个主分片都有两个副本, 如果某个节点挂了也不怕,比如节点 1 挂了,我们可以位于节点 3 上的副本 0 提升为 主分片 0, 只不过每个主分片的副本数不够两个了 ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821140950.png?picgo) 等到节点1再次启动的时候,还可以恢复副本 ## 集群 现在我们建立了一个集群, 这个集群中可以包含若干 node , 有数据的备份,能实现高可用性。 那么另一个问题出现了对于客户端来说,通过哪个 node 来读写‘文档’呢 可以让请求发送到集群的任意一个节点,每个节点都具备处理任何请求的能力 具体为: (1)假设用户把请求发给了节点 1 (2)系统通过余数算法得知这个'文档'应该属于主分片 2,于是请求被转发到保存该主分片的节点 3 (3)系统把文档保存在节点 3 的主分片 2 中,然后将请求转发至其他两个保存副本的节点。副本保存成功以后,节点 3 会得到通知,然后通知节点 1, 节点 1 再通知用户。 ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821141106.png?picgo) 如果是做查询呢? 比如说用户查询一个文档: GET /coolspace/blog/1001 查询的请求也可以分发到任意一个节点,然后该节点可以找到主分片或者任意一个副本,返回即可 (1) 请求被发给了节点 1 (2)节点 1 计算出该数据属于主分片 2,这时候,有三个选择,分别是位于节点 1 的副本 2, 节点 2 的副本 2,节点 3 的主分片 2, 假设节点 1 为了负载均衡,采用轮询的方式,选中了节点 2,把请求转发。 (3) 节点 2 把数据返回给节点 1, 节点 1 最后返回给客户端。 ![](http://p8a6vmhkm.bkt.clouddn.com/picgo20180821141210.png?picgo) 这种方式比较灵活,但是要求各个节点互通有无。 对于一个集群来说,还得有一个主节点(master node),这个主节点在处理数据请求上和其他节点是平等的,但是它还有更重要的工作,需要维护整个集群的状态,增加移除节点,创建 / 删除索引库,维护主分片和集群的关系等等 如果这个主节点挂了,就只好从剩下的节点中重新选举了 就涉及到分布式系统的各种问题了,什么一致性,脑裂 所以我们可以只选取一个Master, 比如一个Bully 的算法, 改进一下应该就可以用了