当前位置: 移动技术网 > IT编程>开发语言>c# > 基于JieBaNet+Lucene.Net实现全文搜索

基于JieBaNet+Lucene.Net实现全文搜索

2019年10月17日  | 移动技术网IT编程  | 我要评论

 

实现效果:

  上一篇文章有附全文搜索结果的设计图,下面截一张开发完成上线后的实图:

  基本风格是模仿的百度搜索结果,绿色的分页略显小清新。

  目前已采集并创建索引的文章约3w多篇,索引文件不算太大,查询速度非常棒。

  

刀不磨要生锈,人不学要落后。每天都要学一些新东西。 

 

基本技术介绍:

  还记得上一次做全文搜索是在2013年,主要核心设计与代码均是当时的架构师写的,自己只能算是全程参与。

  当时使用的是经典搭配:盘古分词+lucene.net。

  前几篇文章有说到,盘古分词已经很多年不更新了,我在supportyun系统一直引用的jiebanet来做分词技术。

  那么是否也有成型的jiebanet+lucene.net的全文搜索方案呢?

  经过多番寻找,在github上面找到一个简易的例子:https://github.com/anderscui/jiebaforlucenenet

  博主下面要讲的实现方案就是从这个demo得到的启发,大家有兴趣可以去看看这个demo。

  博主使用的具体版本:lucene.net 3.0.3.0 ,jiebanet 0.38.3.0(做过简易的调整与扩展,前面文章有讲到)

  首先我们对lucene.net的分词器tokenizer、分析器analyzer做一个基于jiebanet的扩展。

  1.基于lucenenet扩展的jieba分析器jiebaforluceneanalyzer  

复制代码
复制代码
 1     /// <summary>
 2     /// 基于lucenenet扩展的jieba分析器
 3     /// </summary>
 4     public class jiebaforluceneanalyzer : analyzer
 5     {
 6         protected static readonly iset<string> defaultstopwords = stopanalyzer.english_stop_words_set;
 7 
 8         private static iset<string> stopwords;
 9 
10         static jiebaforluceneanalyzer()
11         {
12             stopwords = new hashset<string>();
13             var stopwordsfile = path.getfullpath(jiebanet.analyser.configmanager.stopwordsfile);
14             if (file.exists(stopwordsfile))
15             {
16                 var lines = file.readalllines(stopwordsfile);
17                 foreach (var line in lines)
18                 {
19                     stopwords.add(line.trim());
20                 }
21             }
22             else
23             {
24                 stopwords = defaultstopwords;
25             }
26         }
27 
28         public override tokenstream tokenstream(string fieldname, textreader reader)
29         {
30             var seg = new jiebasegmenter();
31             tokenstream result = new jiebaforlucenetokenizer(seg, reader);
32             result = new lowercasefilter(result);
33             result = new stopfilter(true, result, stopwords);
34             return result;
35         }
36     }
复制代码
复制代码

  2.基于lucenenet扩展的jieba分词器:jiebaforlucenetokenizer

复制代码
复制代码
 1     /// <summary>
 2     /// 基于lucene的jieba分词扩展
 3     /// </summary>
 4     public class jiebaforlucenetokenizer:tokenizer
 5     {
 6         private readonly jiebasegmenter segmenter;
 7         private readonly itermattribute termatt;
 8         private readonly ioffsetattribute offsetatt;
 9         private readonly itypeattribute typeatt;
10 
11         private readonly list<token> tokens;
12         private int position = -1;
13 
14         public jiebaforlucenetokenizer(jiebasegmenter seg, textreader input):this(seg, input.readtoend()) { }
15 
16         public jiebaforlucenetokenizer(jiebasegmenter seg, string input)
17         {
18             segmenter = seg;
19             termatt = addattribute<itermattribute>();
20             offsetatt = addattribute<ioffsetattribute>();
21             typeatt = addattribute<itypeattribute>();
22 
23             var text = input;
24             tokens = segmenter.tokenize(text, tokenizermode.search).tolist();
25         }
26 
27         public override bool incrementtoken()
28         {
29             clearattributes();
30             position++;
31             if (position < tokens.count)
32             {
33                 var token = tokens[position];
34                 termatt.settermbuffer(token.word);
35                 offsetatt.setoffset(token.startindex, token.endindex);
36                 typeatt.type = "jieba";
37                 return true;
38             }
39 
40             end();
41             return false;
42         }
43 
44         public ienumerable<token> tokenize(string text, tokenizermode mode = tokenizermode.search)
45         {
46             return segmenter.tokenize(text, mode);
47         }
48     }
复制代码
复制代码

