Lucene (6) Query子类

1. TermQuery

对索引中特定项进行搜索, Term是最小的索引片段, 每个Term包含了一个域名和一个文本值.

Term t = new Term(“title”, “java”);

Query q = new TermQuery(t);

使用这个TermQuery对象进行搜索, 可以返回在title域包含单词java的所有文档

注意域值是区分大小写的

2. TermRangeQuery

索引中的各个Term对象会按照字典编排顺序(通过String.compareTo方法)

搜索时包含或不包含起始项和终止项, 如果这个项为空, 那么它对应的端就是无边界的

Query q = new TermRangeQuery(“title”, new BytesRef(“a”), new BytesRef(“z”), true, true);

末尾的两个布尔型参数表示 

includeLower – If true, the lowerTerm is included in the range.
includeUpper – If true, the upperTerm is included in the range.

也就是说在上面的例子中, 如果第三个参数为true, 则>=a, 如果为false则 >a, 第四个参数如果为true <=z , 如果为false 则 <z

3. NumericRangeQuery

doc.add(new IntField(“num”, num, Field.Store.YES));

Query q = NumericRangeQuery.newIntRange(“num”, 1, 2, true, true);

4. PrefixQuery

使用PrefixQuery可以搜索包含以指定字符串开头的项的文档

Query q = new PrefixQuery(new Term(“isbn”,”55″));

5. BooleanQuery

BooleanQuery可以将之前讨论的这些查询组合成复杂的查询

下面这个例子同时查询了title为L并且isbn以55开头的文档

String querystr = args.length > 0 ? args[0] : “L”;
Query q1 = new QueryParser(“title”, analyzer).parse(querystr);
Query q2 = new PrefixQuery(new Term(“isbn”,”55″));
BooleanQuery q = new BooleanQuery();
q.add(q1, Occur.MUST);
q.add(q2, Occur.MUST);

searcher.search(q, collector);

6. WildcardQuery

通配符查询可以让我们使用不完整的, 缺少某些字幕的项来进行查询, 但仍然可以找到相关的匹配结果

*代表0个或多个字母

?代表0个或者1个字母

Query q = new WildcardQuery(new Term(“isbn”,”5*5*”));

Lucene (5) 分页

package lzlucene;

import java.io.IOException;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;

public class HelloLucene4 {
    public static void main(String[] args) throws IOException, ParseException {
        StandardAnalyzer analyzer = new StandardAnalyzer();
        Directory index = new RAMDirectory();
        IndexWriterConfig config = new IndexWriterConfig(analyzer);
        IndexWriter w = new IndexWriter(index, config);
        addDoc(w, "Lu cene in Action", "193398817");
        addDoc(w, "Lu cene for Dummies", "55320055Z");
        addDoc(w, "Lu", "55063554A");
        addDoc(w, "Lu", "9900333X");
        addDoc(w, "Lu", "2134124C");
        w.close();
        
        Query q = new TermQuery(new Term("title", "lu"));
        IndexReader reader = DirectoryReader.open(index);
        IndexSearcher searcher = new IndexSearcher(reader);

        int pageIndex = 3;
        int pageSize = 2;
        
        ScoreDoc lastSd = getLastScoreDoc(pageIndex, pageSize, q, searcher);  
        TopDocs td = searcher.searchAfter(lastSd, q, pageSize);
        ScoreDoc[] hits = td.scoreDocs;

        System.out.println("Found " + hits.length + " hits.");
        for (int i = 0; i < hits.length; ++i) {
            int docId = hits[i].doc;
            Document d = searcher.doc(docId);
            System.out.println((i + 1) + ". " + d.get("isbn") + "\t" + d.get("title"));
        }

        reader.close();
    }

    private static void addDoc(IndexWriter w, String title, String isbn) throws IOException {
        Document doc = new Document();
        doc.add(new Field("title", title, Field.Store.YES, Field.Index.ANALYZED));

        // use a string field for isbn because we don't want it tokenized
        doc.add(new StringField("isbn", isbn, Field.Store.YES));
        w.addDocument(doc);
    }

