Lucene

爬虫从网络上爬取巨量数据,数据保存若是放DB中不只插入慢,随着数据量增大,查询性能也会愈加差。最近有一个设想,可否将爬取的数据以文件形式保存,经过lucene框架创建索引的方式来知足快速搜索呢。html

菜鸟起飞:前端

1、Lucene简介:

Lucene是Apache Jakarta家族中的一个开源项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎、索引引擎和部分文本分析引擎。java

Lucene提供了一个简单却强大的应用程式接口,可以作全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具,是目前最为流行的基于 Java 开源全文检索工具包。git

数据整体分为两种:github

结构化数据:指具备固定格式或有限长度的数据,如数据库、元数据等。
非结构化数据:指不定长或无固定格式的数据,如邮件、word文档等磁盘上的文件。
对于结构化数据的全文搜索很简单,由于数据都是有固定格式的,例如搜索数据库中数据使用SQL语句便可。web

对于非结构化数据,有如下两种方法:算法

顺序扫描法(Serial Scanning)
全文检索(Full-text Search)spring


顺序扫描法
若是要找包含某一特定内容的文件,对于每个文档,从头至尾扫描内容,若是此文档包含此字符串,则此文档为咱们要找的文件,接着看下一个文件,直到扫描完全部的文件,所以速度很慢。数据库

全文检索
将非结构化数据中的一部分信息提取出来,从新组织,使其变得具备必定结构,而后对此有必定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的而后从新组织的信息,咱们称之索引。apache

例如字典的拼音表和部首检字表就至关于字典的索引,经过查找拼音表或者部首检字表就能够快速的查找到咱们要查的字。

这种先创建索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。

2、全文检索流程

2.1 索引过程

绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:

得到原始文档(原始内容即要搜索的内容)
采集文档
建立文档
分析文档
索引文档

2.1.1 得到原始文档

原始文档是指要索引和搜索的内容。原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等等。

从互联网、数据库、文件系统中获取要搜索的原始信息,这个过程就是信息采集,信息采集的目的是对原始内容进行索引。

在互联网上采集信息的程序称为爬虫。Lucene不提供信息采集的类库,须要本身编写一个爬虫程序实现信息采集,也可使用一些开源软件实现信息采集,如Nutch、JSoup、Heritrix等等。

对于磁盘上文件内容,能够经过IO流来读取文本文件内容,对于pdf、doc、xls等文件能够经过第三方解析工具来读取文件内容,如Apache POI等。

2.1.2 建立文档对象

得到原始内容的目的是为了建立索引,在索引前须要将原始内容建立成文档(Document),文档中包含多个域(Field),在域中存储内容。

域能够被理解为一个原始文档的属性。例若有一个文本文件test.txt,咱们将这个文本文件的内容建立成文档(Document),它就包含了许多域,好比有文件名、文件大小、最后修改时间等等,如图:

注意:每一个Document能够有多个Field,不一样的Document能够有不一样的Field,同一个Document能够有相同的Field。

2.1.3 分析文档

将原始内容建立和包含域(Field)的文档(Document)后,须要对域中的内容进行分析,分析的过程是通过对原始文档提取单词、字母大小写转换、去除符号、去除停用词等过程后生成最终的语汇单元。

例如分析如下文档后:

Lucene is a Java full-text search engine. Lucene is not a complete application, but rather a code library and API that can easily be used to add search capabilities to applications

分析后获得的语汇单元:

lucene、java、full 、search、engine…

将每一个语汇单元叫作一个term,不一样的域中拆分出来的相同的语汇单元是不一样的 term 。term 中包含两部分一部分是文档的域名,另外一部分是内容。例如:文件名中包含 java 和文件内容中包含的 java是不一样的 term 。

2.1.4 建立索引

对全部文档分析得出的语汇单元进行索引,最终要实现只搜索语汇单元就可以找到文档(Document)。 

2.2 搜索过程

红色表示搜索过程,从索引库中搜索内容,搜索过程包括:

用户经过搜索界面
建立查询
执行搜索,从索引库搜索
渲染搜索结果

2.2.1 用户搜索

用户经过前端页面,将要搜索的关键字传递到后端。

2.2.2 建立查询

用户输入查询关键词执行搜索前须要先构建一个查询对象,查询对象中能够指定查询要搜索的Field文档域、查询关键字等,查询对象会生成具体的查询语法。

2.2.3执行查询

根据查询对象得到对应的索引,从而找到索引所对应的文档。

2.2.4 渲染结果

以一个友好的界面将查询结果展现给用户,用户根据搜索结果找本身想要的信息,为了帮助用户很快找到本身的结果,提供了不少展现的效果,好比搜索结果中将关键字高亮显示,百度提供的快照等。

3、实现

找了个Lucene 6.6.0 的API,凑合着用

http://lucene.apache.org/core/6_6_0/core/index.html

git-hub:

https://github.com/xiaozhuanfeng/luceneProj

