Lucene就是这么容易

图片描述
公众号阅读
https://mp.weixin.qq.com/s/M3...css

Lucene

[TOC]git

什么是Lucene ???

The Apache LuceneTM project develops open-source search software, including:
Lucene Core, our flagship sub-project, provides Java-based indexing and search technology, as well as spellchecking, hit highlighting and advanced analysis/tokenization capabilities.

lucene官网(http://lucene.apache.org/)github

Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础创建起完整的全文检索引擎。Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程式接口,可以作全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具。就其自己而言,Lucene是当前以及最近几年最受欢迎的免费Java信息检索程序库。人们常常提到信息检索程序库,虽然与搜索引擎有关,但不该该将信息检索程序库与搜索引擎相混淆。

为何使用Lucene

如今Lucene在互联网行业的用的很是普遍,尤为是大数据时代的今天,那么根据本身的理解给你们简单的介绍一下为何要学习Lucene。
传统的sql查询方式,数据量过多时,数据库的压力就会变得很大,查询速度会变得很是慢。咱们须要使用更好的解决方案来分担数据库的压力。为了解决数据库压力和速度的问题,咱们的数据库就变成了索引库,咱们使用Lucene的API的来操做服务器上的索引库。这样彻底和数据库进行了隔离。
索引查询方式(图片来源引用)算法

1、快速入门

如今咱们已经了解了Lucene。sql

  1. Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供
  2. Lucene提供了一个简单却强大的应用程序接口(API),可以作全文索引和搜寻,在Java开发环境里Lucene是一个成熟的免费开放源代码工具
  3. Lucene并非现成的搜索引擎产品,但能够用来制做搜索引擎产品

总结:Lucene全文检索就是对文档中所有内容进行分词,而后对全部单词创建倒排索引的过程。数据库

  • 目前最新的版本是7.x系列,可是在企业中仍是用4.x比较多,因此咱们学习4.x的版本。

插入图片位置(图片引用来源)
检索数据须要咱们先分词,存入索引库apache

  • 文档Document:数据库中一条具体的记录
  • 字段Field:数据库中的每一个字段
  • 目录对象Directory:物理存储位置
  • 写出器的配置对象:须要分词器和lucene的版本
开发须要的jar包
<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
        <lunece.version>4.10.2</lunece.version>
    </properties>

    <dependencies>
                <!-- 分词器 -->
        <dependency>
            <groupId>com.janeluo</groupId>
            <artifactId>ikanalyzer</artifactId>
            <version>2012_u6</version>
        </dependency>
        <!-- lucene核心库 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>${lunece.version}</version>
        </dependency>
        <!-- Lucene的查询解析器 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>${lunece.version}</version>
        </dependency>
        <!-- lucene的默认分词器库 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>${lunece.version}</version>
        </dependency>
        <!-- lucene的高亮显示 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>${lunece.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
  1. 建立文档对象
  2. 建立存储目录
  3. 建立分词器
  4. 建立索引写入器的配置对象
  5. 建立索引写入器对象
  6. 将文档交给索引写入器
  7. 提交
  8. 关闭
// 建立索引
    @Test
    public void testCreate() throws Exception {
        // 1 建立文档对象
        Document document = new Document();
        // 建立并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES表明存储到文档列表。Store.NO表明不存储
        document.add(new StringField("id", "1", Store.YES));
        // 这里咱们title字段须要用TextField,即建立索引又会被分词。StringField会建立索引,可是不会被分词
        document.add(new TextField("title", "谷歌地图之父跳槽facebook", Store.YES));

        // 2 索引目录类,指定索引在硬盘中的位置
        Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
        // 3 建立分词器对象
        // Analyzer analyzer = new StandardAnalyzer();
        Analyzer analyzer = new IKAnalyzer();
        // 4 索引写出工具的配置对象
        IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
        // 是否清空索引库;设置打开方式:OpenMode.APPEND
        // 会在索引库的基础上追加新索引。OpenMode.CREATE会先清空原来数据,再提交新的索引
        conf.setOpenMode(OpenMode.CREATE);
        // 5 建立索引的写出工具类。参数:索引的目录和配置信息
        IndexWriter indexWriter = new IndexWriter(directory, conf);

        // 6 把文档交给IndexWriter
        indexWriter.addDocument(document);
        // 7 提交
        indexWriter.commit();
        // 8 关闭
        indexWriter.close();
    }

索引文件图片位置

索引查看工具

索引查看工具文件图片
启动run.bat
索引效果数组

***
    @Test
    public void testSearch() throws Exception {
        // 索引目录对象
        Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
        // 索引读取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 索引搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);

        // 建立查询解析器,两个参数:默认要查询的字段的名称,分词器
        QueryParser parser = new QueryParser("title", new IKAnalyzer());
        // 建立查询解析器,俩个参数:默认要查询的字段名称,分词器
        // MultiFieldQueryParser parser2 = new MultiFieldQueryParser(new
        // String[] {
        // "id", "title" }, new IKAnalyzer());
        // Query query2 = parser2.parse("1");
        // 建立查询对象
        Query query = parser.parse("谷歌之父");

        // 搜索数据,两个参数:查询条件对象要查询的最大结果条数
        // 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、全部符合条件的文档的编号信息)。
        TopDocs topDocs = searcher.search(query, 10);
        // 获取总条数
        System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据");
        // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取出文档编号
            int docID = scoreDoc.doc;
            // 根据编号去找文档
            Document doc = reader.document(docID);
            System.out.println("id: " + doc.get("id"));
            System.out.println("title: " + doc.get("title"));
            // 取出文档得分
            System.out.println("得分: " + scoreDoc.score);
        }
    }

2、工具类

查询

注:代码中加了必要注释服务器

public void search(Query query) throws Exception {
        // 索引目录对象
        Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
        // 索引读取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 索引搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);

        // 搜索数据,两个参数:查询条件对象要查询的最大结果条数
        // 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、全部符合条件的文档的编号信息)。
        TopDocs topDocs = searcher.search(query, 10);
        // 获取总条数
        System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据");
        // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;

        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取出文档编号
            int docID = scoreDoc.doc;
            // 根据编号去找文档
            Document doc = reader.document(docID);
            System.out.println("id: " + doc.get("id"));
            System.out.println("title: " + doc.get("title"));
            // 取出文档得分
            System.out.println("得分: " + scoreDoc.score);
        }
    }

