Fork me on GitHub

Lucene

爬虫从网络上爬取巨量数据,数据保存如果放DB中不仅插入慢,随着数据量增大,查询性能也会越发差。最近有一个设想,能否将爬取的数据以文件形式保存,通过lucene框架建立索引的方式来满足快速搜索呢。

菜鸟起飞:

一、Lucene简介:

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

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

数据总体分为两种:

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

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

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


顺序扫描法
如果要找包含某一特定内容的文件,对于每一个文档,从头到尾扫描内容,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件,因此速度很慢。

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

例如字典的拼音表和部首检字表就相当于字典的索引,通过查找拼音表或者部首检字表就可以快速的查找到我们要查的字。

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

二、全文检索流程

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 渲染结果

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

三、实现

找了个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

posted @ 2019-05-15 16:13  小传风  阅读(352)  评论(0编辑  收藏  举报