理想如果不向现实做一点点屈服,那么理想也将归于尘土。 

 

实现方案设计:

  我们做全文搜索的设计时一定会考虑的一个问题就是:我们系统是分很多模块的,不同模块的字段差异很大,怎么才能实现同一个索引,既可以单个模块搜索又可以全站搜索,甚至按一些字段做条件来搜索呢?

  这些也是supportyun系统需要考虑的问题,因为目前的数据就天然的拆分成了活动、文章两个类别,字段也大有不同。博主想实现的是一个可以全站搜索(结果包括活动、文章),也可以在文章栏目/活动栏目分别搜索,并且可以按几个指定字段来做搜索条件。

  要做一个这样的全文搜索功能,我们需要从程序设计上来下功夫。下面就介绍一下博主的设计方案:

  一、索引创建

    

    1.我们设计一个indexmanager来处理最基本的索引创建、更新、删除操作。

 

    2.创建、更新使用到的标准数据类:indexcontent。

    我们设计tablename(对应db表名)、rowid(对应db主键)、collecttime(对应db数据创建时间)、moduletype(所属系统模块)、title(检索标题)、indextextcontent(检索文本)等六个基础字段,所有模块需要创建索引必须构建该6个字段(大家可据具体情况扩展)。

    然后设计10个预留字段tag1-tag10,用以兼容各大模块其他不同字段。

    预留字段的存储、索引方式可独立配置。

 

    其中baseindexcontent含有六个基础字段。

    3.创建一个子模块索引构建器的接口:iindexbuilder。

    各子模块通过继承实现iindexbuilder,来实现索引的操作。

 

    4.下面我们以活动模块为例,来实现索引创建。

    a)首先创建一个基于活动模块的数据类:activityindexcontent,可以将我们需要索引或存储的字段都设计在内。

 

    b)我们再创建activityindexbuilder并继承iindexbuilder,实现其创建、更新、删除方法。

 

    代码就不解释了,很简单。主要就是调用indexmanager来执行操作。

    我们只需要在需要创建活动数据索引的业务点,构建activityindexbuilder对象,并构建activityindexcontent集合作为参数,调用buildindex方法即可。

 

  二、全文搜索

    全文搜索我们采用同样的设计方式。

    1.设计一个抽象的搜索类:baseindexsearch,所有搜索模块(包括全站)均需继承它来实现搜索效果。