注:普通词条查询架构

/*
     * 测试普通词条查询 注意:Term(词条)是搜索的最小单位,不可再分词。值必须是字符串!
     */
    @Test
    public void testTermQuery() throws Exception {
        // 建立词条查询对象
        Query query = new TermQuery(new Term("title", "谷歌地图"));
        search(query);
    }

注:通配符查询

/*
     * 测试通配符查询 ? 能够表明任意一个字符 * 能够任意多个任意字符
     */
    @Test
    public void testWildCardQuery() throws Exception {
        // 建立查询对象
        Query query = new WildcardQuery(new Term("title", "*歌*"));
        search(query);
    }

注:模糊查询;数组范围查询;布尔查询

/*
     * 测试模糊查询
     */
    @Test
    public void testFuzzyQuery() throws Exception {
        // 建立模糊查询对象:容许用户输错。可是要求错误的最大编辑距离不能超过2
        // 编辑距离:一个单词到另外一个单词最少要修改的次数 facebool --> facebook 须要编辑1次,编辑距离就是1
        // Query query = new FuzzyQuery(new Term("title","fscevool"));
        // 能够手动指定编辑距离,可是参数必须在0~2之间
        Query query = new FuzzyQuery(new Term("title", "facevool"), 2);
        search(query);
    }

    /***************************************************************
     * 测试:数值范围查询 注意:数值范围查询,能够用来对非String类型的ID进行精确的查找
     */
    @Test
    public void testNumericRangeQuery() throws Exception {
        // 数值范围查询对象,参数:字段名称,最小值、最大值、是否包含最小值、是否包含最大值
        Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);

        search(query);
    }

    /*****************************************************************
     * 布尔查询: 布尔查询自己没有查询条件,能够把其它查询经过逻辑运算进行组合! 交集:Occur.MUST + Occur.MUST
     * 并集:Occur.SHOULD + Occur.SHOULD 非:Occur.MUST_NOT
     */
    // @Test
    // public void testBooleanQuery() throws Exception {
    //
    // 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);
    // }

