Lucene 基础知识

1. 数据分类

  • 结构化数据: 指具备固定格式或有限长度的数据,如数据库等;
  • 非结构化数据: 指不定长或无固定格式的数据, 如邮件,word 文档等磁盘上的文件;

1.1 非结构化数据查询方法

  1. 顺序扫描法(Serial Scanning)
  2. 全文检索(Full-text Search)
    • 将非结构化数据中的一部分信息提取出来,从新组织,使其变得有必定结构,而后对此有必定结构的数据进行搜索,
      从而达到搜索相对较快的目的; 这部分从非结构化数据中提取出,而后从新组织的信息,称之为索引, 例如字典.
    • 这种先创建索引,而后再对索引进行搜索的过程就叫全文检索;

2. Lucene 概述

  • Lucene 是 apache 下的一个开放源代码的全文检索引擎工具包,提供了完整的查询引擎和索引引擎,部分文本分析引擎;

2.1 Lucene 实现全文检索的流程

2.2 建立文档对象

  • 获取原始内容的目的是为了索引,在索引前,须要将原始内容建立成文档(Document),文档中包括一个一个的域(Field),
    域中存储内容;
  • 咱们能够将磁盘上的一个文件当成一个 document, Document 中包括一些Field(file_name 文件名称, file_path
    文件路径, file_size 文件大小, file_content 文件内容);
  • 每个 Document 能够有多个 Field,同一个Document,能够有相同的 Field(域名和域值都相同);
  • 每个 Document 都有一个惟一的编号,就是文档 id;

2.3 分析文档

  • 将原始内容建立为包含域(Field)的文档(document),须要再对域中的内容进行分析,分析的过程是通过对原始文档提取单词,
    将字母转为小写,去除标点符号,去除停用词等过程生成最终的语汇单元,能够将语汇单元理解为一个一个的单词;
  • 每个单词叫作一个Term,不一样的域中拆分出来的相同的单词是不一样的term; term中包含两部分,一部分是文档的域名, 另外一部分
    是单词的内容;
  • Field 域的属性
    • 是否分析: 是否对域的内容进行分词处理;
    • 是否索引: 将 Field 分析后的词或整个 Field 值进行索引,只有创建索引,才能搜索到;
    • 是否存储: 存储在文档中的 Field 才能够从 Document 中获取;

2.4 建立索引

  • 对全部文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到 Document;
    这种索引的结构叫倒排索引结构;
  • 传统方法是根据文件找到该文件的内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大,搜索慢;
  • 倒排索引结构是根据内容(词语)找文档; 顺序扫描方法是根据文档查找里面的内容;

// 建立索引库
// 环境: Lucene 4.10.3
// jar 包
    /*
     * lucene-core-4.10.3
     * lucene-analyzers-common-4.10.3
     * lucene-queryparser-4.10.3
     * commons-io
     * junit
     */

// 测试类
public class FirstLucene{

    // 建立索引
    @Test
    public void testIndex() throws Exception{
        // 1. 建立一个 indexWriter 对象 new IndexWriter(arg0, arg1);
        //     arg0: 指定索引库的存放位置(Directory 对象)
        //     arg1: config
        // FSDirectory: File System Directory : 磁盘存储
        // Directory directory = new RAMDirectory(); 保存索引到内存中
        Directory directory = FSDirectory.open(new File("/Users/用户名/Documents/dic"));

        // 指定一个分词器
        Analyzer analyzer = new StandardAnalyzer();
        IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3,analyzer);

        // 建立 indexWriter 对象
        IndexWriter indexWriter = new IndexWriter(directory, config);

        // 指定原始文件的目录
        File f = new File("/Users/用户名/Documents/searchsource");
        File[] listFiles = f.listFiles();

        for(File file : listFiles){
            // 建立文档对象
            Document document = new Document();

            // 文件名称
            String file_name = file.getName();
            Field fileNameField = new TextField("fileName",file_name,Store.YES);

            // 文件大小
            long file_size = FileUtils.sizeOf(file);
            Field fileSizeField = new LongField("fileSize",file_size, Store.YES);

            // 文件路径
            String file_path = file.getPath();
            Field filePathField = new StoredField("filePath",file_path);

            // 文件内容
            String file_content = FileUtils.readFileToString(file);
            Field fileContentField = new TextField("fileContent",file_content,Store.YES);

            document.add(fileNameField);
            document.add(fileSizeField);
            document.add(filePathField);
            document.add(fileContentField);

            // 使用indexWriter 对象将 document 对象写入索引库,此过程将 索引和document 对象写入索引库
            indexWriter.addDocument(document);
        }

        // 关闭 IndexWriter 对象
        indexWriter.close();
    }
}


