Muzik-Online中的古典音乐搜索引擎

去年中的时候,公司设立的搜索引擎系统因为年久失修,也没人知道当初建立的方式和如何应用,为因应产品新需求,重建和开发的责任就落在我身上了。

搜索引擎算术是信息检索(IR)领域的一环,而IR的目的是希望能够从各样不同结构的原始资料中,简单且快速地找寻到正确的资讯。目前比较主流的开源搜索引擎为Lucene, Solr和Elasticsearch,而Solr和Elasticsearch均基于Lucene。

为什么需要搜寻引擎

在我们的资料库中,包含了许多古典音乐领域的元数据,包含作曲家,乐章,乐曲,公版音乐和演出者等各种资料。考量到使用者大多都是透过文字方式搜寻想要的乐曲,且搜寻的栏位会需要加入各个表格。目前RDBMS对于文本搜索的支持度不一且效率不佳,因此通过专门处理文字搜寻的搜索引擎是一个更好的解决方案。

技术评估

考量Elasticsearch的生态系统(ELK)最终更好且目前声势持续看涨中(和阿里云合作),再加上线上系统亦是旧版本的Elasticsearch,最终决定将版号升级至Elasticsearch v.5.6。

为什么是v.5.6?

因为在v.6.0以上版本,已经强制限制一个索引仅允许使用一个Type,由于人力和时间的不足,为了减少程序码修改的幅度,先将版本升级至和v.6.0相容性更好且且尚无此限制的v.5.6版本。

基础概念

Elasticsearch的观念十分简单易懂,大致上组成如下:

  1. 文献

行数据的最小单位,可预先定义各字段的数据类型。

2.键入(从v.6.X删除)

原先类型是设计成RDBMS中的Table概念,在RDBMS中,各Table列彼此间独立不互相影响,但在Elasticsearch中,同样的类型仅能在同一个索引建立一次,有可能会造成Data Model设计上的替换,因此在6.X的版本以后就将类型删除。

3.索引

定义文档的集合,例如用户索引的文档字段设计为用户的个人资料包括姓名,性别或喜好等资料。

4.碎片

由于一个索引可能包含数百万至数百万的文档,导致存储容量超过一个硬碟的最大容量,为了解决这个问题,Elasticsearch以一个索引为基础,将资料分散到N个碎片中,以达到分散资源的目的。

5.节点

Node为Elasticsearch Cluster中的一个节点,每个Node在Cluster中皆有一个唯一的UUID作为识别的标记。每个单独唯一加入一个Cluster。

6.集群

群集为Node的集合。

开发的一些事

  • 概念

在研究基础概念之后,有必要再对搜索引擎有更深入的理解。非常建议阅读这篇论文,搜索引擎,可以帮助开发者更了解逐步的运作机制。如果你非常懒惰,我也整理好一张图,就把它当成是一个Concepts吧。

如果您对Text Mining本来就有些概念的话,这一张图应该非常容易理解。对于初次接触搜索引擎要么想了解的人可能有点困难,让我分别以最基本的两个动作,索引(存)和Search(搜),来说明这张图。

  • 指数

绿绿的内部包含了所有在索引阶段会实际处理的行为,其中可以分割成黄黄蓝蓝红红

黄黄 :在Text Mining中,为了将原始数据转换成后续重组分析的模式,首先应透过字典档,将机器无法识别的字串断成数个可用的术语。在Elasticsearch内本身就提供几个分析器来断词,另外也可以通过Plugin的方式安装第三方分析器。分析器由Character Filter,Tokenizer和Token Filter所组成。CharacterFilter能将原始数据的字元转换成你所对应的字元。Tokenizer能,将转换完后的资料,读入字典后将资料断词成术语数组。最后,令牌过滤器读取同义词(名词)后将术语数组转换成Pattern Array。大致上的流程如以下的例子:

  #原始资料 
贝多芬交响曲-> ->#可删除
贝多芬交响曲-> ->#条款Arrray
[“贝多芬”,“交响曲”]-> ->
#模式数组
[“贝多芬”,“交响乐”]->

由于本文中的非文本挖掘专文,因此许多分析的细节未包含内部文中,请多多包涵QQ。