修改-删除

注:修改和删除操做

/*
     * 测试:修改索引 注意: A:Lucene修改功能底层会先删除,再把新的文档添加。
     * B:修改功能会根据Term进行匹配,全部匹配到的都会被删除。这样很差 C:所以,通常咱们修改时,都会根据一个惟一不重复字段进行匹配修改。例如ID
     * D:可是词条搜索,要求ID必须是字符串。若是不是,这个方法就不能用。
     * 若是ID是数值类型,咱们不能直接去修改。能够先手动删除deleteDocuments(数值范围查询锁定ID),再添加。
     */
    @Test
    public void testUpdate() throws Exception {
        // 建立目录对象
        Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
        // 建立配置对象
        IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST,
                new IKAnalyzer());
        // 建立索引写出工具
        IndexWriter writer = new IndexWriter(directory, conf);

        // 建立新的文档数据
        Document doc = new Document();
        doc.add(new StringField("id", "1", Store.YES));
        doc.add(new TextField("title", "谷歌地图之父跳槽facebook ", Store.YES));
        /*
         * 修改索引。参数: 词条:根据这个词条匹配到的全部文档都会被修改 文档信息:要修改的新的文档数据
         */
        writer.updateDocument(new Term("id", "1"), doc);
        // 提交
        writer.commit();
        // 关闭
        writer.close();
    }

    /*
     * 演示:删除索引 注意: 通常,为了进行精确删除,咱们会根据惟一字段来删除。好比ID 若是是用Term删除,要求ID也必须是字符串类型!
     */
    @Test
    public void testDelete() throws Exception {
        // 建立目录对象
        Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
        // 建立配置对象
        IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST,
                new IKAnalyzer());
        // 建立索引写出工具
        IndexWriter writer = new IndexWriter(directory, conf);

        // 根据词条进行删除
        // writer.deleteDocuments(new Term("id", "1"));

        // 根据query对象删除,若是ID是数值类型,那么咱们能够用数值范围查询锁定一个具体的ID
        // Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true,
        // true);
        // writer.deleteDocuments(query);

        // 删除全部
        writer.deleteAll();
        // 提交
        writer.commit();
        // 关闭
        writer.close();
    }

高亮显示

  1. 建立目录 对象
  2. 建立索引读取工具
  3. 建立索引搜索工具
  4. 建立查询解析器
  5. 建立查询对象
  6. 建立格式化器
  7. 建立查询分数工具
  8. 准备高亮工具
  9. 搜索
  10. 获取结果
  11. 用高亮工具处理普通的查询结果

百度搜索图片
插入图片位置

  • 关键字增长css样式
// 高亮显示
    @Test
    public void testHighlighter() throws Exception {
        // 目录对象
        Directory directory = FSDirectory.open(new File("indexDir"));
        // 建立读取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 建立搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);

        QueryParser parser = new QueryParser("title", new IKAnalyzer());
        Query query = parser.parse("谷歌地图");

        // 格式化器
        Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
        QueryScorer scorer = new QueryScorer(query);
        // 准备高亮工具
        Highlighter highlighter = new Highlighter(formatter, scorer);
        // 搜索
        TopDocs topDocs = searcher.search(query, 10);
        System.out.println("本次搜索共" + topDocs.totalHits + "条数据");

        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 获取文档编号
            int docID = scoreDoc.doc;
            Document doc = reader.document(docID);
            System.out.println("id: " + doc.get("id"));

            String title = doc.get("title");
            // 用高亮工具处理普通的查询结果,参数:分词器,要高亮的字段的名称,高亮字段的原始值
            String hTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", title);

            System.out.println("title: " + hTitle);
            // 获取文档的得分
            System.out.println("得分:" + scoreDoc.score);
        }

    }