    private static ScoreDoc getLastScoreDoc(int pageIndex, int pageSize, Query query, IndexSearcher searcher)
            throws IOException {
        if (pageIndex == 1)
            return null;// 如果是第一页就返回空
        int num = pageSize * (pageIndex - 1);// 获取上一页的最后数量
        TopDocs tds = searcher.search(query, num);
        return tds.scoreDocs[num - 1];
    }
}

Lucene (4) 搜索

当你查询Lucene索引时, 它将会返回一个包含有序的ScoreDoc对象数组的TopDocs对象.

在输入查询后, Lucene会为每个文档计算评分(用以表示相关性的数值)

IndexSearcher所有搜索都通过IndexSearcher进行, 它们会调用该类中重载的search方法
Query封装某种查询类型的具体子类. Query实例将被传递给IndexSearcher的search方法
QueryParser将用户输入的查询表达式处理成具体的Query对象
TopDocs保持由IndexSearcher.search()方法返回的具有较高评分的顶部文档
ScoreDoc提供对TopDocs中没跳搜索结果的访问接口
 // 3. search
    int hitsPerPage = 10;
    IndexReader reader = DirectoryReader.open(index);
    IndexSearcher searcher = new IndexSearcher(reader);
    TopScoreDocCollector collector = TopScoreDocCollector.create(hitsPerPage);
    searcher.search(q, collector);
    ScoreDoc[] hits = collector.topDocs().scoreDocs;
     
    // 4. display results
    System.out.println("Found " + hits.length + " hits.");
    for(int i=0;i<hits.length;++i) {
      int docId = hits[i].doc;
      Document d = searcher.doc(docId);
      System.out.println((i + 1) + ". " + d.get("isbn") + "\t" + d.get("title"));
    }
 
    // reader can only be closed when there
    // is no need to access the documents any more.
    reader.close();

Lucene的搜索方法需要一个Query对象作为参数.

我们可以通过QueryParser类对查询表达式进行解析, 它把诸如 +A +B -C 或者 A OR B 这样的多个查询条件解析成一个Query类

Query q = new QueryParser("title", analyzer).parse(querystr);

如果表达式解析不成功, Lucene会抛出一个ParseException异常.

A默认域包含A的文档
A BA OR B默认域包含A和B中的一个或两个的文档
+A +BA AND B默认域中同时包含A和B的文档
title:Atitle域中包含A项的文档
title:A -subject:Btitle:A AND NOT subject:Btitle域中包含A 并且 subject域中不包含B的文档
(A OR B) AND C默认域中包含C 并且包含 A和B中的其中一个或者两个的文档
title: “hello world”title域为hello world的文档
title: “hello world” ~5title域中hello和world之间距离小于5的文档
hello*包含由hello开头的文档, 包括hello本身
hello~包含与单词hello相近的文档,例如Jello
lastmodified:[1/1/2017 TO 12/31/2017]lastmodified域在2017年1月1日和2017年12月31日之间的文档

用Lucene进行搜索操作异常简单, 首先建立一个IndexSearcher实例, 它负责打开索引, 然后改实例的search方法即可进行搜索操作.

程序返回TopDocs对象表示顶部搜索结果

要创建IndexSearcher类的实例, 首先需要一个用于存储索引的目录

上一个例子我们使用了内存目录, 这次使用一个真实的磁盘路径

Directory dir = FSDirectory.open(Paths.get(“c:\\myindex”));

通过这个dir可以方便的创建index reader/writer

IndexWriter indexWriter = new IndexWriter(dir, indexWriterConfig);

IndexReader reader = DirectoryReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);

这里需要注意的是, IndexReader完成了诸如打开所有索引文件和提供底层reader API 等复杂的工作, 而 IndexSearcher 则简单的多.

由于打开一个IndexReader需要较大的系统开销, 因此最好在所有搜索期间都重复使用同一个IndexReader实例, 只有在必要的时候才建议打开新的IndexReader

在创建IndexReader时,它会搜索已有的索引快照. 如果你需要搜索索引中的变更信息, 那么必须打开新的reader.

IndexReader.reopen方法是一个获取新IndexReader的有效手段, 重启的IndexReader能在耗费较少系统资源的情况下使用当前reader来获取索引中的所有变更信息.

