zoukankan      html  css  js  c++  java
  • Lucene:一、入门

    1 Lucene简介

    Lucene是apache下的一个开源的全文检索引擎工具包。

    1.1 全文检索(Full-text Search

    1.1.1 定义

    全文检索就是先分词创建索引,再执行搜索的过程。

    分词:就是将一段文字分成一个个单词

    全文检索就将一段文字分成一个个单词去查询数据!!!

    1.1.2 应用场景

    1.1.2.1 搜索引擎(了解)

    搜索引擎是一个基于全文检索、能独立运行、提供搜索服务的软件系统。

    wps1

     

    1.2 Lucene实现全文检索的流程

    wps3

    全文检索的流程分为两大部分:索引流程、搜索流程。

    索引流程:采集数据--->构建文档对象--->创建索引(将文档写入索引库)。

    搜索流程:创建查询--->执行搜索--->渲染搜索结果。

    2 入门示例

    2.1 需求

    使用Lucene实现电商项目中图书类商品的索引和搜索功能。

    2.2 配置步骤说明

    (1)搭建环境(先下载Lucene)

    (2)创建索引库

    (3)搜索索引库

    2.3 配置步骤

    2.3.1 第一部分:搭建环境(创建项目,导入包)

    前提:已经创建好了数据库(直接导入book.sql文件)

    wps4

    2.3.1.1 第一步下载Lucene

    Lucene是开发全文检索功能的工具包,使用时从官方网站下载,并解压。

    官方网站:http://lucene.apache.org/

    下载地址:http://archive.apache.org/dist/lucene/java/

    下载版本:4.10.3(要求:jdk1.7及以上)

    核心包lucene-core-4.10.3.jar(附常用API)

    wps5

    2.3.1.2 第二步创建项目导入包

    mysql5.1驱动包:mysql-connector-java-5.1.7-bin.jar

    核心包:lucene-core-4.10.3.jar

    分析器通用包:lucene-analyzers-common-4.10.3.jar

    查询解析器包:lucene-queryparser-4.10.3.jar

    项目结构如下:

    wps6

    2.3.2 第二部分:创建索引

    步骤说明:

    (1)采集数据

    (2)将数据转换成Lucene文档

    (3)将文档写入索引库,创建索引

    2.3.2.1 第一步:采集数据

    Lucene全文检索,不是直接查询数据库,所以需要先将数据采集出来。

    (1)创建Book类

     1 public class Book {
     2 
     3   private Integer bookId; // 图书ID
     4 
     5   private String name; // 图书名称
     6 
     7   private Float price; // 图书价格
     8 
     9   private String pic; // 图书图片
    10 
    11   private String description; // 图书描述
    12 
    13   // 补全getset方法
    14 
    15 }
    View Code

    public class Book {

      private Integer bookId; // 图书ID

      private String name; // 图书名称

      private Float price; // 图书价格

      private String pic; // 图书图片

      private String description; // 图书描述

      // 补全getset方法

    }

    (2)创建一个BookDao类

      1 package cn.gzsxt.lucene.dao;
      2 
      3 import java.sql.Connection;
      4 
      5 import java.sql.DriverManager;
      6 
      7 import java.sql.PreparedStatement;
      8 
      9 import java.sql.ResultSet;
     10 
     11 import java.sql.SQLException;
     12 
     13 import java.util.ArrayList;
     14 
     15 import java.util.List;
     16 
     17 import cn.gzsxt.lucene.pojo.Book;
     18 
     19 public class BookDao {
     20 
     21   public List<Book> getAll() {
     22 
     23     // 数据库链接
     24 
     25     Connection connection = null;
     26 
     27     // 预编译statement
     28 
     29     PreparedStatement preparedStatement = null;
     30 
     31     // 结果集
     32 
     33     ResultSet resultSet = null;
     34 
     35     // 图书列表
     36 
     37     List<Book> list = new ArrayList<Book>();
     38 
     39     try {
     40 
     41       // 加载数据库驱动
     42 
     43       Class.forName("com.mysql.jdbc.Driver");
     44 
     45       // 连接数据库
     46 
     47       connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/lucene", "root", "gzsxt");
     48 
     49       // SQL语句
     50 
     51       String sql = "SELECT * FROM book";
     52 
     53       // 创建preparedStatement
     54 
     55       preparedStatement = connection.prepareStatement(sql);
     56 
     57       // 获取结果集
     58 
     59       resultSet = preparedStatement.executeQuery();
     60 
     61       // 结果集解析
     62 
     63       while (resultSet.next()) {
     64 
     65         Book book = new Book();
     66 
     67         book.setBookId(resultSet.getInt("id"));
     68 
     69         book.setName(resultSet.getString("name"));
     70 
     71         book.setPrice(resultSet.getFloat("price"));
     72 
     73         book.setPic(resultSet.getString("pic"));
     74 
     75         book.setDescription(resultSet.getString("description"));
     76 
     77         list.add(book);
     78 
     79       }
     80 
     81     } catch (Exception e) {
     82 
     83       e.printStackTrace();
     84 
     85     }finally {
     86 
     87       if(null!=resultSet){
     88 
     89         try {
     90 
     91           resultSet.close();
     92 
     93         } catch (SQLException e) {
     94 
     95           // TODO Auto-generated catch block
     96 
     97           e.printStackTrace();
     98 
     99         }
    100 
    101       }
    102 
    103       if(null!=preparedStatement){
    104 
    105         try {
    106 
    107           preparedStatement.close();
    108 
    109         } catch (SQLException e) {
    110 
    111           // TODO Auto-generated catch block
    112 
    113           e.printStackTrace();
    114 
    115         }
    116 
    117       }
    118 
    119       if(null!=connection){
    120 
    121         try {
    122 
    123           connection.close();
    124 
    125         } catch (SQLException e) {
    126 
    127           // TODO Auto-generated catch block
    128 
    129           e.printStackTrace();
    130 
    131         }
    132 
    133       }
    134 
    135     }
    136 
    137     return list;
    138 
    139   }
    140 
    141 }
    View Code

    package cn.gzsxt.lucene.dao;

    import java.sql.Connection;

    import java.sql.DriverManager;

    import java.sql.PreparedStatement;

    import java.sql.ResultSet;

    import java.sql.SQLException;

    import java.util.ArrayList;

    import java.util.List;

    import cn.gzsxt.lucene.pojo.Book;

    public class BookDao {

      public List<Book> getAll() {

        // 数据库链接

        Connection connection = null;

        // 预编译statement

        PreparedStatement preparedStatement = null;

        // 结果集

        ResultSet resultSet = null;

        // 图书列表

        List<Book> list = new ArrayList<Book>();

        try {

          // 加载数据库驱动

          Class.forName("com.mysql.jdbc.Driver");

          // 连接数据库

          connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/lucene", "root", "gzsxt");

          // SQL语句

          String sql = "SELECT * FROM book";

          // 创建preparedStatement

          preparedStatement = connection.prepareStatement(sql);

          // 获取结果集

          resultSet = preparedStatement.executeQuery();

          // 结果集解析

          while (resultSet.next()) {

            Book book = new Book();

            book.setBookId(resultSet.getInt("id"));

            book.setName(resultSet.getString("name"));

            book.setPrice(resultSet.getFloat("price"));

            book.setPic(resultSet.getString("pic"));

            book.setDescription(resultSet.getString("description"));

            list.add(book);

          }

        } catch (Exception e) {

          e.printStackTrace();

        }finally {

          if(null!=resultSet){

            try {

              resultSet.close();

            } catch (SQLException e) {

              // TODO Auto-generated catch block

              e.printStackTrace();

            }

          }

          if(null!=preparedStatement){

            try {

              preparedStatement.close();

            } catch (SQLException e) {

              // TODO Auto-generated catch block

              e.printStackTrace();

            }

          }

          if(null!=connection){

            try {

              connection.close();

            } catch (SQLException e) {

              // TODO Auto-generated catch block

              e.printStackTrace();

            }

          }

        }

        return list;

      }

    }

    (3)创建一个测试类BookDaoTest

     1 package cn.gzsxt.lucene.test;
     2 
     3 import java.util.List;
     4 
     5 import org.junit.Test;
     6 
     7 import cn.gzsxt.lucene.dao.BookDao;
     8 
     9 import cn.gzsxt.lucene.pojo.Book;
    10 
    11 public class BookDaoTest {
    12 
    13     @Test
    14 
    15     public void getAll(){
    16 
    17         BookDao dao = new BookDao();
    18 
    19         List<Book> books = dao.getAll();
    20 
    21         for (Book book : books) {
    22 
    23         System.out.println("图书id:"+book.getBookId()+",图书名称:"+book.getName());
    24 
    25         }
    26 
    27     }
    28 
    29 } 
    View Code

    package cn.gzsxt.lucene.test;

    import java.util.List;

    import org.junit.Test;

    import cn.gzsxt.lucene.dao.BookDao;

    import cn.gzsxt.lucene.pojo.Book;

    public class BookDaoTest {

    @Test

    public void getAll(){

    BookDao dao = new BookDao();

    List<Book> books = dao.getAll();

    for (Book book : books) {

    System.out.println("图书id:"+book.getBookId()+",图书名称:"+book.getName());

    }

    }

    }

    (4)测试结果,采集数据成功

    wps7

    2.3.2.2 第二步:将数据转换成Lucene文档

    Lucene是使用文档类型来封装数据的,所有需要先将采集的数据转换成文档类型。其格式为:

    wps8

    修改BookDao,新增一个方法,转换数据

    public List<Document> getDocuments(List<Book> books){

    // Document对象集合

    List<Document> docList = new ArrayList<Document>();

    // Document对象

    Document doc = null;

    for (Book book : books) {

    // 创建Document对象,同时要创建field对象

    doc = new Document();

    // 根据需求创建不同的Field

    Field id = new TextField("id", book.getBookId().toString(), Store.YES);

    Field name = new TextField("name", book.getName(), Store.YES);

    Field price = new TextField("price", book.getPrice().toString(),Store.YES);

    Field pic = new TextField("pic", book.getPic(), Store.YES);

    Field desc = new TextField("description", book.getDescription(), Store.YES);

    // 把域(Field)添加到文档(Document)中

    doc.add(id);

    doc.add(name);

    doc.add(price);

    doc.add(pic);

    doc.add(desc);

    docList.add(doc);

    }

    return docList;

    }

    2.3.2.3 第三步创建索引库

    说明:Lucene是在将文档写入索引库的过程中,自动完成分词、创建索引的。因此创建索引库,从形式上看,就是将文档写入索引库!

    修改测试类,新增createIndex方法

     1 @Test
     2 
     3 public void createIndex(){
     4 
     5   try {
     6 
     7     BookDao dao = new BookDao();
     8 
     9     // 分析文档,对文档中的field域进行分词
    10 
    11     Analyzer analyzer = new StandardAnalyzer();
    12 
    13     // 创建索引
    14 
    15     // 1) 创建索引库目录
    16 
    17     Directory directory = FSDirectory.open(new File("F:\lucene\0719"));
    18 
    19     // 2) 创建IndexWriterConfig对象
    20 
    21     IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, analyzer);
    22 
    23     // 3) 创建IndexWriter对象
    24 
    25     IndexWriter writer = new IndexWriter(directory, cfg);
    26 
    27     // 4) 通过IndexWriter对象添加文档对象(document)
    28 
    29     writer.addDocuments(dao.getDocuments(dao.getAll()));
    30 
    31     // 5) 关闭IndexWriter
    32 
    33     writer.close();
    34 
    35     System.out.println("创建索引库成功");
    36 
    37   } catch (Exception e) {
    38 
    39     e.printStackTrace();
    40 
    41   }
    42 
    43 }
    View Code

    @Test

    public void createIndex(){

      try {

        BookDao dao = new BookDao();

        // 分析文档,对文档中的field域进行分词

        Analyzer analyzer = new StandardAnalyzer();

        // 创建索引

        // 1) 创建索引库目录

        Directory directory = FSDirectory.open(new File("F:\lucene\0719"));

        // 2) 创建IndexWriterConfig对象

        IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, analyzer);

        // 3) 创建IndexWriter对象

        IndexWriter writer = new IndexWriter(directory, cfg);

        // 4) 通过IndexWriter对象添加文档对象(document)

        writer.addDocuments(dao.getDocuments(dao.getAll()));

        // 5) 关闭IndexWriter

        writer.close();

        System.out.println("创建索引库成功");

      } catch (Exception e) {

        e.printStackTrace();

      }

    }

    测试结果,创建成功!!!

    wps9

    2.3.3 第三部分搜索索引

    2.3.3.1 说明

    搜索的时候,需要指定搜索哪一个域(也就是字段),并且,还要对搜索的关键词做分词处理。

    2.3.3.2 执行搜索

    修改测试类,新增searchDocumentByIndex方法

     1 @Test
     2 
     3 public void searchDocumentByIndex(){
     4 
     5   try {
     6 
     7     // 1、 创建查询(Query对象)
     8 
     9     // 创建分析器
    10 
    11     Analyzer analyzer = new StandardAnalyzer();
    12 
    13     QueryParser queryParser = new QueryParser("name", analyzer);
    14 
    15     Query query = queryParser.parse("name:java教程");
    16 
    17     // 2、 执行搜索
    18 
    19     // a) 指定索引库目录
    20 
    21     Directory directory = FSDirectory.open(new File("F:\lucene\0719"));
    22 
    23     // b) 创建IndexReader对象
    24 
    25     IndexReader reader = DirectoryReader.open(directory);
    26 
    27     // c) 创建IndexSearcher对象
    28 
    29     IndexSearcher searcher = new IndexSearcher(reader);
    30 
    31     // d) 通过IndexSearcher对象执行查询索引库,返回TopDocs对象
    32 
    33     // 第一个参数:查询对象
    34 
    35     // 第二个参数:最大的n条记录
    36 
    37     TopDocs topDocs = searcher.search(query, 10);
    38 
    39     // e) 提取TopDocs对象中前n条记录
    40 
    41     ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    42 
    43     System.out.println("查询出文档个数为:" + topDocs.totalHits);
    44 
    45     for (ScoreDoc scoreDoc : scoreDocs) {
    46 
    47       // 文档对象ID
    48 
    49       int docId = scoreDoc.doc;
    50 
    51       Document doc = searcher.doc(docId);
    52 
    53       // f) 输出文档内容
    54 
    55       System.out.println("===============================");
    56 
    57       System.out.println("文档id:" + docId);
    58 
    59       System.out.println("图书id:" + doc.get("id"));
    60 
    61       System.out.println("图书name:" + doc.get("name"));
    62 
    63       System.out.println("图书price:" + doc.get("price"));
    64 
    65       System.out.println("图书pic:" + doc.get("pic"));
    66 
    67       System.out.println("图书description:" + doc.get("description"));
    68 
    69     }
    70 
    71     // g) 关闭IndexReader
    72 
    73     reader.close();
    74 
    75   } catch (Exception e) {
    76 
    77     // TODO Auto-generated catch block
    78 
    79     e.printStackTrace();
    80 
    81   }
    82 
    83 }

    @Test

    public void searchDocumentByIndex(){

      try {

        // 1、 创建查询(Query对象)

        // 创建分析器

        Analyzer analyzer = new StandardAnalyzer();

        QueryParser queryParser = new QueryParser("name", analyzer);

        Query query = queryParser.parse("name:java教程");

        // 2、 执行搜索

        // a) 指定索引库目录

        Directory directory = FSDirectory.open(new File("F:\lucene\0719"));

        // b) 创建IndexReader对象

        IndexReader reader = DirectoryReader.open(directory);

        // c) 创建IndexSearcher对象

        IndexSearcher searcher = new IndexSearcher(reader);

        // d) 通过IndexSearcher对象执行查询索引库,返回TopDocs对象

        // 第一个参数:查询对象

        // 第二个参数:最大的n条记录

        TopDocs topDocs = searcher.search(query, 10);

        // e) 提取TopDocs对象中前n条记录

        ScoreDoc[] scoreDocs = topDocs.scoreDocs;

        System.out.println("查询出文档个数为:" + topDocs.totalHits);

        for (ScoreDoc scoreDoc : scoreDocs) {

          // 文档对象ID

          int docId = scoreDoc.doc;

          Document doc = searcher.doc(docId);

          // f) 输出文档内容

          System.out.println("===============================");

          System.out.println("文档id:" + docId);

          System.out.println("图书id:" + doc.get("id"));

          System.out.println("图书name:" + doc.get("name"));

          System.out.println("图书price:" + doc.get("price"));

          System.out.println("图书pic:" + doc.get("pic"));

          System.out.println("图书description:" + doc.get("description"));

        }

        // g) 关闭IndexReader

        reader.close();

      } catch (Exception e) {

        // TODO Auto-generated catch block

        e.printStackTrace();

      }

    }

    测试结果,非常成功!!!

    wps10

    2.4 小结

    Lucene全文检索,确实可以实现对关键词做分词、再执行搜索功能。并且结果更精确。

    3 分词

    3.1 重要性

    分词是全文检索的核心。

    所谓的分词,就是将一段文本,根据一定的规则,拆分成一个一个词。

    Lucene是根据分析器实现分词的。针对不同的语言提供了不同的分析器。并且提供了一个通用的标准分析器StandardAnalyzer

    3.2 分词过程

    --说明:我们通过分析StandardAnalyzer核心源码来分析分词过程

     1 @Override
     2 
     3 protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) {
     4 
     5   final StandardTokenizer src = new StandardTokenizer(getVersion(), reader);
     6 
     7   src.setMaxTokenLength(maxTokenLength);
     8 
     9   TokenStream tok = new StandardFilter(getVersion(), src);
    10 
    11   tok = new LowerCaseFilter(getVersion(), tok);
    12 
    13   tok = new StopFilter(getVersion(), tok, stopwords);
    14 
    15   return new TokenStreamComponents(src, tok) {
    16 
    17     @Override
    18 
    19     protected void setReader(final Reader reader) throws IOException {
    20 
    21     src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);
    22 
    23     super.setReader(reader);
    24 
    25         }
    26 
    27   };
    28 
    29 }

    @Override

    protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) {

      final StandardTokenizer src = new StandardTokenizer(getVersion(), reader);

      src.setMaxTokenLength(maxTokenLength);

      TokenStream tok = new StandardFilter(getVersion(), src);

      tok = new LowerCaseFilter(getVersion(), tok);

      tok = new StopFilter(getVersion(), tok, stopwords);

      return new TokenStreamComponents(src, tok) {

        @Override

        protected void setReader(final Reader reader) throws IOException {

        src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);

        super.setReader(reader);

            }

      };

    }

    对应Lucene分词的过程,我们可以做如下总结:

    (1)分词的时候,是以域为单位的。不同的域,相互独立。

    同一个域中,拆分出来相同的词,视为同一个词(Term)

    不同的域中,拆分出来相同的词,不是同一个词。由下图可以看出,在name域和desc域中都有java。

    其中,Term是Lucene最小的语汇单元,不可再细分。

    (2)分词的时候经历了一系列的过滤器。如大小写转换、去除停用词等。

    3.3 分词后索引库结构

    我们这里借助前面的示例来说明

    wps11

    从上图中,我们发现:

    (1)索引库中有两个区域:索引区、文档区。

    (2)文档区存放的是文档。Lucene给每一个文档自动加上一个文档编号docID。

    (3)索引区存放的是索引。注意:

    索引是以域为单位的,不同的域,彼此相互独立。

    索引是根据分词规则创建出来的,根据索引就能找到对应的文档。

    3.4 Luke客户端连接索引库

    Luke作为Lucene工具包中的一个工具(http://www.getopt.org/luke/),可以通过可视化界面,连接操作索引库。

    3.4.1 启动方法

    (1)双击start.bat启动!

    wps12

    (2)连接索引库

    wps13

    3.4.2 验证分词效果

    wps14


    4 Field域

    问题:我们已经知道,Lucene是在写入文档时,完成分词、索引的。那Lucene是怎么知道如何分词的呢?

    答:Lucene是根据文档中的域的属性,来确定是否要分词、创建索引的。所以,我们必须搞清楚域有哪些属性。

    4.1 域的属性

    4.1.1 三大属性

    4.1.1.1 是否分词tokenized

    只有设置了分词属性为true,lucene才会对这个域进行分词处理。

    在实际的开发中,有一些字段是不需要分词的,比如商品id,商品图片等。

    而有一些字段是必须分词的,比如商品名称,描述信息等。

    4.1.1.2 是否索引(indexed)

    只有设置了索引属性为true,lucene才为这个域的Term词创建索引。

    在实际的开发中,有一些字段是不需要创建索引的,比如商品的图片等。我们只需要对参与搜索的字段做索引处理。

    4.1.1.3 是否存储(stored)

    只有设置了存储属性为true,在查找的时候,才能从文档中获取这个域的值。

    在实际开发中,有一些字段是不需要存储的。比如:商品的描述信息。

    因为商品描述信息,通常都是大文本数据,读的时候会造成巨大的IO开销。而描述信息是不需要经常查询的字段,这样的话就白白浪费了cpu的资源了。

    因此,像这种不需要经常查询,又是大文本的字段,通常不会存储到索引库。

    4.1.2 特点

    (1)三大属性彼此独立。

    (2)通常分词是为了创建索引。

    (3)不存储这个域文本内容,也可以对这个域先分词、创建索引。

    4.2 Field常用类型

    域的常用类型有很多,每一个类都有自己默认的三大属性。如下:

    Field类

    数据类型

    Analyzed

    是否分词

    Indexed

    是否索引

    Stored

    是否存储

    StringField(FieldName, FieldValue,Store.YES))

    字符串

    N

    Y

    Y或N

    LongField(FieldName, FieldValue,Store.YES)

    Long型

    Y

    Y

    Y或N

    FloatField(FieldName, FieldValue,Store.YES)

    Float型

    Y

    Y

    Y或N

    StoredField(FieldName, FieldValue)

    重载方法,支持多种类型

    N

    N

    Y

    TextField(FieldName, FieldValue, Store.NO)

    字符串

    Y

    Y

    Y或N

    (不单单是这些,还有像DoubleField等等)

    4.3 改造入门示例中的域类型

    4.3.1 分析

    (1)图书id:

    是否分词:不用分词,因为不会根据商品id来搜索商品

    是否索引:不索引,因为不需要根据图书ID进行搜索

    是否存储:要存储,因为查询结果页面需要使用id这个值。

    (2)图书名称:

    是否分词:要分词,因为要将图书的名称内容分词索引,根据关键搜索图书名称抽取的词。

    是否索引:要索引。

    是否存储:要存储。

    (3)图书价格:

    是否分词:要分词,lucene对数字型的值只要有搜索需求的都要分词和索引,因为lucene对数字型的内容要特殊分词处理,本例子可能要根据价格范围搜索, 需要分词和索引。

    是否索引:要索引

    是否存储:要存储

    (4)图书图片地址:

    是否分词:不分词

    是否索引:不索引

    是否存储:要存储

    (5)图书描述:

    是否分词:要分词

    是否索引:要索引

    是否存储:因为图书描述内容量大,不在查询结果页面直接显示,不存储。

    不存储是来不在lucene的索引文件中记录,节省lucene的索引文件空间,如果要在详情页面显示描述,思路:

    从lucene中取出图书的id,根据图书的id查询关系数据库中book表得到描述信息。

    4.3.2 代码修改

    修改BookDao的getDocument方法

     1 public List<Document> getDocuments(List<Book> books){
     2 
     3   // Document对象集合
     4 
     5   List<Document> docList = new ArrayList<Document>();
     6 
     7   // Document对象
     8 
     9   Document doc = null;
    10 
    11   for (Book book : books) {
    12 
    13     // 创建Document对象,同时要创建field对象
    14 
    15     doc = new Document();
    16 
    17     // 图书ID
    18 
    19     // 参数:域名、域中存储的内容、是否存储
    20 
    21     // 不分词、索引、要存储
    22 
    23     // Field id = new TextField("id", book.getId().toString(),Store.YES);
    24 
    25     Field id = new StoredField("id", book.getBookId().toString());
    26 
    27     // 图书名称
    28 
    29     // 分词、索引、存储
    30 
    31     Field name = new TextField("name", book.getName(),Store.YES);
    32 
    33     // 图书价格
    34 
    35     // 分词、索引、存储
    36 
    37     Field price = new FloatField("price", book.getPrice(), Store.YES);
    38 
    39     // 图书图片
    40 
    41     // 不分词、不索引、要存储
    42 
    43     Field pic = new StoredField("pic", book.getPic());
    44 
    45     // 图书描述
    46 
    47     // 分词、索引、不存储
    48 
    49     Field desc = new TextField("description",book.getDescription(), Store.NO);
    50 
    51     // 把域(Field)添加到文档(Document)中
    52 
    53     doc.add(id);
    54 
    55     doc.add(name);
    56 
    57     doc.add(price);
    58 
    59     doc.add(pic);
    60 
    61     doc.add(desc);
    62 
    63     docList.add(doc);
    64 
    65   }
    66 
    67   return docList;
    68 
    69 }
    View Code

    public List<Document> getDocuments(List<Book> books){

      // Document对象集合

      List<Document> docList = new ArrayList<Document>();

      // Document对象

      Document doc = null;

      for (Book book : books) {

        // 创建Document对象,同时要创建field对象

        doc = new Document();

        // 图书ID

        // 参数:域名、域中存储的内容、是否存储

        // 不分词、索引、要存储

        // Field id = new TextField("id", book.getId().toString(),Store.YES);

        Field id = new StoredField("id", book.getBookId().toString());

        // 图书名称

        // 分词、索引、存储

        Field name = new TextField("name", book.getName(),Store.YES);

        // 图书价格

        // 分词、索引、存储

        Field price = new FloatField("price", book.getPrice(), Store.YES);

        // 图书图片

        // 不分词、不索引、要存储

        Field pic = new StoredField("pic", book.getPic());

        // 图书描述

        // 分词、索引、不存储

        Field desc = new TextField("description",book.getDescription(), Store.NO);

        // 把域(Field)添加到文档(Document)中

        doc.add(id);

        doc.add(name);

        doc.add(price);

        doc.add(pic);

        doc.add(desc);

        docList.add(doc);

      }

      return docList;

    }

    4.3.3 测试

    (1)去索引库目录中,手动清空索引库。

    (2)重新创建索引库。

    (3)使用Luke验证分词、索引效果。

    wps15

    改造成功!!!

    5 索引库维护

    在第4节,我们需要重新创建索引的时候,是去索引库目录下,手动删除的。

    而在实际的开发中,我们可能压根就不知道索引库在哪,就算知道,我们也不可能每次都去手动删除,非常之麻烦!!!

    所以,我们必须学习如何维护索引库,使用程序来操作索引库。

    需要注意的是,索引是与文档紧密相连的,因此对索引的维护,实际上就是对文档的增删改

    5.1 添加索引(文档)

    5.1.1 需求

    数据库中新上架了图书,必须把这些图书也添加到索引库中,不然就搜不到该新上架的图书了。

    5.1.2 代码实现

    调用 indexWriter.addDocument(doc)添加索引。

    参考入门示例中的创建索引。

    5.2 删除索引(文档)

    5.2.1 需求

    某些图书不再出版销售了,我们需要从索引库中移除该图书。

    5.2.2 代码实现

     1 @Test
     2 
     3 public void deleteIndex() throws Exception {
     4 
     5   // 1、指定索引库目录
     6 
     7   Directory directory = FSDirectory.open(new File("F:\lucene\0719"));
     8 
     9   // 2、创建IndexWriterConfig
    10 
    11   IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST,
    12 
    13   new StandardAnalyzer());
    14 
    15   // 3、 创建IndexWriter
    16 
    17   IndexWriter writer = new IndexWriter(directory, cfg);
    18 
    19   // 4、通过IndexWriter来删除索引
    20 
    21   // 删除指定索引
    22 
    23   writer.deleteDocuments(new Term("name", "apache"));
    24 
    25   // 5、关闭IndexWriter
    26 
    27   writer.close();
    28 
    29   System.out.println("删除成功");
    30 
    31 }

    @Test

    public void deleteIndex() throws Exception {

      // 1、指定索引库目录

      Directory directory = FSDirectory.open(new File("F:\lucene\0719"));

      // 2、创建IndexWriterConfig

      IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST,

      new StandardAnalyzer());

      // 3、 创建IndexWriter

      IndexWriter writer = new IndexWriter(directory, cfg);

      // 4、通过IndexWriter来删除索引

      // 删除指定索引

      writer.deleteDocuments(new Term("name", "apache"));

      // 5、关闭IndexWriter

      writer.close();

      System.out.println("删除成功");

    }

    5.2.3 清空索引库

     1 @Test
     2 
     3 public void deleteIndex() throws Exception {
     4 
     5   // 1、指定索引库目录
     6 
     7   Directory directory = FSDirectory.open(new File("F:\lucene\0719"));
     8 
     9   // 2、创建IndexWriterConfig
    10 
    11   IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST,
    12 
    13   new StandardAnalyzer());
    14 
    15   // 3、 创建IndexWriter
    16 
    17   IndexWriter writer = new IndexWriter(directory, cfg);
    18 
    19   // 4、通过IndexWriter来删除索引
    20 
    21   // 删除指定索引
    22 
    23   writer.deleteAll();
    24 
    25   // 5、关闭IndexWriter
    26 
    27   writer.close();
    28 
    29   System.out.println("清空索引库成功");
    30 
    31 }

    @Test

    public void deleteIndex() throws Exception {

      // 1、指定索引库目录

      Directory directory = FSDirectory.open(new File("F:\lucene\0719"));

      // 2、创建IndexWriterConfig

      IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST,

      new StandardAnalyzer());

      // 3、 创建IndexWriter

      IndexWriter writer = new IndexWriter(directory, cfg);

      // 4、通过IndexWriter来删除索引

      // 删除指定索引

      writer.deleteAll();

      // 5、关闭IndexWriter

      writer.close();

      System.out.println("清空索引库成功");

    }


    5.3 更新索引(文档)

    5.3.1 说明

    Lucene更新索引比较特殊,是先删除满足条件的索引,再添加新的索引。

    5.3.2 代码实现

     1 // 修改索引
     2 
     3 @Test
     4 
     5 public void updateIndex() throws Exception {
     6 
     7   // 1、指定索引库目录
     8 
     9   Directory directory = FSDirectory.open(new File("F:\lucene\0719"));
    10 
    11   // 2、创建IndexWriterConfig
    12 
    13   IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST,
    14 
    15   new StandardAnalyzer());
    16 
    17   // 3、 创建IndexWriter
    18 
    19   IndexWriter writer = new IndexWriter(directory, cfg);
    20 
    21   // 4、通过IndexWriter来修改索引
    22 
    23   // a)、创建修改后的文档对象
    24 
    25   Document document = new Document();
    26 
    27   // 文件名称
    28 
    29   Field filenameField = new StringField("name", "updateIndex", Store.YES);
    30 
    31   document.add(filenameField);
    32 
    33   // 修改指定索引为新的索引
    34 
    35   writer.updateDocument(new Term("name", "apache"), document);
    36 
    37   // 5、关闭IndexWriter
    38 
    39   writer.close();
    40 
    41   System.out.println("更新成功");
    42 
    43 }

    // 修改索引

    @Test

    public void updateIndex() throws Exception {

      // 1、指定索引库目录

      Directory directory = FSDirectory.open(new File("F:\lucene\0719"));

      // 2、创建IndexWriterConfig

      IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST,

      new StandardAnalyzer());

      // 3、 创建IndexWriter

      IndexWriter writer = new IndexWriter(directory, cfg);

      // 4、通过IndexWriter来修改索引

      // a)、创建修改后的文档对象

      Document document = new Document();

      // 文件名称

      Field filenameField = new StringField("name", "updateIndex", Store.YES);

      document.add(filenameField);

      // 修改指定索引为新的索引

      writer.updateDocument(new Term("name", "apache"), document);

      // 5、关闭IndexWriter

      writer.close();

      System.out.println("更新成功");

    }

    6 搜索

    问题:我们在入门示例中,已经知道Lucene是通过IndexSearcher对象,来执行搜索的。那我们为什么还要继续学习Lucene的查询呢?

    答:因为在实际的开发中,我们的查询的业务是相对复杂的,比如我们在通过关键词查找的时候,往往进行价格、商品类别的过滤。

    而Lucene提供了一套查询方案,供我们实现复杂的查询。

    6.1 创建查询的两种方法

    执行查询之前,必须创建一个查询Query查询对象。

    Query自身是一个抽象类,不能实例化,必须通过其它的方式来实现初始化。

    在这里,Lucene提供了两种初始化Query查询对象的方式。

    6.1.1 使用Lucene提供Query子类

    Query是一个抽象类,lucene提供了很多查询对象,比如TermQuery项精确查询,NumericRangeQuery数字范围查询等。

    使用TermQuery实例化

    Query query = new TermQuery(new Term("name", "lucene"));

    6.1.2 使用QueryParse解析查询表达式

    QueryParser会将用户输入的查询表达式解析成Query对象实例。如下代码:

    QueryParser queryParser = new QueryParser("name", new IKAnalyzer());

    Query query = queryParser.parse("name:lucene");

    6.2 常用的Query子类搜索

    6.2.1 TermQuery

    特点:查询的关键词不会再做分词处理,作为整体来搜索。代码如下:

      1 /**
      2 
      3 * Query子类查询之 TermQuery
      4 
      5 * 
      6 
      7 * 特点:不会再对查询的关键词做分词处理。
      8 
      9 *
     10 
     11 * 需要:查询书名与java教程相关书。
     12 
     13 */
     14 
     15 @Test
     16 
     17 public void queryByTermQuery(){
     18 
     19   //1、获取一个查询对象
     20 
     21   Query query = new TermQuery(new Term("name", "编程思想"));
     22 
     23   doSearch(query);
     24 
     25 }
     26 
     27 private void doSearch(Query query) {
     28 
     29   try {
     30 
     31     //2、创建一个查询的执行对象
     32 
     33     //指定索引库的目录
     34 
     35     Directory d = FSDirectory.open(new File("F:\lucene\0719"));
     36 
     37     //创建流对象
     38 
     39     IndexReader reader = DirectoryReader.open(d);
     40 
     41     //创建搜索执行对象
     42 
     43     IndexSearcher searcher = new IndexSearcher(reader);
     44 
     45     //3、执行搜索
     46 
     47     TopDocs result = searcher.search(query, 10);
     48 
     49     //4、提出结果集,获取图书的信息
     50 
     51     int totalHits = result.totalHits;
     52 
     53     System.out.println("共查询到"+totalHits+"条满足条件的数据!");
     54 
     55     System.out.println("-----------------------------------------");
     56 
     57     //提取图书信息。
     58 
     59     //score即相关度。即搜索的关键词和 图书名称的相关度,用来做排序处理
     60 
     61     ScoreDoc[] scoreDocs = result.scoreDocs;
     62 
     63     for (ScoreDoc scoreDoc : scoreDocs) {
     64 
     65       /**
     66 
     67       * scoreDoc.doc的返回值,是文档的id, 即 将文档写入索引库的时候,lucene自动给这份文档做的一个编号。
     68 
     69       *
     70 
     71       * 获取到这个文档id之后,即可以根据这个id,找到这份文档。
     72 
     73       */
     74 
     75       int docId = scoreDoc.doc;
     76 
     77       System.out.println("文档在索引库中的编号:"+docId);
     78 
     79       //从文档中提取图书的信息
     80 
     81       Document doc = searcher.doc(docId);
     82 
     83       System.out.println("图书id:"+doc.get("id"));
     84 
     85       System.out.println("图书name:"+doc.get("name"));
     86 
     87       System.out.println("图书price:"+doc.get("price"));
     88 
     89       System.out.println("图书pic:"+doc.get("pic"));
     90 
     91       System.out.println("图书description:"+doc.get("description"));
     92 
     93       System.out.println();
     94 
     95       System.out.println("------------------------------------");
     96 
     97     }
     98 
     99     //关闭连接,释放资源
    100 
    101     if(null!=reader){
    102 
    103       reader.close();
    104 
    105     }
    106 
    107   } catch (Exception e) {
    108 
    109     e.printStackTrace();
    110 
    111   }
    112 
    113 }

    /**

    * Query子类查询之 TermQuery

    * 特点:不会再对查询的关键词做分词处理。

    *

    * 需要:查询书名与java教程相关书。

    */

    @Test

    public void queryByTermQuery(){

      //1、获取一个查询对象

      Query query = new TermQuery(new Term("name", "编程思想"));

      doSearch(query);

    }

    private void doSearch(Query query) {

      try {

        //2、创建一个查询的执行对象

        //指定索引库的目录

        Directory d = FSDirectory.open(new File("F:\lucene\0719"));

        //创建流对象

        IndexReader reader = DirectoryReader.open(d);

        //创建搜索执行对象

        IndexSearcher searcher = new IndexSearcher(reader);

        //3、执行搜索

        TopDocs result = searcher.search(query, 10);

        //4、提出结果集,获取图书的信息

        int totalHits = result.totalHits;

        System.out.println("共查询到"+totalHits+"条满足条件的数据!");

        System.out.println("-----------------------------------------");

        //提取图书信息。

        //score即相关度。即搜索的关键词和 图书名称的相关度,用来做排序处理

        ScoreDoc[] scoreDocs = result.scoreDocs;

        for (ScoreDoc scoreDoc : scoreDocs) {

          /**

          * scoreDoc.doc的返回值,是文档的id, 即 将文档写入索引库的时候,lucene自动给这份文档做的一个编号。

          *

          * 获取到这个文档id之后,即可以根据这个id,找到这份文档。

          */

          int docId = scoreDoc.doc;

          System.out.println("文档在索引库中的编号:"+docId);

          //从文档中提取图书的信息

          Document doc = searcher.doc(docId);

          System.out.println("图书id:"+doc.get("id"));

          System.out.println("图书name:"+doc.get("name"));

          System.out.println("图书price:"+doc.get("price"));

          System.out.println("图书pic:"+doc.get("pic"));

          System.out.println("图书description:"+doc.get("description"));

          System.out.println();

          System.out.println("------------------------------------");

        }

        //关闭连接,释放资源

        if(null!=reader){

          reader.close();

        }

      } catch (Exception e) {

        e.printStackTrace();

      }

    }

    6.2.2 NumericRangeQuery

    指定数字范围查询.(创建field类型时,注意与之对应)

     1 /**
     2 
     3 * Query子类查询  之  NumricRangeQuery
     4 
     5 * 需求:查询所有价格在[60,80)之间的书
     6 
     7 * @param query
     8 
     9 */
    10 
    11 @Test
    12 
    13 public void queryByNumricRangeQuery(){
    14 
    15   /**
    16 
    17   * 第一个参数:要搜索的域
    18 
    19   * 第二个参数:最小值
    20 
    21   * 第三个参数:最大值
    22 
    23   * 第四个参数:是否包含最小值
    24 
    25   * 第五个参数:是否包含最大值
    26 
    27   */
    28 
    29   Query query = NumericRangeQuery.newFloatRange("price", 60.0f, 80.0f, true, false);
    30 
    31   doSearch(query);
    32 
    33 }

    /**

    * Query子类查询  之  NumricRangeQuery

    * 需求:查询所有价格在[60,80)之间的书

    * @param query

    */

    @Test

    public void queryByNumricRangeQuery(){

      /**

      * 第一个参数:要搜索的域

      * 第二个参数:最小值

      * 第三个参数:最大值

      * 第四个参数:是否包含最小值

      * 第五个参数:是否包含最大值

      */

      Query query = NumericRangeQuery.newFloatRange("price", 60.0f, 80.0f, true, false);

      doSearch(query);

    }

    6.2.3 BooleanQuery

    BooleanQuery,布尔查询,实现组合条件查询。

     1 /**
     2 
     3      * Query子类查询  之  BooelanQuery查询   组合条件查询
     4 
     5      *
     6 
     7      * 需求:查询书名包含java,并且价格区间在[60,80)之间的书。
     8 
     9 */
    10 
    11 @Test
    12 
    13 public void queryBooleanQuery(){
    14 
    15   //1、要使用BooelanQuery查询,首先要把单个创建出来,然后再通过BooelanQuery组合
    16 
    17   Query price = NumericRangeQuery.newFloatRange("price", 60.0f, 80.0f, true, false);
    18 
    19   Query name = new TermQuery(new Term("name", "java"));
    20 
    21   //2、创建BooleanQuery实例对象
    22 
    23   BooleanQuery query = new BooleanQuery();
    24 
    25   query.add(name, Occur.MUST_NOT);
    26 
    27   query.add(price, Occur.MUST);
    28 
    29   /**
    30 
    31   * MSUT  表示必须满足                          对应的是  +
    32 
    33   * MSUT_NOT  必须不满足                   应对的是  -
    34 
    35   * SHOULD  可以满足也可以不满足     没有符号
    36 
    37   *
    38 
    39   * SHOULD 与MUST、MUST_NOT组合的时候,SHOULD就没有意义了。
    40 
    41   */
    42 
    43   doSearch(query);
    44 
    45 }

    /**

         * Query子类查询  之  BooelanQuery查询   组合条件查询

         *

         * 需求:查询书名包含java,并且价格区间在[60,80)之间的书。

    */

    @Test

    public void queryBooleanQuery(){

      //1、要使用BooelanQuery查询,首先要把单个创建出来,然后再通过BooelanQuery组合

      Query price = NumericRangeQuery.newFloatRange("price", 60.0f, 80.0f, true, false);

      Query name = new TermQuery(new Term("name", "java"));

      //2、创建BooleanQuery实例对象

      BooleanQuery query = new BooleanQuery();

      query.add(name, Occur.MUST_NOT);

      query.add(price, Occur.MUST);

      /**

      * MSUT  表示必须满足                          对应的是  +

      * MSUT_NOT  必须不满足                   应对的是  -

      * SHOULD  可以满足也可以不满足     没有符号

      *

      * SHOULD 与MUST、MUST_NOT组合的时候,SHOULD就没有意义了。

      */

      doSearch(query);

    }

    6.3 通过QueryParser搜索

    6.3.1 特点

    对搜索的关键词,做分词处理。

    6.3.2 语法

    6.3.2.1 基础语法

    域名:关键字

    实例:name:java

    6.3.2.2 组合条件语法

    条件1 AND 条件2 

    条件1 OR 条件2

    条件1 NOT 条件2

    6.3.3 QueryParser

    6.3.3.1 代码实现

     1 /**
     2 
     3 * 查询解析器查询  之  QueryParser查询
     4 
     5 */
     6 
     7 @Test
     8 
     9 public void queryByQueryParser(){
    10 
    11   try {
    12 
    13     //1、加载分词器
    14 
    15     Analyzer analyzer = new StandardAnalyzer();
    16 
    17     /**
    18 
    19     * 2、创建查询解析器实例对象
    20 
    21     * 第一个参数:默认搜索的域。
    22 
    23     *          如果在搜索的时候,没有特别指定搜索的域,则按照默认的域进行搜索
    24 
    25     *          如何在搜索的时候指定搜索域呢?
    26 
    27     *          答:格式  域名:关键词        即   name:java教程
    28 
    29     *
    30 
    31     * 第二个参数:分词器   ,对关键词做分词处理
    32 
    33     */
    34 
    35     QueryParser parser = new QueryParser("description", analyzer);
    36 
    37     // 设置组合条件查询
    38 
    39     //      Query query = queryParser.parse("name:java教程 or description:apache");
    40 
    41     // Query query = queryParser.parse("name:java教程 or apache");
    42 
    43     Query query = parser.parse("name:java教程");
    44 
    45     doSearch(query);
    46 
    47   } catch (Exception e) {
    48 
    49     e.printStackTrace();
    50 
    51   }
    52 
    53 }

    /**

    * 查询解析器查询  之  QueryParser查询

    */

    @Test

    public void queryByQueryParser(){

      try {

        //1、加载分词器

        Analyzer analyzer = new StandardAnalyzer();

        /**

        * 2、创建查询解析器实例对象

        * 第一个参数:默认搜索的域。

        *          如果在搜索的时候,没有特别指定搜索的域,则按照默认的域进行搜索

        *          如何在搜索的时候指定搜索域呢?

        *          答:格式  域名:关键词        即   name:java教程

        *

        * 第二个参数:分词器   ,对关键词做分词处理

        */

        QueryParser parser = new QueryParser("description", analyzer);

        // 设置组合条件查询

        //      Query query = queryParser.parse("name:java教程 or description:apache");

        // Query query = queryParser.parse("name:java教程 or apache");

        Query query = parser.parse("name:java教程");

        doSearch(query);

      } catch (Exception e) {

        e.printStackTrace();

      }

    }

    6.3.4 MultiFieldQueryParser

    通过MulitFieldQueryParse对多个域查询。

     1 /**
     2 
     3 * 查询解析器查询  之  MultiFieldQueryParser查询
     4 
     5 * 
     6 
     7 *     特点:同时指定多个搜索域,并且对关键做分词处理
     8 
     9 */
    10 
    11 @Test
    12 
    13 public void queryByMultiFieldQueryParser(){
    14 
    15   try {
    16 
    17     //1、定义多个搜索的  name、description
    18 
    19     String[] fields = {"name","description"};
    20 
    21     //2、加载分词器
    22 
    23     Analyzer analyzer = new StandardAnalyzer();
    24 
    25     //3、创建 MultiFieldQueryParser实例对象
    26 
    27     MultiFieldQueryParser mParser = new MultiFieldQueryParser(fields, analyzer);
    28 
    29     Query query = mParser.parse("lucene教程");
    30 
    31     doSearch(query);
    32 
    33   } catch (Exception e) {
    34 
    35     e.printStackTrace();
    36 
    37   }
    38 
    39 }

    /**

    * 查询解析器查询  之  MultiFieldQueryParser查询

    *     特点:同时指定多个搜索域,并且对关键做分词处理

    */

    @Test

    public void queryByMultiFieldQueryParser(){

      try {

        //1、定义多个搜索的  name、description

        String[] fields = {"name","description"};

        //2、加载分词器

        Analyzer analyzer = new StandardAnalyzer();

        //3、创建 MultiFieldQueryParser实例对象

        MultiFieldQueryParser mParser = new MultiFieldQueryParser(fields, analyzer);

        Query query = mParser.parse("lucene教程");

        doSearch(query);

      } catch (Exception e) {

        e.printStackTrace();

      }

    }

  • 相关阅读:
    安卓获取双IMEI
    NodeJS异步、同步 创建多层文件夹
    Winfrom 控件名称缩写
    Unobtrusive Ajax
    ID 为 17608的进程当前未运行
    欢迎
    路由
    VS快捷键
    Test
    并查集与带权并查集---由浅入深
  • 原文地址:https://www.cnblogs.com/wozibi/p/11252289.html
Copyright © 2011-2022 走看看