Lucene.Net 2.3.1开发介绍 —— 二、分词(五)

原文:Lucene.Net 2.3.1开发介绍 —— 二、分词(五)

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

Code
 1/**//// <summary>
 2/// 获取Char类型
 3/// </summary>
 4/// <param name="c">字符</param>
 5/// <returns>返回类型</returns>
 6public static CharType GetCharType(char c)
 7{
 8    switch (char.GetUnicodeCategory(c))
 9    {
10        //大小写字符判断为英文字符
11        case System.Globalization.UnicodeCategory.UppercaseLetter:
12        case System.Globalization.UnicodeCategory.LowercaseLetter:
13            return CharType.English;
14        //其它字符判断问中文(CJK)
15        case System.Globalization.UnicodeCategory.OtherLetter:
16            return CharType.Chinese;
17        //十进制数字
18        case System.Globalization.UnicodeCategory.DecimalDigitNumber:
19            return CharType.Number;
20        //其他都认为是符号
21        default:
22            return CharType.Control;
23    }
24}

代码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

Code
 1using System;
 2using System.Collections.Generic;
 3using System.Text;
 4using NUnit.Framework;
 5using System.IO;
 6using Lucene.Net.Analysis;
 7
 8namespace Test.Analysis
 9{
10    [TestFixture]
11    public class DoubleTokenizerTest
12    {
13        [Test]
14        public void NextTest()
15        {
16            string testwords = "我是一个中国人,代码yurow001,真是个好名字啊!!!哈哈哈。。。";
17            DoubleTokenizer tk = new DoubleTokenizer(new StringReader(testwords));
18            Token token;
19            while ((token = tk.Next()) != null)
20            {
21                Console.WriteLine(token.TermText() + "\t" + token.StartOffset() + "\t" + token.EndOffset());
22            }
23            tk.Close();
24        }
25    }
26}
27

代码 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

Code
 1using System;
 2using System.Collections.Generic;
 3using System.Text;
 4using Lucene.Net.Analysis;
 5
 6namespace Test.Analysis
 7{
 8    public class DoubleAnalyzer : Analyzer
 9    {
10        public override TokenStream TokenStream(string fieldName, System.IO.TextReader reader)
11        {
12            return new DoubleTokenizer(reader);
13        }
14    }
15}
16

代码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:长时间编写,可能内容太长了,造成我机器编写这个章节有点卡,所以,这里提前结束。)

时间: 2024-10-13 16:38:19

Lucene.Net 2.3.1开发介绍 —— 二、分词(五)的相关文章

Lucene.Net 2.3.1开发介绍 —— 二、分词(四)

原文:Lucene.Net 2.3.1开发介绍 -- 二.分词(四) 2.1.2 可以使用的内置分词 简单的分词方式并不能满足需求.前文说过Lucene.Net内置分词中StandardAnalyzer分词还算比较实用(见1.1.2小节).StandardAnalyzer为什么能满足我们的部分需求,而它又有哪些不足呢?看分词的好坏还是要从效果说起.简单的说,在中英文混合的情况下,StandardAnalyzer会把英文按空格拆,而中文则按单字拆.因为中文是按单字拆,所以对分词的准确性起到了干扰,

Lucene.Net 2.3.1开发介绍 —— 二、分词(三)

原文:Lucene.Net 2.3.1开发介绍 -- 二.分词(三) 1.3 分词器结构 1.3.1 分词器整体结构 从1.2节的分析,终于做到了管中窥豹,现在在Lucene.Net项目中添加一个类关系图,把TokenStream和他的儿孙们统统拉上去,就能比较好的把握他们之间的关系. 图 1.3.1.1 如图1.3.1.1 就是他们的类关系图.看出如果要做一个分词器,最短的路,就是继承第二代,成为第三代.然后再写一个Analyzer的子类,专门用来做新分词器的适配器就好了.转换器.  呵呵,写

Lucene.Net 2.3.1开发介绍 —— 四、搜索(二)

