Lucene4.X 高级应用

jopen 9年前

Lucene 简介以及使用

Lucene,一个基于 Java 的开源的全文搜索工具包,可以方便的嵌入到各种应用系统中,实现针对应用的全文索引以及检索功能。目前是 Apache  jakarta 项目组的一个子项目,它的目的是为程序员提供工具包,让程序员利用工具包里的强大接口来完成全文检索。下面我们将以 Lucene4.7 版本为例,为您详细讲解索引的创建、创建时的参数配置、Lucene4.7 版本的各种 query 查询、Lucene 神器 Luke 的使用等内容。

准备工作

本文需要的 jar 包:

lucene-analyzers-common-4.7.0.jar

lucene-core-4.7.0.jar

lucene-queryparser-4.7.0.jar

Lucene 相关问题可以参考 Lucene 的官方 API

Lucene 常用包 官方网站 下载。

重要关键字介绍

IndexWriter:用于处理索引,如增加、更改或者删除索引。

FSDirectory:索引目录,除此之外还有一个 Ramdirectry。FSDirectory 是将索引创建到磁盘里,而 Ramdirectry 是将索引创建到内存里。

IndexWriterConfig:这里可配置版本号,分词器,打开模式等等,合理的应用该对象属性可以大大提高创建索引的性能。

Document:文档,我们将每个字段放在 document 里。

Field :域,类似数据库中的 column。

回页首

Lucene 实战

创建索引

首先我们介绍下如何创建索引。相关步骤分为:建立索引器 IndexWriter,建立文档对象 Document,建立信息字段对象 Field,将 Field 添加到 Document,将 Document 添加到 IndexWriter 里面,最后不要忘记关闭 IndexWriter。

清单 1. 建立索引

