使用Lucene.Net达成全文检索!基础解说(一)

Lucene.Net是一套C#开源全文索引库,其主要包含了:
· Index : 提供索引的管理与词组的排序
· Search : 提供查询相关功能
· Store : 支援资料储存管理,包括I/O操作
· Util : 共用套件
· Documents : 负责描述索引储存时的文件结构管理
· QueryParsers : 提供查询语法
· Analysis : 负责分析内容

要达到高效能的全文检索让机器可以明白我们的语言,最重要的关键就是"分词器"了。
试想一下这一句话你会如何拆分成一段一段的关键字呢?
"一天一苹果,医生远离我"
还有英文版本
"An apple a day, doctor keep me away."

中文版本的拆分:
"一天"、"一"、"苹果"、"医生"、"远离"、"我"
英文版本的拆分:
"apple"、"day"、"doctor"、"keep"、"me"、"away"

有没有注意到不同语系所分析出来的关键字有一点不一样呢?
而在Lucene中分词的工作会交给Analysis来完成,
不过我们可以依照不同的语系去选择想使用的分词器(Analyzer)!

首先简单说明一下Lucene的实作流程

  1. 确认主要搜寻的语系来决定使用的分词器(analyzer)

  2. 建立Document依照analyzer汇入资料
    (前置完成)

  3. 建立IndexSearcher导入准备好的Document

  4. 建立Parser来分析SearchValue

  5. 使用IndexSearcher分析Parser取得结果(Hits)

本专案使用的是Lucene.Net 3.0.3
接下来我们来建立一个提供查询使用的Document。

        // 取得或建立Lucene文件资料夹
        if (!File.Exists(_dir.FullName))
        {
            System.IO.Directory.CreateDirectory(_dir.FullName);
        }

        // Asp.Net Core需要於Nuget安装System.Configuration.ConfigurationManager提供用户端应用程序的组态档存取
        Lucene.Net.Store.Directory directory = FSDirectory.Open(_dir);

        // 选择分词器
        var analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_CURRENT);

        // 资料来源
        var repository = new Repository();

        // 依照指定的文件结构来建立
        var indexWriter = new IndexWriter(directory, analyzer, true, IndexWriter.MaxFieldLength.LIMITED);

        foreach (var index in repository)
        {
            var document = new Document();
            document.Add(new Field("Id", index.Id.ToString(), Field.Store.YES, Field.Index.NO));
            document.Add(new Field("Name", index.Name, Field.Store.YES, Field.Index.ANALYZED));
            document.Add(new Field("Description", index.Description, Field.Store.NO, Field.Index.ANALYZED));
            indexWriter.AddDocument(document);
        }

        indexWriter.Optimize();
        indexWriter.Commit();
        indexWriter.Dispose();

如此一来我们就建立好Lucene的基本配备罗!
其中analyzer的部分我们使用Lucene.Net预设,
要特别注意的是,其处理中文语系的能力非常之烂!
之後再写一篇文章深入探讨。

再来值得一提的是

        document.Add(new Field("Id", index.Id.ToString(), Field.Store.YES, Field.Index.NO));

前两个参数就是Key跟Value,可以简单理解为栏位与其内容。
後面两个参数是重点!

Store: 代表是否储存这个Key的Value
例如在google打上台南美食会搜索出许多不同的文章连结,
不过google给你的资料中最重要的不是文章内容(Description),
而是哪一篇文章(Name)与台南美食最有关系。
假如今天我只要回传一个列表而不用提示文章中有哪些内容,
那麽我就可以选择给"Description" Field.Store.No来节省空间。

Index:
· NO - 不加入索引,这个内容只需要随着结果出炉,不需要在查询的时候被考虑。
· ANALYZED、NOT_ANALYZED - 是否使用分词
· NO_NORMS - 关闭权重功能
或许许多人会对权重功能(NORMS)感到疑惑,
简单的举个例子
{ Id=1, Key="苹果", Value="一天一苹果,医生远离我。"}
{ Id=2, Key="橘子", Value="医生给娜美最珍贵的宝藏。"}
{ Id=3, Key="梨子", Value="我是梨子,比苹果苹果好吃多罗!"}
当我搜寻"苹果"的时候结果会是
{ Id=1, MatchKey=1, MatchValue=1, Score=(15) + (12) = 7}
{ Id=3, MatchKey=0, MatchValue=1, Score=(05) + (22) = 4}
有发现了吗?
虽然同样都对中两个结果但是Id 1的资料Key值中有包含关键字,
因此得到较高的分数排在Id 3前方

准备好Document了,我们可以开始来实际使用看看罗!

        // 决定所要搜索的栏位
        var parser = new QueryParser(Lucene.Net.Util.Version.LUCENE_CURRENT, "Description", analyzer).Parse(searchValue);

        // 提供刚刚建立的Document
        var indexSearcher = new IndexSearcher(directory);

        // 搜寻取出结果的数量
        var queryLimit = 20;

        // 开始搜寻!
        var hits = indexSearcher.Search(parser, queryLimit);

        if (!hits.ScoreDocs.Any())
        {
            Console.WriteLine("查无相关结果。");
            return;
        }

        Document doc;

        foreach (var hit in hits.ScoreDocs)
        {
            doc = indexSearcher.Doc(hit.Doc);
            Console.WriteLine("Score :" + hit.Score + ", Id :" + doc.Get("Id") + ", Name :" + doc.Get("Name") + ", Description :" + doc.Get("Description"));
        }

最後的结果(Hits),是需要再回到Document去捞出对应的资料喔!
是不是非常简单呢?
笔者写了一个简单的范例在GitHub上,秉持着追求新技术的心使用了.Net 6,还请各位大大多多包涵。
有中英文两种Repository,只需要在上方的DI注入切换就可以罗!

GitHub连结: https://github.com/g13579112000/Lucene

笔者第一次撰写这种教学文章,有哪边错误的非常欢迎一起来讨论指教。
之後有机会再撰写Lucene更深入的应用方面,
例如权重的分配与分词器的选择与使用。

感谢您的阅读。

参考文献:
1.黑暗大大的全文检索笔记: https://blog.darkthread.net/blog/lucene-net-notes-1/
2.使用.Net实现全文检索: https://blog.csdn.net/huwei2003/article/details/53408388
3.伊凡的部落格: http://irfen.me/5-lucene4-9-learning-record-lucene-analysis-tokenizer/
4.纯净天空代码范例: https://vimsky.com/zh-tw/examples/detail/csharp-ex-Lucene.Net.Documents-Document---class.html


<<:  AAC 转档 MP3

>>:  Laravel 技术笔记 (一)【Routing 路由】

Day 5— 自动化回信机(2) 读取试算表内容

昨天我们把试算表的前置作业完成,今天我们来看看将别人填表单後送到试算表中的内容怎麽被读取。 首先先来...

[ Day 21 ] - 认识 JS 原生写法处理 AJAX

JS 原生写法处理 AJAX XMLHttpRequest 透过建立一个XMLHttpRequest...

Day3 - 接案网不是拿来接案的

我以前最常用的接案网叫做爱苏活,当年不像现在有这麽多免费的接案社团,大部分都是要付年费才能取得发案者...

XSS&CSRF&Replay

跨站点脚本(Cross-Site Scripting:XSS)和注入(Injection) 输入验证...

【day26】Span翻转TextView

好的,连假最後一天,我们来个小篇章,就是Span啦,Span可以做到的事情有很多,如 *更改特定位...