全文检索技术 - Lucene

Lucene

  1. 全文检索有关的概念:
    • 数据的分类:两种
      • 结构化数据:指的是具有固定格式或者是有限长度的数据 例如:数据库中的数据 元数据等
      • 非结构化数据:指的是不定长或者是没有固定格式的数据 例如:邮件 文档中的数据(生活中常见的数据都是非结构化的数据)
    • 数据的查询方式:
      • 结构化数据的查询方式:使用SQL语句(结构化查询语言)使用简单 速度快。
        • 在数据库中进行查询非常容易的原因:数据库中数据的存储是有规律的,数据的长度和格式是固定的。
      • 非结构化数据的查询方式:两种
        • 顺序扫描法: 就是将一个个的文档从头到尾的进行扫描,直到扫描完所有的文件(但是这样的方式在文件的内容或者是个数非常多的时候效率低下)
        • 全文扫描:将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之为索引(这样的方式就是将非结构化的数据 转变成结构化的数据 通过先建立索引 再查询索引
          • 索引建立存在的问题:创建索引的过程是非常耗时的,但是索引一旦创建就可以多次使用,全文检索主要处理的是查询,所以耗时间创建索引是值得的
    • 如何实现全文检索:
      • 使用Lucene实现全文检索。Luceneapache下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能
    • 全文检索使用的场景:(适用在数据量大 数据结构不固定的场景)
      • 搜索引擎:百度、360搜索、谷歌、搜狗等
      • 站内搜索:论坛搜索 微博 文章搜索等
      • 电商搜索:淘宝搜索 京东搜索等
  2. Lucene实现全文检索的过程:
    在这里插入图片描述
    • 总的来说分两步:创建索引(对要搜索的原始内容进行索引构建一个索引库) 和 查询索引(从索引库中搜索内容)
      • 创建索引的步骤:
        • 获取原始文档:原始文档是指要索引和搜索的内容。原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等 这里的原始文档是磁盘中的文件。 在这里插入图片描述
        • 创建文档对象:获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document)文档中包括一个一个的(Field),域中存储内容。这里我们可以将磁盘上的一个文件当成一个document,Document中包括一些Field(file_name文件名称、file_path文件路径、file_size文件大小、file_content文件内容),如下图:在这里插入图片描述
          • 注意
            • 每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)
            • 文档都有一个唯一的编号 就是文档的id
        • 分析文档:对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单元
          • 每个单词叫做一个Term不同的域中拆分出来的相同的单词不同的term。term中包含两部分一部分是文档的域名,另一部分是单词的内容
        • 创建索引: 对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到Document(文档)在这里插入图片描述
          • 注意:创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构 倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大
      • 查询索引的步骤:
        • 用户查询的接口:
          • 全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果(就像是百度中的搜索框)但是Lucene不提供制作用户搜索界面的功能,需要根据自己的需求开发搜索界面 在这里插入图片描述
        • 创建查询: 用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要搜索的Field文档域查询关键字等,查询对象会生成具体的查询语法 例如:fileName:lucene表示的是要搜索的是Field域中的内容包含lucene关键字的文档
        • 执行查询:根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表 比如搜索语法为“fileName:lucene”表示搜索出fileName域中包含Lucene的文档。搜索过程就是在索引上查找域为fileName,并且关键字为Lucene的term,并根据term找到文档id列表
        • 渲染结果:以一个友好的界面将查询结果展示给用户 例如:关键字高亮显示 以相关度进行排列等。在这里插入图片描述
  3. Lucene使用:
    • 环境配置:
      • 下载:Lucene是开发全文检索功能的工具包,从官方网站下载 并解压。
        • 注意:不同的Lucene版本和 jdk版本对应的关系。(不同的版本对编译环境有不同的要求 这里使用的是lucene-7.4.0 jdk要求是1.8以上)
    • 导入入门程序中所需要的jar包
      在这里插入图片描述
    • 创建索引:
      • 创建索引的步骤:

        • 创建一个IndexWriter对象。
        • 构造方法中的参数
          • 指定索引库的存放位置Directory对象
          • 指定一个IndexWriterConfig对象
        • 创建Document对象。
        • 创建field对象,将field添加到document对象中
        • 使用IndexWriter对象将Document对象写入索引库,此过程进行索引创建。并将索引和document对象写入索引库。
        • 关闭IndexWriter对象
      • 代码实现:

        // 创建索引
        @Test
        public void createIndex() throws Exception{
            // 创建一个Directory对象 
            // 这样的形式是将Directory对象保存在内存中 速度快 但是 当程序终止的时候 索引就消失了 这样的方式不常使用
            //Directory directory = new RAMDirectory();
            // 一般是将索引库保存到磁盘中
            Directory directory = FSDirectory.open(new File("E:\\lucene\\index").toPath());
            // 基于directory对象来创建IndexWriter对象
            // IndexWriterConfig包含了一些配置 这里使用的是标准的默认分析器(无参的构造函数)
            IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig());
            // 读取磁盘中的文件 对应的每一个文件创建一个文档对象
            File file = new File("E:\\lucene\\searchsource");
            File[] files = file.listFiles();
            for (File file1 : files) {
                // 取出文件名
                String name = file1.getName();
                // 取出文件的路径
                String path = file1.getPath();
                // 取出文件的内容 这里使用的是commons io来进行文件的内容的读取
                String string = FileUtils.readFileToString(file1, "utf-8");
                // 文件的大小
                long l = FileUtils.sizeOf(file1);
        
                // 创建域 field 执行不同类型的数据进行 不同域的创建
                // 参数; 1 参数1 field名称 2 field内容 3 是否是存储
                Field fieldName = new TextField("name", name, Field.Store.YES);
                Field fieldPath = new TextField("path", path, Field.Store.YES);
                Field fieldContent = new TextField("content", string, Field.Store.YES);
                Field fieldValue = new LongPoint("value", l);// 长整型使用LongPoint来创建 int类型 使用IntPoint来创建
                // 创建文档对象
                Document document = new Document();
                // 想文档对象中添加域对象 field
                document.add(fieldName);
                document.add(fieldPath);
                document.add(fieldContent);
                document.add(fieldValue);
                // 将文档对象写入索引库
                indexWriter.addDocument(document);
            }
            // 关闭indexWriter对象
            indexWriter.close();
        }
        
      • 使用Luke工具查看索引文件

        • luke的版本是luke-7.4.0,跟lucene的版本对应的。可以打开7.4.0版本的lucene创建的索引库。需要注意的是此版本的Luke是jdk9编译的,所以要想运行此工具还需要jdk9才可以在这里插入图片描述
    • 查询索引:
      • 查询索引的步骤:
        • 创建一个Directory对象,也就是索引库存放的位置
        • 创建一个indexReader对象,需要指定Directory对象。
        • 创建一个indexsearcher对象,需要指定IndexReader对象
        • 创建一个TermQuery对象,指定查询的域和查询的关键词。
        • 执行查询。
        • 返回查询结果。遍历查询结果并输出。
        • 关闭IndexReader对象
      • 代码实现:
        // 搜索索引
        @Test
        public void searchIndex() throws Exception{
            // 创建一个Directory对象 存储到磁盘中
            Directory directory = FSDirectory.open(new File("E:\\lucene\\index").toPath());
            // 创建一个indexReader对象
            IndexReader indexReader = DirectoryReader.open(directory);
            // 创建一个indexSearcher对象 构造方法中需要传入 indexReader对象
            IndexSearcher  indexSearcher = new IndexSearcher(indexReader);
            // 创建一个Query对象 是一个抽象类: 创建子类对象TermQuery对象
            Query query = new TermQuery(new Term("name", "spring"));
            // 执行查询 得到topDocs对象 search 中传入的参数:query对象 int 指定的是返回记录的条数(最大记录数 )
            TopDocs search = indexSearcher.search(query, 10);
            // 取出查询的记录总数:
            System.out.println("查询的记录总数:"+ search.totalHits);
            // 取出文档列表
            ScoreDoc[] scoreDocs = search.scoreDocs;
            for (ScoreDoc scoreDoc : scoreDocs) {
               // 取出文档的id
                int doc = scoreDoc.doc;
                // 根据id来进行取出文档对象 取出来的和创建的对象是一致的
                Document document = indexSearcher.doc(doc);
                System.out.println(document.get("name"));
                System.out.println(document.get("path"));
        //          System.out.println(document.get("content"));
                System.out.println(document.get("value"));
                System.out.println("--------------------");
            }
            // 进行文档对象的关闭
            indexReader.close();
        }
        
    • 分析器:
      • 默认使用 :标准分析器StandardAnalyzer
      • 查看分析器的分析效果: 使用Analyzer对象的tokenStream方法返回一个TokenStream对象。词对象中包含了最终分词结果
      • 代码实现:
        @Test
        public void tokenStream() throws Exception{
            // 创建一个analyzer 对象 使用的是StandardAnalyzer对象
            Analyzer analyzer = new StandardAnalyzer();
            // 使用分析器对象的tokenStream方法获取一个TokenSteam 对象
            TokenStream tokenStream = analyzer.tokenStream("", "lucene是一个java开发全文搜索的工具包");
            //向TokenStream对象中设置一个引用 相当于设置一个指针
            CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
            // 调用TokenStream对象的reset方法 不调用会抛出异常
            tokenStream.reset();
            // 使用while进行遍历
            while (tokenStream.incrementToken()){
                System.out.println(charTermAttribute.toString());
            }
            // 关闭tokenStream对象
            tokenStream.close();
        }
        
      • 存在的问题:支持中文但是对中文的进行分词结果不适用(会将每一个字进行分词 而进行搜索的时候都是使用词进行搜索 标准的分析器不适合中文)在这里插入图片描述
      • 中文分析器:
        • SmartChineseAnalyzer :对中文支持较好,但扩展性差,扩展词库,禁用词库和同义词库等不好处理
        • IKAnalyzer
          • 使用前需要注意的是:hotword.dicext_stopword.dic文件的格式为UTF-8,注意是无BOM 的UTF-8 编码。也就是说禁止使用windows记事本编辑扩展词典文件 可以使用EditPlus 或者是IDEA等进行编辑
          • 使用方式:
            • 把jar包添加到工程中
            • 把配置文件和扩展词典和停用词词典添加到classpath下在这里插入图片描述
            • 自定义分析器使用方式 代码实现:
              @Test
              public void addDocument() throws Exception {
                  //索引库存放路径
                  Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());
                  // IndexWriterConfig包含了一些配置 在创建对象的时候将自定义的分析器传入
                  IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
                  //创建一个indexwriter对象
                  IndexWriter indexWriter = new IndexWriter(directory, config);
              //...
              }
              
  4. 索引库的维护:就是对索引库的增删改查
    • 前提条件:索引库的维护需要使用IndexWriter对象

      private IndexWriter indexWriter;
      			
      @Before
      public void init() throws Exception{
         // 创建一个indexWriter对象 需要使用IK分析器: 需要使用对索引对象进行写操作 创建indexWriter对象
         Directory directory = FSDirectory.open(new File("E:\\lucene\\index").toPath());
         // 基于directory对象来创建IndexWriter对象 IndexWriterConfig包含了一些配置 这里使用的是标准的默认分析器
         indexWriter = new IndexWriter(directory, new IndexWriterConfig(new IKAnalyzer()));
      }
      			
      
    • 索引库的添加

      • Field域的属性添加的依据:

        • 是否分析:是否对域的内容进行分词处理。前提是我们要对域的内容进行查询。
        • 是否索引:将Field分析后的词或整个Field值进行索引,只有索引方可搜索到。比如:商品名称、商品简介分析后进行索引,订单号、身份证号不用分析但也要索引,这些将来都要作为查询条件。
        • 是否存储:将Field值存储在文档中,存储在文档中的Field才可以从Document中获取比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储(就是是否要将内容展示给用户)
      • 添加文档的代码实现:

        
        @Test
        public void addDocument() throws Exception{
        
           // 创建一个Document对象
           Document document = new Document();
           // 向对象中添加域
           document.add(new TextField("name", "新添加的文件", Field.Store.YES));
           document.add(new TextField("content", "新添加的文件内容", Field.Store.NO));
           document.add(new StoredField("path", "E:\\lucene\\searchsource"));
           // 将文档存储到索引库中
           indexWriter.addDocument(document);
           // 将indexWriter关闭
           indexWriter.close();
        
        }
        
      • 不同Field类的说明:

      Field类数据类型Analyzed是否分析Indexed是否索引Stored是否存储说明
      tringField(FieldName, FieldValue,Store.YES))字符串NYY或者N这个Field用来构建一个字符串Field,但是不会进行分析,会将整个串存储在索引中,比如(订单号,姓名等)是否存储在文档中用Store.YES或Store.NO决定
      ngPoint(String name, long… point)Long类型YYN可以使用LongPoint、IntPoint等类型存储数值类型的数据。让数值类型可以进行索引。但是不能存储数据,如果想存储数据还需要使用StoredField
      toredField(FieldName, FieldValue)重载方法,支持多种类型NNY这个Field用来构建不同类型Field不分析,不索引,但要Field存储在文档中
      extField(FieldName, FieldValue, Store.NO)或TextField(FieldName, reader)字符串或者流YYY或者N如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略.
    • 索引库的删除:两种方式

      • 删除全部:将索引目录的索引信息全部删除,直接彻底删除,无法恢复

        @Test
            public void deleteIndex() throws  Exception{
                // 删除全部文档: 两种方式之一 全部删除
                indexWriter.deleteAll();
                // 进行indexWriter的关闭:
                indexWriter.close();
            }
        
      • 指定查询条件进行删除:通过Query或者是Term对象进行删除

         @Test
            public void deleteDocumentByQuery() throws Exception{
                // 删除文档通过Query 或者是 Term:文件名中包含apache的 进行的是按照query 查询 或者是 term关键词来删除
                indexWriter.deleteDocuments(new Term("name", "apache"));
                indexWriter.close();
            }
        
    • 索引库的更新:(就是先进行删除 再进行创建)

      @Test
          public void updateDocument() throws Exception{
              // 跟新文档的操作是通过先删除在创建
              // 创建一个Document对象
              Document document= new Document();
              // 象文档对象中添加域
              document.add(new TextField("name","更新后的内容", Field.Store.YES));
              // 更新操作:
              indexWriter.updateDocument(new Term("name","spring"), document);
              // 关闭索引库:
              indexWriter.close();
          }
      
    • 索引库的查询

      • 对要搜索的信息创建Query查询对象 创建的方式有两种:
        • 使用Lucene提供Query子类
        • 使用`QueryParse解析查询表达式
      • TermQuery
        • 说明: TermQuery,通过项查询,TermQuery不使用分析器所以建议匹配不分词的Field域查询,比如订单号、分类ID号等。指定要查询的域和要查询的关键词

        • 代码实现:

          //使用Termquery查询
          @Test
          public void testTermQuery() throws Exception {
              Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());
              IndexReader indexReader = DirectoryReader.open(directory);
              IndexSearcher indexSearcher = new IndexSearcher(indexReader);
              
              //创建查询对象
              Query query = new TermQuery(new Term("content", "lucene"));
              //执行查询
              TopDocs topDocs = indexSearcher.search(query, 10);
              //共查询到的document个数
              System.out.println("查询结果总数量:" + topDocs.totalHits);
              //遍历查询结果
              for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
                  Document document = indexSearcher.doc(scoreDoc.doc);
                  System.out.println(document.get("filename"));
                  //System.out.println(document.get("content"));
                  System.out.println(document.get("path"));
                  System.out.println(document.get("size"));
              }
              //关闭indexreader
              indexSearcher.getIndexReader().close();
          }
          
      • 数值范围查询:
        • 代码实现:

            @Test
              public void rangeQuery() throws Exception{
                  // 创建一个query对象 使用的是LongPoint 参数 域名称 最小的值 最大的值
                  Query size = LongPoint.newRangeQuery("value", 0L, 100L);
                  // 执行查询:
                  TopDocs search = indexSearcher.search(size, 10);
                  long totalHits = search.totalHits;
                  System.out.println("总记录数"+totalHits);
                  ScoreDoc[] scoreDocs = search.scoreDocs;
                  for (ScoreDoc scoreDoc : scoreDocs) {
                      int doc = scoreDoc.doc;
                      // 根据id来进行取出文档对象 取出来的和创建的对象是一致的
                      Document document = indexSearcher.doc(doc);
                      System.out.println(document.get("name"));
                      System.out.println(document.get("path"));
                      System.out.println(document.get("content"));
                      System.out.println(document.get("value"));
                      System.out.println("--------------------");
                  }
                  indexReader.close();
              }
          
      • 使用QueryParser进行查询:(就是先进行分词 在进行查询)
        • 代码实现:

          @Test
              public void testQueryParser() throws Exception{ // 这样的形式 是先进行分词在进行查询
                  // 创建一个QueryParser对象 两个参数: 1 默认的搜索域 2 分析器对象
                  QueryParser queryParser = new QueryParser("name", new IKAnalyzer());
                  //使用QueryParser对象来创建一个query对象
                  Query query = queryParser.parse("lucene是一个java开发全文搜索的工具包");
                  // 执行查询
                  TopDocs search = indexSearcher.search(query,5);
                  long totalHits = search.totalHits;
                  System.out.println("总记录数"+totalHits);
                  ScoreDoc[] scoreDocs = search.scoreDocs;
                  for (ScoreDoc scoreDoc : scoreDocs) {
                      int doc = scoreDoc.doc;
                      // 根据id来进行取出文档对象 取出来的和创建的对象是一致的
                      Document document = indexSearcher.doc(doc);
                      System.out.println(document.get("name"));
                      System.out.println(document.get("path"));
                      System.out.println(document.get("content"));
                      System.out.println(document.get("value"));
                      System.out.println("--------------------");
                  }
                  indexReader.close();
              }
          
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

上山打卤面

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值