本节介绍了如何开发自己的分词器,介绍了二元分词器的开发。因为版本的更新,需要对一些地方进行修改,下一节会对版本更新后导致问题做一个大致的分析。
2.1.3 二元分词
上一节通过变换查询表达式满足了需求,但是在实际应用中,如果那样查询,会出现另外一个问题,因为,那样搜索,是只要出现这个字,不管它出现在什么位置。这就产生了上一小节开头讲的,对准确性产生了极大干扰。比如,如果有一段这样的话:“这是一个英雄!他有无法用词汇形容的孤单,但是他并没有用言语来表达。”这句话包含了“英 语 单 词”这四个字,但是却和“英语单词”一点关系都没有。首先想到的解决方法,就是把句子按词来划分,那么就能有效的降低干扰。最简单的解决方法,莫过于每两个字组成一个部分。
下面来构造核心算法。首先我们期望,只有中文(广义上指双字节文字,比如日文,韩文也在这个范围。)是按照二元拆分,而符号则是单符号拆分,对于英文则保持原样。因此,需要一个判断当前字符类型的函数。首先,构造一个枚举,如代码2.1.3.1。
代码 2.1.3.1
Code
/// <summary>
/// Char类型枚举,用于分词中类型状态比较
/// </summary>
public enum CharType
{
None, //默认值,不可识别类型
English, //拉丁字符,用英文标识
Chinese, //CJK字符,以中文代表
Number, //阿拉伯数字
Control //控制符号,指控制符号已经各种标点符号等
}
接下来需要有一个函数能够识别字符,把字符类型转换成我们需要的CharType。
代码2.1.3.2粗略完成了我们想要的功能。现在就可以构造我们想要的算法了。
代码 2.1.3.3
Code
1using System;
2using System.Collections.Generic;
3using System.Text;
4using Lucene.Net.Analysis;
5using System.IO;
6
7namespace Test.Analysis
8{
9 public class DoubleTokenizer : Tokenizer
10 {
11 /**//// <summary>
12 /// 保持传入的流
13 /// </summary>
14 private TextReader reader;
15 /**//// <summary>
16 /// 控制分词器只打开一次
17 /// </summary>
18 private bool done = true;
19 /**//// <summary>
20 /// 保存分词结果
21 /// </summary>
22 private List<Token> tokenlist;
23
24 public DoubleTokenizer(TextReader reader)
25 {
26 this.reader = reader;
27 }
28 /**//// <summary>
29 /// 上一个字的类型
30 /// </summary>
31 private CharType lastype = CharType.None;
32 /**//// <summary>
33 /// 当前读取到分词的记录数
34 /// </summary>
35 private int ptr = 0;
36 /**//// <summary>
37 /// 重写Next方法
38 /// </summary>
39 /// <param name="result"></param>
40 /// <returns></returns>
41 public override Token Next(Token result)
42 {
43 if (done) //第一次可以运行,运行后将被设置为false,在一个实例中只会运行一次
44 {
45 done = false;
46 string text = reader.ReadToEnd();
47 //输入为空,则返回结束符号
48 if (string.IsNullOrEmpty(text))
49 return null;
50 //初始化分词结果
51 tokenlist = new List<Token>();
52 //缓冲器,主要用于暂时保存英文数字字符。
53 StringBuilder buffer = new StringBuilder();
54 Token token;
55 for (int i = 0; i < text.Length; i++)
56 {
57 char nowchar = text[i];
58 char nextchar = new char();
59 CharType nowtype = GetCharType(nowchar);
60 if (i < text.Length - 1) //取下一个字符
61 nextchar = text[i + 1];
62 //状态转换
63 if (nowtype != lastype)
64 {
65 lastype = nowtype;
66 if (buffer.Length > 0)
67 {
68 token = new Token(buffer.ToString(), i - buffer.Length, i);
69 tokenlist.Add(token);
70 buffer.Remove(0, buffer.Length);
71 }
72 }
73
74 switch (nowtype)
75 {
76 case CharType.None:
77 case CharType.Control:
78 goto SingleChar;
79 case CharType.Chinese:
80 break;
81 case CharType.English:
82 case CharType.Number:
83 buffer.Append(nowchar);
84 continue;
85 }
86 //处理连续两个中文字符
87 if (GetCharType(nextchar) == CharType.Chinese)
88 {
89 token = new Token(nowchar.ToString() + nextchar.ToString(), i, i + 2);
90 tokenlist.Add(token);
91 i++;
92 continue;
93 }
94
95 SingleChar: //处理单个字符
96 token = new Token(nowchar.ToString(), i, i + 1);
97 tokenlist.Add(token);
98 continue;
99 }
100 //返回第一个分词结果,并且把指针移向下一位
101 return tokenlist[ptr++];
102 }
103 else
104 {
105 //在分词结果范围内取词
106 if (ptr < tokenlist.Count)
107 return tokenlist[ptr++];
108 //超出则返回结束符号
109 return null;
110 }
111 }
112 /**//// <summary>
113 /// 获取Char类型
114 /// </summary>
115 /// <param name="c">字符</param>
116 /// <returns>返回类型</returns>
117 public static CharType GetCharType(char c)
118 {
119 switch (char.GetUnicodeCategory(c))
120 {
121 //大小写字符判断为英文字符
122 case System.Globalization.UnicodeCategory.UppercaseLetter:
123 case System.Globalization.UnicodeCategory.LowercaseLetter:
124 return CharType.English;
125 //其它字符判断问中文(CJK)
126 case System.Globalization.UnicodeCategory.OtherLetter:
127 return CharType.Chinese;
128 //十进制数字
129 case System.Globalization.UnicodeCategory.DecimalDigitNumber:
130 return CharType.Number;
131 //其他都认为是符号
132 default:
133 return CharType.Control;
134 }
135 }
136 }
137 /**//// <summary>
138 /// Char类型枚举,用于分词中类型状态比较
139 /// </summary>
140 public enum CharType
141 {
142 None, //默认值,不可识别类型
143 English, //拉丁字符,用英文标识
144 Chinese, //CJK字符,以中文代表
145 Number, //阿拉伯数字
146 Control //控制符号,指控制符号已经各种标点符号等
147 }
148
149}
代码2.1.3.3就是构造完后的算法。意思就是把英文字母,数字按空格或者符号划分,而中文则二元拆分。现在来测试下效果。
代码 2.1.3.4 就是测试代码,测试的输入包含了各种字符。来看一下效果。
测试结果:
我是 0 2
一个 2 4
中国 4 6
人 6 7
, 7 8
代码 8 10
yurow 10 15
001 15 18
, 18 19
真是 19 21
个好 21 23
名字 23 25
啊 25 26
! 26 27
! 27 28
! 28 29
哈哈 29 31
哈 31 32
。 32 33
。 33 34
。 34 35
应该说结果符合我们的预期。下来写个Analyzer包装,并把这个包装应用到上一节2.1.2 的方案里去。
代码2.1.3.5就是包装的结果。测试结果:
搜索词:英语
结果:
content:英语
-----------------------------------
搜索词:语法
结果:
content:语法
-----------------------------------
搜索词:单词
结果:
content:单词
-----------------------------------
搜索词:口语
结果:
content:口语
-----------------------------------
搜索词:+content:"英" +content:"语" +content:"单" +content:"词"
结果:
+content:英 +content:语 +content:单 +content:词
-----------------------------------
What's happened? 为什么没有结果?分词器写错了?不要灰心!让我们来分析一下。在DoubleTokenizer类构造函数下一个断点,调试。因为,如果能正确运行,这个构造函数肯定要进入的。调试后看到了什么?传入的TextReader的类型是Lucene.Net.Index.DocumentsWriter.ReusableStringReader。查看Lucene.Net.Index.DocumentsWriter.ReusableStringReader类的定义,它继承自StringReader类,但是它重写掉了一些方法,而且,我们并没有发现我们使用的ReadToEnd方法。问题可能出在这里。看到ReusableStringReader类重写的Read(char[],int,int)方法,试试这个。
代码 2.1.3.6
Code
1using System;
2using System.Collections.Generic;
3using System.Text;
4using Lucene.Net.Analysis;
5using System.IO;
6
7namespace Test.Analysis
8{
9 public class DoubleTokenizer : Tokenizer
10 {
11 /**//// <summary>
12 /// 保持传入的流
13 /// </summary>
14 //private TextReader reader;
15 /**//// <summary>
16 /// 控制分词器只打开一次
17 /// </summary>
18 private bool done = true;
19 /**//// <summary>
20 /// 保存分词结果
21 /// </summary>
22 private List<Token> tokenlist;
23
24 public DoubleTokenizer(TextReader reader)
25 {
26 this.input = reader;
27 }
28 /**//// <summary>
29 /// 上一个字的类型
30 /// </summary>
31 private CharType lastype = CharType.None;
32 /**//// <summary>
33 /// 当前读取到分词的记录数
34 /// </summary>
35 private int ptr = 0;
36 /**//// <summary>
37 /// 重写Next方法
38 /// </summary>
39 /// <param name="result"></param>
40 /// <returns></returns>
41 public override Token Next(Token result)
42 {
43 if (done) //第一次可以运行,运行后将被设置为false,在一个实例中只会运行一次
44 {
45 done = false;
46
47 //-------------------------------------------------------
48 //使用传入参数作为缓冲区
49 char[] charbuffer = result.TermBuffer();
50 int upto = 0;
51 result.Clear();
52 while (true)
53 {
54 int length = input.Read(charbuffer, upto, charbuffer.Length - upto);
55 if (length <= 0)
56 break;
57 upto += length;
58 if (upto == charbuffer.Length)
59 charbuffer = result.ResizeTermBuffer(1 + charbuffer.Length);
60 }
61 result.SetTermLength(upto);
62 //------------------------------------------------------
63 string text = result.TermText();
64 //输入为空,则返回结束符号
65 if (string.IsNullOrEmpty(text))
66 return null;
67 //初始化分词结果
68 tokenlist = new List<Token>();
69 //缓冲器,主要用于暂时保存英文数字字符。
70 StringBuilder buffer = new StringBuilder();
71 Token token;
72 for (int i = 0; i < text.Length; i++)
73 {
74 char nowchar = text[i];
75 char nextchar = new char();
76 CharType nowtype = GetCharType(nowchar);
77 if (i < text.Length - 1) //取下一个字符
78 nextchar = text[i + 1];
79 //状态转换
80 if (nowtype != lastype)
81 {
82 lastype = nowtype;
83 if (buffer.Length > 0)
84 {
85 token = new Token(buffer.ToString(), i - buffer.Length, i);
86 tokenlist.Add(token);
87 buffer.Remove(0, buffer.Length);
88 }
89 }
90
91 switch (nowtype)
92 {
93 case CharType.None:
94 case CharType.Control:
95 goto SingleChar;
96 case CharType.Chinese:
97 break;
98 case CharType.English:
99 case CharType.Number:
100 buffer.Append(nowchar);
101 continue;
102 }
103 //处理连续两个中文字符
104 if (GetCharType(nextchar) == CharType.Chinese)
105 {
106 token = new Token(nowchar.ToString() + nextchar.ToString(), i, i + 2);
107 tokenlist.Add(token);
108 i++;
109 continue;
110 }
111
112 SingleChar: //处理单个字符
113 token = new Token(nowchar.ToString(), i, i + 1);
114 tokenlist.Add(token);
115 continue;
116 }
117 //返回第一个分词结果,并且把指针移向下一位
118 return tokenlist[ptr++];
119 }
120 else
121 {
122 //在分词结果范围内取词
123
124 if (ptr < tokenlist.Count)
125 {
126 return tokenlist[ptr++];
127 }
128 //超出则返回结束符号
129 return null;
130 }
131 }
132 /**//// <summary>
133 /// 获取Char类型
134 /// </summary>
135 /// <param name="c">字符</param>
136 /// <returns>返回类型</returns>
137 public static CharType GetCharType(char c)
138 {
139 switch (char.GetUnicodeCategory(c))
140 {
141 //大小写字符判断为英文字符
142 case System.Globalization.UnicodeCategory.UppercaseLetter:
143 case System.Globalization.UnicodeCategory.LowercaseLetter:
144 return CharType.English;
145 //其它字符判断问中文(CJK)
146 case System.Globalization.UnicodeCategory.OtherLetter:
147 return CharType.Chinese;
148 //十进制数字
149 case System.Globalization.UnicodeCategory.DecimalDigitNumber:
150 return CharType.Number;
151 //其他都认为是符号
152 default:
153 return CharType.Control;
154 }
155 }
156 }
157 /**//// <summary>
158 /// Char类型枚举,用于分词中类型状态比较
159 /// </summary>
160 public enum CharType
161 {
162 None, //默认值,不可识别类型
163 English, //拉丁字符,用英文标识
164 Chinese, //CJK字符,以中文代表
165 Number, //阿拉伯数字
166 Control //控制符号,指控制符号已经各种标点符号等
167 }
168
169}
170
代码改造成了2.1.3.6。主要的改变在于用父类的input字段保持了读入流,然后用Token作为缓冲区,因为它实现了可变缓冲区,简化了我们的开发。测试结果。
搜索词:英语
结果:
content:英语
英语单词,语法,口语都很重要。
口语,语法,单词都是英语的重要组成部分。
-----------------------------------
搜索词:语法
结果:
content:语法
英语单词,语法,口语都很重要。
口语,语法,单词都是英语的重要组成部分。
-----------------------------------
搜索词:单词
结果:
content:单词
英语单词,语法,口语都很重要。
口语,语法,单词都是英语的重要组成部分。
我们要学好英语不但要学语法,单词还有口语。
-----------------------------------
搜索词:口语
结果:
content:口语
英语单词,语法,口语都很重要。
口语,语法,单词都是英语的重要组成部分。
我们要学好英语不但要学语法,单词还有口语。
-----------------------------------
搜索词:+content:"英" +content:"语" +content:"单" +content:"词"
结果:
+content:英 +content:语 +content:单 +content:词
-----------------------------------
终于OK了!!!呵呵。
(PS:长时间编写,可能内容太长了,造成我机器编写这个章节有点卡,所以,这里提前结束。)