【大数据】Lucene全文搜索引擎入门篇(零基础小白也适用)

前言:Lucene是apache老爹开源的一款全文搜索引擎,虽然目前已被市面上一些更好用的搜索引擎逐步替代,但作为搜索引擎的鼻祖,仍然有必要学习一番,而且有了Lucene的基础之后,学习solr,elastichSearch也会更容易理解。

看完本篇,你将了解到Lucene是什么,Lucene的使用场景,原理,什么是倒排索引,如何分词,以及如何使用lucene实战构建简易的搜索引擎等。


目录

1.Lucene可以用来做什么?

2.Lucene的原理

3.Lucene的倒排索引

4.Lucene分词

5.Lucene索引建立与窥探

6.Lucene的实际使用

7.索引编辑(更新)

 8.拓展功能

9.总结


1.Lucene可以用来做什么?

我们知道,传统关系型数据库在数据量大到一定量级后(一般是单表超过500W行或者2G左右),查询速度会变得非常缓慢,也比较吃资源,加上需求中经常会有一些模糊查询,模糊查询可能会导致传统数据库的索引失效,这时候如果再用传统数据库就呵呵了。所以在好的电商系统中,搜素引擎都是标配。通过搜素引擎,可以很好的减轻数据库的读压力。除此之外,像我们熟知的百度,谷歌等搜索大佬,也都有用到搜索引擎,搜索引擎还可以用于搭建文库,总之很强大,可以做很多事情。

2.Lucene的原理

Lucene的原理我说白话一点,就是先把被搜索内容按照一定规则进行分词并存储,生成一个目录,然后把你输入的搜索关键词也进行分词,然后与前面生成的目录中的关键词进行匹配,如果匹配到,就可以快速定位到该被搜索内容的存储位置,从而实现搜索。说简单点就是这样,当然里面有很多细节,先不着急,等看完倒排索引之后你会对Lucene的原理有更深的了解。

如图,比如我搜索了程序员秃头,搜索引擎一般会根据一定的拆分规则,把我输入的“程序员秃头”拆分成:程序员,秃头,头等关键字。然后与爬虫爬取到的大量文档里的关键词进行匹配(这些大量文档会按照一定规则预先分词,建立索引,然后存储起来),如果匹配到了就可以返回给用户了,返回的结果可以对关键词添加一些“高亮”,标红等特效,也可以按照一定规则对搜索结果进行排序。


3.Lucene的倒排索引

这里我援引别人已经画好的图来解释下倒排索引,描述也力求简洁,帮助快速理解:

下面第一张图是文档的原始内容和编号,第二张图是建立的倒排索引。

倒排索引的建立步骤:

①先对原始文档中的内容按照一定规则进行分词,至于如何分先不用管,我后面会讲到。

②然后对分词后的单词在原始文档中进行定位,比如“谷歌”在原始文档中的1-5号文档都有出现过,所以谷歌的倒排列表就是:1,2,3,4,5. 其它倒排列表以此类推。

有了这样一套对应列表之后,下次用户如果输入了“网站”这个关键词,系统就可以通过倒排列表快速定位到原始文档中的第5项数据,类似于生成了一本书的一个目录,告诉你几个关键词,可以在目录中快速定位具体的文档在多少页,不用再一页一页翻了。

看到这里你应该明白倒排索引的原理了,可以先思考下为啥叫“倒排”索引,而不是别的名字?

回答这个问题前,先看下什么是正向索引:正向索引建立的关系是由文档->关键词的,也就是给定一篇文档,记录关键词在文档中出现的位置,通过文档来关联关键词的,这种方式在数据量小的情况下完全OK,但互联网这片大海中,文档的量及是宇宙级的,这种方式要扫描的文档驴辈子都扫不完。倒排索引正好是逆向思维,把文档的关键词提取,然后通过关键词取关联文档,这样就极大的减轻了扫描的量级。

关于倒排索引就提这么多,实际上要比这个复杂一些,感兴趣的建议读完之后可以回头来深入研究,以免打断思路,这里直接给出我援引的博主图片的博客地址:https://blog.csdn.net/csdnliuxin123524/article/details/91581209

4.Lucene分词

Lucene提供了几种分词的实现,在4.0+的版本后还提供了对中文分词的支持,据说效果不太好,于是后来有人开源了更好用的中文分词器:IK Analysis,词库可以自定义,不太友好的地方就是这款基于Lucene的中文分词器好像因为Lucene的落寞而年久失修了,版本还停留在2012年的版本,对新版本的Lucene无法兼容,据说当年创造了每秒160万分词的神话。

5.Lucene索引建立与窥探

这里我直接通过代码来实现Lucene的分词,分词器我采用lucene提供的代码其实很简单,不要被其篇幅所蛊惑。

第一步,引入依赖,这里我建立一个Maven工程为例:

pom.xml中引入如下依赖:

   <dependencies>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queries</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-memory</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-smartcn</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-join</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
    </dependencies>

版本号我这里使用截至目前2019年11月18日最新版本:8.3.0

    <properties>
        <org.apache.lucene>8.3.0</org.apache.lucene>
    </properties>

 实现代码如下:

