Lunce全文检索学习总结

参考链接

https://blog.csdn.net/forfuture1978/article/details/4711308
https://blog.51cto.com/u_15133569/5895049
https://blog.csdn.net/qq_41861558/article/details/102792470
https://www.bilibili.com/video/BV1Na411h7kk?p=2&vd_source=ed806c6daa358b1d73b17235aad841ba

Lucene 是什么

Lucene是apache软件基金会 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。方便软件开发人员是以此为基础建立起完整的全文检索引擎。
Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具。


Lucene架构图

为什么要用 Lucene

结构化数据: 指具有固定格式或有限长度的数据,如数据库,元数据等。
非结构化数据(全文数据): 指不定长或无固定格式的数据,如邮件,word文档等。

对非结构化数据的搜索有一种简单原始的方法:
顺序扫描法 (Serial Scanning): 比如要从全部文档里找一个字符串,就按文档顺序遍历每一个文档,每一个文档都从头到尾去找字符串,直到扫描完所有的文档。
优点:准确率高;缺点:数据量大之后速度慢。
例如:数据库中的like关键字模糊查询;ctrl+F 的文本查找。

但是数据量过多时,查询速度会变得非常慢,需要使用更好的解决方案来分担查询的压力。

全文检索

索引 :为了提升非结构化数据的检索速度,从非结构化数据中提取出“有结构”的然后重新组织的信息,称之索引。比如字典,字典的拼音表和部首检字表就相当于字典的索引。
全文检索(Full-text Search):先建立索引,再对索引进行搜索的过程就叫全文检索。

全文检索大体分两个过程,索引创建 (Indexing) 和搜索索引 (Search) 。
索引创建:将现实世界中所有的结构化和非结构化数据提取信息,创建索引的过程。
搜索索引:就是得到用户的查询请求,搜索创建的索引,然后返回结果的过程。

索引里面究竟存些什么?(Index)

简单来说,保存从字符串到文件的映射。由于这是文件到字符串映射的反向过程,于是保存这种信息的索引称为反向索引。

反向索引的所保存的信息

左边保存的是一系列字符串,称为词典。
每个字符串都指向包含此字符串的文档(Document)链表,此文档链表称为倒排表(Posting List)。
检索的时候只需要检索词典就能获取文档链表,通过合并多个链表就能快速获取检索结果。

建立索引是需要花时间的,不过以后便是一劳永逸了。一次索引,多次使用。

如何创建索引?(Indexing)

  1. 原文档(Document)传给分词组件(Tokenizer),经过分词后得到的结果称为词元(Token)
    分词(中文分词会比英文分词麻烦);除去标点符号;停用词
  2. 将得到的词元(Token)传给语言处理组件(Linguistic Processor),对得到的词元(Token)做一些同语言相关的处理得到结果称为词(Term)
    英文变为小写(Lowercase);单词还原,如“cars”到“car”等(stemming),如“drove”到“drive”等(lemmatization)。
  3. 将得到的词(Term)传给索引组件(Indexer),生成文档倒排(Posting List)链表
    利用得到的词(Term)创建一个字典,并对字典进行排序。最后合并相同的词(Term)成为文档倒排(Posting List)链表
    文档倒排链表

    Document Frequency 即文档频次,表示总共有多少文件包含此词(Term)。
    Frequency 即词频率,表示此文件中包含了几个此词(Term)。

如何对索引进行搜索?(Search)

  1. 用户输入查询语句
    查询语句的语法根据全文检索系统的实现而不同。最基本的有比如:AND, OR, NOT等
  2. 对查询语句进行词法分析,语法分析,及语言处理。
    词法分析主要用来识别单词和关键字
    语法分析主要是根据查询语句的语法规则来形成一棵语法树
    语言处理同索引过程中的语言处理(Linguistic Processor)几乎相同
  3. 搜索索引,得到符合语法树的文档
    在反向索引表中,分别找出包含关键字的文档链表。对找出的链表进行交,差,并等操作,得到最终符合要求的文档。
  4. 根据得到的文档和查询语句的相关性,对结果进行排序。
    找出词(Term)对文档的重要性的过程称为计算词的权重(Term weight)的过程。
    词的权重(Term weight)表示此词(Term)在此文档中的重要程度,越重要的词(Term)有越大的权重(Term weight),因而在计算文档之间的相关性中将发挥更大的作用。有一种叫做向量空间模型的算法(Vector Space Model),可以判断词(Term)之间的关系从而得到文档相关性。实现全文检索系统的人可以自己的实现相关性排序算法。
    影响一个词(Term)在一篇文档中的重要性主要有两个因素:
    • Term Frequency (tf):即此Term在此文档中出现了多少次。tf 越大说明越重要。
    • Document Frequency (df):即有多少文档包含次Term。df 越大说明越不重要。
