2.0 入门案例及详解
Lucene可以在Java SE程序中使用,也可以在Java EE程序中使用。作为第一个案例,我们采用一个java项目来进行讲解。
假 设我们有这样一个场景:我们开发了一个博客应用,用户可以注册并且发表文章。为了访问者可以高效的搜索博客网站的所有文章,我们打算使用Lucene对文 章建立索引。一个文章可能包含的字段有:id,标题、摘要、关键字、内容、发表时间、作者等信息。我们希望不论哪一个字段包含用户搜索的关键字,都可以搜 索到这片文章。因此在用户发表文章的时候,我们往数据库中存储记录的时候,同时通过Lucene对文章建立索引。
为了简单,在本案例中,我们自己构建文章Article对象实例,并且往其中填写内容。模拟已经获取到的用户输入的内容。查询时,我们自己指定搜索关键字。并且,我们并不真正的将文章数据存入数据库,这个太简单,我们关心的是Lucene索引库的创建与维护。
1、新建maven项目lucene
pom.xml依赖
<dependencies> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>4.10.4</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>4.10.4</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-queryparser</artifactId> <version>4.10.4</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.9</version> </dependency> <dependency> <groupId>com.thihy</groupId> <artifactId>elasticsearch-analysis-paoding</artifactId> <version>1.4.2.1</version> </dependency> </dependencies>
2 新建文章实体Article
注:Article实体表示的是一个文章的信息,我们在创建索引的时候,需要将其转换为Document对象。
public class Article { private Integer id; private String title; private String content; private String author; //-----------constructors---------- public Article(Integer id, String title, String content, String author) { super(); this.id = id; this.title = title; this.content = content; this.author = author; } @Override public String toString() { return "Artical [id=" + id + ", title=" + title + ", content=" + content + "]"; } //-------------------getters and setters---------------------- ...
3 创建索引:Indexer.java
Lucene提供了一系列的API来创建索引,在Lucene中,用Document
对象来表示索引库中的一条记录,每个Document由很多字段Field
组成,可以将Document类比为数据库中的一条记录。而Document最终是通过IndexWriter
来创建索引。在后面我们将会详细介绍涉及到的每个API。
/** 建立索引 发表过文章过后,不仅数据库中有存储记录 索引库中也必须有一条*/ public static void main(String args[]) throws Exception { // 模拟一条数据库中的记录 Article artical = new Article(1, "Lucene全文检索框架", "Lucene如果信息检索系统在用户发出了检索请求后再去网上找答案","田守枝"); // 建立索引 // 1、把Article转换为Doucement对象 Document doc = new Document(); //根据实际情况,使用不同的Field来对原始内容建立索引, Store.YES表示是否存储字段原始内容 doc.add(new LongField("id", artical.getId(), Store.YES)); doc.add(new StringField("author", artical.getAuthor(), Store.YES)); doc.add(new TextField("title", artical.getTitle(), Store.YES)); doc.add(new TextField("content", artical.getContent(), Store.NO)); // 2、建立索引 // 指定索引库的位置,本例为项目根目录下indexDir Directory directory = FSDirectory.open(new File("./indexDir/")); // 分词器,不同的分词器有不同的规则 Analyzer analyzer = new StandardAnalyzer(); IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LATEST, analyzer); IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig); indexWriter.addDocument(doc); indexWriter.close(); }
运行createIndex()方法,在项目根目录下会出现一些索引文件,如下图所示:
indexDir目录下的文件就是Lucene的索引库文件,我们可以通过lukeall工具来查看索引库中的内容,在后面将会介绍。
4 搜索Searcher.java
在Lucene中,搜索通过IndexSearcher
完成。
// 搜索索引库 public static void main(String args[]) throws Exception { // 搜索条件(不区分大小写) String queryString = "lucene"; // String queryString = "compass"; // 进行搜索得到结果 // ============================== Directory directory = FSDirectory.open(new File("./indexDir/"));// 索引库目录 Analyzer analyzer = new StandardAnalyzer(); // 1、把查询字符串转为查询对象(存储的都是二进制文件,普通的String肯定无法查询,因此需要转换) QueryParser queryParser = new QueryParser("title",analyzer);// 只在标题里面查询 Query query = queryParser.parse(queryString); // 2、查询,得到中间结果 IndexReader indexReader=DirectoryReader.open(directory); IndexSearcher indexSearcher = new IndexSearcher(indexReader); TopDocs topDocs = indexSearcher.search(query, 100);// 根据指定查询条件查询,只返回前n条结果 int count = topDocs.totalHits;// 总结果数 ScoreDoc[] scoreDocs = topDocs.scoreDocs;// 按照得分进行排序后的前n条结果的信息 List<Article> articalList = new ArrayList<Article>(); // 3、处理中间结果 for (ScoreDoc scoreDoc : scoreDocs) { float score = scoreDoc.score;// 相关度得分 int docId = scoreDoc.doc; // Document在数据库的内部编号(是唯一的,由lucene自动生成) // 根据编号取出真正的Document数据 Document doc = indexSearcher.doc(docId); // 把Document转成Article Article artical = new Article( Integer.parseInt(doc.getField("id").stringValue()),//需要转为int型 doc.getField("title").stringValue(), null, doc.getField("author").stringValue() ); articalList.add(artical); } indexReader.close(); // ============查询结束==================== // 显示结果 System.out.println("总结果数量为:" + articalList.size()); for (Article artical : articalList) { System.out.println("id="+artical.getId()); System.out.println("title="+artical.getTitle()); System.out.println("content="+artical.getContent()); } }
运行search方法,控制台输出:
总结果数量为:1 id=1 title=Lucene全文检索框架 content=null
注意目前我们搜索的是小写lucene,依然搜索到了结果。读者可以尝试搜索其他的关键字。
5 入门案例详解
5.1 Indexer class
IndexWriter
Directory
Analyzer
Document
Field
索引建立过程图示
IndexWriter类似于数据库的SessionFactory,每次使用完都关闭,是非常浪费资源的,因此,我们应该保证在全局范围内只使用一个IndexWriter。
而每次使用IndexWriter在操作索引库的时候,都会给索引库加上一把锁,当关闭这个IndexWriter时,会把锁释放掉。
而我们开发的web应用是多线程的,这就意味着一个线程在操作索引库的时候,索引库就会被锁住,其他线程无法访问。
此时有两种方法来解决这个问题:
1、 针对每个线程都创建一个IndexWriter,强烈不建议
2、 全局范围内使用一个IndexWriter,但是每次使用完不是close掉,是使用commit方法,当操作提交之后,锁就会被释放掉,别的线程就可以操作索引库了。
在多线程并发访问时,只要保证每个人的结果集不是全局的,就不会出现数据混乱的情况。
Directory directory = FSDirectory.open(new File("./indexDir/")); // 分词器,不同的分词器有不同的规则 Analyzer analyzer = new StandardAnalyzer(); IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LATEST, analyzer); IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
IndexWriter只有一个构造函数,接受一个Directory和IndexWriterConfig对象。
Directory
指的是:索引库的路径,也就是将原始建立索引后,索引的存放位置
IndexWriterConfig是创建索引时的配置信息,例如指定使用的分词器。关于分词我们在后面会详细讲解。
对于大段的文本,IndexWriter
并不能针对其建立索引,必须要经过Analyzer进行分词之后。Analyzer只能针对纯文本的文件内容进行分词,如果文件不是纯文本,我们必须先将文件中的文本内容提取出来,Tika框架可以帮助我们将文本内容从不同格式的文件中提取出来。
Analyzer
是一个抽象类,Lucene为其提供了一实现类。其中有一些Analyzer是处理停用词stopping word
,例如 a、an、the这些词语对于建立索引是没有作用的。一些Analyzer将所有的关键词全部转为小写,这样Lucene就不需要考虑大小写的问题了。等等。
Document
对象代表的就是我们建立索引的文档。其内部包含了众多Field,Field的作用是告诉我们需要针对这个文档的哪些内容建立索引,哪些内容需要进行分词。
Document则是Lucene建立索引的最小单位。Document中包含了很多Field,其包含了真正的索引数据,每一个Field都有一个独一无二的名称,和对应的值,并包含了一些具体的操作信息,例如是否进行分词。如果两个Field名称相同,内容不会覆盖,后建立的索引的内容会添加在原来的Field的后面。Field的值是在搜索的过程中如果匹配对应的关键字,就可以搜索到内容。而name则指定使用哪一个Field的value进行匹配。例如,我们只想搜索在filename中包含Lucene的文档,就可以指定搜索filename:Lucene。这样名称为contents的field的value就不再检索范围内。
建立索引实际上是针对Field的value建立索引,当指定value建立索引后,Lucene会根据value的值计算得到一个Token
,这是Lucene使用一些算法得到的。
在 本案例中,我们使用到的Field类型包括:LongField、StringField、TextField。当然还有很多其他类型分Field。需要 注意的是,当我们使用Document对象的add方法添加字段时,只有TextField的值会进行分词,其他类型的Field都不会进行分词。不进行 分词的后果是,用户检索的关键字必须与这个字段的内容完全匹配上,才返回这条记录。在我们案例中,我们是按照如下方式添加Field的:
doc.add(new LongField("id", artical.getId(), Store.YES)); doc.add(new StringField("author", artical.getAuthor(), Store.YES)); doc.add(new TextField("title", artical.getTitle(), Store.YES)); doc.add(new TextField("content", artical.getContent(), Store.NO));
对于文章的作者信息,我们希望用户输入的名字必须完全匹配上,才返回这条记录,因此使用StringField,而不是TextField;而对于文章的标题信息,我们希望用户检索的关键字分词后,只要匹配上一部分就可以搜索到,因此对标题进行分词,类似的我们对文章的内容也进行了分词。
Store.YES和Store.NO的作用是,如果选择了YES,在建立索引后,会把这个字段的原始值也保存在数据库中,而NO则是不保存。这将会影响到搜索的结果的展示。例如我们希望文章的内容content可以进行检索,但是我们不希望将其原始内容保存到索引库中,因为一个文章的内容通常都是很大的。我们搜索的结果只是展示部分信息,而不是展示文章的所有信息,因为我们对文章的id使用Store.YES,所以检索到的结果我们可以获取到文章的id,当检索结果展示在页面上时,只要超链接后面跟上这个id作为参数,在数据库中检索是非常快的,因为有主键索引。
Lucene只处理文本内容,这是由于搜索的时候,用户只会输入文本内容进行搜索。因此对于不同类型的文档,我们在建立索引的时候,会将其的文本内容提取出来,针对图片建立索引没有任何意义。
5.2 searching classes
IndexSearcher
Term
Query
TermQuery
TopDocs
我
们的案例代码中并没有涉及到Term
这个类,Term是索引库中最基本的搜索单元。这是因为,在建立索引的时候,如果指定不分词(如案例中文件名和文件路
径),那么整个内容就是一个Term,而如果指定分词(如案例中的文件内容),内容就会被拆分成多个Term,这些过程是在建立索引的过程中有
Lucene框架自动完成的,因此Term是Lucene中最基本的搜索单元,在建立索引的时候我们不需要考虑Term。
在搜索的时候,我们可以使用Term配合TermQuery进行查询。
Query q = new TermQuery(new Term("contents", "lucene")); TopDocs hits = searcher.search(q, 10);
Term中的参数对应建立索引时Field中指定的索引的名称,第二个参数是检索的关键字。
TermQuery
与Query对象的区别是:Query对象是通过QueryParser解析用户输入的搜索内容解析得到的,会将用户输入的内容分词后再进行搜索。而TermQuery由于搜索的是最小的分词单元,因此不会对搜索关键字再次进行分词。
Query对象是一个抽象类,代表用户的查询,其有很多子类:
TermQuery. BooleanQuery, PhraseQuery, PrefixQuery, PhrasePrefixQuery, TermRangeQuery, NumericRangeQuery, FilteredQuery, SpanQuery.
Query对象本身也实现了一些方法,setBoost(float)
方法是一个比较有趣的方法,当我们使用多个子查询对象进行查询的时候,如果某个子查询对象使用了这个方法,可以提高这个子查询对象查询出来的数据的相关度得分。