一旦获取了IndexSearcher实例, 就可以通过调用它的search方法来进行搜索了

search方法会快速完成大量工作. 它会访问所有候选的搜索匹配文档, 并只返回符合每个查询条件的结果.

最后它会收集最靠前的几个搜索结果并返回给调用程序.

TopDocs search(Query query, int n) 直接进行搜索. int n 参数表示返回的最高评分的文档数量
TopDocs search(Query query, Filter filter, int n)搜索受文档子集约束, 约束条件基于过滤策略
TopFieldDocs search(Query query, Filter filter, int n, Sort sort)搜索受文档子集约束, 约束条件基于过滤策略, 结果排序通过自定义的Sort完成
void search(Query query, Collector results)当使用自定义文档访问策略时使用, 或者不想以默认的前n个搜索结果培训策略收集结果时使用
void search(Query query, Filter filter, Collector results)同上. 区别在于结果文档只有在传入过滤策略时才能被接收

Topdocs.totalHits 属性会返回匹配文档数量. 在默认情况下, 匹配文档是按照评分降序排序的(DESC)

TopDocs.scoreDocs属性是一个数组, 它包含程序所要求的顶部匹配文档数量.

每个scoreDoc实例都有一个浮点类型的评分, 该评分是相关性评分, 该实例还包含一个整型数字用于标示文档ID, 能够用于检索文档中保存的域

ScoreDoc[] hits = collector.topDocs().scoreDocs;

for(int i=0;i<hits.length;++i) {
      int docId = hits[i].doc;
      Document d = searcher.doc(docId);
      System.out.println((i + 1) + ". " + d.get("filename") + "\t" + d.get("filepath"));
}
package lzlucene;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
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.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

public class HelloLucene2 {
  public static void main(String[] args) throws IOException, ParseException {
      
    // 1. create the index
    Directory dir = FSDirectory.open(Paths.get("c:\\myindex"));
    Analyzer analyzer = new StandardAnalyzer();
    IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
    IndexWriter indexWriter = new IndexWriter(dir, indexWriterConfig);
    indexWriter.deleteAll();
    
    File dFile = new File("C:/");
    File[] files = dFile.listFiles();
    for (File file : files) {
        Document document = new Document();
        document.add(new Field("filename", file.getName(), TextField.TYPE_STORED));
        document.add(new Field("filepath", file.getAbsolutePath(), TextField.TYPE_STORED));
        indexWriter.addDocument(document);

    }
 
    indexWriter.commit();
    indexWriter.close();
    
    // 2. query
    String querystr = args.length > 0 ? args[0] : "out.txt";
    Query q = new QueryParser("filename", analyzer).parse(querystr);
 
    // 3. search
    int hitsPerPage = 10;
    IndexReader reader = DirectoryReader.open(dir);
    IndexSearcher searcher = new IndexSearcher(reader);
    TopScoreDocCollector collector = TopScoreDocCollector.create(hitsPerPage);
    searcher.search(q, collector);
    ScoreDoc[] hits = collector.topDocs().scoreDocs;
     
    // 4. display results
    System.out.println("Found " + hits.length + " hits.");
    for(int i=0;i<hits.length;++i) {
      int docId = hits[i].doc;
      Document d = searcher.doc(docId);
      System.out.println((i + 1) + ". " + d.get("filename") + "\t" + d.get("filepath"));
    }
 
    reader.close();
  }

}

Lucene (3) 并发,线程安全和锁

任意数量的只读属性的IndexReader类都可以同时打开同一个索引.

无论这些Reader是否属于同一个JVM, 以及是否属于同一台计算机都无关紧要.

但需要记住, 在单个JVM内, 利用资源和发挥效率的最好办法是用线程共享单个的IndexReader实例

对于一个索引来说, 一次只能打开一个Writer. Lucene采用文件锁来提供保障. 一旦建立起IndexWriter对象, 系统会立刻分配一个锁给它.

该锁只有当IndexWriter对象被关闭时才会释放.

注意如果你使用IndexReader对象来改变索引的话, 比如修改norms或者删除文档, 这时IndexReader对象会作为Writer使用, 它必须在修改上述内容之前成功地获取Write锁, 并在被关闭时释放该锁  