蓝蓝 :倒排索引是搜索引擎的精髓所在,相较于RDBMS以主键作为各行的存储位置,搜索引擎则以术语指向各文档(行)的存储位置。例如,当有一笔行数据, “贝多芬第五号交响曲”,要存入Elasticsearch时,假设最终被断词为两个术语来代表这个文档,“贝多芬”和“交响曲”,则在倒排索引内便会加入此两个术语并指向文档实际所在的位置。

红红 :这区块链为各文件实际存放的位置,每个文件会包含预先设计好的Fields以及相对应的资料。

  • 搜索

再贴一次这张图,此图完整的描述了搜寻步骤。

  1. 使用者输入“贝多芬交响曲”。
  2. Analyzer遵循字库的规则,将输入转成字词数组,[“贝多芬”,“交响曲”]。
  3. 从Inverted Index中找到相对应的文档。
  4. 透过排名模块算出文件的顺序,按Elasticsearch顺序使用BM 25。
  curl  /  /  / _ search?解释\ 
-d'{“ query”:{“ multi_match”:{“ query”:“ multi_match”:{“ query”:“贝多芬交响曲”,“字段”:[“ work_name”,“ composer_name”]}}}} '

5.按照分数的大小由高至低回传给使用者。

  • 设计数据模型

有了概念之后,最重要的便是决定实际上要以某种方式存储资料。我通常都建议一个规则, 取决于您的搜索方案。

情境:我们希望使用者能搜寻到“乐曲”列表。

但在搜寻时,使用者可能将会想要透过其他资讯,例如音乐家姓名或是乐章,搜寻到相关的乐曲。为了连结不同实体的关联性,弹性官方文件有提供一些设计的原理可以参考,其中差异最大的是嵌套对象和父子关系。

嵌套对象将不同的实体合并到同一个索引映射,以Muzikair的情境来说,“乐曲”的索引映射如下:

  #嵌套对象关系 
#工作->动作(一个动作有很多动作){
“ MUZIK_INDEX”:{
“映射”:{
“ MUZIK_TYPE”:{
“ composer_name”:{
“ type”:“文字”,
“ analyzer”:“ muzik_analyzer”
},
“ work_name”:{
“ type”:“文字”,
“ analyzer”:“ muzik_analyzer”
},
#嵌套对象
“运动”:{
“属性”:{
“ movement_name”:{
“ type”:“文字”,
“ analyzer”:“ muzik_analyzer”
}
}
}
}
}
}
}

亲子关系则是将不同的实体用不同的文档来存储。听起来有点玄,何谓不同的Document?在Elasticsearch中,若要使用亲子关系的设计样式,由于填充数据的存储方式,因此限制不同的实体必须要存储在同一个Shard中。为了要在同一个Shard连结不同的实体,Elasticsearch使用了Join Field来描述不同实体的关联。因此,和Nested Object相同的关联性表示方式改成用亲子关系看起来会类似如下表示:

  #亲子关系 
#工作->动作(一个动作有很多动作){
“ MUZIK_INDEX”:{
“映射”:{
“ MUZIK_TYPE”:{
“ composer_name”:{
“ type”:“文字”,
“ analyzer”:“ muzik_analyzer”
},
“ work_name”:{
“ type”:“文字”,
“ analyzer”:“ muzik_analyzer”
},
“ movement_name”:{
“ type”:“文字”,
“ analyzer”:“ muzik_analyzer”
},
#亲子联接字段
“ join_field”:{
“ type”:“ join”,
“关系”:{
“工作”:“运动”
}
}
}
}
}
}

毕竟Elasticsearch不是RDBMS,亲子关系有一些先天限制,在此节录较重要的副本,详细描述可参考官方文件:

  1. 父母和孩子必须被存在相同的Shard内。
  2. 父母可以关联多个孩子;但一个孩子只有能有一个父母。
  • 索引步骤

决定数据资料结构和关系后,接着需要实作资料更新的方式。通常资料来源遵循需求而定,可能是rsyslog,Fluentd或RDBMS。若资料属性不是时间序列资料的话,则依造需求编制更新的时间点和频率。

  • 搜索步骤