复制代码
复制代码
  1     public abstract class baseindexsearch<tindexsearchresultitem>
  2         where tindexsearchresultitem : indexsearchresultitem
  3     {
  4         /// <summary>
  5         /// 索引存储目录
  6         /// </summary>
  7         private static readonly string indexstorepath = configurationmanager.appsettings["indexstorepath"];
  8         private readonly string[] fieldstosearch;
  9         protected static readonly simplehtmlformatter formatter = new simplehtmlformatter("<em>", "</em>");
 10         private static indexsearcher indexsearcher = null;
 11 
 12         /// <summary>
 13         /// 索引内容命中片段大小
 14         /// </summary>
 15         public int fragmentsize { get; set; }
 16 
 17         /// <summary>
 18         /// 构造方法
 19         /// </summary>
 20         /// <param name="fieldstosearch">搜索文本字段</param>
 21         protected baseindexsearch(string[] fieldstosearch)
 22         {
 23             fragmentsize = 100;
 24             this.fieldstosearch = fieldstosearch;
 25         }
 26 
 27         /// <summary>
 28         /// 创建搜索结果实例
 29         /// </summary>
 30         /// <returns></returns>
 31         protected abstract tindexsearchresultitem createindexsearchresultitem();
 32 
 33         /// <summary>
 34         /// 修改搜索结果(主要修改tag字段对应的属性)
 35         /// </summary>
 36         /// <param name="indexsearchresultitem">搜索结果项实例</param>
 37         /// <param name="content">用户搜索内容</param>
 38         /// <param name="docindex">索引库位置</param>
 39         /// <param name="doc">当前位置内容</param>
 40         /// <returns>搜索结果</returns>
 41         protected abstract void modifyindexsearchresultitem(ref tindexsearchresultitem indexsearchresultitem, string content, int docindex, document doc);
 42 
 43         /// <summary>
 44         /// 修改筛选器(各模块)
 45         /// </summary>
 46         /// <param name="filter"></param>
 47         protected abstract void modifysearchfilter(ref dictionary<string, string> filter);
 48 
 49         /// <summary>
 50         /// 全库搜索
 51         /// </summary>
 52         /// <param name="content">搜索文本内容</param>
 53         /// <param name="filter">查询内容限制条件,默认为null,不限制条件.</param>
 54         /// <param name="fieldsorts">对字段进行排序</param>
 55         /// <param name="pageindex">查询结果当前页,默认为1</param>
 56         /// <param name="pagesize">查询结果每页结果数,默认为20</param>
 57         public pagedindexsearchresult<tindexsearchresultitem> search(string content
 58             , dictionary<string, string> filter = null, list<fieldsort> fieldsorts = null
 59             , int pageindex = 1, int pagesize = 20)
 60         {
 61             try
 62             {
 63                 if (!string.isnullorempty(content))
 64                 {
 65                     content = replaceindexsensitivewords(content);
 66                     content = getkeywordssplitbyspace(content,
 67                         new jiebaforlucenetokenizer(new jiebasegmenter(), content));
 68                 }
 69                 if (string.isnullorempty(content) || pageindex < 1)
 70                 {
 71                     throw new exception("输入参数不符合要求(用户输入为空,页码小于等于1)");
 72                 }
 73 
 74                 var stopwatch = new stopwatch();
 75                 stopwatch.start();
 76 
 77                 analyzer analyzer = new jiebaforluceneanalyzer();
 78                 // 索引条件创建
 79                 var query = makesearchquery(content, analyzer);
 80                 // 筛选条件构建
 81                 filter = filter == null ? new dictionary<string, string>() : new dictionary<string, string>(filter);
 82                 modifysearchfilter(ref filter);
 83                 filter lucenefilter = makesearchfilter(filter);
 84 
 85                 #region------------------------------执行查询---------------------------------------
 86 
 87                 topdocs topdocs;
 88                 if (indexsearcher == null)
 89                 {
 90                     var dir = new directoryinfo(indexstorepath);
 91                     fsdirectory entitydirectory = fsdirectory.open(dir);
 92                     indexreader reader = indexreader.open(entitydirectory, true);
 93                     indexsearcher = new indexsearcher(reader);
 94                 }
 95                 else
 96                 {
 97                     indexreader indexreader = indexsearcher.indexreader;
 98                     if (!indexreader.iscurrent())
 99                     {
100                         indexsearcher.dispose();
101                         indexsearcher = new indexsearcher(indexreader.reopen());
102                     }
103                 }
104                 // 收集器容量为所有
105                 int totalcollectcount = pageindex*pagesize;
106                 sort sort = getsortbyfieldsorts(fieldsorts);
107                 topdocs = indexsearcher.search(query, lucenefilter, totalcollectcount, sort ?? sort.relevance);
108 
109                 #endregion
110 
111                 #region-----------------------返回结果生成-------------------------------
112 
113                 scoredoc[] hits = topdocs.scoredocs;
114                 var start = (pageindex - 1)*pagesize + 1;
115                 var end = math.min(totalcollectcount, hits.count());
116 
117                 var result = new pagedindexsearchresult<tindexsearchresultitem>
118                 {
119                     pageindex = pageindex,
120                     pagesize = pagesize,
121                     totalrecords = topdocs.totalhits
122                 };
123 
124                 for (var i = start; i <= end; i++)
125                 {
126                     var scoredoc = hits[i - 1];
127                     var doc = indexsearcher.doc(scoredoc.doc);
128 
129                     var indexsearchresultitem = createindexsearchresultitem();
130                     indexsearchresultitem.docindex = scoredoc.doc;
131                     indexsearchresultitem.moduletype = doc.get("moduletype");
132                     indexsearchresultitem.tablename = doc.get("tablename");
133                     indexsearchresultitem.rowid = guid.parse(doc.get("rowid"));
134                     if (!string.isnullorempty(doc.get("collecttime")))
135                     {
136                         indexsearchresultitem.collecttime = datetime.parse(doc.get("collecttime"));
137                     }
138                     var title = gethighlighter(formatter, fragmentsize).getbestfragment(content, doc.get("title"));
139                     indexsearchresultitem.title = string.isnullorempty(title) ? doc.get("title") : title;
140                     var text = gethighlighter(formatter, fragmentsize)
141                         .getbestfragment(content, doc.get("indextextcontent"));
142                     indexsearchresultitem.content = string.isnullorempty(text)
143                         ? (doc.get("indextextcontent").length > 100
144                             ? doc.get("indextextcontent").substring(0, 100)
145                             : doc.get("indextextcontent"))
146                         : text;
147                     modifyindexsearchresultitem(ref indexsearchresultitem, content, scoredoc.doc, doc);
148                     result.add(indexsearchresultitem);
149                 }
150                 stopwatch.stop();
151                 result.elapsed = stopwatch.elapsedmilliseconds*1.0/1000;
152 
153                 return result;
154 
155                 #endregion
156             }
157             catch (exception exception)
158             {
159                 logutils.errorlog(exception);
160                 return null;
161             }
162         }
163 
164         private sort getsortbyfieldsorts(list<fieldsort> fieldsorts)
165         {
166             if (fieldsorts == null)
167             {
168                 return null;
169             }
170             return new sort(fieldsorts.select(fieldsort => new sortfield(fieldsort.fieldname, sortfield.float, !fieldsort.ascend)).toarray());
171         }
172 
173         private static filter makesearchfilter(dictionary<string, string> filter)
174         {
175             filter lucenefilter = null;
176             if (filter != null && filter.keys.any())
177             {
178                 var booleanquery = new booleanquery();
179                 foreach (keyvaluepair<string, string> keyvaluepair in filter)
180                 {
181                     var termquery = new termquery(new term(keyvaluepair.key, keyvaluepair.value));
182                     booleanquery.add(termquery, occur.must);
183                 }
184                 lucenefilter = new querywrapperfilter(booleanquery);
185             }
186             return lucenefilter;
187         }
188 
189         private query makesearchquery(string content, analyzer analyzer)
190         {
191             var query = new booleanquery();
192             // 总查询参数
193             // 属性查询
194             if (!string.isnullorempty(content))
195             {
196                 queryparser parser = new multifieldqueryparser(version.lucene_30, fieldstosearch, analyzer);
197                 query queryobj;
198                 try
199                 {
200                     queryobj = parser.parse(content);
201                 }
202                 catch (parseexception parseexception)
203                 {
204                     throw new exception("在filelibraryindexsearch中构造query时出错。", parseexception);
205                 }
206                 query.add(queryobj, occur.must);
207             }
208             return query;
209         }
210 
211         private string getkeywordssplitbyspace(string keywords, jiebaforlucenetokenizer jiebaforlucenetokenizer)
212         {
213             var result = new stringbuilder();
214 
215             var words = jiebaforlucenetokenizer.tokenize(keywords);
216 
217             foreach (var word in words)
218             {
219                 if (string.isnullorwhitespace(word.word))
220                 {
221                     continue;
222                 }
223 
224                 result.appendformat("{0} ", word.word);
225             }
226 
227             return result.tostring().trim();
228         }
229 
230         private string replaceindexsensitivewords(string str)
231         {
232             str = str.replace("+", "");
233             str = str.replace("+", "");
234             str = str.replace("-", "");
235             str = str.replace("-", "");
236             str = str.replace("!", "");
237             str = str.replace("!", "");
238             str = str.replace("(", "");
239             str = str.replace(")", "");
240             str = str.replace("(", "");
241             str = str.replace(")", "");
242             str = str.replace(":", "");
243             str = str.replace(":", "");
244             str = str.replace("^", "");
245             str = str.replace("[", "");
246             str = str.replace("]", "");
247             str = str.replace("【", "");
248             str = str.replace("】", "");
249             str = str.replace("{", "");
250             str = str.replace("}", "");
251             str = str.replace("{", "");
252             str = str.replace("}", "");
253             str = str.replace("~", "");
254             str = str.replace("~", "");
255             str = str.replace("*", "");
256             str = str.replace("*", "");
257             str = str.replace("?", "");
258             str = str.replace("?", "");
259             return str;
260         }
261 
262         protected highlighter gethighlighter(formatter formatter, int fragmentsize)
263         {
264             var highlighter = new highlighter(formatter, new segment()) { fragmentsize = fragmentsize };
265             return highlighter;
266         }
267     }
复制代码
复制代码

    几个protected abstract方法,是需要继承的子类来实现的。

    其中为了实现搜索结果对命中关键词进行高亮显示,特引用了盘古分词的highlighter。原则是此处应该是参照盘古分词的源码,自己使用jiebanet来做实现的,由于工期较紧,直接引用了盘古。

    2.我们设计一个indexsearchresultitem,表示搜索结果的基类。

 

    3.我们来看看具体的实现,先来看全站搜索的searchservice