// 查看分词完成后的文件: Luke
java -jar lukeall-4.10.3.jar

3. 查询索引

3.1 建立查询

  • 用户输入查询关键字执行搜索前,须要先建立一个查询对象,查询对象中能够指定查询要搜索的 Field 文档域,查询关键字等,
    查询对象会生成具体的查询语法;
  • 例如: fileName:lucene: 表示要搜索Field域的内容为"lucene"的文档;

3.2 执行查询

  • 根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所连接的文档链表;
  • 好比: fileName:lucene 的搜索过程: 在索引上查找域为 fileName, 而且关键字为Lucene的term, 并根据 term 找到
    文档 id 列表;

3.3 渲染结果

3.4 IndexSearcher 搜索方法

// 查询索引
/*
 * 步骤:
 *    1. 建立一个 Directory 对象,用于指定索引库存放的位置;
 *    2. 建立一个 indexReader 对象, 须要指定 Directory 对象, 用于读取索引库中的文件;
 *    3. 建立一个 indexSearcher 对象, 须要指定 indexReader 对象;
 *    4. 建立一个 TermQuery 对象,指定查询的域和查询的关键词
 *    5. 执行查询
 *    6. 返回查询结果,遍历查询结果并输出;
 *    7. 关闭 indexReader
 */

 public class IndexSearchTest{

    @Test
    public void testIndexSearch() throws Exception{

        Directory directory = FSDirectory.open(new File("/Users/用户名/Documents/dic"));

        IndexReader indexReader = DirectoryReader.open(directory);

        IndexSearcher indexSearcher = new IndexSearcher(indexReader);

        // 建立一个 TermQuery 对象,指定查询的域和查询的关键词
        Query query = new TermQuery(new Term("fileName","java"));
        // 执行查询
        TopDocs topDocs = indexSearcher.search(query,2);

        SocreDoc[] scoreDocs = topDocs.scoreDocs;
        for(ScoreDoc scoreDoc : scoreDocs){
            // 获取文档 id
            int docID = scoreDoc.doc;

            // 经过id,从索引中读取出对应的文档
            Document document = indexReader.document(docID);
            // 获取文件名称
            System.out.println(document.get("fileName"));
            // 获取文件内容
            System.out.println(document.get("fileContent"));
            // 文件路径
            System.out.println(document.get("filePath"));
            // 文件大小
            System.out.println(document.get("fileSize"));

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

        indexReader.close();
    }
}

4. 支持中文分词器(IKAnalyzer)

4.1 分词器(Analyzer)的执行过程

  • 从一个 Reader 字符流开始,建立一个基于 Reader 的 Tokenizer分词器,通过三个 TokenFilter,生成语汇单元 Tokens;
  • 若是要查看分词器的分词效果,只须要看Tokenstream中的内容就能够了,每一个分词器都有一个方法tokenStream,返回一个
    tokenStream 对象;

// 查看标准分词器的分词效果
public void testTokenStream() throws Exception {
        //建立一个标准分析器对象
        Analyzer analyzer = new StandardAnalyzer();
        //得到tokenStream对象
        //第一个参数:域名,能够随便给一个
        //第二个参数:要分析的文本内容
        TokenStream tokenStream = analyzer.tokenStream("test",
                                "The Spring Framework provides a comprehensive"
                                      +"programming and configuration model.");

        //添加一个引用,能够得到每一个关键词
        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
        //添加一个偏移量的引用,记录了关键词的开始位置以及结束位置
        OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
        //将指针调整到列表的头部
        tokenStream.reset();
        //遍历关键词列表,经过incrementToken方法判断列表是否结束
        while(tokenStream.incrementToken()) {
            //关键词的起始位置
            System.out.println("start->" + offsetAttribute.startOffset());
            //取关键词
            System.out.println(charTermAttribute);
            //结束位置
            System.out.println("end->" + offsetAttribute.endOffset());
        }
        tokenStream.close();
    }

5.索引库的维护

// 索引库维护: 就是索引的增删改查

public class LuceneManager{

    public IndexWriter getIndexWriter(){
        Directory directory = FSDirectory.open(new File("/Users/用户名/Documents/dic"));
        Analyzer analyzer = new StandardAnalyzer();
        IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);

        return new IndexWriter(directory,config);
    }

    // 全删除
    @Test
    public void testAllDelete() throws Exception{
        IndexWriter indexWriter = getIndexWriter();
        indexWriter.deleteAll();
        indexWriter.close();
    }

    // 根据条件删除
    @Test
    public void testDelete() throws Exception{
        IndexWriter indexWriter = getIndexWriter();
        Query query = new TermQuery(new Term("fileName","apache"));
        indexWriter.deleteDocuments(query);
        indexWriter.close();
    }

    // 修改
    @Test
    public void testUpdate() throws Exception{
        IndexWriter indexWriter = getIndexWriter();
        Document doc = new Document();
        doc.add(new TextField("fileN","测试文件名",Store.YES));
        doc.add(new TextField("fileC","测试文件内容",Store.YES));

        // 将 lucene 删除,而后添加 doc
        indexWriter.updateDocument(new Term("fileName","lucene"),doc, new IKAnalyzer());
        indexWriter.close();
    }
}