public class Test {
    //生成索引的存放位置
    private final static String IDX_DIR = "D:\\lucene";
    public static void main(String[] args) {
        IndexWriter indexWriter = null;
        try {
            //创建索引库
            Directory dir = FSDirectory.open(Paths.get(IDX_DIR));
            //新建分析器对象
            Analyzer analyzer = new SmartChineseAnalyzer();
            //新建配置对象
            IndexWriterConfig config = new IndexWriterConfig(analyzer);
            //创建一个IndexWriter对象
            indexWriter = new IndexWriter(dir, config);
            //创建文档对象
            Document document1 = new Document();
            //添加文档字段,字段名可以自己指定,Store.YES 说明该字段要被存储,false则不需要被存储到文档列表
            document1.add(new StringField("id", "1L", Store.YES));
            document1.add(new TextField("title", "三旬老汉隔人暴扣", Store.YES));
            document1.add(new TextField("content", "某23号三旬老汉一记抢断之后运球过人后隔人劈扣拿下2分!", Store.YES));

            Document document2 = new Document();
            document2.add(new StringField("id", "2L", Store.YES));
            document2.add(new TextField("title", "浓眉哥怒砍3双", Store.YES));
            document2.add(new TextField("content", "浓眉哥本场状态佳,仅前三节比赛就拿下3双,将比赛胜局牢牢锁定!", Store.YES));

            //写出索引数据
            indexWriter.addDocument(document1);
            indexWriter.addDocument(document2);
            indexWriter.commit();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                indexWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

点击main方法运行后,我们可以在对应的目录(我这里是D盘的lucene目录)下发现被新建的几个文件

到这里说明索引文件已被创建,那么索引里到底有啥玩意,想窥探一下但发现根本打不开,这个时候lukeall大哥就登场了:下载地址:https://github.com/DmitryKey/luke/releases

一定要匹配你使用的lucene版本,否则可能解析报错。

下载后解压打开,然后选择你索引所在目录(我这里是D盘的lucene目录),就可以看到分词详情了:

点击OK后,在show top terms可以看到被分词的内容:

借助Luke我们可以看到被Lucene分词后建立的倒排索引,再结合3中提到的倒排索引原理,应该可以很好的理解倒排索引和Lucene的索引建立.

6.Lucene的实际使用

有了索引之后,接下来就是要借助索引去实现搜索了.Luncene提供了多种搜索方式,以此来满足不同的业务需求,我们先从最普通的关键词搜索来看,在上面已经建好的Maven工程下新增TestQuery类:

public class TestQuery {
    private final static String IDX_DIR = "D:\\lucene";

    public static void main(String[] args) {
        try {
            //创建索引库对象
            Directory directory = FSDirectory.open(Paths.get(IDX_DIR));
            //索引读取工具
            IndexReader indexReader = DirectoryReader.open(directory);
            //索引搜索工具
            IndexSearcher indexSearcher = new IndexSearcher(indexReader);

            //创建查询解析器
            QueryParser parser = new QueryParser("content", new SmartChineseAnalyzer());
            //创建查询对象
            Query query = parser.parse("浓眉");
            //获取搜索结果 第二个参数n是返回多少条,可以根据情况限制
            TopDocs docs = indexSearcher.search(query, 10);

            //获取总条数
            System.out.println("本次查询共搜索到:" + docs.totalHits + " 条相关数据");
            //获取得分对象
            ScoreDoc[] scoreDocs = docs.scoreDocs;
            for (ScoreDoc scoreDoc : scoreDocs) {
                //获取文档编号
                int docId = scoreDoc.doc;
                //根据文档编号获取文档内容
                Document document = indexReader.document(docId);
                System.out.println("id: " + docId);
                System.out.println("title: " + document.getField("title"));
                System.out.println("content: " + document.getField("content"));
                System.out.println("搜索得分: " + scoreDoc.score);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

运行上述代码,效果如图:

以上便是最普通的根据关键词查询,查询时Lucene会根据匹配度算出一个得分,得分越高排名越靠前.

Lucene还提供了:

WildcardQuery(通配符查询)

// 创建查询对象
Query query = new WildcardQuery(new Term("title", "*三*"));

FuzzyQuery(模糊查询)

// 创建模糊查询对象:允许用户输错。但是要求错误的最大编辑距离不能超过2
// 编辑距离:一个单词到另一个单词最少要修改的次数 loohan --> laohan 需要编辑1次,编辑距离就是1
// 可以手动指定编辑距离,区间[0,2]
Query query = new FuzzyQuery(new Term("title","laohan"),1);

NumericRangeQuery(数值范围查询) 

// 针对数值字段的范围查询,参数:字段名称,最小值、最大值、是否包含最小值、是否包含最大值
Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);

 BooleanQuery(组合查询)

/**
 * 布尔查询:
 * 布尔查询本身没有查询条件,可以把其它查询通过逻辑运算进行组合
 * 交集:Occur.MUST + Occur.MUST
 * 并集:Occur.SHOULD + Occur.SHOULD
 * 非:Occur.MUST_NOT
 */
Query query1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true);
Query query2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
// 创建布尔查询的对象
BooleanQuery query = new BooleanQuery();

// 组合其它查询
query.add(query1, BooleanClause.Occur.MUST_NOT);
query.add(query2, BooleanClause.Occur.SHOULD);

search(query);

7.索引编辑(更新)

Lucene提供的索引是可以编辑的,否则一旦数据发生变化,搜出来的东西可能并不是我们想要的,特别是在电商场景下,商品的列表可能会经常发生变化,如果不能及时的更新索引,会给用户和商家带来很大不便.

public class TestUpdate {
    private final static String IDX_DIR = "D:\\lucene";

    public static void main(String[] args) {
        IndexWriter indexWriter = null;
        try {
            //创建目录
            Directory directory = FSDirectory.open(Paths.get(IDX_DIR));
            //创建配置对象
            IndexWriterConfig config = new IndexWriterConfig(new SmartChineseAnalyzer());
            //创建索引写出工具
            indexWriter = new IndexWriter(directory, config);

            //创建新的文档数据
            Document document = new Document();
            document.add(new StringField("id","1L", Store.YES));
            document.add(new TextField("title","湖人三大核心全面爆发,仅两节比赛就领先对手三十分!",Store.YES));

            //修改索引,修改指定文档中所有的匹配字段的索引,一般选id,因为id唯一
            indexWriter.updateDocument(new Term("id","1L"),document);
            indexWriter.commit();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                indexWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Lucene也支持索引的删除:

//根据匹配关键词来删除
writer.deleteDocuments(new Term("id", "1L"));

//根据匹配数字范围来删除
Query query = NumericRangeQuery.newLongRange("id", 1L, 2L, true, true);
writer.deleteDocuments(query);

//删除全部
write.deleteAll();

 8.拓展功能

Lucene除了可以对建立的索引进行增删改查之外,还可以让查询结果更"用户体验友好",内置提供了查询关键词高亮显示功能,查询结果排序功能等。

高亮功能:

// 设置HTML格式化样式
Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
QueryScorer scorer = new QueryScorer(query);
// 创建高亮工具
Highlighter highlighter = new Highlighter(formatter, scorer);

//省略对socre[]循环部分...

//通过分词器确定需要高亮显示的关键词
String highlight = highlighter.getBestFragment(new SmartChineseAnalyzer(), "content", "浓眉哥");
//展示效果
System.out.println("高亮展示的关键词:" + highlight);

排序功能:

//老版本的Lucene可以采用这种方式排序
//设置排序字段
Sort sort = new Sort(new SortField("id", Type.LONG,true));
//获取搜索结果
TopDocs docs = indexSearcher.search(query, 10,sort);

如果你是在新版本下按照这种方式排序会抛下面这种异常:

 网上的解决方法是为需要排序的字段设置Numeric值,确实可以解决:

document.add(new NumericDocValuesField("title",10L));

但又发现了新的问题: 

一用排序字段得到的搜索得分就变成NAN了.

效果如下图: 

网上说是BUG,于是我尝试着把Lucene的版本从7一直升到8,发现这个问题依旧存在,而且网友说是从4开始就有这个问题,考虑到Apache的影响力,我觉得这样的bug肯定活不过这么多版本,于是看了下Sort的源码,最终找到了解决方案,原来是因为搜索得分算法是比较耗性能的,特别是自定义排序字段后,所以在高版本中,使用自定义排序字段后,搜索得分是默认被关闭的,如果需要计算得分需要手动开启:

//设置排序字段
Sort sort = new Sort(new SortField("title", Type.LONG,true));
//获取搜索结果 两个布尔类型参数,第一个是:doDocScores,第二个是doMaxScore
TopDocs docs = indexSearcher.search(query,10,sort,true,true);

第一个参数doDocScores的意思就是开启搜索得分. 第二个参数是在搜索过程中,如果搜到了搜索得分最高的那条数据,就自动终止不再继续搜了,直接返回,以此来提高性能.,对于这个功能背后的算法,感兴趣的可以继续研究这篇博主总结的:

《Lucene之MaxScorer算法简介》https://blog.csdn.net/wzhg0508/article/details/12953119

遗憾的时,Lucene并没提供分页功能,需要我们手动实现,思路与在Mysql下手动分页查询差不多,这里不再赘述。

另外对得分算法感兴趣的同学可以参考这篇:

《深入理解Lucene默认打分算法》https://blog.csdn.net/huaishu/article/details/77648377

本人学识浅薄,就不班门弄斧自己总结了,鲁迅老爹的拿来主义真香!

9.总结

本篇主要介绍了Lucene的使用场景,原理,以及实操,适合新手入门,如果要吃透Lucene的话没个几个月还真搞不定,里面的源码和算法都比较厉害,现在市面上有很多基于Lucene二次开发或拓展的搜索引擎,比如solr,elasticsearch等,正是因为有了Lucene底层强有力的支持,才让我们今天在互联网上获取信息变得如此便捷与轻松,最后,感谢你的阅读,文中若有不正之处欢迎留言斧正。

发布了89 篇原创文章 · 获赞 69 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/lovexiaotaozi/article/details/103125034