当IndexWriter对象已经打开一个索引时, 你可以同时直接使用IndexReader打开同一个索引.

每个IndexReader对象会得到一个对应时间点的索引”快照”, IndexReader只有在IndexWriter提交修改 或者 IndexReader自己被重新打开后才能获知索引的修改情况

Lucene (2) 构建索引

如果想要搜索存储在硬盘上的文件,电子邮件,网页或是数据库中的数据, Lucene都可以做到

在开始搜索之前, 你必须对搜索内容进行索引.

一个文档Document对象包含若干个域Field, 每个Field都有一个标示名称, 该名称为一个文本值或者二进制值

Lucene可以对域进行3种操作

1 域值可以被索引

2 域被索引后, 还可以选择性的存储项向量

3 域值可以被单独存储

addDocument(Document) 使用默认分析器添加文档, 该分析器在创建IndexWriter对象时指定

删除索引中的文档

deleteDocuments(Term) 删除项的所有文档
deleteDocuments(Term[])删除包含项数组任一元素的所有文档
deleteDocuments(Query)删除匹配查询语句的所有文档
deleteDocuments(Query[])删除匹配查询语句数组任一元素的所有文档
deleteAll()负责删除索引中的所有文档. 该功能与关闭writer再用参数create=true重新打开writer等效

在所有情况下删除操作不会马上执行,而是放入内存缓冲区

与加入文档一样, 你必须调用writer的commit()或者close()方法向索引提交更改

更新文档

updateDocument(Term,Document)

分词配置

doc.add(new Field(“title”, title, Field.Store.YES, Field.Index.NO));

doc.add(new Field(“title”, title, Field.Store.YES, Field.Index.ANALYZED));

Field.Index.ANALYZED使用分析器将域值分解成独立的语汇单元流,并使每个语汇单元能被搜索.该选项适用于普通文本域(如正文,标题,摘要等)
Field.Index.NOT_ANALYZED对域进行索引,但不对String值进行分析. 该操作实际上将域值作为单一语汇单元并使之能被搜索.该选项适合用于索引那些不能被分解的域值. 如 URL,文件路径,日期,人名,社保号码和电话号码等.
Index.ANALYZED_NO_NORMS这是Index.ANALYZED选项的一个变体, 它不会在索引中存储norms信息. norms记录了索引中的index-time boost信息, 但是当你进行搜索时可能会比较耗费内存.
Index.NOT_ANALYZED_NO_NORMS与Index.NOT_ANALYZED选项类似, 但也是不存储norms.该选项常用于在搜索期间节省索引空间和减少内存耗费
Index.NO使对应的域值不被搜索

稍微举个例子

addDoc(w, “Lu cene in Action”, “193398817”); 

当使用Query q = new TermQuery(new Term(“title”,”lu”));来进行查询时

doc.add(new Field(“title”, title, Field.Store.YES,Field.Index.NOT_ANALYZED)); 这个得不到结果,因为Lu cene…被作为整词处理

doc.add(new Field(“title”, title, Field.Store.YES,Field.Index.ANALYZED)); 可以得到结果, Lu cene 和 in , action 会被切割索引

Field.Store.YES 

存储域值. 原始的字符串值全部被保存在索引中,并且可由IndexReader类恢复

Field.Store.No

不存储域值

对文档和域进行加权操作

文档加权操作, 默认情况下, 所有文档都没有加权值 或者说 他们都拥有同样的加权值1.0

通过改变文档的加权因子, 你就能指示Lucene在计算相关性时或多或少地考虑到该文档针对索引中其他文档的重要程度.

调用加权操作的API只包含一个方法 setBoost(float)

doc.setBoost(1.5F)

域的加权操作和对文档的加权操作一样

field.setBoost(1.5F)

Lucene (1)

在Lucene中, 要经过如下步骤才能获得检索结果

1. 创建分析器

StandardAnalyzer analyzer = new StandardAnalyzer();

2. 将原始内容转换成”文档”

Directory index = new RAMDirectory();

3. 向索引添加文档

