• 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:xxxxideaProjectsluceneProjluceneResource
         */
        private static final String RESOURCE_PATH = ProjectPathUtils.getProjPath("luceneProj") + FILE_PKG.getPkgName();
    
        /**
         * F:xxxxideaProjectsluceneProjluceneIndex
         */
        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

  • 相关阅读:
    关于程序中以时间判断接收数据结束时,接收数据长度设置为1时,出现接收不全的问题解释。
    stm32 外部8M晶振 改为12M的方法
    django iis 部署
    电信NB卡
    socketserver
    APScheduler简介
    三极管开关电路
    mysql授权
    解决VMware无法共享ubuntu虚拟机文件
    Python解析yaml配置文件
  • 原文地址:https://www.cnblogs.com/xiaozhuanfeng/p/10870129.html
Copyright © 2020-2023  润新知