基本的查询DSL主要包含两个块,查询上下文和过滤上下文。查询上下文描述搜寻的逻辑,而过滤上下文则是将搜寻后两个结果根据条件进行过滤再回传给用户。在两个块内,实际表现逻辑的语法则称为Leaf Query。

假如我们提供给使用者能量搜寻的栏位包含“乐曲名称(work_name)”,“作曲家(composer_name)”和“乐章名称(movement_name)”,且须“完全符合”使用者输入的查询字串,并仅回传QC过的乐曲给使用者。

如果此情境使用RDBMS的SQL查询表示可能如下:

 选择 
work.work_name,
composer.composer_name,
Movement.movement_name
从工作
加入composer_work
composer_work.work_uuid = work.work_uuid
加入作曲家
composer.composer_uuid = composer_work.composer_uuid
加入work_movement
work_movement.work_uuid =工作.work_uuid
参加运动
Movement.movement_uuid = work_movement.movement_uuid
哪里
中的work.work_name和work.qc ='pass';

转换至Elasticsearch查询DSL如下:

  GET  /  / _搜索 
'{
“查询”:{
“布尔”:{
“必须”:[
{
“ multi_match”:{
“查询”:,
“ type”:“ cross_fields”,
“ operator”:“和”,
“字段”:[“ composer_name”,“ work_name”,“ movement.movement_name”]
}
}
]
“过滤器”:[
{
“ term”:{“ work.qc”:“ pass”}
}
]
}
}
}'
  • bool为搜索引擎中很重要的基本概念,亦即所有的搜寻语句,皆是由各叶子查询通过“ OR”或“ AND”的逻辑组合构成。在Elasticsearch中提供了逻辑,必须,应,过滤器和绝对不能,详细的说明请参考官方文件。
  • 在必须查询上下文内,有一个multi_match的Leaf查询,其主要用途为可用单一搜寻语句跨多个栏位搜寻,并且可以自行决定判定比对成功的类型和逻辑运算符。根据情境的描述,我们希望回传的资料需要完全符合使用者的查询字串,因此类型使用cross_fields且运算子使用及,代表只要搜寻的全部栏位能包含所有的查询字串便能符合条件,且不需要其他栏位完全符合即可以回传资料。
  • 而在Filter Content内,过滤掉QC没过的乐曲,最终符合条件的所有资料便会回传给使用者。

如果一切都顺利的话,再经过一些网页前重新的处理后,结果便会和我们的搜寻相近:

‘贝多芬奏鸣曲’搜寻结果| 听古典| MUZIK航空

编辑说明

www.muzikair.com

维运的一些事

由于Elasticsearch由Java所编写,因此监视的重点和一般的Java应用程序。Java开发人员最头痛的GC问题我暂时还没有深入的研究,希望有很大的能量可以提供一些建议。

  • 节点角色

遵循官方文件的描述,Elasticsearch Node可以按照不同的功用分离成多个节点,例如Master Node和Data Node分离。目前我们的使用状况尚未遇到搜寻或储存,因此每个Node所有的角色都启用。

  • 打开文件

在Linux中所有的物理文件,虚拟文件​​或Socket都是文件,因此为避免打开文件限制,必须提高系统ulimit。

  • 记忆锁

为了避免JVM内存开销造成交换,更严重地影响群集的完整性,Elasticsearch建议启用bootstrap.memory_lock来固定JVM的堆大小。

Elasticsearch最小HA规模需要至少三台主节点,按照用户对实例的容错程度,如三台主节点可忍受一台挂掉,而五台主节点可忍受两台挂掉。

  • 字典更新

字典的的更新一直是一个很麻烦的问题,在我们的系统内使用处理中文断词的分析器。ik_analyzer提供远程的字典设置,让用户不用重新启动整个群集便能得到最新的词库。虽然已经处理过的文章仍旧需要Reindex,但减少了重新启动Cluster的次数及可能造成的风险。

总结

本文简单的解释Muzikair为何需要通过由Elasticsearch来协助使用者搜寻古典音乐的相关资讯。什么样的概念和知识才能够符合自己的需求。

信息检索是一个非常复杂的领域,许多文字处理和特征检索取的副本更是影响最终资料质量的重要因素,路过的大大如果有各种想法的话欢迎留言讨论QQ。