索引创建和搜索过程

搭建Lucene简单框架

以下代码只是简单的移植,并不完整,且没有经过测试
项目初始化的时候要初始化Lucene

   public LuceneService() throws IOException {

        //RAMDriectory: 内存目录, 将索引库信息存放到内存中
        //FSDirectory用来指定文件系统的目录, 将索引信息保存到磁盘上
        FSDirectory luceneDB;
        WhitespaceAnalyzer whitespaceAnalyzer = new WhitespaceAnalyzer();
        //Lucene存储库的位置
        File fileIdx = new File("存储路径,可以写在配置文件中去调用");
        try {
            //打开Lucene存储库
            luceneDB = FSDirectory.open(fileIdx.toPath());

            //IndexWriterConfig: 索引写入器的配置类,需要传递Lucene的版本和分词器
            IndexWriterConfig writerConfig = new IndexWriterConfig(whitespaceAnalyzer);

            //IndexWriter:索引写入器对象,功能为:添加索引、修改索引和删除索引。 需要传入Directory和indexWriterConfig对象 ,即索引的目录和配置
            indexWriter = new IndexWriter(luceneDB, writerConfig);

            //QueryParser:查询解析器
            //LuceneName是自己建立规范名称的类
            objectParser = new QueryParser(LuceneName.IDX_OBJECT_NAME, whitespaceAnalyzer);

            log.info("索引服务初始化完成,索引目录为 " + fileIdx.toPath().toString());
        } catch (IOException e) {
            log.error("无法初始化索引,请检查提供的索引目录是否可用:[{}]", sysConfig.getIdx());
            throw e;
        }
    }

可以建立一个类【LuceneName】去规范化Lucene名称

public  class  LuceneName {
    public static final String comma_seg=",";
    public static final String TERM_TRUE = "true";
    public static final String TERM_FALSE = "false";
    public static final AhoCorasickDoubleArrayTrie boolTrie = new AhoCorasickDoubleArrayTrie<>();
    static {
        Map convertable = new HashMap<>();
        convertable.put("真", LuceneName.TERM_TRUE);
        convertable.put("假", LuceneName.TERM_FALSE);
        convertable.put("是", LuceneName.TERM_TRUE);
        convertable.put("否", LuceneName.TERM_FALSE);
        convertable.put("true", LuceneName.TERM_TRUE);
        convertable.put("false", LuceneName.TERM_FALSE);
        convertable.put("1", LuceneName.TERM_TRUE);
        convertable.put("0", LuceneName.TERM_FALSE);
        boolTrie.build(convertable);
    }
    //----------------------------java对象------------------------------------------------------
    /**
     * java对象的某个字段在索引中的字段名
     * String
     */
    public static final String IDX_OBJECT_ID = "OBJECT_ID";

    /**
     *  java对象的某个字段在索引中的字段名
     * String
     */
    public static final String IDX_OBJECT_NAME = "OBJECT_NAME";
    /**
     *  java对象的某个字段在索引中的内容
     * String
     */
    public static final String IDX_OBJECT_CONTENT = "OBJECT_CONTENT";
    //.......
}