原文:Lucene.Net 2.3.1开发介绍 -- 四.搜索(二) 4.3 表达式用户搜索,只会输入一个或几个词,也可能是一句话.输入的语句是如何变成搜索条件的上一篇已经略有提及. 4.3.1 观察表达式在研究表达式之前,一定要知道,任何一个Query都会对于一个表达式.不光可以通过Query构造表达式,还可以通过拼接字符串构造.这里说的观察表达式是指,用Query完成查询语句后,用ToString()方法输出Query的表达式.很简单是吧,呵呵. 4.3.2 表达式的与或非“与或非”让我想起

Lucene.Net 2.3.1开发介绍 —— 三、索引(二)

原文:Lucene.Net 2.3.1开发介绍 -- 三.索引(二) 2.索引中用到的核心类 在Lucene.Net索引开发中,用到的类不多,这些类是索引过程的核心类.其中Analyzer是索引建立的基础,Directory是索引建立中或者建立好存储的介质,Document和Field类是逻辑结构的核心,IndexWriter是操作的核心.其他类的使用都被隐藏掉了,这也是为什么Lucene.Net使用这么方便的原因. 2.1 Analyzer 前面已经对Analyzer进行了很详细的讲解,Ana

Lucene.Net 2.3.1开发介绍 —— 三、索引(四)

原文:Lucene.Net 2.3.1开发介绍 -- 三.索引(四) 4.索引对搜索排序的影响 搜索的时候,同一个搜索关键字和同一份索引,决定了一个结果,不但决定了结果的集合,也确定了结果的顺序.那个这个结果是怎么得出来的?这个顺序又是怎么排的呢?这两个问题不是本节讨论的重点,但是这两个问题却关系到本节要讨论的,索引对结果的影响问题.在不使用字段排序的情况下,Lucene.Net默认是按文档的得分来排序的,这个公式看着很复杂,感觉像是大学时高数书上的那些个公式,其实说清楚了也简单. 关于文档排序

Lucene.Net 2.3.1开发介绍 —— 三、索引(五)

原文:Lucene.Net 2.3.1开发介绍 -- 三.索引(五) 话接上篇,继续来说权重对排序的影响.从上面的4个测试,只能说是有个直观的理解了.“哦,是!调整权重是能影响排序了,但是好像没办法来分析到底怎么调啊!”.似乎是这样,现在需要把问题放大,加大索引的内容.到博客园新闻区,用zzk找了4篇内容包含“测试”的文章.代码变成 2.1.5 代码2.1.5  1using System;  2using System.Collections.Generic;  3using Lucene.N

Lucene.Net 2.3.1开发介绍 —— 三、索引(七)

原文:Lucene.Net 2.3.1开发介绍 -- 三.索引(七) 5.IndexWriter 索引这部分最后讲的是IndexWriter.如果说前面提到的都是数据的结构,那么IndexWriter就是业务的封装.无论述Document,Field还是看不见的Segment,Term都是对数据存储逻辑的抽象,IndexWriter包装了操作的过程. 当然,这里不会讨论IndexWriter的每个细节,这里主要介绍IndexWriter的常用法和实际使用中遇到的部署问题. 5.1 IndexWr

Lucene.Net 2.3.1开发介绍 —— 四、搜索(三)

原文:Lucene.Net 2.3.1开发介绍 -- 四.搜索(三) Lucene有表达式就有运算符,而运算符使用起来确实很方便,但另外一个问题来了. 代码 4.3.4.1 Analyzer analyzer = new StandardAnalyzer(); QueryParser parser = new QueryParser("title", analyzer); Query query = parser.Parse(@":"); Console.Write

Lucene.Net 2.3.1开发介绍 —— 四、搜索(一)

原文:Lucene.Net 2.3.1开发介绍 -- 四.搜索(一) 既然是内容筛选,或者说是搜索引擎,有索引,必然要有搜索.搜索虽然与索引有关,那也只是与索引后的文件有关,和索引的程序是无关的,因此,搜索和索引一般是分开部署.简单地说,就是一个应用程序(桌面程序)来索引,一个WEB程序来实现搜索.当然,为了测试的时候简单,这里还是使用NUnit的方式运行.搜索讲完后,将会简单介绍单机搜索引擎如何部署. 4.1 搜索与什么有关 搜索与什么有关呢?即使没有看过前面的文章,那么现在来随便猜一猜. 首