Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包,由资深全文检索专家Doug Cutting所撰写,它是一个全文检索引擎的架构,提供了完整的建立索引和查询索引,以及部分文本分析的引擎,Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便在目标系统中实现全文检索的功能,或者是以此为基础创建起完整的全文检索引擎,Lucene在全文检索领域是一个经典的祖先,如今不少检索引擎都是在其基础上建立的,思想是相通的。css
Lucene是根据关健字来搜索的文本搜索工具,只能在某个网站内部搜索文本内容,不能跨网站搜索java
既然谈到了网站内部的搜索,那么咱们就谈谈咱们熟悉的百度、google那些搜索引擎又是基于什么搜索的呢....程序员
从图上已经看得很清楚,baidu、google等搜索引擎实际上是经过网络爬虫的程序来进行搜索的...算法
在介绍Lucene的时候,咱们已经说了:Lucene又不是搜索引擎,仅仅是在网站内部进行文本的搜索。那咱们为何要学他呢???数据库
咱们以前编写纳税服务系统的时候,其实就已经使用过SQL来进行站内的搜索..apache
既然SQL能作的功能,咱们还要学Lucene,为何呢???微信
咱们来看看咱们用SQL来搜索的话,有什么缺点:网络
咱们来看看在baidu中搜索Lucene为关键字搜索出的内容是怎么样的:架构
以上所说的,咱们若是使用SQL的话,是作不到的。所以咱们就学习Lucene来帮咱们在站内根据文本关键字来进行搜索数据!工具
咱们若是网站须要根据关键字来进行搜索,可使用SQL,也可使用Lucene...那么咱们Lucene和SQL是同样的,都是在持久层中编写代码的。。
接下来,咱们就讲解怎么使用Lucene了.....在讲解Lucene的API以前,咱们首先来说讲Lucene存放的到底是什么内容...咱们的SQL使用的是数据库中的内存,在硬盘中为DBF文件...那么咱们Lucene内部又是什么东西呢??
Lucene中存的就是一系列的二进制压缩文件和一些控制文件,它们位于计算机的硬盘上, 这些内容统称为索引库,索引库有二部份组成:
也就是说:Lucene存放数据的地方咱们一般称之为索引库,索引库又分为两部分组成:原始记录和词汇表....
当咱们想要把数据存到索引库的时候,咱们首先存入的是将数据存到原始记录上面去....
又因为咱们给用户使用的时候,用户使用的是关键字来进行查询咱们的具体记录。所以,咱们须要把咱们原始存进的数据进行拆分!将拆分出来的数据存进词汇表中。
词汇表就是相似于咱们在学Oracle中的索引表,拆分的时候会给出对应的索引值。
一旦用户根据关键字来进行搜索,那么程序就先去查询词汇表中有没有该关键字,若是有该关键字就定位到原始记录表中,将符合条件的原始记录返回给用户查看。
咱们查看如下的图方便理解:
到了这里,有人可能就会疑问:难道原始记录拆分的数据都是一个一个汉字进行拆分的吗??而后在词汇表中不就有不少的关键字了???
其实,咱们在存到原始记录表中的时候,能够指定咱们使用哪一种算法来将数据拆分,存到词汇表中.....咱们的图是Lucene的标准分词算法,一个一个汉字进行拆分。咱们可使用别的分词算法,两个两个拆分或者其余的算法。
首先,咱们来导入Lucene的必要开发包:
建立User对象,User对象封装了数据....
/** * Created by ozc on 2017/7/12. */
public class User {
private String id ;
private String userName;
private String sal;
public User() {
}
public User(String id, String userName, String sal) {
this.id = id;
this.userName = userName;
this.sal = sal;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getSal() {
return sal;
}
public void setSal(String sal) {
this.sal = sal;
}
}
复制代码
咱们想要使用Lucene来查询出站内的数据,首先咱们得要有个索引库吧!因而咱们先建立索引库,将咱们的数据存到索引库中。
建立索引库的步骤:
@Test
public void createIndexDB() throws Exception {
//把数据填充到JavaBean对象中
User user = new User("1", "钟福成", "将来的程序员");
//建立Document对象【导入的是Lucene包下的Document对象】
Document document = new Document();
//将JavaBean对象全部的属性值,均放到Document对象中去,属性名能够和JavaBean相同或不一样
/** * 向Document对象加入一个字段 * 参数一:字段的关键字 * 参数二:字符的值 * 参数三:是否要存储到原始记录表中 * YES表示是 * NO表示否 * 参数四:是否须要将存储的数据拆分到词汇表中 * ANALYZED表示拆分 * NOT_ANALYZED表示不拆分 * * */
document.add(new Field("id", user.getId(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("userName", user.getUserName(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("sal", user.getSal(), Field.Store.YES, Field.Index.ANALYZED));
//建立IndexWriter对象
//目录指定为E:/createIndexDB
Directory directory = FSDirectory.open(new File("E:/createIndexDB"));
//使用标准的分词算法对原始记录表进行拆分
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
//LIMITED默认是1W个
IndexWriter.MaxFieldLength maxFieldLength = IndexWriter.MaxFieldLength.LIMITED;
/** * IndexWriter将咱们的document对象写到硬盘中 * * 参数一:Directory d,写到硬盘中的目录路径是什么 * 参数二:Analyzer a, 以何种算法来对document中的原始记录表数据进行拆分红词汇表 * 参数三:MaxFieldLength mfl 最多将文本拆分出多少个词汇 * * */
IndexWriter indexWriter = new IndexWriter(directory, analyzer, maxFieldLength);
//将Document对象经过IndexWriter对象写入索引库中
indexWriter.addDocument(document);
//关闭IndexWriter对象
indexWriter.close();
}
复制代码
程序执行完,咱们就会在硬盘中见到咱们的索引库。
那咱们如今是不知道记录是否真真正正存储到索引库中的,由于咱们看不见。索引库存放的数据放在cfs文件下,咱们也是不能打开cfs文件的。
因而,咱们如今用一个关键字,把索引库的数据读取。看看读取数据是否成功。
根据关键字查询索引库中的内容:
@Test
public void findIndexDB() throws Exception {
/** * 参数一: IndexSearcher(Directory path)查询以xxx目录的索引库 * * */
Directory directory = FSDirectory.open(new File("E:/createIndexDB"));
//建立IndexSearcher对象
IndexSearcher indexSearcher = new IndexSearcher(directory);
//建立QueryParser对象
/** * 参数一: Version matchVersion 版本号【和上面是同样的】 * 参数二:String f,【要查询的字段】 * 参数三:Analyzer a【使用的拆词算法】 * */
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
QueryParser queryParser = new QueryParser(Version.LUCENE_30, "userName", analyzer);
//给出要查询的关键字
String keyWords = "钟";
//建立Query对象来封装关键字
Query query = queryParser.parse(keyWords);
//用IndexSearcher对象去索引库中查询符合条件的前100条记录,不足100条记录的以实际为准
TopDocs topDocs = indexSearcher.search(query, 100);
//获取符合条件的编号
for (int i = 0; i < topDocs.scoreDocs.length; i++) {
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
//用indexSearcher对象去索引库中查询编号对应的Document对象
Document document = indexSearcher.doc(no);
//将Document对象中的全部属性取出,再封装回JavaBean对象中去
String id = document.get("id");
String userName = document.get("userName");
String sal = document.get("sal");
User user = new User(id, userName, sal);
System.out.println(user);
}
复制代码
效果:
咱们的Lucene程序就是大概这么一个思路:将JavaBean对象封装到Document对象中,而后经过IndexWriter把document写入到索引库中。当用户须要查询的时候,就使用IndexSearcher从索引库中读取数据,找到对应的Document对象,从而解析里边的内容,再封装到JavaBean对象中让咱们使用。
咱们再次看回咱们上一篇快速入门写过的代码,我来截取一些有表明性的:
如下代码在把数据填充到索引库,和从索引库查询数据的时候,都出现了。是重复代码!
Directory directory = FSDirectory.open(new File("E:/createIndexDB"));
//使用标准的分词算法对原始记录表进行拆分
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
复制代码
如下的代码其实就是将JavaBean的数据封装到Document对象中,咱们是能够经过反射来对其进行封装....若是不封装的话,咱们若是有不少JavaBean都要添加到Document对象中,就会出现不少相似的代码。
document.add(new Field("id", user.getId(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("userName", user.getUserName(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("sal", user.getSal(), Field.Store.YES, Field.Index.ANALYZED));
复制代码
如下代码就是从Document对象中把数据取出来,封装到JavaBean去。若是JavaBean中有不少属性,也是须要咱们写不少次相似代码....
//将Document对象中的全部属性取出,再封装回JavaBean对象中去
String id = document.get("id");
String userName = document.get("userName");
String sal = document.get("sal");
User user = new User(id, userName, sal);
复制代码
在编写工具类的时候,值得注意的地方:
import org.apache.commons.beanutils.BeanUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.junit.Test;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/** * Created by ozc on 2017/7/12. */
/** * 使用单例事例模式 * */
public class LuceneUtils {
private static Directory directory;
private static Analyzer analyzer;
private static IndexWriter.MaxFieldLength maxFieldLength;
private LuceneUtils() {}
static{
try {
directory = FSDirectory.open(new File("E:/createIndexDB"));
analyzer = new StandardAnalyzer(Version.LUCENE_30);
maxFieldLength = IndexWriter.MaxFieldLength.LIMITED;
} catch (Exception e) {
e.printStackTrace();
}
}
public static Directory getDirectory() {
return directory;
}
public static Analyzer getAnalyzer() {
return analyzer;
}
public static IndexWriter.MaxFieldLength getMaxFieldLength() {
return maxFieldLength;
}
/** * @param object 传入的JavaBean类型 * @return 返回Document对象 */
public static Document javaBean2Document(Object object) {
try {
Document document = new Document();
//获得JavaBean的字节码文件对象
Class<?> aClass = object.getClass();
//经过字节码文件对象获得对应的属性【所有的属性,不能仅仅调用getFields()】
Field[] fields = aClass.getDeclaredFields();
//获得每一个属性的名字
for (Field field : fields) {
String name = field.getName();
//获得属性的值【也就是调用getter方法获取对应的值】
String method = "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
//获得对应的值【就是获得具体的方法,而后调用就好了。由于是get方法,没有参数】
Method aClassMethod = aClass.getDeclaredMethod(method, null);
String value = aClassMethod.invoke(object).toString();
System.out.println(value);
//把数据封装到Document对象中。
document.add(new org.apache.lucene.document.Field(name, value, org.apache.lucene.document.Field.Store.YES, org.apache.lucene.document.Field.Index.ANALYZED));
}
return document;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/** * @param aClass 要解析的对象类型,要用户传入进来 * @param document 将Document对象传入进来 * @return 返回一个JavaBean */
public static Object Document2JavaBean(Document document, Class aClass) {
try {
//建立该JavaBean对象
Object obj = aClass.newInstance();
//获得该JavaBean全部的成员变量
Field[] fields = aClass.getDeclaredFields();
for (Field field : fields) {
//设置容许暴力访问
field.setAccessible(true);
String name = field.getName();
String value = document.get(name);
//使用BeanUtils把数据封装到Bean中
BeanUtils.setProperty(obj, name, value);
}
return obj;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Test
public void test() {
User user = new User();
LuceneUtils.javaBean2Document(user);
}
}
复制代码
@Test
public void createIndexDB() throws Exception {
//把数据填充到JavaBean对象中
User user = new User("2", "钟福成2", "将来的程序员2");
Document document = LuceneUtils.javaBean2Document(user);
/** * IndexWriter将咱们的document对象写到硬盘中 * * 参数一:Directory d,写到硬盘中的目录路径是什么 * 参数二:Analyzer a, 以何种算法来对document中的原始记录表数据进行拆分红词汇表 * 参数三:MaxFieldLength mfl 最多将文本拆分出多少个词汇 * * */
IndexWriter indexWriter = new IndexWriter(LuceneUtils.getDirectory(), LuceneUtils.getAnalyzer(), LuceneUtils.getMaxFieldLength());
//将Document对象经过IndexWriter对象写入索引库中
indexWriter.addDocument(document);
//关闭IndexWriter对象
indexWriter.close();
}
@Test
public void findIndexDB() throws Exception {
//建立IndexSearcher对象
IndexSearcher indexSearcher = new IndexSearcher(LuceneUtils.getDirectory());
//建立QueryParser对象
QueryParser queryParser = new QueryParser(Version.LUCENE_30, "userName", LuceneUtils.getAnalyzer());
//给出要查询的关键字
String keyWords = "钟";
//建立Query对象来封装关键字
Query query = queryParser.parse(keyWords);
//用IndexSearcher对象去索引库中查询符合条件的前100条记录,不足100条记录的以实际为准
TopDocs topDocs = indexSearcher.search(query, 100);
//获取符合条件的编号
for (int i = 0; i < topDocs.scoreDocs.length; i++) {
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
//用indexSearcher对象去索引库中查询编号对应的Document对象
Document document = indexSearcher.doc(no);
//将Document对象中的全部属性取出,再封装回JavaBean对象中去
User user = (User) LuceneUtils.Document2JavaBean(document, User.class);
System.out.println(user);
}
}
复制代码
咱们已经能够建立索引库而且从索引库读取对象的数据了。其实索引库还有地方能够优化的....
咱们把数据添加到索引库中的时候,每添加一次,都会帮咱们自动建立一个cfs文件...
这样其实很差,由于若是数据量一大,咱们的硬盘就有很是很是多的cfs文件了.....其实索引库会帮咱们自动合并文件的,默认是10个。
若是,咱们想要修改默认的值,咱们能够经过如下的代码修改:
//索引库优化
indexWriter.optimize();
//设置合并因子为3,每当有3个cfs文件,就合并
indexWriter.setMergeFactor(3);
复制代码
咱们的目前的程序是直接与文件进行操做,这样对IO的开销实际上是比较大的。并且速度相对较慢....咱们可使用内存索引库来提升咱们的读写效率...
对于内存索引库而言,它的速度是很快的,由于咱们直接操做内存...可是呢,咱们要将内存索引库是要到硬盘索引库中保存起来的。当咱们读取数据的时候,先要把硬盘索引库的数据同步到内存索引库中去的。
Article article = new Article(1,"培训","传智是一家Java培训机构");
Document document = LuceneUtil.javabean2document(article);
Directory fsDirectory = FSDirectory.open(new File("E:/indexDBDBDBDBDBDBDBDB"));
Directory ramDirectory = new RAMDirectory(fsDirectory);
IndexWriter fsIndexWriter = new IndexWriter(fsDirectory,LuceneUtil.getAnalyzer(),true,LuceneUtil.getMaxFieldLength());
IndexWriter ramIndexWriter = new IndexWriter(ramDirectory,LuceneUtil.getAnalyzer(),LuceneUtil.getMaxFieldLength());
ramIndexWriter.addDocument(document);
ramIndexWriter.close();
fsIndexWriter.addIndexesNoOptimize(ramDirectory);
fsIndexWriter.close();
复制代码
咱们在前面中就已经说过了,在把数据存到索引库的时候,咱们会使用某些算法,将原始记录表的数据存到词汇表中.....那么这些算法总和咱们能够称之为分词器
分词器: ** 采用一种算法,将中英文本中的字符拆分开来,造成词汇,以待用户输入关健字后搜索**
对于为何要使用分词器,咱们也明确地说过:因为用户不可能把咱们的原始记录数据完完整整地记录下来,因而他们在搜索的时候,是经过关键字进行对原始记录表的查询....此时,咱们就采用分词器来最大限度地匹配相关的数据
步一:按分词器拆分出词汇
复制代码
步二:去除停用词和禁用词
复制代码
步三:若是有英文,把英文字母转为小写,即搜索不分大小写
复制代码
咱们在选择分词算法的时候,咱们会发现有很是很是多地分词器API,咱们能够用如下代码来看看该分词器是怎么将数据分割的:
private static void testAnalyzer(Analyzer analyzer, String text) throws Exception {
System.out.println("当前使用的分词器:" + analyzer.getClass());
TokenStream tokenStream = analyzer.tokenStream("content",new StringReader(text));
tokenStream.addAttribute(TermAttribute.class);
while (tokenStream.incrementToken()) {
TermAttribute termAttribute = tokenStream.getAttribute(TermAttribute.class);
System.out.println(termAttribute.term());
}
}
复制代码
在实验完以后,咱们就能够选择恰当的分词算法了....
这是一个第三方的分词器,咱们若是要使用的话须要导入对应的jar包
这个第三方的分词器有什么好呢????他是中文首选的分词器...也就是说:他是按照中文的词语来进行拆分的!
咱们在使用SQL时,搜索出来的数据是没有高亮的...而咱们使用Lucene,搜索出来的内容咱们能够设置关键字为高亮...这样一来就更加注重用户体验了!
String keywords = "钟福成";
List<Article> articleList = new ArrayList<Article>();
QueryParser queryParser = new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer());
Query query = queryParser.parse(keywords);
IndexSearcher indexSearcher = new IndexSearcher(LuceneUtil.getDirectory());
TopDocs topDocs = indexSearcher.search(query,1000000);
//设置关键字高亮
Formatter formatter = new SimpleHTMLFormatter("<font color='red'>","</font>");
Scorer scorer = new QueryScorer(query);
Highlighter highlighter = new Highlighter(formatter,scorer);
for(int i=0;i<topDocs.scoreDocs.length;i++){
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
Document document = indexSearcher.doc(no);
//设置内容高亮
String highlighterContent = highlighter.getBestFragment(LuceneUtil.getAnalyzer(),"content",document.get("content"));
document.getField("content").setValue(highlighterContent);
Article article = (Article) LuceneUtil.document2javabean(document,Article.class);
articleList.add(article);
}
for(Article article : articleList){
System.out.println(article);
}
}
复制代码
若是咱们搜索出来的文章内容太大了,而咱们只想显示部分的内容,那么咱们能够对其进行摘要...
值得注意的是:搜索结果摘要须要与设置高亮一块儿使用
String keywords = "钟福成";
List<Article> articleList = new ArrayList<Article>();
QueryParser queryParser = new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer());
Query query = queryParser.parse(keywords);
IndexSearcher indexSearcher = new IndexSearcher(LuceneUtil.getDirectory());
TopDocs topDocs = indexSearcher.search(query,1000000);
Formatter formatter = new SimpleHTMLFormatter("<font color='red'>","</font>");
Scorer scorer = new QueryScorer(query);
Highlighter highlighter = new Highlighter(formatter,scorer);
//设置摘要
Fragmenter fragmenter = new SimpleFragmenter(4);
highlighter.setTextFragmenter(fragmenter);
for(int i=0;i<topDocs.scoreDocs.length;i++){
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
Document document = indexSearcher.doc(no);
String highlighterContent = highlighter.getBestFragment(LuceneUtil.getAnalyzer(),"content",document.get("content"));
document.getField("content").setValue(highlighterContent);
Article article = (Article) LuceneUtil.document2javabean(document,Article.class);
articleList.add(article);
}
for(Article article : articleList){
System.out.println(article);
}
}
复制代码
咱们搜索引擎确定用得也很多,使用不一样的搜索引擎来搜索相同的内容。他们首页的排行顺序也会不一样...这就是它们内部用了搜索结果排序....
影响网页的排序有很是多种:
head/meta/【keywords关键字】
复制代码
网页的标签整洁
复制代码
网页执行速度
复制代码
采用div+css
复制代码
等等等等
复制代码
而在Lucene中咱们就能够设置相关度得分来使不一样的结果对其进行排序:
IndexWriter indexWriter = new IndexWriter(LuceneUtil.getDirectory(),LuceneUtil.getAnalyzer(),LuceneUtil.getMaxFieldLength());
//为结果设置得分
document.setBoost(20F);
indexWriter.addDocument(document);
indexWriter.close();
复制代码
固然了,咱们也能够按单个字段排序:
//true表示降序
Sort sort = new Sort(new SortField("id",SortField.INT,true));
TopDocs topDocs = indexSearcher.search(query,null,1000000,sort);
复制代码
也能够按多个字段排序:在多字段排序中,只有第一个字段排序结果相同时,第二个字段排序才有做用 提倡用数值型排序
Sort sort = new Sort(new SortField("count",SortField.INT,true),new SortField("id",SortField.INT,true));
TopDocs topDocs = indexSearcher.search(query,null,1000000,sort);
复制代码
在咱们的例子中,咱们使用的是根据一个关键字来对某个字段的内容进行搜索。语法相似于下面:
QueryParser queryParser = new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer());
复制代码
其实,咱们也可使用关键字来对多个字段进行搜索,也就是多条件搜索。咱们实际中经常用到的是多条件搜索,多条件搜索可使用咱们最大限度匹配对应的数据!
QueryParser queryParser = new MultiFieldQueryParser(LuceneUtil.getVersion(),new String[]{"content","title"},LuceneUtil.getAnalyzer());
复制代码
这篇这是Lucene的冰山一角,通常如今用的可能都是Solr、Elasticsearch的了,但想要更加深刻了解Lucene可翻阅其余资料哦~
若是文章有错的地方欢迎指正,你们互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同窗,能够关注微信公众号:Java3y