145. 搜索search

搜索原理:

有些搜索是面向数据库字段的,比如用户搜索通常只需进行用户名匹配,这样的搜索比较简单,直接进行数据库匹配即可,但有些搜索是面向页面的,即URL,而不是面向字段,这也是最常使用的搜索,毕竟用户只关注最终看到的整体信息,而不管系统内部是如何存储的,谷歌、百度等搜索引擎即如此;在系统内部(不管是Drupal还是其他系统),通常一个页面由多个字段共同构成,任何一个字段都可能会影响最终页面,如果搜索直接作用在这些字段上,那么会有很多问题,比如数据库操作会很复杂,权限难于控制,性能消耗大,难以彻底解决的问题还在于最终页面形成前可能还涉及特殊修改,而导致修改的因素并不存在于数据库中,因此面向页面的搜索不能直接作用在数据库的字段上,需要真实的进行页面渲染,得到渲染结果后再进行搜索,搜索也不能直接作用于页面缓存,因为有些页面是不可缓存的,且不能保证缓存一直存在;这样一来新的问题又产生了,如果每次搜索都把站点的内容全部渲染一遍那么在性能上是不可接受的,何况搜索使用的非常频繁,那么如何解决呢?你可能已经想到了,我们可以在系统闲时一点一点的将站点内容实时渲染出来,然后将页面中有意义的、用户可能会使用的词提取出来保存,保存时和该页面关联,这样一来用户搜索时,我们就可以简单的对数据库表进行查询即可,这个过程就叫做建立索引(Index,有时将“索引”当做动词使用,同指建立索引),从页面提取可能的搜索关键词关键在于分词,由此诞生了分词技术,了解SEO的读者是不是感觉很熟悉了,英文有空格分隔单词,基本的分词相对简单,而中文就较复杂了,分词技术是搜索的核心,其提取出来的词就是用户将要使用的关键词,没有意义的、用户不会使用的词会浪费储存空间和运算性能,但如果用户需要的词没有被提取出来,就会导致搜索不到结果,提取关键词时还要考虑在页面中出现的频率、位置因素,因为这些因素直接关联着用户意向匹配度,需要通过这些综合计算一个权重值,这个权重值和其他一些权重值(如发布时间、评论数、访问数、置顶情况等)一起决定了搜索结果的排序,因此分词技术的好坏很大程度上影响着搜索的优劣。

 

Drupal搜索概述:

Drupal搜索由搜索模块提供,具体执行搜索任务的是搜索插件,系统默认提供了两个搜索插件:

用户搜索:用于执行用户搜索

节点搜索:用于搜索各种内容类型的节点内容

注意这两插件分别是由用户模块和节点模块提供的,搜索模块仅提供搜索框架

在下文将对这两插件详细介绍,用户搜索较简单,其直接搜索数据库,而节点搜索就是前文所述的带索引的搜索,相对复杂。

总的来说一个搜索插件就是执行某一大类搜索任务的执行程序,用户也可提供自定义的搜索插件,但提供了插件并不意味着系统将直接使用她,最终用户是否能使用某个插件受控于“搜索页search_page”配置实体(详见下文),Drupal允许我们给用户提供多种类型的搜索页,即搜索入口页面,该页面提供搜索用的表单,每个搜索页都有独立的URL,要提供某个搜索页只需建立一个“搜索页”配置实体即可,在该实体中储存了提供的搜索页将采用哪一种搜索插件、插件配置、搜索页URL路径等,如果搜索页实体被禁用或删除,那么对应的搜索入口页面也就没有了;搜索插件和搜索页配置实体的关系类似于动作插件和动作配置实体的关系,插件提供执行逻辑,配置实体提供管理和配置储存,在drupal中这样的结构还有很多。

 

搜索入口:

即搜索页,系统会在路由系统中为每个搜索页(或者说启用的搜索页配置实体)真实建立路由,所有路由名采用统一的格式,如下:

search.view_+ “搜索页配置实体ID

如节点的综合搜索页路由名为:“search.view_node_search

路径统一采用前缀:“/search/”,后加搜索页配置实体中储存的路径

如节点的综合搜索页路径为:“/search/node

控制器统一为:Drupal\search\Controller\SearchController::view

可设置一个默认搜索页,此时搜索模块会为其建立默认的搜索路由“search.view”,地址为:'/search'

路由生成:

系统是如何为搜索页动态建立路由的呢?原因在于搜索页配置实体在保存后会设置路由构建器的需要重建路由旗标,这样路由构建器将在本次请求结束时去重建路由,重建即删除整个路由数据表,重新收集全部路由数据并写入(以前表中的全部旧路由数据将消失),重建的具体时机是在“kernel.terminate”事件派发时,在核心析构订阅器服务(服务idkernel_destruct_subscriber)中进行,搜索模块在路由定义文件中设置了以下路由回调:

\Drupal\search\Routing\SearchPageRoutes::routes

详见:search.routing.yml,在路由重建时会调用该方法进行路由重建,会新建以下路由:

默认搜索页路由(路由名:search.view,路径:“/search”)

搜索页路由(路由名:search.view_$searchPageEntityID

搜索帮助页路由(路由名:search.help_$searchPageEntityID

参数传递:

在搜索页中,搜索表单提交后,我们会看到URL改变了,这里用户极易误认为表单是以GET方式提交,提交变量由前端js控制,这是不对的,真实情况是搜索表单默认采用POST方式提交,提交后,在提交处理器中请求被中转(重定向)给搜索页路由处理,中转时使用GET查询参数名“keys来传递用户输入的关键词信息,这是由默认的搜索页控制器决定的,如果有其他参数则由插件自行决定(在插件的buildSearchUrlQuery方法中处理,见下文)。

在搜索时用户可直接访问某个搜索页,也可采用系统提供的搜索块:

块插件idsearch_form_block

插件类:Drupal\search\Plugin\Block\SearchBlock

这是一个很简单的块插件,在其中使用了如下表单:

\Drupal\search\Form\SearchBlockForm

该表单会将用户输入的关键词中转给块配置中指定的搜索页处理。

 

搜索系统配置:

关于系统搜索相关的配置页面路径如下:

/admin/config/search/pages

即菜单:管理>配置>搜索及元数据>搜索页面

该页面实际上是 “搜索页search_page 配置实体的列表页,路由名如下:

entity.search_page.collection

列表构建器类:

\Drupal\search\SearchPageListBuilder

由于该构建器继承自可拖拽列表构建器,因此该类被用作表单类,被表单构建器构建

各配置解释如下:

索引进度(Indexing progress):

创建索引是在自动任务中执行的,见钩子函数:search_cron(),自动任务详见本系列自动任务篇,该项会显示当前索引完成情况,注意这里是所有搜索页的索引完成情况汇总,而不是指某一个。

点击按钮“重建站点索引(Re-index site)”后,并不是立即开始重建,而是进行标记,真正的重新索引操作是在下一次自动任务运行时进行

索引负荷管制Indexing throttle):

设置每次索引处理多少内容,默认每次100条,在自动任务执行时,索引太多可能会超时或内存超限,注意这里设置的是每个可索引搜索页每次索引处理的条数,如果有多个搜索页,那么每次自动任务总的索引处理数量是其值乘以搜索页数量

索引关键词最小字数Minimum word length to index

关键词最短字符数,内部采用“mb_strlen”函数计算,即一个中文汉字算一个,一个字母也算一个,用户搜索节点时提供的关键词组中至少应有一个正向关键词大于等于该值,否则不会进行搜索,下文将进一步解释;在内部该值也作为最小分词长度,改变该项需要重建索引,系统将自动重建。

简单CJK(中日韩字符)处理(Simple CJK handling

这里CJK是中国、日本、韩国国家名的首字母组合,代表东亚字符群,该项控制是否启用系统默认的中日韩分词机制,如果没有自定义分词器时建议开启,否则不会对中日韩语言的内容进行分词,也就无法搜索中日韩语言内容,但默认分词简单粗暴(见下文)因此推荐自定义分词器;改变该项会引起索引重建。

搜索日志(Log searches

是否在日志中记录用户搜索记录,开启后如果搜索量大会影响性能

搜索页面(Search pages

该版块用于建立或配置搜索页(或搜索页实体),搜索页类型对应着“搜索插件”,搜索页对应着“搜索页配置实体”,每个搜索页使用的路径应是唯一的,如果已存在则不允许提交。

其中节点搜索类型可以配置如何对搜索结果进行排序,由于排序较为重要,我们单独用一节来解释。

 

搜索排名(Content ranking):

要理解搜索排序配置需要先理解系统是如何进行排序的,通常我们希望用来排序的因素(factor,有时也叫因子)有很多,常见的如发布时间、评论数、查看次数、是否置顶等等,那么如何将这些因素应用到排序中呢?

用数据库的“ORDER BY”关键词可以吗?该关键词的特性是前面的排序字段具有绝对控制权,后面的排序字段仅在前面的值相同时才参与排序,假设用时间作为首要排序,那么其他因素不管有多么符合我们的期望也不可能在排序上竞争过时间因素,这显然不是我们想要的。

聪明的读者可能已经想到了让每一个排序因素计算一个排序分,然后以所有排序因素的分加总进行排序,这很棒,可是在实施过程中,我们会发现每个排序因素的分值范围可能相差很大,比如同一篇内容的评论有一百,其查看次数可能就会超过一万,如果直接将一百和一万作为各自的分,那么依然导致评论和查看次数这两个排序因素竞争不平等,那么如何处理呢?

这里就要用到一种算法:归一化算法(Normalization),其作用是把数值映射到01范围之内,这样一来就可以做到不论原数值范围如何,每个排序因素都获得公平的竞争,映射处理后的那个01之间的值称为归一化值;在每个排序因素都平等时,用每个排序因素的归一化分加总来排序是非常适合的,但通常我们会希望一些排序因素的重要性高于另一些,此时在各因素的归一化分上加上权重即可。

说到这里言归正传回到节点搜索结果排序配置中来,这里的配置就是为每一个排序因子的归一化分值指定权重值,数值越高权重越大,最终排序分值占比就越大,越可能靠前;当权重值为0时,该排序因子不参与排序,当所有排序因子的权重值均为0时,默认以关键词相关度做排序因子,这个“相关度”是什么意思呢?即用户输入的关键词,在页面中位置越好(即所属标签权重大)、出现次数越多就越相关。

系统依据各排序因子的归一化分值和其权重值综合计算排序分,最终进行加总作为唯一的搜索结果排序依据,各排序因子由模块通过钩子提供,见下文。

注:其实归一化算法也有很多种类,不同排序因子应该采用不同的归一化算法,这会影响到最终排序。

 

搜索配置对象:

以上配置信息大多储存在搜索配置对象中,对应search.settings.yml文件,各含义及初始值如下:

and_or_limit: 7 //搜索时可用的连接词数量,见下文,目的是预防拒绝服务攻击
default_page: node_search //默认搜索页
index:
  cron_limit: 100 //见前文:索引负荷管制
  overlap_cjk: true //见前文:简单CJK(中日韩字符)处理
  minimum_word_size: 3 //见前文:索引关键词最小字数
  tag_weights: //关键词相关度计算中,标签对应的权重值,在搜索源字符串中其他标签被剔除
    h1: 25
    h2: 18
    h3: 15
    h4: 14
    h5: 9
    h6: 6
    u: 3
    b: 3
    i: 3
    strong: 3
    em: 3
    a: 10
logging: false //见前文,是否记录搜索日志


搜索页知识库服务:

该服务在搜索系统中多次用到,先来看一看:

服务idsearch.search_page_repository

类:Drupal\search\SearchPageRepository

提供关于搜索页配置实体相关操作的辅助方法,各方法简要介绍如下:

getActiveSearchPages()

找出启用的搜索页,返回搜索页配置实体

isSearchActive()

是否有启用的搜索页,返回布尔值

getIndexableSearchPages()

找出所有使用索引的搜索页,返回搜索页配置实体

getDefaultSearchPage()

找出默认搜索页,返回搜索页实体idfalse,如果设置了默认搜索页且是启用的则返回,否则返回启用的第一个搜索页,如果没有则返回false

clearDefaultSearchPage()

setDefaultSearchPage(SearchPageInterface $search_page)

清除和设置默认搜索页

sortSearchPages($search_pages)

排序搜索页

注:在Drupal世界中,但凡遇到不好分类的、零碎的辅助方法,通常会建立一个称为“知识库repository”的服务,将其放入里面

 

搜索页配置实体:

如前所述,用于管理某搜索页是否启用、使用何种搜索插件、储存搜索配置等,搜索页配置实体和搜索入口页面一一对应,多个搜索页配置实体可使用同一个搜索插件,同一个搜索页配置实体只能指定一个搜索插件。实体定义如下:

实体IDsearch_page

类:\Drupal\search\Entity\SearchPage

接口:\Drupal\search\SearchPageInterface

各处理器如下:

Array
                (
                    [access] => Drupal\search\SearchPageAccessControlHandler
                    [list_builder] => Drupal\search\SearchPageListBuilder
                    [form] => Array
                        (
                            [add] => Drupal\search\Form\SearchPageAddForm
                            [edit] => Drupal\search\Form\SearchPageEditForm
                            [delete] => Drupal\Core\Entity\EntityDeleteForm
                        )
                    [storage] => Drupal\Core\Config\Entity\ConfigEntityStorage
                ) 

插件配置信息的提交与保存:

由于该实体实现了“实体插件集接口EntityWithPluginCollectionInterface”,因此在实体的添加和编辑表单中,由插件提供的配置收集表单不需要考虑各项在表单中的位置,提交时只需要保存到插件的“configuration”属性中即可,保存实体时,系统会自动提取到实体的“configuration”属性下,详见:

\Drupal\Core\Config\Entity\ConfigEntityBase::preSave

在实例化插件时保存的插件配置又会传递给插件。

 

搜索插件:

对应的插件管理器如下:

服务idplugin.manager.search

管理器类:\Drupal\search\SearchPluginManager

插件目录:src/Plugin/Search

搜索插件用于执行具体的搜索工作,相关接口如下:

通用搜索接口:

所有搜索插件均需实现:

\Drupal\search\Plugin\SearchInterface

系统提供了实现该接口的默认基类:

\Drupal\search\Plugin\SearchPluginBase

可配置搜索接口:

如果搜索插件是可配置的,推荐实现可配置搜索接口:

\Drupal\search\Plugin\ConfigurableSearchPluginInterface

该接口继承自通用搜索接口,系统提供了实现该接口的默认基类:

\Drupal\search\Plugin\ConfigurableSearchPluginBase

搜索索引接口:

如果搜索插件是使用索引的,须额外实现搜索索引接口:

\Drupal\search\Plugin\SearchIndexingInterface

该接口不继承任何接口

系统默认提供了两个搜索插件:用户和节点搜索,下文将详细介绍

搜索页的执行流程和表单构造详见:

\Drupal\search\Controller\SearchController::view

\Drupal\search\Form\SearchPageForm

 

用户搜索:

插件iduser_search

插件类:Drupal\user\Plugin\Search\UserSearch

这是一个不可配置、不可索引的搜索插件,其直接作用在用户实体数据表上,这是一个很简单的插件,但为这一类插件起到了一个很好的示范。

用户搜索仅提供给具备“access user profiles”权限的用户,其不使用索引,而是直接用SQL的“like”操作符去搜索用户实体基本表,在搜索时,每次仅应提供一个关键词,可使用“*”作为通配符(在内部一个或连续多个星号会被替换成SQL的“%”通配符), 关键词不受限于最小关键词长度限制。

如果执行搜索的用户具备“administer users”权限,那么关键词会作用到用户名和邮件字段,且不受账户是否禁用限制,否则只能作用在用户名字段,且只能搜索到非禁用的用户。

搜索结果默认每页显示15条,被添加了缓存标签:user_list,因此当有用户变化时,将及时更新。

 

节点搜索:

插件idnode_search

插件类:Drupal\node\Plugin\Search\NodeSearch

这是一个可配置、使用索引的搜索插件,也是在drupal中最多使用的搜索,这里将详细解读。

 

使用节点搜索:

节点搜索的关键词等信息在URL中传递,使用的URL类似如下:

http://www.yourdomain.com/search/node?keys=a b x OR y -m -n "i am yunke"&f[0]=type:article&f[1]=type:page&f[2]=language:zh-hans&f[3]=language:en&f[4]=author:1&advanced-form=1

和用户搜索不同,节点搜索可使用“keys”查询参数传递多个关键词,形成关键词组,组中每个关键词可以是一个字词,或者一句短语,短语需要使用英文双引号包裹(这样才被视为一个整体关键词使用),在手动构造搜索URL时,关键词组中各关键词可以使用以下连接词连接:

OR

表示内容包含其一即可,该连接词不能小写,两侧须各有一个或多个空格,仅作用于其前后两个关键词,这一点很重要,如:“a  b  OR  c  d”用程序表示相当于“a && (b || c) && d

AND

表示内容须包含所有关键词,也可以写为“and”(这和OR不同,OR不允许小写),两侧须有一个或多个空格,通常我们使用空格代替该连接词

空格:

一个或连续多个空格,起到的作用同AND一样,因此通常用空格代替AND

减号:

即“-”,表示排除某关键词,即内容包含该词时不要列出,前面须有一个或多个空格,后面必须紧随关键词,其仅作用于紧跟的这个关键词,对后续的关键词不起作用,如果有多个关键词需要排除,则每个前面都需要有减号,注意:用减号排除的关键词称为负向(negative)关键词,相对的,ORAND连接的关键词称为正向(Positive)关键词,搜索时不能仅提供负向关键词。

 

在前文的搜索配置中可设置搜索关键词最短字符数,内部采用“mb_strlen”函数计算,其精确含义是:用户搜索节点时提供的关键词组中至少应有一个正向关键词的长度大于等于该值,否则不会进行搜索,而不是指整个关键词组的长度。

连接词类似程序中的逻辑操作符,但不能像程序中那样深层嵌套组合,且有连接词数量限制,默认ORAND一共可以出现7次,这是因为太多连接词会导致系统计算开销巨大,为预防洪水攻击而设置

 

在前文的URL示例中,如你所见,除用“keys”查询参数传递关键词组外,还可以用“f”查询参数传递过滤器,“f”是“filters”的简写,过滤器用于控制搜索的范围,系统默认提供了四种过滤类型:

内容类型(type)、语言(langcode)、作者(author)、分类术语(term

过滤器以类型和值的方式呈现,中间用英文冒号分隔,过滤器数量不限,系统得到的f参数值应类似如下:

$_GET['f'] = ['type:page', 'langcode:en', 'author:1', 'term:13', 'term:27'];

不同类型的过滤器间默认是“AND”逻辑,如果有同类型的多个过滤器,其内部的多个值之间的链接逻辑由过滤器自己决定,以上默认的4种在其内部均是以“OR”链接

 

通过手动构造搜索URL通常是在程序中使用,人类用户通常会直接使用搜索页提供的表单,由系统根据表单输入自动构造URL。在使用上还有如下注意事项:

1、如果用户输入的关键词组中,某个关键词长度大于系统设定的最小关键词长度,在内部会先进行分词处理后再搜索

2、如果系统配备了“从数据库”,那么搜索将在从库上执行,且仅能搜索已发布的、有权访问的节点

3、搜索结果每页显示10条,没有提供配置接口,被硬编码在代码中,这一点需要改进

4、搜索结果页面会缓存,但缓存依赖于节点实体和用户,换句话说节点实体改变或用户改变将引起缓存更新,所以不必担心搜索结果不更新问题

5、未建立索引的节点无法被搜索到,因为搜索是作用在索引上

 

节点索引源数据

即用于建立索引的源字符串数据,换句话说即从其中提取关键词的源字符串,默认采用节点的“search_index”视图模式的渲染输出,标题已被高权重方式合并,其他模块可通过钩子“node_update_index”提供额外的源数据,默认会对节点的每种语言均建立索引,该过程详见以下方法:

\Drupal\node\Plugin\Search\NodeSearch::indexNode(NodeInterface $node)

索引是搜索的灵魂,接下来看一看系统如何建立索引以及索引的数据结构。

 

索引数据结构:

即索引是如何储存于数据库中的, drupal用了三个数据库表来储存索引数据,其结构经过精心设计,是一种通用的索引结构,而不仅仅适用于节点,如下所示:

搜索索引表:

表名:search_index,这是索引的主表(搜索SQL语句的主表,或左表),通过该表将确定搜索的范围,各字段含义如下

word搜索关键词(分词后的结果),主要用途是匹配关键词以缩小搜索范围

sid关键词所属的条目id,如条目是一个节点,那么为节点id

langcode搜索条目的语言代码

type搜索条目的类型,通常采用搜索插件的插件id,如节点搜索将是“node_search

score关键词的相关度分值,值越高和条目就越相关,这是搜索结果的排序因素之一

 

搜索数据集表

表名:search_dataset,用于储存某条目的完整分词结果,这样做的目的是支持搜索中使用ORAND连接词,搜索时将结联(join)到上面的索引表,各字段含义如下

sid关键词所属的条目id,同上,是结联依据之一

langcode搜索条目的语言代码,,同上,是结联依据之一

type条目类型,同上,是结联依据之一

data空格分隔的关键词列表,对条目完整分词的结果,精确搜索条件(ORAND)即作用在该字段

reindex是否重建索引旗标,整数型,值如果为0表示不需要重建索引,如果为时间戳则需要重建索引,时间先后代表重建优先级,时间越小越优先

注:总的来说搜索是先通过search_index表的word字段缩小搜索范围,在通过search_dataset表的data字段进行精确匹配

 

搜索总计表:

表名:search_total,主要用于相关度分值的归一算法,避免关键词分的两级分化,有些关键词出现频率非常高,比如英语单词“the”,这将导致其在索引表中的分值很高,但事实上这不见得是用户会使用的重要关键词,因此这里用齐夫定律(Zipf's law)进行对冲,各字段含义如下

word主键,索引表中储存的关键词,是和索引表结联的依据

count关键词的相关度分值经过齐夫定律原理转化后的值,详见下文搜索索引服务的更新权重方法。

 

齐夫定律Zipf's law

是描述自然语言词频概率统计规律的一个实验性定律,即“在自然语言的语料库里,一个单词出现的频率与它在频率表里的排名成反比”。中文与英语、西班牙语、法语等在内的多种语言研究结果一致,这种现象不仅适用于语料全体,也适用于单独的一篇文章,在语言统计外也有呈现,比如:网页访问频率、城市人口、固体碎片大小等。

请先思考该定律中频率和排名的乘积代表着什么?对于搜索意味着什么?

 

搜索索引服务

前文解释了索引的数据结构,搜索索引服务则用于建立和维护其中的数据:

服务idsearch.index

类:Drupal\search\SearchIndex

方法解释如下:

public function index($type, $sid, $langcode, $text, $update_weights = TRUE)

用于从文本(即索引源数据,通常是页面渲染输出,见前文的节点索引源数据)中提取关键词,即分词处理,并将提取到的关键词保存到数据库表search_datasetsearch_index中,可选的更新数据库表search_total;各参数含义如下:

$type:搜索类型,通常采用插件id,应少于64字节

$sid:被索引条目的id值,比如节点id

$langcode 字符串值,被索引字符串的语言代码,如简体中文为zh-hans

$text:将被索引的源字符,即索引源数据,必须是html片段或纯字符串

$update_weights:布尔值,是否更新数据库表search_total,默认为true(更新)

返回一个数组,键名为提取到的关键词,键值统一为true

该方法在计算关键词分值时,影响分值的因素仅包含以下三种:

所在HTML标签的权重:标签权重越高分越高,默认h1最高,详见配置

出现的频率:出现次数越多分越高

在文中的位置:越在文首分越高

并不包含诸如评论数、阅读量、是否置顶、发布时间等等之类的其他因素,因为该分仅代表页面相关度。

注:该方法的设计对中日韩文其实不太好,比如在html标签中提取的关键词超过15个时,后面的关键词分值将不考虑标签权重,而在系统提供的默认分词方法下,中文关键词数量通常会超过15个,因此中文用户自定义分词逻辑十分必要,系统如何分词见下文。

 

public function markForReindex($type = NULL, $sid = NULL, $langcode = NULL)

将索引标记为需要重建,即把数据库表search_datasetreindex字段值由0标记为当前请求的时间戳,在运行自动任务时,将据此重建索引;没有传递参数时将重建全部索引,传递参数时可精确控制需要重建索引的条目范围

 

public function clear($type = NULL, $sid = NULL, $langcode = NULL)

清除数据库表:search_indexsearch_dataset中的索引内容,并失效搜索结果缓存,参数用于控制清除条目的范围

 

public function updateWordWeights(array $words)

更新搜索总计表:search_total,其中字段count的值是一个应用齐夫定律后算出的值,计算过程如下:

$count= log10(1 + 1 / (max(1, $total)));

可能取值不会超过“0.30102999566398”,这里$total是关键词在索引表(search_index)中分值的总和,$total越大,$count就越小,约成反比关系,对应着齐夫定律大约的讲:$total代表频率,而$count代表排名,当数据量足够大时排名相对是固定的。索引表(search_index)中的分值字段和search_total表的count字段的乘积值有特定的意义,即:避免了两级分化,实现了对冲,在搜索重要性上得到更合理的结果。

此外该方法会将已经消失的关键词(在search_index表中已经不存在)从表中删除

 

分词:

即从文本中提取搜索用关键词,这一过程又可叫做“简化文本”,默认在以下方法中执行:

search_simplify($text, $langcode = NULL)

参数如下:

$text:源文本,已经去掉了html标签,但可能包含空白、换行、标点符号以及各语种文字等

$langcode:字符串,语言代码,指示文本所属的语言

该方法的执行入口在“search.index”服务index方法的search_index_split函数调用中。

内部处理过程如下:

1、解码HTML实体

2、将源文本转化为小写,由此可见搜索对大小写不敏感

3、调用音译服务去掉变音符号(特别用于部分国家的语言,中文不适用)

4、执行钩子“search_preprocess”,模块可通过该钩子去分词

5、如果开启了中日韩字符的重叠分词处理“overlap_cjk”,则调用search_expand_cjk($matches)函数进行默认中日韩字符处理(见下)

6、处理数字加标点的字符,她们可能是日期、ip、版本号等,改善搜索

7、将两个及以上的点号和连字符“.-”替换成空格

8、移除所有的“._-

9、将所有单词边界(如标点符号)替换成空格

10、将每个关键词裁剪到50字符以内

 

中日韩字符的默认分词处理:

是否开启中日韩字符处理由以下选项控制:

\Drupal::config('search.settings')->get('index.overlap_cjk')

在以下函数中执行:

search_expand_cjk($matches)

提交给该函数的字符串只包含中日韩字符,且不带任何中英文标点符号,是来自正则匹配的结果。

这种默认处理是一种简单的重叠式分词,举个列子,假设源文本如下:

“云客期待在中国的读者朋友们中产生很多核心开发者”

在最小搜索关键词字数为2时,将得到如下结果:

“云客 客期 期待 待在 在中 中国 国的 的读 读者 者朋 朋友 友们 们中 中产 产生 生很 很多 多核 核心 心开 开发 发者”

在最小搜索关键词字数为3时,将得到如下结果:

“云客期 客期待 期待在 待在中 在中国 中国的 国的读 的读者 读者朋 者朋友 朋友们 友们中 们中产 中产生 产生很 生很多 很多核 多核心 核心开 心开发 开发者”

你会发现这是一种“暴力的、一字不漏”的分词方式,每个词的开始都是前一个词的结尾,这样的结果导致了大量无意义的词,理想的分词结果可以是这样的:

“云客 期待 中国 读者 产生 核心开发者”

 

自定义中文分词:

中文分词是件较复杂的事情,是计算机领域自然语言处理的一个难点,有时在缺失上下文时,甚至没有办法判断分词是否合适,例如:单独的一句话“乒乓球拍卖完了”,可以切分成“乒乓球拍 卖完了”、也可切分成“乒乓球 拍卖 完了”,如果没有上下文信息,无法判断“拍卖”在这里算不算一个词,还有很多新生事物的名字也要结合现实世界中的信息才能判断是不是一个词。

虽然中文分词很难,但还是有许多开源的中文专用分词库或第三方接口可进行基本的分词,但不要指望百分百正确,就目前而言有些接口要比Drupal默认的分词好很多,采用它们时模块可用以下钩子去处理:

search_preprocess

注意:在有自定义中文分词时,须确保不被应用默认的中日韩分词处理,即确保将该选项关闭。

云客已为大家开发了分词模块,见本文末尾。

 

节点搜索插件:

插件idnode_search

插件类:Drupal\node\Plugin\Search\NodeSearch

这里仅列出该插件的一些重要信息

插件各索引操作方法简介:

更新索引方法:

updateIndex()

标记重建索引方法:

markForReindex();

返回索引进度:

indexStatus()

清除本插件所有索引数据:

markForReindex()

 

执行搜索时插件各方法调用次序:

如下所示:

$plugin->setSearch($keys, $request->query->all(), $request->attributes->all());
$plugin->suggestedTitle();
$plugin->searchFormAlter($form, $form_state); 构建表单
$plugin->isSearchExecutable()
$plugin->buildResults();

查询构建过程:

1、在从数据库上选择索引表search_index初始化查询对象

2、使用扩展模式装饰查询对象,以获得辅助方法和分页功能

3、通过搜索id和语言全结联节点数据表,以获得节点字段数据

4、添加条件仅搜索已发布的和用户有权访问的节点

5、如果有过滤器参数,构建过滤查询条件

6、添加排名因素的分值计算表单式

7、限制搜索结果为10

8、解析关键词,并构建查询条件,并判断是否为复杂查询(是否会用到search_dataset表)

9、结联搜索总计表search_total

10、结联search_dataset

11、如果没有排序因素,那么添加默认的关键词相关性做唯一的排序因素

12、将所有排名因素的分值表达式汇总成为一个表达式,以该表达式值来排名

 

注:

1、构建的查询对象具备查询标签:'search_' . $type,因此模块可以通过查询标签钩子去修改查询

2、何为简单查询:如果需要进行“LIKE”匹配就不属于简单查询,或者说如果需要对“search_dataset”表的“data”字段进行匹配就不属于简单查询,如果关键词中有短语,那么也不属于简单查询,因为其会查询“search_dataset”表的“data”字段

 

搜索查询扩展类:

类:\Drupal\search\SearchQuery

采用包装设计模式扩展基本的查询对象,提供搜索查询的辅助方法,各方法解释如下:

protected function parseSearchExpression()

将搜索关键词组解析成查询条件,构造查询对象的条件子句

 

public function prepareAndNormalize()

预备查询

 

protected function parseWord($word)

收集正向关键词

 

public function addScore($score, $arguments = [], $multiply = FALSE)

该方法用于向查询添加排序因子,整个搜索过程中,务必不要使用orderBy()方法,因为搜索结果是以分数排序,参数分别为:

$score:一个用于计算排序分的SQL表达式,被计算后的值应是归一化值,即0~1之间的值,含01

$arguments:如表达式中有参数,通过该参数传入

$multiply:归一化分值的乘数,即权重

这些参数通常由ranking钩子提供,见下文

 

搜索结果的主题化:

搜索结果列表主题钩子:

'item_list__search_results__' . $plugin->getPluginId()
'item_list__search_results'

搜索结果列表中条目主题钩子:

'search_result'

 

搜索相关钩子:

添加搜索源数据钩子:

钩子名:node_update_index

参数:节点实体对象

派发位置:\Drupal\node\Plugin\Search\NodeSearch::indexNode

在为节点建立索引前,可通过该钩子添加该节点的搜索源数据,返回一个数组,元素为字符串或翻译对象,表示将添加到源数据中的字符串

默认仅评论模块实现了该钩子

 

分词钩子:

钩子名:search_preprocess

参数: $text(搜索源文本)、 $langcode(字符串、语言代码)

派发位置:search_invoke_preprocess(&$text, $langcode = NULL)

钩子的返回值将作为参数继续传递给其他模块的该钩子,如需处理则返回以空格分隔的关键词字符串

该钩子无修改钩,默认没有模块实现该钩子

 

搜索结果排序钩子:

钩子名:ranking

无需参数,没有修改钩

派发位置:\Drupal\node\Plugin\Search\NodeSearch::getRankings

用于为搜索结果收集排名因子,钩子应返回一个数组,键名为排序因子名(自行指定一个标识符,不与其他排序因子冲突即可),键值为该排序因子的排序定义,有如下键名:

必选键名:

title:一个翻译对象,在排序因子权重配置表单中显示给人类看的因子名称

score:用来计算本排序因子的排序分值的SQL表达式,值应是归一化处理后的值,即在01之间(含01),值越大则在该因素的排序越靠前,搜索结果总排名还涉及其他排序因子及权重配置;表达式中可用变量“i.relevance”,代表关键词相关性,其值由系统计算提供,也在01之间

可选键名:

arguments:分值表达式中的查询参数设置(如果有的话)

join:如果分值表达式中所用字段需要结联表查询,用该项给出结联配置,默认已结联的表有节点数据表和三个索引数据表

在核心中实现了该钩子的模块有:节点、评论、访问统计,可参考她们

 

节点搜索结果钩子:

钩子名:node_search_result

参数:节点实体对象

派发位置:\Drupal\node\Plugin\Search\NodeSearch::prepareResults

用于向搜索结果条目中添加额外信息

 

添加节点索引源数据钩子:

钩子名:node_update_index

没有修改钩,参数为节点实体对象

派发位置:\Drupal\node\Plugin\Search\NodeSearch::indexNode

用在为节点建立索引前,向索引源数据中添加额外的源数据,返回字符串值或翻译对象,实现该钩子后应当重新建立索引,系统不会自动重建

 

补充:

1、如果已经建立了某个搜索页配置实体,但无法访问对应的页面,原因可能是路由重建时发生故障,重新保存配置实体即可

2、失效全部搜索结果的缓存可用缓存标签“search_index”,如果仅针对某一类搜索可用“search_index:”加插件类型,即$plugin->getType(),比如节点:“search_index:node_search

3、系统默认没有提供搜索频率限制,仅有搜索权限控制,如需进行频率限制可结合洪水服务进行控制,详见本系列“洪水控制防护flood”主题

4、搜索结果排序分值仅用于排序搜索结果,并不影响搜索结果的数量

5、节点搜索对大小写不敏感

6、用户权限更改或节点内容更新会实时失效搜索结果页面缓存,当节点更新时会被标记需要重新索引,详见方法:node_reindex_node_search($nid),该方法在节点实体的保存后方法(postSave)中调用,新建的节点在自动任务执行时会被索引,不管是新建还是更新,只有被索引后才能被搜索到

7、默认节点搜索表单中没有提供分类术语和用户过滤器,但是在系统逻辑中已经包含了处理,只需通过表单修改钩子打开该功能即可

8、如需屏蔽某关键词,可实现表单\Drupal\search\Form\SearchPageForm的验证处理器

 

后记:

在写作该篇时,云客了解到目前官网尚无CJK分词模块提供下载,因此为大家开发了“cjk_tokenizer”模块,该模块专门为中日韩等东亚语言的分词处理而设计,针对中国用户默认提供了百度分词,在使用该模块时如需自定义的分词器仅需提供一个插件即可,模块已实现了所需的基础设施,如配置表单等等,比直接实现分词钩子简单很多,更多详情请到作者博客“水滴间www.indrupal.com”或drupal官网查看,免费下载使用。

本书共152小节。


目前全部收费内容共275.00元。购买全部

评论 (0)