创建倒排索引

    //创建多个文档的文件
    @Test
    public boolean lunceneWriter(Object object) {
        if (object == null) {
            log.error("输入的新闻为空,无法添加到索引中");
            return false;
        }
        try {
            //1 创建文档对象集合
           //  Collection documents = new ArrayList();
 
            // Document ducument = new Document();
            // 添加字段信息。参数:字段的名称、字段的值、是否存储,yes储存,no不储存
            // ducument.add(new StringField("id", "1", Field.Store.YES));
            // title字段用TextField,创建索引被分词。StringField创建索引,但不会被分词
            // ducument.add(new TextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES));
            // documents.add(ducument);
 
            Document doc = new Document();
             if (!StringUtil.isEmpty(object.getContent()) {
                 String content = object.getContent();
                // todo  可以手动对文本进行分词,然后中间添加空格。后续就直接用lucene的空格分词,有利于全文检索。后续检索出来展示时还需要删除空格保证文档易读

                // 添加字段信息。参数:字段的名称、字段的值、是否存储,yes储存,no不储存
                doc.add(new TextField(LuceneName.OBJECT_CONTENT, content, Field.Store.YES));
            }

            if (!StringUtil.isEmpty(object.getTime()) {
                //对于时间戳的存储
                doc.add(new LongPoint(LuceneName.IDX_OBJECT_DATE, object.getTime()));
                //LongPoint类型字段是无法存储的,因此需要单独设置一个域用于存储发布时间的原始值
                doc.add(new StoredField(LuceneName.IDX_OBJECT_DATE_STORED, String.valueOf(object.getTime()));
                //NumericDocValuesField用于排序
                doc.add(new NumericDocValuesField(LuceneName.IDX_OBJECT_DATE,object.getTime()));
            }
          
            indexWriter.updateDocument(new Term(LuceneName.IDX_OBJECT_ID, object.getId()), doc);
            indexWriter.commit();
            return true;

 
            //2 创建存储目录,本地
           //  FSDirectory directory = FSDirectory.open(new File("e:/hadoop/hdpdata/lucene"));
            //3 创建分词器
            // StandardAnalyzer analyzer = new StandardAnalyzer();
             // IKAnalyzer ikAnalyzer = new IKAnalyzer();
            //4 创建索引写入器的配置对象
           //  IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer);
            //如果有数据,覆盖
           //  config.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
 
            //5 创建索引写入器对象
           //  IndexWriter indexWriter = new IndexWriter(directory, config);
            //6 将文档交给索引写入器
          //   indexWriter.addDocuments(documents);
            //7 提交
           //  indexWriter.commit();
            //8 关闭
           //  indexWriter.close();
        }catch (Exception e){
            e.printStackTrace();
            log.error("无法更新索引,异常为:[{}]", e.getMessage());
        }
        return false;
    }

lucene检索

 public SearchResult queryNews(String keyword, long startDate, long endDate, int results, int page) {
        SearchResult result = new SearchResult();
        IndexSearcher searcher = null;
        BooleanQuery.Builder builder = null;
        try {
            log.debug("执行基于关键词的检索," +
                    "检索关键词为:[{}], " +
                    "时间范围为:[{}={}]" , keyword, startDate, endDate);
            IndexReader reader = DirectoryReader.open(indexWriter);
            searcher = new IndexSearcher(reader);
            //查询结果的object ID列表
            builder = new BooleanQuery.Builder();
            //添加关键词检索
            if (!StringUtil.isEmpty(keyword)) {
                //用的HanLP包
                List keyWords = HanLP.segment(keyword);
                for (com.hankcs.hanlp.seg.common.Term keyWord : keyWords) {
                    BooleanQuery.Builder blder = new BooleanQuery.Builder();
                    if (keyWord.word.equals(" ") || keyWord.word.contains(" ")) {
                        continue;
                    }
                    /**
                     * 转义特殊字符
                     */
                    String thisWord = QueryParser.escape(keyWord.word);
                    /**
                     * 查OBJECT的名字
                     */
                    Query titleQuery = objectParser.parse(thisWord);
                    /**
                    /**
                     * 查OBJECT的内容
                     */
                    Query contentQuery = objectParser.parse(LuceneName.IDX_OBJECT_CONTENT + ":" + thisWord);
                    blder.add(new BoostQuery(titleQuery, 25.0f), BooleanClause.Occur.SHOULD);
                    blder.add(contentQuery, BooleanClause.Occur.SHOULD);
                    builder.add(blder.build(), BooleanClause.Occur.MUST);
                }
            }

            //添加时间检索
            Query pubRangeQuery = null;
            if(!(startDate ==0 && endDate == 0)){
                pubRangeQuery = LongPoint.newRangeQuery(LuceneName.IDX_OBJECT_DATE, startDate, endDate);
                builder.add(pubRangeQuery, BooleanClause.Occur.MUST);
            }

        } catch (IOException | ParseException e) {
            e.printStackTrace();
        }
        try {
            //分页查询 根据校对事件进行排序
            if(StringUtil.isEmpty(keyword)){
                Sort sortByProofDate = new Sort(new SortField(LuceneName.IDX_NEWS_PROOFDATE, SortField.Type.LONG, true));
                result = LuceneUtils.LuceneSearchSortBySortField(results, page, searcher, builder.build(),sortByProofDate);
            }else{
                result = LuceneUtils.LuceneSearch(results, page, searcher, builder.build());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

删除索引

    public boolean onNewsDelete(String objectId) {
        try {
            indexWriter.deleteDocuments(new Term(LuceneName.IDX_OBJECT_ID, objectId));
            indexWriter.commit();
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

你可能感兴趣的:(Lunce全文检索学习总结)