6. 索引库查询

  1. 对要搜索的信息建立 Query 查询对象,Lucene会根据 Query 查询对象生成最终的查询语法;
  2. 可经过两种方法建立查询对象:
    • 使用 Lucene 提供的 Query子类;
    • 使用 QueryParse 解析查询表达式, 须要加入lucene-queryparser-4.10.3.jar
public class LuceneManager{

    // 获取 IndexSearcher
    public IndexSearcher getIndexSearcher() throws Exception{
        Directory directory = FSDirectory.open(new File("/Users/用户名/Documents/dic"));
        IndexReader indexReader = DirectoryReader.open(directory);

        return new IndexSearcher(indexReader);
    }

    // 获取执行结果
    public void printResult(IndexSearcher indexSearcher, Query query) throws Exception{
        TopDocs topDocs = indexSearcher.search(query,10);

        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for(ScoreDoc scoreDoc : scoreDocs){
            int doc = scoreDoc.doc;
            Document document = indexSearcher.doc(doc);

            String fileName = docment.get("fileName");
            System.out.println(fileName);

            String fileContent = document.get("fileContent");
            System.out.println(fileContent);

            String fileSize = document.get("fileSize");
            System.out.println(fileSize);

            String filePath = document.get("filePath");
            System.out.println(filePath);

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

    // 查询全部
    @Test
    public void testMatchAllDocsQuery() throws Exception{
        IndexSearcher indexSearcher = getIndexSearcher();

        Query query = new MatchAllDocsQUery();

        printResult(indexSearcher,query);
        // 关闭资源
        indexSearcher.getIndexReader().close();
    }

    // 精准查询(TermQuery)


    // NumericRangeQuery(按数值范围查询)
    @Test
    public void testNumericRangeQuery() throws Exception{
        IndexSearcher indexSearcher = getIndexSearcher();

        /*
         * 建立查询
         *     参数: 域名, 最小值, 最大值, 是否包含最小值, 是否包含最大值
         */
         Query query = NumericRangeQuery.newLongRange("fileSize",100L,200L,true,true);

         // 执行查询
         printResult(query,indexSearcher);
    }

    // BooleanQuery(组合查询)
    public void testBooleanQuery() throws Exception{
        IndexSearcher indexSearcher = getIndexSearcher();

        BooleanQuery booleanQuery = new BooleanQuery();

        Query query1 = new TermQuery(new Term("fileName","apache"));
        Query query2 = new TermQuery(new Term("fileName","lucene"));

        // Occur.MUST: 必须知足此条件, 至关于 and
        // Occur.SHOULD: 应该知足此条件, 可是不知足也能够, 至关于 or
        // Occur.MUST_NOT: 必须不知足, 至关于 not
        booleanQuery.add(query1,Occur.SHOULD);
        booleanQuery.add(query2,Occur.SHOULD);

        printResult(indexSearcher,booleanQuery);
        // 关闭资源
        indexSearcher.getIndexReader().close();
    }


    // 使用 QueryParse 解析查询表达式
    @Test
    public void testQueryParser() throws Exception{
        IndexSearcher indexSearcher = getIndexSearcher();

        // 建立 QueryParser 对象, 其中 arg0: 表示默认查询域, arg1: 分词器
        QueryParser queryParser = new QueryParser("fileName",new IKAnalyzer());

        // 此时,表示使用默认域: fileName
        // Query query = queryParser.parse("apache");
        // 表示查询 fileContent 域
        Query query = queryParser.parse("fileContent:apache");

        printResult(indexSearcher, query);
        // 关闭资源
        indexSearcher.getIndexReader().close();
    }

    // 指定多个默认搜索域
    @Test
    public void testMultiFieldQueryParser() throws Exception{
        IndexSearcher indexSearcher = getIndexSearcher();
        // 指定多个默认搜索域
        String[] fields = {"fileName", "fileContent"};

        // 建立 MultiFiledQueryParser 对象
        MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fields, new IKAnalyzer());
        Query query = queryParser.parse("apache");

        // 输出查询条件
        System.out.println(query);

        // 执行查询
        printResult(indexSearcher, query);
        // 关闭资源
        indexSearcher.getIndexReader().close();
    }
}
相关文章
相关标签/搜索