package lucene;  ……  public class IndexUtil {  private String[] idArr = {"1","2","3","4","5","6"};  private String[] emailArr = {"abc@us.ibm.com","ert@cn.ibm.com","lucy@us.ibm.com",  "rock@cn.ibm.com","test@126.com","deploy@163.com"};  private String[] contentArr = {  "welcome to Lucene,I am abc","This is ert,I am from China",  "I'm Lucy,I am english","I work in IBM",  "I am a tester","I like Lucene in action"  };    private String[] nameArr = {"abc","ert","lucy","rock","test","deploy"};  private Directory directory = null;  public void index() {  IndexWriter writer = null;  try {  directory = FSDirectory.open(new File("C:/lucene/index02"));  IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_47,  new StandardAnalyzer(Version.LUCENE_47));  conf.setOpenMode(OpenMode.CREATE_OR_APPEND);  LogMergePolicy mergePolicy = new LogDocMergePolicy();  mergePolicy.setMergeFactor(10);  mergePolicy.setMaxMergeDocs(10);  conf.setMaxBufferedDocs(10);  writer = new IndexWriter(directory, conf);  Document doc = null;  int date = 1;  for(int i=0;i<idArr.length;i++) {  doc = new Document();  doc.add(new StringField("id",idArr[i],Field.Store.YES));  doc.add(new StringField("email",emailArr[i],Field.Store.YES));  doc.add(new StringField("content",contentArr[i],Field.Store.YES));  doc.add(new StringField("name",nameArr[i],Field.Store.YES));  doc.add(new StringField("date","2014120"+date+“222222”,Field.Store.YES));  writer.addDocument(doc);  date++;  }    //新的版本对 Field 进行了更改,StringField 索引但是不分词、StoreField 至存储不索引、TextField 索引并分词  } catch (CorruptIndexException e) {  e.printStackTrace();  } catch (LockObtainFailedException e) {  e.printStackTrace();  } catch (IOException e) {  e.printStackTrace();  } finally {  try {  if(writer!=null)writer.close();  } catch (CorruptIndexException e) {  e.printStackTrace();  } catch (IOException e) {  e.printStackTrace();  }  }  }    public static void main(String args[]){  IndexUtil indexUtil = new IndexUtil();  indexUtil.index();  }  }

参数解释:

SetMergeFactor(合并因子),是控制 segment 合并频率的,其决定了一个索引块中包括多少个文档,当硬盘上的索引块达到这个值时,将它们合并成一个较大的索引块。当 MergeFactor 值较大时,生成索引的速度较快。MergeFactor 的默认值是 10。

SetMaxMergeDocs 最大合并文档数,默认是 Integer.MAX_VALUE。设置 segment 最大合并文档 (Document) 数值较小越有利于追加索引的速度,值较大, 越适合批量建立索引和更快的搜索。

setMaxBufferedDocs 最大缓存文档数,是控制写入一个新的 segment 前内存中保存的 document 的数目,设置较大的数目可以加快建索引速度,默认为 10。

在创建创 IndexWriter 实例的时候应注意以下几个地方:

  1. 尽量保持 IndexWriter 在全局中只有一个实例,因为一个 directory 中只允许一个 IndexWriter 实例访问,如果两个或者两个以上的实例同时访问一个 directory 会出现 Lock obtain timed Out 异常, 在文件夹里会出现一个 write.lock 文件。
  2. 在配置 LogMergePolicy 的时候不要盲目的去设置,要根据物理机器的配置来进行次测试,来达到一个理想的配置。

查询索引

当我们创建好索引后,就可以利用 Lucene 进行索引查询,Lucene 提供了多个查询功能,下面我们进行简单介绍。

Query:一个查询的抽象类,有多个子类实现,TermQuery, BooleanQuery, PrefixQuery ,WildcardQuery 等。

Term:是搜索的基本单位,一个 Term 是由两个 String 的 field 组成。比如,Term("name",“rock”), 此时该语句是查询 name 为 rock 的条件。

IndexSearcher:当索引建立好后,用该对象进行查询。该对象只能以只读的方式打开索引,所以多个 IndexSearcher 对象可以查询一个索引目录。我们要注意一下这个现象。

在介绍几种查询方式之前,首先要初始化 directory:

directory = FSDirectory.open(new File("C:/lucene/index02 "));

其次获取 IndexSearcher:

public IndexSearcher getSearcher() {    IndexReader reader = null;    try {  reader = DirectoryReader.open(directory);      IndexSearcher searcher = new IndexSearcher(reader);      return searcher;    } catch (IOException e) {  e.printStackTrace();    }    return null;    }

清单 2. 使用 TermQuery 搜索

public void searchByTerm(String field, String name, int num) {    try {      IndexSearcher searcher = getSearcher();      Query query = new TermQuery(new Term(field, name));      TopDocs tds = searcher.search(query, num);  System.out.println("count:" + tds.totalHits);      for (ScoreDoc sd : tds.scoreDocs) {        Document doc = searcher.doc(sd.doc);        System.out.println("docId:"+doc.get("id"));        System.out.println("name:"+doc.get("name"));        System.out.println("email:"+doc.get("email"));        System.out.println("date:"+doc.get("date"));      }      } catch (CorruptIndexException e) {        e.printStackTrace();      } catch (IOException e) {        e.printStackTrace();      }    }

结果:

count:1

docId:4

name:rock

email:rock@cn.ibm.com

date:2014-12-4

说明:

TermQuery 是 Lucene 查询中最基本的一种查询,它只能针对一个字段进行查询。

清单 3. 范围查询 RangeQuery (搜索指定范围的数据)

public void searchByTermRange(String field,String start,String end,int num) {  try {  IndexSearcher searcher = getSearcher();  BytesRef lowerTerm = new BytesRef(start);  BytesRef upperTerm = new BytesRef(end);  Query query = new TermRangeQuery(field,lowerTerm,upperTerm,true, true);  TopDocs tds = searcher.search(query, num);  System.out.println("count:"+tds.totalHits);  for(ScoreDoc sd:tds.scoreDocs) {  Document doc = searcher.doc(sd.doc);  System.out.println("docId:"+doc.get("id"));  System.out.println("name:"+doc.get("name"));  System.out.println("email:"+doc.get("email"));  System.out.println("date:"+doc.get("date"));  }  } catch (CorruptIndexException e) {  e.printStackTrace();  } catch (IOException e) {  e.printStackTrace();  }  }

结果:

count:3

docId:1

name:abc

email:abc@us.ibm.com

date:20141201222222

docId:2

name:ert

email:ert@cn.ibm.com

date:20141202222222

docId:3

name:lucy

email:lucy@us.ibm.com

date:20141203222222

说明:

TermRangeQuery query=new TermRangeQuery(字段名, 起始值, 终止值, 起始值是否包含边界, 终止值是否包含边界)。

清单 4.PrefixQuery 前缀查询

//查询以 ro 开头的 name  public void searchByPrefix(String field, String value, int num) {    try {      IndexSearcher searcher = getSearcher();      Query query = new PrefixQuery(new Term(field, value));      TopDocs tds = searcher.search(query, num);  System.out.println("count:" + tds.totalHits);      for (ScoreDoc sd : tds.scoreDocs) {        Document doc = searcher.doc(sd.doc);  System.out.println("docId:"+doc.get("id"));  System.out.println("name:"+doc.get("name"));  System.out.println("email:"+doc.get("email"));  System.out.println("date:"+doc.get("date"));      }  } catch (CorruptIndexException e) {        e.printStackTrace();      } catch (IOException e) {        e.printStackTrace();      }    }

结果:

count:1

docId:4

name:rock

email:rock@cn.ibm.com

date:20141204222222

说明:

前缀查询, 搜索匹配开始位置的数据类似百度的输入框。

清单 5.WildcardQuery 通配符查询

//查询 email 是 test 的  public void searchByWildcard(String field, String value, int num) {      try {        IndexSearcher searcher = getSearcher();        Query query = new WildcardQuery(new Term(field, value));        TopDocs tds = searcher.search(query, num);  System.out.println("count" + tds.totalHits);        for (ScoreDoc sd : tds.scoreDocs) {          Document doc = searcher.doc(sd.doc);  System.out.println("docId:"+doc.get("id"));  System.out.println("name:"+doc.get("name"));  System.out.println("email:"+doc.get("email"));  System.out.println("date:"+doc.get("date"));        }      } catch (CorruptIndexException e) {        e.printStackTrace();      } catch (IOException e) {        e.printStackTrace();      }    }

结果:

count1

docId:5

name:test

email:test@126.com

date:20141205222222

说明:

通配符分为两种,“*”和“?”,“*”表示任何字符,“?”表示任意一个字符。

Term term=new Term(字段名, 搜索关键字+通配符)。

清单 6.FuzzyQuery 模糊搜索

public void searchByFuzzy(int num) {    try {      IndexSearcher searcher = getSearcher();      FuzzyQuery query = new FuzzyQuery(new Term("name","acc"),1,1);      //System.out.println(query.getPrefixLength());  TopDocs tds = searcher.search(query, num);  System.out.println("count:"+tds.totalHits);      for(ScoreDoc sd:tds.scoreDocs) {        Document doc = searcher.doc(sd.doc);  System.out.println("docId:"+doc.get("id"));  System.out.println("name:"+doc.get("name"));  System.out.println("email:"+doc.get("email"));  System.out.println("date:"+doc.get("date"));      }      } catch (CorruptIndexException e) {        e.printStackTrace();      } catch (IOException e) {        e.printStackTrace();      }    }

结果:

count:1

docId:1

name:abc

email:abc@us.ibm.com

date:20141201222222

说明:

FuzzyQuery(new Term("name","acc"),1,1),需要 3 个参数,第一个参数是词条对象,第二个参数是 levenshtein 算法的最小相似度,第三个参数是指与多少个前缀字符匹配。

清单 7.BooleanQuery 查询

public void searchByBoolean(int num) {    try {  IndexSearcher searcher = getSearcher();      BooleanQuery query = new BooleanQuery();    query.add(new TermQuery(new Term("name", "abc")),BooleanClause.Occur.SHOULD);   query.add(new TermQuery(new Term("email","lucy@us.ibm.com")), BooleanClause.Occur.SHOULD);      TopDocs tds = searcher.search(query, num);      System.out.println("count" + tds.totalHits);      for (ScoreDoc sd : tds.scoreDocs) {        Document doc = searcher.doc(sd.doc);        System.out.println("docId:"+doc.get("id"));  System.out.println("name:"+doc.get("name"));        System.out.println("email:"+doc.get("email"));        System.out.println("date:"+doc.get("date"));      }      } catch (CorruptIndexException e) {        e.printStackTrace();      } catch (IOException e) {        e.printStackTrace();      }  }

结果:

count2

docId:1

name:abc

email:abc@us.ibm.com

date:20141201222222

docId:3

name:lucy

email:lucy@us.ibm.com

date:20141203222222

说明:

BooleanQuery,也就是组合查询,允许进行逻辑 AND、OR 或 NOT 的组合,通过 BooleanQuery 的 add 方法将一个查询子句增加到某个 BooleanQuery 对象中。

BooleanClause.Occur.MUST:必须包含,相当于逻辑运算的与

BooleanClause.Occur.MUST_NOT:必须不包含,相当于逻辑运算的非

BooleanClause.Occur.SHOULD:可以包含,相当于逻辑运算的或

清单 8. 分页查询

private static void testPageSearch1(int currentPage) {    int PAGE_SIZE = 10;    IndexReader reader = null;    try {    reader = DirectoryReader.open(FSDirectory.open(new File("")));    IndexSearcher searcher = new IndexSearcher(reader);    Query query = new TermQuery(new Term("name", "rock"));    TopDocs topDocs = searcher.search(query, currentPage * PAGE_SIZE);    ScoreDoc[] hits = topDocs.scoreDocs;    int endNuM = Math.min(topDocs.totalHits, currentPage * PAGE_SIZE);      for (int i = (currentPage - 1) * PAGE_SIZE; i < endNuM; i++) {        Document doc = searcher.doc(hits[i].doc);        System.out.print(doc.get("USERNAME"));      }    } catch (IOException e) {      e.printStackTrace();    }  }  //在 Lucene 的 3.5 以后的版本,Lucene 的 API 里提供了一个分页方法 searchafter。  private 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];    }    public void searchPageByAfter(String query, int pageIndex, int pageSize) {    try {      IndexSearcher searcher = getSearcher();      QueryParser parser = new QueryParser(Version.LUCENE_47,       "content",new StandardAnalyzer(Version.LUCENE_47));      Query q = parser.parse(query);      ScoreDoc lastSd = getLastScoreDoc(pageIndex, pageSize, q, searcher);      TopDocs tds = searcher.searchAfter(lastSd, q, pageSize);      for (ScoreDoc sd : tds.scoreDocs) {        Document doc = searcher.doc(sd.doc);        System.out.println("docId:"+doc.get("id") + ",name:" +        doc.get("name")+",email:"+ doc.get("email") );      }      } catch (IOException e) {        e.printStackTrace();      } catch (org.apache.lucene.queryparser.classic.ParseException e) {        e.printStackTrace();      }    }

排序

Lucene 除了提供大量的查询功能外,还提供了一个可改变查询结果顺序的类 Sort,用户可根据自己的需求进行 Sort 排序设置。

Sort sort = new Sort();  ortField sf=new SortField("name",Type.STRING_VAL, false);

以上语句表示根据 name 进行排序,false 代表升序,如果是 true 代表降序,可以有多个 SortField,利用 Sort 的 sort.setSort(sf,sf1...) 将每个 SortField 添加到 sort 中,最后返回按 sort 进行排序的搜索结果。

TopDocs topDocs = searcher.search(query, currentPage * PAGE_SIZE,sort);

回页首

Luke 的使用

Luke 是 Lucene 搜索引擎的查看、诊断工具,如果在进程中出现搜索不到或者搜索出的结果与预期不匹配时可用 Luke 工具进行查询、修改和调试。使用 Luke 的前提是在创建索引的时候使用的是 FSDirectory。

点此进行 LUKE 下载 ,同时,Luke 需要安装 Java 1.5 或更高版本。

下载的版本必须要与 Lucene 的版本相匹配,否则会造成索引目录打不开现象。

以下是以 lukeall-4.7.1.jar 来进行演示的结果,双击该 jar 包进行打开,如打不开,可选择以 cmd 命令行的方式进行打开,java -jar lukeall-4.7.1.jar。打开后,显示界面如下图所示:

图 1.lukeall 首页面

Lucene4.X 高级应用

接下来,需要选择 index director 的路径,图 1 中红色箭头所示,然后点击 OK。详细索引信息见图 2.

图 2.Luke-Overview

Lucene4.X 高级应用

上图详细的展示了 Overview 选项下的一些主要索引信息,例如 field 数量,document 的数量和 term 的数量等信息。

图 3 是 Luke 下索引的具体列表展示,可以得到每个 term 的详细信息。

图 3.Luke-Search

Lucene4.X 高级应用

Luke 除了可以宏观的看到索引信息外,还提供了可视化的界面查询,查询语法是 [字段名:内容],如图 3 左上方所示。

Luke 是一个开源工具,开发人员也可以通过插件和脚本进行自定义功能定制和扩展。

回页首

结束语

本文主要介绍了如何去创建索引以及创建索引时 LogMergePolicy 的配置,合理的配置 LogMergePolicy 可以提高创建索引的效率。由于一个索引文件夹只能允许一个 IndexWriter 访问,所以最好将 IndexWriter 写成单例模式,保持全局只有一个 IndexWriter,最后一定要记得关闭 IndexWriter。. 然后依次介绍了 Lucene 里的各种 query 查询,要根据项目的实际需要选择相应的 query。还有就是要注意 Directory 的用法,API 里提供了各种 Directory,但要根据自己的实际情况选择最佳的 Directory。同时,为了减少内存开支,最好只实例化一个 Directory,因为每次打开 Directory 都需要消耗大量内存。新版本的 Lucene 提供了 SearchManager 去管理 IndexReader 和 IndexSearcher,所以不需要大家再去实现这两个对象的单例模式了。最后介绍了 Lucene 索引查询工具,该工具可以帮助开发人员快速、有效的进行索引数据的查看、添加、修改或删除。希望本文章可以为 Lucene 的学习使用人员提供一些帮助。

原文  http://www.ibm.com/developerworks/cn/java/j-lo-Lucene/index.html?ca=drs-