复制代码
复制代码
 1     public class indexsearch : baseindexsearch<indexsearchresultitem>
 2     {
 3         public indexsearch()
 4             : base(new[] { "indextextcontent", "title" })
 5         {
 6         }
 7 
 8         protected override indexsearchresultitem createindexsearchresultitem()
 9         {
10             return new indexsearchresultitem();
11         }
12 
13         protected override void modifyindexsearchresultitem(ref indexsearchresultitem indexsearchresultitem, string content,
14             int docindex, document doc)
15         {
16             //不做修改
17         }
18 
19         protected override void modifysearchfilter(ref dictionary<string, string> filter)
20         {
21             //不做筛选条件修改
22         }
23     }
复制代码
复制代码

    是不是非常简单。由于我们此处搜索的是全站,结果展示直接用基类,取出基本字段即可。

    4.再列举一个活动的搜索实现。

    a)我们首先创建一个活动搜索结果类activityindexsearchresultitem,继承自结果基类indexsearchresultitem

 

    b)然后创建活动模块的搜索服务:activityindexsearch,同样需要继承baseindexsearch,这时候activityindexsearch只需要相对全站搜索修改几个参数即可。

复制代码
复制代码
 1     public class activityindexsearch: baseindexsearch<activityindexsearchresultitem>
 2     {
 3         public activityindexsearch()
 4             : base(new[] { "indextextcontent", "title" })
 5         {
 6         }
 7 
 8         protected override activityindexsearchresultitem createindexsearchresultitem()
 9         {
10             return new activityindexsearchresultitem();
11         }
12 
13         protected override void modifyindexsearchresultitem(ref activityindexsearchresultitem indexsearchresultitem, string content,
14             int docindex, document doc)
15         {
16             indexsearchresultitem.activitytypes = doc.get("tag1");
17             indexsearchresultitem.url = doc.get("tag2");
18             indexsearchresultitem.sourcename = doc.get("tag3");
19             indexsearchresultitem.sourceofficialhotline = doc.get("tag4");
20             indexsearchresultitem.sourceurl = doc.get("tag5");
21             indexsearchresultitem.cityid=new guid(doc.get("tag6"));
22             indexsearchresultitem.address = doc.get("tag7");
23             indexsearchresultitem.activitydate = doc.get("tag8");
24         }
25 
26         protected override void modifysearchfilter(ref dictionary<string, string> filter)
27         {
28             filter.add("moduletype", "活动");
29         }
30     }
复制代码
复制代码

    筛选条件加上模块=活动,返回结果数据类指定,活动特有字段返回赋值。

    业务调用就非常简单了。

    全站全文搜索:我们直接new indexsearch(),然后调用其search()方法

    活动全文搜索:我们直接new activityindexsearch(),然后调用其search()方法

    search()方法几个参数:

    ///<param name="content">搜索文本内容</param>
    /// <param name="filter">查询内容限制条件,默认为null,不限制条件.</param>
    /// <param name="fieldsorts">对字段进行排序</param>
    /// <param name="pageindex">查询结果当前页,默认为1</param>
    /// <param name="pagesize">查询结果每页结果数,默认为20</param>

 如果我们用软能力而不是用技术能力来区分程序员的好坏 – 是不是有那么点反常和变态。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网