排序

// 排序
    @Test
    public void testSortQuery() throws Exception {
        // 目录对象
        Directory directory = FSDirectory.open(new File("indexDir"));
        // 建立读取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 建立搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);

        QueryParser parser = new QueryParser("title", new IKAnalyzer());
        Query query = parser.parse("谷歌地图");

        // 建立排序对象,须要排序字段SortField,参数:字段的名称、字段的类型、是否反转若是是false,升序。true降序
        Sort sort = new Sort(new SortField("id", SortField.Type.LONG, true));
        // 搜索
        TopDocs topDocs = searcher.search(query, 10,sort);
        System.out.println("本次搜索共" + topDocs.totalHits + "条数据");

        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 获取文档编号
            int docID = scoreDoc.doc;
            Document doc = reader.document(docID);
            System.out.println("id: " + doc.get("id"));
            System.out.println("title: " + doc.get("title"));
        }
    }

分页

// 分页
    @Test
    public void testPageQuery() throws Exception {
        // 实际上Lucene自己不支持分页。所以咱们须要本身进行逻辑分页。咱们要准备分页参数:
        int pageSize = 2;// 每页条数
        int pageNum = 3;// 当前页码
        int start = (pageNum - 1) * pageSize;// 当前页的起始条数
        int end = start + pageSize;// 当前页的结束条数(不能包含)
        
        // 目录对象
        Directory directory = FSDirectory.open(new File("indexDir"));
        // 建立读取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 建立搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);
        
        QueryParser parser = new QueryParser("title", new IKAnalyzer());
        Query query = parser.parse("谷歌地图");
        
        // 建立排序对象,须要排序字段SortField,参数:字段的名称、字段的类型、是否反转若是是false,升序。true降序
        Sort sort = new Sort(new SortField("id", Type.LONG, false));
        // 搜索数据,查询0~end条
        TopDocs topDocs = searcher.search(query, end,sort);
        System.out.println("本次搜索共" + topDocs.totalHits + "条数据");
        
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (int i = start; i < end; i++) {
            ScoreDoc scoreDoc = scoreDocs[i];
            // 获取文档编号
            int docID = scoreDoc.doc;
            Document doc = reader.document(docID);
            System.out.println("id: " + doc.get("id"));
            System.out.println("title: " + doc.get("title"));
        }
    }

3、优化

Lucene打分算法

  1. 当谈论到查询的相关性,很重要的一件事就是对于给定的查询语句,如何计算文档得分。文档得分是一个用来描述查询语句和文档之间匹配程度的变量。若是你但愿经过干预Lucene查询来改变查询结果的排序,你就须要对Lucene的得分计算有所理解。
  1. 当一个文档出如今了搜索结果中,这就意味着该文档与用户给定的查询语句是相匹配的。Lucene会对匹配成功的文档给定一个分数。至少从Lucene这个层面,从打分公式的结果来看,分数值越高,表明文档相关性越高。 天然而然,咱们能够得出:两个不一样的查询语句对同一个文档的打分将会有所不一样,可是比较这两个得分是没有意义的。用户须要记住的是:咱们不只要避免去比较不一样查询语句对同一个文档的打分结果,还要避免比较不一样查询语句对文档打分结果的最大值。这是由于文档的得分是多个因素共同影响的结果,不只有权重(boosts)和查询语句的结构起做用,还有匹配关键词的个数,关键词所在的域,查询归一化因子中用到的匹配类型……。在极端状况下,只是由于咱们用了自定义打分的查询对象或者因为倒排索引中词的动态变化,类似的查询表达式对于同一个文档都会产生大相径庭的打分。

