师兄推荐我学习Lucene这门技术,用了两天时间,大概整理了一下相关知识点。
一、什么是Lucene
Lucene即全文检索。全文检索是计算机程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当用户查询时根据建立的索引查找,类似于通过字典的检索字表查字的过程。
二、Lucece全文检索和数据库检索的区别
三、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”<“23”
(7)关于过滤
使用Filter可以对搜索结果进行过滤以获得更小范围的结果。使用Filter对性能的影响很大(有可能会使查询慢上百倍)。但也可使用相应的查询实现一样的效果。
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("抑郁症"); /** * 1:需要根据那个字段进行过滤 * * 2:字段对应的最小值 * * 3:字段对应的最大值 * * 4:是否包含最小值 * * 5:是否包含最大值... * * */ // filter 是一个抽象类,定义不同的filter 相当于是不同的过滤规则... Filter filter = NumericRangeFilter .newIntRange("id", 1, 10, false, true); TopDocs topDocs = indexSearcher.search(query, filter, 100);
(8)关于查询(这个非常非常重要,这是结合前面的分词器分出的词进行不同的查询的)
import org.apache.lucene.document.Document; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.NumericRangeQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import com.tarena.utils.LuceneUtils; /** * * indexSearcher.searcher(Query ) * * Query 是一个查询,条件,不同的子类相当于不同的查询规则 * * 我们可以扩展.. * * @author Administrator * */ public class TestQuery { public static void main(String[] args) throws Exception { // testQuery(); /** * 第一种查询,TermQuery * 这是关键字查询 * 如果按照author查,因为author没有分词,所以查"马化腾"可以查询出来 * 如果按照content查,因为content分词了,如果是单字分词器,只能通过某一个字查出来,比如"中" */ //Query query=new TermQuery(new Term("content","中")); /** * 第二种查询:字符串搜索.. * 使用查询字符串:QueryParser+ MultiFieldQueryParser的查询方式 * 1、QueryParser:只在一个字段中查询 * 2、MultiFieldQueryParser:可以在多个字段查询 * 用来查询可以分词的字段,只要你输入的一段文本中包含分词,就会检索出来 */ // String[] fields={"content"}; // // QueryParser queryParser=new // MultiFieldQueryParser(LuceneUtils.getMatchVersion(),fields,LuceneUtils.getAnalyzer()); // Query query=queryParser.parse("中文"); /** * 第三种查询:查询所有.. */ // Query query=new MatchAllDocsQuery(); /** * 第四种查询:范围查询,可以使用此查询来替代过滤器... */ // 我们完成一种需求有两种方式,我们推荐用这种...性能比filter要高 // Query query=NumericRangeQuery.newIntRange("id", 1, 10, true, true); /** * 第五种查询:通配符。。。 */ // ?代表单个任意字符,* 代表多个任意字符... // Query query=new WildcardQuery(new Term("title", "luce*")); /** * 第六种查询:模糊查询..。。。 */ // author String /* * 1:需要根据查询的条件 * * 2:最大可编辑数 取值范围0,1,2 允许我的查询条件的值,可以错误(或缺少)几个字符... * */ // Query query = new FuzzyQuery(new Term("author", "爱新觉罗杜小"), 1); /** * * 第七种查询:短语查询 * */ // PhraseQuery query=new PhraseQuery(); // //(1)直接指定角标... // // query.add(new Term("title","solr"),0); // // query.add(new Term("title","全"),8); // (2)设置两个短语之间的最大间隔数... // //设置间隔数范围越大,它被匹配的结果就越多,性能也就越慢.. // query.add(new Term("title","solr")); // query.add(new Term("title","全")); // query.setSlop(18); // 第八种查询:布尔查询 BooleanQuery query = new BooleanQuery(); // id 1~10 Query query1 = NumericRangeQuery.newIntRange("id", 1, 10, true, true); Query query2 = NumericRangeQuery.newIntRange("id", 5, 15, true, true); // select * from table where title=? or content=? // 必须满足第一个条件... query.add(query1, Occur.MUST); // 可以满足第二个条件 query.add(query2, Occur.SHOULD); testQuery(query); } public static void testQuery(Query query) throws Exception { IndexSearcher indexSearcher = LuceneUtils.getIndexSearcher(); TopDocs topDocs = indexSearcher.search(query, 100); System.out.println("总记录数:" + topDocs.totalHits); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { Document document = indexSearcher.doc(scoreDoc.doc); System.out.println(document.get("id")); System.out.println(document.get("title")); System.out.println(document.get("content")); System.out.println(document.get("author")); System.out.println(document.get("link")); } } }
其中, 布尔查询很重要,可以实现像京东、淘宝上面的多级检索的效果,示例代码如下:
//封装查询条件(使用BooleanQuery对象,连接多个查询条件) BooleanQuery query = new BooleanQuery(); //条件一(所属单位) if(StringUtils.isNotBlank(projId)){ //词条查询 TermQuery query1 = new TermQuery(new Term("projId", projId)); //Occur.SHOULD相当于or query.add(query1, Occur.MUST);//Occur.MUST相当与sql语句的and } //条件二(图纸类别) if(StringUtils.isNotBlank(belongTo)){ //词条查询 TermQuery query2 = new TermQuery(new Term("belongTo", belongTo)); query.add(query2, Occur.MUST);//相当与sql语句的and } //条件三(文件名称和文件描述) if(StringUtils.isNotBlank(queryString)){ //多个字段进行检索的时候,查询使用QueryPaser //要是直接new QueryParser(),也可以,但是只能查询一个字段 QueryParser queryParser = new MultiFieldQueryParser(Version.LUCENE_CURRENT,new String[]{"fileName","comment"},Configuration.getAnalyzer()); Query query3 = queryParser.parse(queryString); query.add(query3, Occur.MUST);//相当与sql语句的and }
(9)设置文字高亮
效果图:
1)高亮需要的jar包
highlighter\lucene-highlighter-4.4.0.jar
memory\lucene-memory-4.4.0.jar
2)高亮的特点
1、高亮将文本生成一段摘要,用于搜索,并把摘要中的关键词高亮显示
2、摘要的大小可以配置,默认100个字符。
3、文本实现高亮的效果,就是在需要高亮的文字前后加前缀与后缀(类似Html)
示例如下:
import java.util.ArrayList; import java.util.List; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; 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.TopDocs; import org.apache.lucene.search.highlight.Formatter; import org.apache.lucene.search.highlight.Fragmenter; import org.apache.lucene.search.highlight.Highlighter; import org.apache.lucene.search.highlight.QueryScorer; import org.apache.lucene.search.highlight.Scorer; import org.apache.lucene.search.highlight.SimpleFragmenter; import org.apache.lucene.search.highlight.SimpleHTMLFormatter; import org.wltea.analyzer.lucene.IKAnalyzer; import com.tarena.bean.Article; import com.tarena.dao.LuceneDao; import com.tarena.utils.ArticleUtils; import com.tarena.utils.LuceneUtils; public class TestHighLight { public static void main(String[] args) { String keywords = "中华人民共和国"; List<Article> list = findIndex(keywords, 0, 10); 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()); } } /** * 查询是通过IndexSearch提供的(分页) */ public static List<Article> findIndex(String keywords, int start, int count) { Analyzer analyzer = new IKAnalyzer(); try { IndexSearcher indexSearcher = LuceneUtils.getIndexSearcher(); // =========================================================== // 这里是第二种query方式,不是termQuery QueryParser queryParser = new MultiFieldQueryParser( LuceneUtils.getMatchVersion(), new String[] { "title", "content" }, analyzer); Query query = queryParser.parse(keywords); TopDocs topDocs = indexSearcher.search(query, 100); System.out.println("总记录数:" + topDocs.totalHits); /** * 添加设置文字高亮begin 使用lucene自带的高亮器进行高亮显示 */ // html页面高亮显示的格式化,默认是<b></b> Formatter formatter = new SimpleHTMLFormatter( "<font color='red'><b>", "</b></font>"); // 执行查询条件,因为高亮的值就是查询条件 Scorer scorer = new QueryScorer(query); Highlighter highlighter = new Highlighter(formatter, scorer); // 设置文字摘要,此时摘要大小 int fragmentSize = 100; Fragmenter fragmenter = new SimpleFragmenter(fragmentSize); highlighter.setTextFragmenter(fragmenter); /** 添加设置文字高亮end */ // 表示返回的结果集 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); /** 获取文字高亮的信息 begin */ // 获取文字的高亮,一次只能获取一个字段高亮的结果,如果获取不到,返回null值 // 高亮之后的title // 注意:如果这个字段当中没有包含搜索关键字,你对这个字段的值进行高亮,返回的是null String title = highlighter.getBestFragment( analyzer, "title", document.get("title")); // 如果null表示没有高亮的结果,如果高亮的结果,应该将原值返回 if (title == null) { title = document.get("title"); if (title != null && title.length() > fragmentSize) { // 截串,从0开始 title = title.substring(0, fragmentSize); } } System.out.println("-------title:" + title); // 高亮之后的content // 注意:如果这个字段当中没有包含搜索关键字,你对这个字段的值进行高亮,返回的是null String content = highlighter.getBestFragment( analyzer, "content", document.get("content")); // 如果null表示没有高亮的结果,如果高亮的结果,应该将原值返回 if (content == null) { content = document.get("content"); if (content != null && content.length() > fragmentSize) { // 截串,从0开始 content = content.substring(0, fragmentSize); } } System.out.println("--------content:" + content); /** 获取文字高亮的信息 end */ Article article = new Article(); article.setId(Integer.parseInt(document.get("id"))); article.setAuthor(document.get("author")); article.setLink(document.get("link")); article.setTitle(title);//高亮之后的 article.setContent(content);//高亮之后的 list.add(article); } return list; } catch (Exception e) { e.printStackTrace(); } return null; } }
六、Lucene优化
(1)MergePolicy 设置合并规则
public void testOptimise1() throws IOException { // 可以通过indexWriterConfig这个对象进行优化 // 在lucene4.0之后的版本会对索引进行自动的优化 // 通过改几个配置 Directory d = FSDirectory.open(new File(Constant.FILEURL)); IndexWriterConfig conf = new IndexWriterConfig( LuceneUtils.getMatchVersion(), LuceneUtils.getAnalyzer()); // 在lucene里面是0配置的 // 通过设置对象的参数来进行配置 LogDocMergePolicy mergePolicy = new LogDocMergePolicy(); /** * * 1:mergeFactor * * 当这个值越小,更少的内存被运用在创建索引的时候,搜索的时候越快,创建索引的时候越慢.. * * 当这个值越大,更多的内存被运用在创建索引的时候,搜索的时候越慢,创建的时候越快... * * * smaller value 2 < smaller value <10 * */ // 设置索引的合并因子... mergePolicy.setMergeFactor(6); conf.setMergePolicy(mergePolicy); IndexWriter indexWriter = new IndexWriter(d, conf); }
(2)排除停用词,排除停用,被分词器过滤掉,词就不会建立索引,索引文件就会变小,这样搜索的时候就会变快。
(3)将索引数据分区存放
(4)将索引放在内存当中,而不是硬盘当中
Lucene的API接口设计的比较通用,输入输出结构都很像:
数据库的表==>记录==>字段。
所以很多传统的应用的文件、数据库等都可以比较方便的映射到Lucene的存储结构/接口中。总体上看:可以先把Lucene当成一个支持全文索引的数据库系统。
Lucene的索引存储位置使用的是一个接口(抽象类),也就可以实现各种各样的实际存储方式(实现类、子类),比如存到文件系统中,存在内存中、存在数据库中等等。
FSDirectory:在文件系统中,是真实的文件夹与文件。
RAMDirectory:在内存中,是模拟的文件夹与文件。
1、因为没有IO操作,所以速度快(优点)。
2,因为在内存中,所以在程序退出后索引库数据就不存在了(缺点)。
public void testOptimise4() throws IOException, ParseException { // 索引在硬盘里面... Directory directory1 = FSDirectory.open(new File(Constant.FILEURL)); IOContext ioContext = new IOContext(); // 索引放在内存当中... Directory directory = new RAMDirectory(directory1, ioContext); 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("抑郁症"); TopDocs topDocs = indexSearcher.search(query, 100); System.out.println(topDocs.totalHits); }
(5)通过前面的查询条件进行优化