IndexWriterConfig config = new IndexWriterConfig(analyzer);
IndexWriter w = new IndexWriter(index, config);
addDoc(w, "Lucene in Action", "193398817");
addDoc(w, "Lucene for Dummies", "55320055Z");
addDoc(w, "Managing Gigabytes", "55063554A");
addDoc(w, "The Art of Computer Science", "9900333X");
w.close();

4. 调用查询

String querystr = args.length &gt; 0 ? args[0] : "lucene";
Query q = new QueryParser("title", analyzer).parse(querystr);
IndexReader reader = DirectoryReader.open(index);
IndexSearcher searcher = new IndexSearcher(reader);
TopScoreDocCollector collector = TopScoreDocCollector.create(hitsPerPage);
searcher.search(q, collector);

5. 获得结果

ScoreDoc[] hits = collector.topDocs().scoreDocs;
package lzlucene;


import java.io.IOException;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;

public class HelloLucene {
  public static void main(String[] args) throws IOException, ParseException {
    // 0. Specify the analyzer for tokenizing text.
    //    The same analyzer should be used for indexing and searching
    StandardAnalyzer analyzer = new StandardAnalyzer();
 
    // 1. create the index
    Directory index = new RAMDirectory();
 
    IndexWriterConfig config = new IndexWriterConfig(analyzer);
 
    IndexWriter w = new IndexWriter(index, config);
    addDoc(w, "Lucene in Action", "193398817");
    addDoc(w, "Lucene for Dummies", "55320055Z");
    addDoc(w, "Managing Gigabytes", "55063554A");
    addDoc(w, "The Art of Computer Science", "9900333X");
    w.close();
 
    // 2. query
    String querystr = args.length > 0 ? args[0] : "lucene";
 
    // the "title" arg specifies the default field to use
    // when no field is explicitly specified in the query.
    Query q = new QueryParser("title", analyzer).parse(querystr);
 
    // 3. search
    int hitsPerPage = 10;
    IndexReader reader = DirectoryReader.open(index);
    IndexSearcher searcher = new IndexSearcher(reader);
    TopScoreDocCollector collector = TopScoreDocCollector.create(hitsPerPage);
    searcher.search(q, collector);
    ScoreDoc[] hits = collector.topDocs().scoreDocs;
     
    // 4. display results
    System.out.println("Found " + hits.length + " hits.");
    for(int i=0;i<hits.length;++i) {
      int docId = hits[i].doc;
      Document d = searcher.doc(docId);
      System.out.println((i + 1) + ". " + d.get("isbn") + "\t" + d.get("title"));
    }
 
    // reader can only be closed when there
    // is no need to access the documents any more.
    reader.close();
  }
 
  private static void addDoc(IndexWriter w, String title, String isbn) throws IOException {
    Document doc = new Document();
    doc.add(new TextField("title", title, Field.Store.YES));
 
    // use a string field for isbn because we don't want it tokenized
    doc.add(new StringField("isbn", isbn, Field.Store.YES));
    w.addDocument(doc);
  }
}

执行简单的索引过程需要用到以下几个类

1. IndexWriter

IndexWriter是索引过程的核心组件.

这个类负责创建新索引或者打开已有索引,以及向索引中添加,删除或是更新被索引文档的信息.

IndexWriter需要开辟一定空间来存储索引, 该功能可以由Directory完成.

2. Directory

Directory类描述Lucenen索引的存放位置.

3. Analyzer

文本文件在被索引之前,需要经过Analyzer(分析器)处理.

4. Document

Document对象代表一些字段(Field)的集合.

5. Field

搜索过程中需要以下核心类

1. IndexSearcher

IndexSearcher用于搜索由IndexWriter类创建的索引

2. Term

Term对象是搜索功能的基本单元.与Field对象类似, Term对象包含一对字符串元素: 域名和单词

以下两种写法在简单应用上是等价的

Query q = new QueryParser(“title”, analyzer).parse(querystr);
Query q = new TermQuery(new Term(“title”,”lucene”));

3. Query

Lucene含有许多具体的Query子类

比如上面的TermQuery

4. TopDocs

Topdocs

Topdocs类是一个简单的指针容器, 指针一般指向前N个排名的搜索结果