计算文档得分,考虑因素以下:

  • 文档权重(Document boost):在索引时给某个文档设置的权重值。
  • 域权重(Field boost):在查询的时候给某个域设置的权重值。
  • 调整因子(Coord):基于文档中包含查询关键词个数计算出来的调整因子。通常而言,若是一个文档中相比其它的文档出现了更多的查询关键词,那么其值越大。
  • 逆文档频率(Inerse document frequency):基于Term的一个因子,存在的意义是告诉打分公式一个词的稀有程度。其值越低,词越稀有(这里的值是指单纯的频率,即多少个文档中出现了该词;而非指Lucene中idf的计算公式)。打分公式利用这个因子提高包含稀有词文档的权重。
  • 长度归一化(Length norm):基于域的一个归一化因子。其值由给定域中Term的个数决定(在索引文档的时候已经计算出来了,而且存储到了索引中)。域越的文本越长,因子的权重越低。这代表Lucene打分公式偏向于域包含Term少的文档。
  • 词频(Term frequency):基于Term的一个因子。用来描述给定Term在一个文档中出现的次数,词频越大,文档的得分越大。
  • 查询归一化因子(Query norm):基于查询语句的归一化因子。其值为查询语句中每个查询词权重的平方和。查询归一化因子使得比较不一样查询语句的得分变得可行,固然比较不一样查询语句得分并不老是那么易于实现和可行的。

Lucene打分公式

Lucene概念上的打分公式是这样的:(TF/IDF公式的概念版)
tf-idf公式图片
上面的公式展现了布尔信息检索模型和向量空间信息检索模型的组合。咱们暂时不去讨论它,直接见识下Lucene实际应用的打分公式:
lucene-tf-idf
能够看到,文档的分数其实是由查询语句q和文档d做为变量的一个函数值。打分公式中有两部分不直接依赖于查询词,它们是coord和queryNorm。公式的值是这样计算的,coord和queryNorm两大部分直接乘以查询语句中每一个查询词计算值的总和。另外一方面,这个总和也是由每一个查询词的词频(tf),逆文档频率(idf),查询词的权重,还有norm,也就是前面说的length norm相乘而得的结果。听上去有些复杂吧?不用担忧,这些东西不须要所有记住。你只须要知道在进行文档打分的时候,哪些因素是起决定做用的就能够了。基本上,从前面的公式中能够提炼出如下的几个规则:

  • 匹配到的关键词越稀有,文档的得分就越高。
  • 文档的域越小(包含比较少的Term),文档的得分就越高。
  • 设置的权重(索引和搜索时设置的均可以)越大,文档得分越高。

正如咱们所看到的那样,Lucene会给具备这些特征的文档打最高分:文档内容可以匹配到较多的稀有的搜索关键词,文档的域包含较少的Term,而且域中的Term可能是稀有的。
简而言之
图片插入位置
图片插入位置

停用词和扩展词加载

将IKAnalyzer.cfg.xml和stopword.dic和xxx.dic文件复制到MyEclipse的src目录下,再进行配置
IK Analyzer默认的停用词词典为IKAnalyzer2012_u6/stopword.dic,这个停用词词典并不完整,只有30多个英文停用词。能够扩展停用词字典,新增ext_stopword.dic,文件和IKAnalyzer.cfg.xml在同一目录,编辑IKAnalyzer.cfg.xml把新增的停用词字典写入配置文件,多个停用词字典用逗号隔开,以下所示。

<entry  key="ext_stopwords">stopword.dic;ext_stopword.dic</entry>

目录结构图

接下来就能够构建本身的搜索引擎了。

上面展现了lucene一些基本操做,更详细的的工具类能够访问https://github.com/wangshiyu777/usefulDemo,分享了详细Demo。

相关文章
相关标签/搜索