Lucene学习笔记
Posted 陈驰字新宇
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Lucene学习笔记相关的知识,希望对你有一定的参考价值。
师兄推荐我学习Lucene这门技术,用了两天时间,大概整理了一下相关知识点。
一、什么是Lucene
Lucene即全文检索。全文检索是计算机程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当用户查询时根据建立的索引查找,类似于通过字典的检索字表查字的过程。
二、Lucene全文检索和数据库检索的区别
三、Lucene的原理
(1)索引库操作原理
注意:这里面有两个关键的对象:分别是IndexWriter和IndexSearcher。
执行增删改操作用的是IndexWriter对象,执行查询操作用的是IndexSearcher对象。
(2)索引库存放数据原理
注意:Lucece库在4.0之前和4.0之后的API发生了很大变化,这个图中的Index到后面的API已经不建议再用了。后面有相应的替代方法。
比如:原来的写法:
String id = NumericUtils.intToPrefixCoded(1);
new Field("id",id,Store.YES,Index.NOT_ANALYZED);
new Field("title", "我是陈驰",Store.YES, Index.NOT_ANALYZED);
new Field(""content",
"国防科学技术大学(National University of Defense Technology),是中华人民共和国中央军事委员会直属的一所涵盖理学、工学、军事学、管理学、经济学、哲学、文学、教育学、法学、历史学等十大学科门类的综合性全国重点大学",
Store.YES,
Index.ANALYZED);
后来的写法:
new IntField("id", 1, Store.YES);
new StringField("title", "我是陈驰", Store.YES);
new TextField(
"content",
"国防科学技术大学(National University of Defense Technology),是中华人民共和国中央军事委员会直属的一所涵盖理学、工学、军事学、管理学、经济学、哲学、文学、教育学、法学、历史学等十大学科门类的综合性全国重点大学",
Store.YES);
四、Lucene开发原理(索引库与数据库同步)
数据库与索引库中存放相同的数据,可以使用数据库中存放的ID用来表示和区分同一条数据。
--数据库中用来存放数据
--索引库中用来查询、检索
检索库支持查询检索多种方式,
特点:
1:由于是索引查询(通过索引查询数据),检索速度快,搜索的结果更加准确
2:生成文本摘要,摘要截取搜索的文字出现最多的地方
3:显示查询的文字高亮
4:分词查询等
注意:添加了索引库,并不意味着不往数据库中存放数据,数据库的所有操作仍和以前一样。只不过现在多维护一个索引库,在查询的时候可以提高效率。
所有的数据(对象),我们都要存到数据库中。对于要进行搜索的数据,还要存到索引库中,以供搜索。一份数据同时存到数据库与索引库中(格式不同),就要想办法保证他们的状态一致。否则,就会影响搜索结果。
对于上一段提出的问题:保证索引库中与数据库中的数据一致(只要针对要进行搜索的数据)。我们采用的方法是,在数据库中做了相应的操作后,在索引库中也做相应的操作。具体的索引库操作,是通过调用相应的IndexDao方法完成的。IndexDao类似于数据库层的Dao。
索引库中存放的数据要转换成Document对象(每条数据就是一个Document对象),并向Document对象中存放Field对象(每条数据对应的字段,例如主键ID、所属单位、图纸类别、文件名称、备注等),将每个字段中的值都存放到Field对象中。
五、开发步骤
(1)需要的jar包
(2)需要的配置文件(注:这里我用的是IKAnalyzer,是第三方的中文分词器,庖丁分词,中文分词,特点:扩展新的词,自定义停用词。只有用该分词器才用到以下配置文件。)
l IKAnalyzer.cfg.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>IK Analyzer 扩展配置</comment> <!--用户可以在这里配置自己的扩展字典--> <entry key="ext_dict">mydict.dic;</entry> <!--用户可以在这里配置自己的扩展停止词字典--> <entry key="ext_stopwords">ext_stopword.dic</entry> </properties>
l ext.dic(扩展词库)
l stopword.dic(停用词库)
(3)一个简单的例子(用的标准分词器StandardAnalyzer,所以暂时没有用到上面的配置文件,标准分词器是汉字一个一个分的,英语还是按单词)
import java.io.File; import java.io.IOException; 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.IntField; import org.apache.lucene.document.StringField; import org.apache.lucene.document.Field.Index; import org.apache.lucene.document.Field.Store; 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.index.IndexableField; import org.apache.lucene.index.Term; 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.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.util.Version; import org.junit.Test; /** * 使用lucene对数据创建索引 * * @author chenchi * */ public class TestLucene { /** * 使用IndexWriter对数据创建索引 * @throws IOException */ @Test public void testCreateIndex() throws IOException { // 索引存放的位置... Directory d = FSDirectory.open(new File("indexDir/")); // 索引写入的配置 Version matchVersion = Version.LUCENE_CURRENT;// lucene当前匹配的版本 Analyzer analyzer = new StandardAnalyzer(matchVersion);// 分词器 IndexWriterConfig conf = new IndexWriterConfig(matchVersion, analyzer); // 构建用于操作索引的类 IndexWriter indexWriter = new IndexWriter(d, conf); // 通过IndexWriter来创建索引 // 索引库里面的数据 要遵守一定的结构(索引结构,document) Document doc = new Document(); /** * 1.字段的名称 2.该字段的值 3.字段在数据库中是否存储 * StringField是一体的 * TextField是可分的 */ IndexableField field = new IntField("id", 1, Store.YES); IndexableField title = new StringField("title", "java培训零基础开始 从入门到精通", Store.YES); IndexableField content = new TextField( "content", "java培训,中软国际独创实训模式,三免一终身,学java多项保障让您无后顾之忧。中软国际java培训,全日制教学,真实项目实战,名企定制培训,四个月速成java工程师!", Store.YES); doc.add(field); doc.add(title); doc.add(content); // document里面也有很多字段 indexWriter.addDocument(doc); indexWriter.close(); } /** * 使用IndexSearcher对数据创建索引 * * @throws IOException */ @Test public void testSearcher() throws IOException { // 索引存放的位置... Directory d = FSDirectory.open(new File("indexDir/")); // 通过indexSearcher去检索索引目录 IndexReader indexReader = DirectoryReader.open(d); IndexSearcher indexSearcher = new IndexSearcher(indexReader); // 这是一个搜索条件,根据这个搜索条件我们来进行查找 // term是根据哪个字段进行检索,以及字段对应值 //================================================ //注意:这样是查询不出,只有单字才能查询出来 Query query = new TermQuery(new Term("content", "培训")); // 搜索先搜索索引目录 // 找到符合条件的前100条数据 TopDocs topDocs = indexSearcher.search(query, 100); System.out.println("总记录数:" + topDocs.totalHits); ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { //得分采用的是VSM算法 System.out.println("相关度得分:" + scoreDoc.score); //获取查询结果的文档的惟一编号,只有获取惟一编号,才能获取该编号对应的数据 int doc = scoreDoc.doc; //使用编号,获取真正的数据 Document document = indexSearcher.doc(doc); System.out.println(document.get("id")); System.out.println(document.get("title")); System.out.println(document.get("content")); } } }
(4)实现Lucene的CURD操作
先写一个LuceneUtils类:
import java.io.File; import java.io.IOException; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; 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.search.IndexSearcher; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.util.Version; public class LuceneUtils { public static Directory d = null; public static IndexWriterConfig conf = null; public static Version matchVersion = null; public static Analyzer analyzer = null; static{ try { d = FSDirectory.open(new File(Constant.FILEURL)); matchVersion = Version.LUCENE_44; //注意:该分词器是单字分词 analyzer = new StandardAnalyzer(matchVersion); conf = new IndexWriterConfig(matchVersion, analyzer); } catch (IOException e) { e.printStackTrace(); } } /** * * @return 返回版本信息 */ public static Version getMatchVersion() { return matchVersion; } /** * * @return 返回分词器 */ public static Analyzer getAnalyzer() { return analyzer; } /** * * @return 返回用于操作索引的对象 * @throws IOException */ public static IndexWriter getIndexWriter() throws IOException{ IndexWriter indexWriter = new IndexWriter(d, conf); return indexWriter; } /** * * @return 返回用于读取索引的对象 * @throws IOException */ public static IndexSearcher getIndexSearcher() throws IOException{ IndexReader r = DirectoryReader.open(d); IndexSearcher indexSearcher = new IndexSearcher(r); return indexSearcher; } }
再写一个Constant常量类:
public class Constant { public static String FILEURL = "E://indexDir/news"; }
封装一个实体类Article:
public class Article { private int id; private String title; private String author; private String content; private String link; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getLink() { return link; } public void setLink(String link) { this.link = link; } }
由于需要实体类和document对象之间彼此转换,所以再写一个转换的工具类ArticleUtils:
import org.apache.lucene.document.Document; import org.apache.lucene.document.TextField; import org.apache.lucene.document.StringField; import org.apache.lucene.document.Field.Store; import org.apache.lucene.document.IntField; import com.tarena.bean.Article; public class ArticleUtils { /** * 将artitle转换成document * @param article * @return */ public static Document articleToDocument(Article article) { Document document = new Document(); IntField idField = new IntField("id", article.getId(), Store.YES); //StringField不进行分词(整体算一个) StringField authoField = new StringField("author", article.getAuthor(), Store.YES); StringField linkField = new StringField("link", article.getLink(), Store.YES); //TextField进行分词 TextField titleField = new TextField("title", article.getTitle(), Store.YES); //============================================== //注意:这里可以添加权重值,默认是1f,添加之后检索的时候排名就会靠前 titleField.setBoost(4f); TextField contentField = new TextField("content", article.getContent(), Store.YES); document.add(idField); document.add(authoField); document.add(linkField); document.add(titleField); document.add(contentField); return document; } /** * 将document转换成article * @param document * @return */ public static Article documentToArticle(Document document){ Article article = new Article(); article.setId(Integer.parseInt(document.get("id"))); article.setAuthor(document.get("author")); article.setLink(document.get("link")); article.setTitle(document.get("title")); article.setContent(document.get("content")); return article; } }
以上准备工作都做完,接下来我们就可以写一个LuceneDao,进行增删改查(分页)操作。
import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.security.auth.login.Configuration; import org.apache.lucene.document.Document; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.Term; import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.queryparser.flexible.core.util.StringUtils; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import com.tarena.bean.Article; import com.tarena.utils.ArticleUtils; import com.tarena.utils.LuceneUtils; /** * 相关度得分: * 内容一样,搜索关键字一样,得分也是一样的 * 我们可以人工干预得分,就是通过ArticleUtils类中的titleField.setBoost(4f);这样 * 得分跟搜索关键字在文章当中出现的频率、次数、位置有关系 * @author chenchi * */ public class LuceneDao { /** * 增删改索引都是通过IndexWriter对象来完成 */ public void addIndex(Article article) { try { IndexWriter indexWriter = LuceneUtils.getIndexWriter(); indexWriter.addDocument(ArticleUtils.articleToDocument(article)); indexWriter.close(); } catch (IOException e) { e.printStackTrace(); } } /** * 删除索引,根据字段对应的值删除 * @param fieldName * @param fieldValue * @throws IOException */ public void deleteIndex(String fieldName, String fieldValue) throws IOException { IndexWriter indexWriter = LuceneUtils.getIndexWriter(); //使用词条删除 Term term = new Term(fieldName, fieldValue); indexWriter.deleteDocuments(term); indexWriter.close(); } /** * 先删除符合条件的记录,再创建一个新的纪录 * @param fieldName * @param fieldValue * @param article * @throws IOException */ public void updateIndex(String fieldName, String fieldValue, Article article) throws IOException { IndexWriter indexWriter = LuceneUtils.getIndexWriter(); Term term = new Term(fieldName, fieldValue); Document doc = ArticleUtils.articleToDocument(article); /** * 1.设置更新的条件 * 2.设置更新的内容和对象 */ indexWriter.updateDocument(term, doc); indexWriter.close(); } /** * 查询是通过IndexSearch提供的(分页) */ public List<Article> findIndex(String keywords, int start, int count) { try { IndexSearcher indexSearcher = LuceneUtils.getIndexSearcher(); //=========================================================== //这里是第二种query方式,不是termQuery QueryParser queryParser = new MultiFieldQueryParser( LuceneUtils.getMatchVersion(), new String[] { "title", "content" }, LuceneUtils.getAnalyzer()); Query query = queryParser.parse(keywords); TopDocs topDocs = indexSearcher.search(query, 100); System.out.println("总记录数:" + topDocs.totalHits); //表示返回的结果集 ScoreDoc[] scoreDocs = topDocs.scoreDocs; List<Article> list = new ArrayList<Article>(); int min = Math.min(scoreDocs.length, start + count); for (int i = start; i < min; i++) { System.out.println("相关度得分:"+scoreDocs[i].score); //获取查询结果的文档的惟一编号,只有获取惟一编号,才能获取该编号对应的数据 int doc = scoreDocs[i].doc; //使用编号,获取真正的数据 Document document = indexSearcher.doc(doc); Article article = ArticleUtils.documentToArticle(document); list.add(article); } return list; } catch (Exception e) { e.printStackTrace(); } return null; } }
对LuceneDao进行测试:
import java.io.IOException; import java.util.List; import org.junit.Test; import com.tarena.bean.Article; import com.tarena.dao.LuceneDao; public class TestLuceneDao { private LuceneDao dao = new LuceneDao(); @Test public void addIndex() { for (int i = 0; i <= 25; i++) { Article article = new Article(); article.setId(i); article.setTitle("腾讯qq"); article.setAuthor("马化腾"); article.setContent("腾讯网(www.QQ.com)是中国浏览量最大的中文门户网站,是腾讯公司推出的集新闻信息、互动社区、娱乐产品和基础服务为一体的大型综合门户网站。腾讯网服务于全球华人..."); article.setLink("http://www.qq.com/"); dao.addIndex(article); } } @Test public void findIndex() { String keywords = "第一"; List<Article> list = dao.findIndex(keywords, 0, 30); for (Article article : list) { System.out.println(article.getId()); System.out.println(article.getTitle()); System.out.println(article.getContent()); System.out.println(article.getAuthor()); System.out.println(article.getLink()); } } @Test public void deleteIndex(){ try { dao.deleteIndex("author", "陈驰"); } catch (IOException e) { e.printStackTrace(); } } @Test public void updateIndex(){ String fieldName = "title"; String fieldValue = "qq"; Article article = new Article(); article.setId(1); article.setAuthor("陈驰"); article.setLink("http://www.baidu.com"); article.setTitle("天下第一"); article.setContent("天下第一一一一一一"); try { dao.updateIndex(fieldName, fieldValue, article); } catch (IOException e) { e.printStackTrace(); } } }
由此,简单的CURD操作就完成了。
(5)关于分词器
相同一段文本,在不同的分词器下面分成的词是不尽相同的,前面我们的程序中分别用了StandardAnalyzer,以及IKAnalyzer。那么分词器究竟有什么作用呢?
在创建索引时会用到分词器,在使用字符串搜索时也会用到分词器,这两个地方要使用同一个分词器,否则可能会搜索不出结果。
Analyzer(分词器)的作用是把一段文本中的词按规则取出所包含的所有词。对应的是Analyzer类,这是一个抽象类,切分词的具体规则是由子类实现的,所以对于不同的语言(规则),要用不同的分词器。如下图:
下面分别用过三种分词器,演示分词结果:
import java.io.IOException; import java.io.StringReader; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.cjk.CJKAnalyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; import org.apache.lucene.util.Version; import org.wltea.analyzer.lucene.IKAnalyzer; public class TestAnalyzer { public static void main(String[] args) { //单字分词器 //Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_44); //二分法分词器 //Analyzer analyzer = new CJKAnalyzer(Version.LUCENE_44); //第三方的中文分词器,庖丁分词,中文分词,特点:扩展新的词,自定义停用词 Analyzer analyzer = new IKAnalyzer(); String text = "腾讯网(www.QQ.com)是中国浏览量最大的中文门户网站,是腾讯公司推出的集新闻信息、互动社区、娱乐产品和基础服务为一体的大型综合门户网站。腾讯网服务于全球华人..."; try { testAnalyzer(analyzer, text); } catch (IOException e) { e.printStackTrace(); } } /** * 分词器的作用 * * @throws IOException */ public static void testAnalyzer(Analyzer analyzer, String text) throws IOException { System.out.println("当前使用的分词器:" + analyzer.getClass().getSimpleName()); TokenStream tokenStream = analyzer.tokenStream("content", new StringReader(text)); tokenStream.addAttribute(CharTermAttribute.class); //这里不写这一句,会报空指针异常 tokenStream.reset(); while (tokenStream.incrementToken()) { CharTermAttribute charTermAttribute = tokenStream .getAttribute(CharTermAttribute.class); System.out.println(new String(charTermAttribute.toString())); } } }
前使可以看到程序中分贝用了三种分词器(单字分词器,即标准分词器,二分法分词器,庖丁分词器),处理相同一段文本——腾讯网(www.QQ.com)是中国浏览量最大的中文门户网站,是腾讯公司推出的集新闻信息、互动社区、娱乐产品和基础服务为一体的大型综合门户网站。腾讯网服务于全球华人...运行结果依次如下:
当前使用的分词器:StandardAnalyzer
腾
讯
网
www.qq.com
是
中
国
浏
览
量
最
大
的
中
文
门
户
网
站
是
腾
讯
公
司
推
出
的
集
新
闻
信
息
互
动
社
区
娱
乐
产
品
和
基
础
服
务
为
一
体
的
大
型
综
合
门
户
网
站
腾
讯
网
服
务
于
全
球
华
人
当前使用的分词器:CJKAnalyzer
腾讯
讯网
www.qq.com
是中
中国
国浏
浏览
览量
量最
最大
大的
的中
中文
文门
门户
户网
网站
是腾
腾讯
讯公
公司
司推
推出
出的
的集
集新
新闻
闻信
信息
互动
动社
社区
娱乐
乐产
产品
品和
和基
基础
础服
服务
务为
为一
一体
体的
的大
大型
型综
综合
合门
门户
户网
网站
腾讯
讯网
网服
服务
务于
于全
全球
球华
华人
当前使用的分词器:IKAnalyzer
加载扩展词典:mydict.dic
加载扩展停止词典:ext_stopword.dic
腾讯网
腾讯
网
www.qq.com
www
qq
com
中国
浏览量
浏览
量
最大
中文
门户网
门户
网站
腾讯
公司
推出
集
新闻
信息
互动
社区
娱乐
产品
和
基础
服务
为
一体
一
体
大型
综合
门户网
门户
网站
腾讯网
腾讯
网
服务于
服务
全球华人
全球
华人
显然最后一种分词器,对中文来说分词效果最好。
(6)关于相关度排序
1,相关度得分是在查询时根据查询条件实进计算出来的
2,如果索引库数据不变,查询条件不变,查出的文档得分也不变
在这里,有两种方式改变相关度排序:
1)通过改变文档Boost值来改变排序结果。Boost是指索引建立过程中,给整篇文档或者文档的某一特定属性设定的权值因子,在检索时,优先返回分数高的。通过Document对象的setBoost()方法和Field对象的setBoost()方法,可以分别为Document和Field指定Boost参数。不同在于前者对文档中每一个域都修改了参数,而后者只针对指定域进行修改。默认值为1F,一般不做修改。前面的ArticleUtils类中已经举例。
//在添加的时候改变权重值,可以对每个document 的属性进行添加, //注:如果添加的索引值没有进行分词,则不能改变权限值. Document document=new Document(); document.add(new StringField("id",article.getId(),Store.YES)); StringField field=new StringField("title",article.getTitle(),Store.YES); TextField textField=new TextField("content",article.getContent(),Store.YES); textField.setBoost(5F); document.add(field); document.add(textField); return document;
2)使用Sort对象定制排序。Sort支持的排序功能以文档当中的域为单位,通过这种方法,可以实现一个或者多个不同域的多形式的值排序。
IndexReader indexReader = DirectoryReader.open(directory); IndexSearcher indexSearcher = new IndexSearcher(indexReader); String fields[] = { "title" }; QueryParser queryParser = new MultiFieldQueryParser( LuceneUtils.getMatchVersion(), fields, LuceneUtils.getAnalyzer()); // 不同的规则构造不同的子类.. // title:keywords ,content:keywords Query query = queryParser.parse("抑郁症"); Sort sort = new Sort(); // 升序 // SortField sortField=new SortField("id", Type.INT); // 降序 SortField sortField = new SortField("id", Type.INT, true); // 设置排序的字段... sort.setSort(sortField); TopDocs topDocs = indexSearcher.search(query, 100, sort); //注意:String类型和Int类型在比较排序的时候不同 //例如:Int:123>23 // String:”123”<以上是关于Lucene学习笔记的主要内容,如果未能解决你的问题,请参考以下文章