用springboot搭了个工程:

pom.xml

 <properties>
        <lucene.version>6.6.2</lucene.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


        <!--工具包 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.1</version>
        </dependency>

        <!-- 引入fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.56</version>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>${lucene.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>${lucene.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>${lucene.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-memory</artifactId>
            <version>${lucene.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-backward-codecs</artifactId>
            <version>${lucene.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queries</artifactId>
            <version>${lucene.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>${lucene.version}</version>
        </dependency>

    </dependencies>

中文分词用IKAnalyzer,只支持到Lucene5x,下载资源,导出jar包,具体放在了gitHub中,放入工程:

 

 

package com.example.pca.lucene;

import org.apache.lucene.search.TopDocs;

import java.io.IOException;

public interface ISearch {
    /**
     * 建立索引
     * @param indexPath  索引文件路径
     * @param resourcePath  资源文件路径
     * @return
     */
    boolean createIndex(String indexPath, String resourcePath) throws IOException;

    /**
     * 关键字查询
     * @param indexPath 索引文件路径
     * @param keyword  关键字
     * @return
     * @throws IOException
     */
    TopDocs queryIndex(String indexPath, String keyword)throws IOException;
}
package com.example.pca.lucene.impl;

import com.example.pca.lucene.ISearch;
import org.apache.commons.io.FileUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.*;
import org.apache.lucene.search.highlight.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

import java.io.File;
import java.io.IOException;
import java.util.Collection;

public abstract class AbstractSearch implements ISearch {

    protected Analyzer analyzer;

    @Override
    public boolean createIndex(String indexPath, String resourcePath) throws IOException {
        /* Step1:建立IndexWrite对象
         * (1)定义词法分析器
         * (2)肯定索引存储位置-->建立Directory对象
         * (3)获得IndexWriterConfig对象
         * (4)建立IndexWriter对象
         */

        Directory directory = FSDirectory.open(new File(indexPath).toPath());
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);

        try (IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig)) {
            // 清除之前的索引
            indexWriter.deleteAll();

            // 获得txt后缀的文件集合
            Collection<File> txtFiles = FileUtils.listFiles(new File(resourcePath), new String[]{"txt"}, true);
            for (File file : txtFiles) {
                String fileName = file.getName();
                String content = FileUtils.readFileToString(file, "UTF-8");
                /*
                 * Step2:建立Document对象,将Field地下添加到Document中
                 * Field第三个参数选项不少,具体参考API手册
                 */
                Document document = new Document();
                document.add(new Field("fileName", fileName, TextField.TYPE_STORED));
                document.add(new Field("content", content, TextField.TYPE_STORED));

                /*
                 *Step4:使用 IndexWrite对象将Document对象写入索引库,并进行索引。
                 */
                indexWriter.addDocument(document);
            }
        }
        return false;
    }

    @Override
    public TopDocs queryIndex(String indexPath, String keyword) throws IOException {
        TopDocs topDocs = null;
        /*
         * Step1:建立IndexSearcher对象
         * (1)建立Directory对象
         * (2)建立DirectoryReader对象
         * (3)建立IndexSearcher对象
         */
        Directory directory = FSDirectory.open(new File(indexPath).toPath());

        try (DirectoryReader reader = DirectoryReader.open(directory)) {
            IndexSearcher indexSearcher = new IndexSearcher(reader);

            /*
             * Step2:建立TermQuery对象,指定查询域和查询关键词
             */
            Term fTerm = new Term("fileName", keyword);
            Term cTerm = new Term("content", keyword);
            TermQuery query1 = new TermQuery(fTerm);
            TermQuery query2 = new TermQuery(cTerm);

            /*
             * Step3:建立Query对象
             */
            Query booleanBuery = new BooleanQuery.Builder().add(query1, BooleanClause.Occur.SHOULD).add(query2,
                    BooleanClause.Occur.SHOULD)
                    .build();

            topDocs = indexSearcher.search(booleanBuery, 100);
            System.out.println("共找到 " + topDocs.totalHits + " 个文件匹配");


            //打印结果
            printTopDocs(topDocs.scoreDocs, indexSearcher, getHighlighter(booleanBuery));

        } catch (InvalidTokenOffsetsException e) {
            e.printStackTrace();
        }

        return topDocs;
    }

    /**
     * 返回高亮对象
     *
     * @param query
     * @return
     */
    private Highlighter getHighlighter(Query query) {
        // 格式化器
        Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");

        //算分
        QueryScorer scorer = new QueryScorer(query);

        // 准备高亮工具
        Highlighter highlighter = new Highlighter(formatter, scorer);

        //显示得分高的片断,片断字符长度fragmentSize=1000
        Fragmenter fragmenter = new SimpleSpanFragmenter(scorer, 200);

        //设置片断
        highlighter.setTextFragmenter(fragmenter);
        return highlighter;
    }

    private void printTopDocs(ScoreDoc[] scoreDocs, IndexSearcher indexSearcher, Highlighter highlighter) throws IOException, InvalidTokenOffsetsException {

        for (ScoreDoc scoreDoc : scoreDocs) {
            Document doc = indexSearcher.doc(scoreDoc.doc);
            String fileName = doc.get("fileName");
            System.out.println("fileName=" + fileName);

            if (null != highlighter) {
                //高亮处理
                String content = doc.get("content");
                System.out.println(content);
                System.out.println("====================");
                String hContent = highlighter.getBestFragment(analyzer, "content", content);
                System.out.println(hContent);
            }
            System.out.println();
        }
    }
}
package com.example.pca.lucene.analyzer;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.wltea.analyzer.lucene.IKAnalyzer;

@Configuration
public class AnalyzerConfig {

    @Bean("standardAnalyzer")
    public Analyzer getStandardAnalyzer() {
        // 使用标准分词器,但对于中文颇为无力
        //lucene自带分词器,SmartChineseAnalyzer()
        //缺点:扩展性差,扩展词库、禁用词库和同义词库等很差处理

       /* 第三方分词器
       paoding:庖丁解牛,可是其最多只支持到Lucene3,已通过时,不推荐使用。
       mmseg4j:目前支持到Lucene6 ,目前仍然活跃,使用mmseg算法。  参考:https://www.jianshu.com/p/03f4a906cfb5
       IK-Analyzer:开源的轻量级的中文分词工具包,官方支持到Lucene5。
        */
        return new StandardAnalyzer();
    }

    @Bean("iKAnalyzer")
    public Analyzer getIKAnalyzer() {
        //IK分词,试了下,也能够支持中文
        return new IKAnalyzer();
    }
}
package com.example.pca.lucene.impl;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service(value = "englishSearch")
public class EnglishSearch extends AbstractSearch implements InitializingBean {

    @Resource(name = "standardAnalyzer")
    private Analyzer standardAnalyzer;

    @Override
    public void afterPropertiesSet() throws Exception {
        super.analyzer = standardAnalyzer;
    }

}
package com.example.pca.lucene.impl;

import org.apache.lucene.analysis.Analyzer;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
import org.wltea.analyzer.lucene.IKAnalyzer;

import javax.annotation.Resource;

@Service(value = "chineseSearch")
public class ChineseSearch extends AbstractSearch implements InitializingBean {
    @Resource(name = "iKAnalyzer")
    private Analyzer iKAnalyzer;

    @Override
    public void afterPropertiesSet() throws Exception {
        super.analyzer = iKAnalyzer;
    }
}
package com.example.pca.lucene;

import com.example.pca.utils.ProjectPathUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;

import java.io.IOException;

import static com.example.pca.lucene.constants.FilePkg.FILE_PKG;
import static com.example.pca.lucene.constants.FilePkg.INDEX_PKG;

@RunWith(SpringRunner.class)
@SpringBootTest
public class luceneTest {

    /**
     * F:\xxxx\ideaProjects\luceneProj\luceneResource\
     */
    private static final String RESOURCE_PATH = ProjectPathUtils.getProjPath("luceneProj") + FILE_PKG.getPkgName();

    /**
     * F:\xxxx\ideaProjects\luceneProj\luceneIndex\
     */
    private static final String INDEX_PATH = ProjectPathUtils.getProjPath("luceneProj") + INDEX_PKG.getPkgName();

    @Resource(name = "englishSearch")
    private ISearch englishSearch;

    @Resource(name = "chineseSearch")
    private ISearch chineseSearch;

    @Test
    public void test1() {
        System.out.println(RESOURCE_PATH);
        System.out.println(INDEX_PATH);
    }

    @Test
    public void test2() {
        try {
            englishSearch.createIndex(INDEX_PATH + "US\\", RESOURCE_PATH +"US\\");
            englishSearch.queryIndex(INDEX_PATH +"US\\", "spring");
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test3() {
        try {
            chineseSearch.createIndex(INDEX_PATH +"EN\\", RESOURCE_PATH + "EN\\");
            chineseSearch.queryIndex(INDEX_PATH +"EN\\", "孙悟空");
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在英文资源文件目录下放入文件,测试test2:

中文资源测试,test3

 

 同时测了下,IK分词也是能够支持英文的

@Test
    public void test4() {
        try {
            chineseSearch.createIndex(INDEX_PATH +"US\\", RESOURCE_PATH + "US\\");
            chineseSearch.queryIndex(INDEX_PATH +"US\\", "spring");
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

 结束语:本人菜鸟一枚,权看成学习,知道有这么个东东。

参考:

https://blog.csdn.net/yuanlaijike/article/details/79452884

https://blog.csdn.net/joker233/article/details/51909833

资源:

https://mvnrepository.com/artifact/com.chenlb.mmseg4j

LK分词器资源

https://blog.csdn.net/m0_37609579/article/details/77865183

相关文章
相关标签/搜索