.net站内搜索hubble.net使用详解之分词技巧
2021-12-03在中文搜索中,分词技术是一个比较关键的技术,我们往往会遇到查询某个关键字无法匹配到相应文档的问题,这种问题往往都是索引的分词不理想造成的,倒排索引的技术特点决定了如果查询的关键字不在索引的分词中,则无法查出相应的文档。为了帮助使用者分析分词问题,hubbledotnet 专门提供几个存储过程来帮助检查索引分词的情况。
分词的测试方法
首先要找到要测试的原始文本
我们往往发现某些记录包含有查询关键字,但查不出来,这个时候我们需要先找到这个出问题的记录的原始文本。查找原始文本的方法很多,你可以通过 docid 或者id 或者其他条件来查找。
下面给出一个通过 id 来查找的示例。如果我们发现id = 1 的记录 title 无法匹配出来,我们可以执行
select * from table where id = 1 找到记录的原始文本。
找到原始文本后我们有两种方法来查看记录在索引中的分词情况。
方法1: SP_TestAnalyzer
SP_TestAnalyzer 这个存储过程用于测试分词器的分词结果,它的作用是在服务器侧执行分词器的 Tokenize 方法。
这个存储过程有两个参数,第一个参数为分词器名字,这里我们输入 ‘PanguSegment’,第二个参数是要测试的句子。
下面我们执行如下语句,看看效果
SP_TestAnalyzer 'PanguSegment', '六方会谈无核化工作组会议将在沈阳召开'
如上图所示,执行后,可以看到分词的结果。从这个分词结果可以看到原始文本的分词有一些问题,比如无核化和工作组这两个词没有分出来,这时如果搜索”工作组”这个词,则无法匹配到这条记录。我们需要把无核化和工作组两个词加入到盘古的字典中再测试,如果分词正确了,重新索引后,问题就可以解决。
方法2:SP_FieldAnalyze
SP_FieldAnalyze 这个存储过程是针对指定表的指定字段的分词器来分词
它有4个参数,参数1为表名,参数2为字段名,参数3为要分词的句子,参数4指定是用 Tokenize 函数还是 TokenizedForSqlClient 函数来分词。第4个参数为可选参数,
如果不输入,就是以 Tokenize 函数分词,如果输入 ‘SqlClient’ 就是以 TokenizedForSqlClient 函数来分词
下面我们首先执行默认的情况,即用 Tokenize 函数分词
SP_FieldAnalyze 'VNews', 'Title', '六方会谈无核化工作组会议将在沈阳召开'
这个语句是采用 VNews 表的 Title 字段的分词器来对 '六方会谈无核化工作组会议将在沈阳召开' 这个句子进行分词,我们可以看出,分词的结果和方法1是一样的。
我们加上 SqlClient 参数后来分词看看效果:
SP_FieldAnalyze 'VNews', 'Title', '六方会谈无核化工作组会议将在沈阳召开','SqlClient'
可以看到后面加了 SqlClient 后,分词结果不同了,这是因为 SqlClient 采用了不同的分词参数。对于盘古分词来说,如果调用 SqlClient 来分词,则服务器侧调用的是program/hubbledotnet/default/PanGuSqlClient.xml 这个配置文件来对文本进行分词,如果不加 SqlClient 参数,则是调用program/hubbledotnet/default/PanGu.xml 这个配置文件分词。
SqlClient 的功能主要是帮助进行查询字符串的分词,HubbleCommand 这个类中有一个函数 GetKeywordAnalyzerStringFromServer 就是用于对查询字符串进行分词的,hubble 的示例代码中也是调用的这个函数,这个函数的里面实际上就是调用 SP_FieldAnalyze 这个存储过程加 SqlClient 来分词。当然在实际项目中,使用着并不是必须要调用这个函数来对查询字符串进行分词,使用者可以用自己的程序对查询字符串分词。
分词的技巧
对于搜索来说,查全率和查准率是一对矛盾,为了尽量平衡这对矛盾,我们在索引和查询时可以采用一些技巧。
技巧1.
索引时最大化分词,如果是用盘古分词进行索引,索引时打开多元分词和强制一元分词。但对查询字符串的分词则采用精确分词,这样可以保证查准的情况下获得较大的查全率。
技巧2.
同义词分词。对于同义词的分词,我们不要在索引中分词,而是在查询字符串中增加同义词的分解,这样可以使查询更灵活,并且可以在查询时设置原词和同义词不同的权重以影响得分排名。
技巧3.
全部一元分词,然后通过索引组件来实现匹配,类似 like ‘%xxx%’ 这样的功能。由于索引组件知道单词在原文本中的位置,所以理论上可以实现类似 like ‘%xxx%’ 这样的功能,这种功能对于短文本的搜索比较有效,而且不需要关心中文的分词。hubbledotnet 将在未来的版本中提供这种方式的快速解决方案,目前版本提供了 like ‘*xxx*’ 的功能,但这个功能还不够完善,速度比较慢。
注意事项
索引分词的检查功能,实际上只是在server 端执行了对应的分词器来进行分词,并不是从倒排索引中查询出对应文本的分词结果。从倒排索引中获取分词结果需要遍历整个倒排索引,效率很低,所以hubble 没有提供这样的功能。由于是动态执行分词器分词,如果在索引过程中分词参数或字典调整过,则得到的结果有可能不是实际索引中的分词结果。
HubbleDotNet 在设计之初就定位为一个开放式的搜索平台,分词器,得分算法,数据库适配器,存储过程,函数等等都可以通过编写自定义的插件来定制。目前版本分词器,数据库适配器的自定义接口已经开放,得分算法的自定义接口也将在最近开放出来。本文将讲述如何编写自己的分词器。
HubbleDotNet 本身自带了3个分词器,分别是盘古分词,简单分词和英文分词。但对于搜索应用来说,仅仅这3种分词器是不够用的,很多应用需要定制化的分词器来提高搜索的准确度。本文通过讲述如何编写一个以逗号分隔的分词器,抛砖引玉。大家可以仿照这个简单的逗号分隔分词器来编写自己的分词器。
逗号分词器主要用于一些分类信息的分解。比如某条记录同时属于 A B C 三个分类,在关系型数据库中,我们可能需要设计2个表,通过主表和分类信息表关联来描述记录的分类关系。在搜索引擎技术中,往往偏向于no-sql ,至少是单表的简单平铺方式,这种方式的查询效率要比关系型要高很多,适合于搜索引擎对大规模数据检索的要求。目前以 google 为代表的搜索引擎基本上都是采用非关系型的设计,所以我们在设计搜索引擎时也最好参照这些搜索巨头的设计理念。回到这个问题,把分类信息平铺到一个表中的方法是增加一个全文索引字段,这个字段中存储这条记录的所属分类,分类之间以逗号分隔(比如 A,B,C)。 这样如果我们要查询属于A或B分类的记录,我们只要写类似如下的语句就可以完成:
select top 10 * from table where title contains ‘xxxx’ and class match ‘A B’ order by score desc
分词器的编写
要实现自定义的分词器,我们只需要简单实现 IAnalyzer 和 INamedExternalReference 接口就可以。要实现这个接口,我们要引用
Hubble.Analyzer.dll 和 Hubble.Framework.dll, 这两个动态库在 hubble 安装路径下中可以找到。
并且 using Hubble.Core.Analysis;
IAnalyzer 接口
public interface IAnalyzer { void Init(); int Count { get; } IEnumerable<WordInfo> Tokenize(string text); IEnumerable<WordInfo> TokenizeForSqlClient(string text); }
上图为IAnalyzer 接口。下面我分别讲讲这个接口中各个成员的含义
Init 函数是用于初始化分词器的,有的分词器比如盘古分词,在调用前需要加载字典,构建内部索引等,这些初始化过程在这个函数中完成。
Count 属性返回 Tokenize 函数分词后的单词总数,这个参数会影响单词的查询权重,由于Tokenize 函数输出是一个 IEnumerable 接口,无法得到单词总数,所以必须通过这个Count属性来得到当前分词的单词总数。
Tokenize 函数输出 text 的分词结果,这个结果以 IEnumerable 接口形式返回,这个函数是用于索引的分词。
TokenizedForSqlClient 函数也是输出text的分词结果,但这个结果是为分解搜索关键字时用的,因为有时候搜索关键字的分词和索引的分词结果不一定完全一致。这个函数只有在搜索时需要通过服务器来帮助分解搜索关键字时才用到,即类似我的例子中的GetKeywordAnalyzerStringFromServer 函数的做法,如果客户端是本地分词,就用不到这个函数。这个函数不影响索引的分词。
INamedExternalReference 接口
public interface INamedExternalReference { string Name { get; } }
上图为 INamedExternalReference 接口,这个接口用于指定分词器的名字
下面为逗号分隔分词器代码,我们可以把这个代码编译为一个单独的 dll,在这里我们假设为 SplitByComma.dll
using System;using System.Collections.Generic;using System.Text;using Hubble.Core.Analysis;namespace SplitByComma { public class Split : IAnalyzer, Hubble.Core.Data.INamedExternalReference { int _Count; #region IAnalyzer Members /// <summary> /// Count of words /// </summary> public int Count { get { return _Count; } } /// <summary> /// Initialize the segment /// </summary> public void Init() { //Write init code here } /// <summary> /// Tokenize for index /// </summary> /// <param name="text">text which tokenized to</param> /// <returns>word info list</returns> public IEnumerable<Hubble.Core.Entity.WordInfo> Tokenize(string text) { _Count = 0; int begin = 0; for (int i = 0; i < text.Length; i++) { if (text[i] == ',') { yield return new Hubble.Core.Entity.WordInfo(
text.Substring(begin, i - begin), begin); yield return new Hubble.Core.Entity.WordInfo(",", i); begin = i + 1; _Count += 2; } } if (begin < text.Length) { yield return new Hubble.Core.Entity.WordInfo(
text.Substring(begin, text.Length - begin), begin); _Count++; } } /// <summary> /// Tokenize for search keywords /// </summary> /// <param name="text">text which tokenized to</param> /// <returns>word info list</returns> public IEnumerable<Hubble.Core.Entity.WordInfo> TokenizeForSqlClient(string text) { int begin = 0; for (int i = 0; i < text.Length; i++) { if (text[i] == ',') { yield return new Hubble.Core.Entity.WordInfo(
text.Substring(begin, i - begin), begin); begin = i + 1; } } if (begin < text.Length) { yield return new Hubble.Core.Entity.WordInfo(
text.Substring(begin, text.Length - begin), begin); } } #endregion #region INamedExternalReference Members public string Name { get { return "SplitByComma"; } } #endregion } }
这段代码中,为了区别,我对 Tokenize 和 TokenizeForSqlClient 采用了不同处理,Tokenize 函数将输出包括逗号在内的所有单词,而TokenizeForSqlClient则输出单词不包含逗号。
把这段代码编译为 SplitByComma.dll ,分词器的编写工作就结束了
分词器的安装
安装分词器有三个步骤
步骤1:
将 SplitByComma.dll 拷贝到 program files/hubbledotnet/default 目录下
步骤2:
在 QueryAnalyzer 下执行如下语句。
SP_AddExternalReference 'Analyzer', 'SplitByComma.dll'
这个存储过程有两个参数,参数一指明外部引用的类型,这里为 Analyzer, 参数二为分词器的动态库文件名,如果文件不在hubble 的安装目录下,则需要输入完整路径名。
步骤3:
重启 Hubble.net 服务。分词器的安装和卸载都需要重启Hubble 服务才能生效。
重启后,我们建一个 TestSplitComma 的表,我们可以看到如下图所示,分词器已经安装成功
我们执行下面语句插入一条记录测试一下
insert TestSplitComma values('today''s sports news', 'sports,news')
然后我们执行下面查询语句:
select top 10 * from TestSplitComma where title contains 'today' and class match 'news' order by score desc
查询成功
分词的测试方法
在实际应用中,我们往往希望知道 hubble 的服务器端对某个句子到底是怎么分词的。
下面就在介绍两种查看分词结果的方法:
方法1: SP_TestAnalyzer
SP_TestAnalyzer 这个存储过程用于测试分词器的分词结果,它的作用是在服务器侧执行分词器的 Tokenize 方法。
这个存储过程有两个参数,第一个参数为分词器名字,这里我们输入 ‘SplitByComma’,第二个参数是要测试的句子。
下面我们执行如下语句,看看效果
SP_TestAnalyzer 'SplitByComma', 'news,sports'
如上图所示,执行后,可以看到分词的结果。
方法2:SP_FieldAnalyze
SP_FieldAnalyze 这个存储过程是针对指定表的指定字段的分词器来分词
它有4个参数,参数1为表名,参数2为字段名,参数3为要分词的句子,参数4指定是用 Tokenize 函数还是 TokenizedForSqlClient 函数来分词。第4个参数为可选参数,
如果不输入,就是以 Tokenize 函数分词,如果输入 ‘SqlClient’ 就是以 TokenizedForSqlClient 函数来分词
下面我们首先执行默认的情况,即用 Tokenize 函数分词
SP_FieldAnalyze 'TestSplitComma', 'Class', 'news,sports'
这个语句是采用 TestSplitComma 表的 Class 字段的分词器来对 ‘news,sports’这个句子进行分词
我们再以 TokenizedForSqlClient 函数来分词看看效果:
SP_FieldAnalyze 'TestSplitComma', 'Class', 'news,sports','SqlClient'
可以看到后面加了 SqlClient 后,分词结果不同了,没有了逗号,这个是执行分词器的 TokenizedForSqlClient 的结果。
分词器的卸载
如果某个分词器我们不再需要,我们可以卸载它。卸载方法分两个步骤:
步骤1:执行SP_DeleteExternalReference存储过程
这个存储过程有两个参数,参数一指明外部引用的类型,这里为 Analyzer, 参数二为分词器的动态库文件名,如果文件不在hubble 的安装目录下,则需要输入完整路径名。
SP_DeleteExternalReference 'Analyzer', 'SplitByComma.dll'
步骤2:重启Hubble 服务