博客地址:http://www.moonxy.comhtml
1、前言java
Lucene 是 apache 软件基金会的一个子项目,由 Doug Cutting 开发,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的库,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene 的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础创建起完整的全文检索引擎。Lucene 是一套用于全文检索和搜寻的开源程式库,由 Apache 软件基金会支持和提供。python
Lucene 提供了一个简单却强大的应用程式接口,可以作全文索引和搜寻。在 Java 开发环境里 Lucene 是一个成熟的免费开源工具。就其自己而言,Lucene 是当前以及最近几年最受欢迎的免费 Java 信息检索程序库。人们常常提到信息检索程序库,虽然与搜索引擎有关,但不该该将信息检索程序库与搜索引擎相混淆。git
最开始 Lucene 只由 java 开发,供 java 程序调用,随着 python 愈来愈火,Lucene 官网也提供了 python 版本的 lucene 库,供 python 程序调用,即 PyLucene。github
2、下载 Lucene算法
2.1 下载数据库
访问 Lucene 官网 http://lucene.apache.org/,能够看到 绿色和红色两个下载按钮,分别提供 Lucene 和 Solr 的下载。apache
这里简要说明一下 Lucene 和 Solr,Lucene 是一个作全文检索的库,开发者能够按照本身的实际业务需求来使用,而 Solr 是一个基于 Lucene 的全文检索服务器。Solr 是在 Lucene 的基础上进行扩展,而且提供了更加丰富的查询语句,可扩展性和可配置性比 Lucene 更高。除此以外 Solr 还提供了一个完善的管理页面,是一个产品级的全文搜索引擎。数组
官网首页提供了最新版本的下载连接,若是须要下载使用历史版本,能够访问 http://archive.apache.org/dist/lucene/java/,能够下载 Lucene 全部的发行版本。此处下载 6.6.0 版本。服务器
2.2 添加依赖
将下载的 Lucene 包解压以后,找到以下的 jar 包,新建本身的工程,此处不使用 Maven,因此手动添加 jar 包到工程的 lib 目录下,以下:
IKAnalyzer2012_u6.jar,此 jar 包在 IK 分词器项目中,是单独的一个工具包,须要额外在网上下载:IKAnalyzer2012_u6。IK 分词器采用了特有的 "正向迭代最细粒度切分算法",即从左到右的 正向最大(最长)和最小(最短)匹配,支持细粒度和智能分词两种切分模式,可将分词器扩展配置文件 IKAnalyzer.cfg.xml 放在项目的 class 根目录,并在其中配置扩展词典路径。当 IKAnalyzer6x() 构造方法参数为空或者 false 时,是最细粒度分词,为 true 时是智能分词。
lucene-analyzers-common-6.6.0.jar:lucene-6.6.0/common/
lucene-analyzers-smartcn-6.6.0.jar:lucene-6.6.0/smartcn/
lucene-core-6.6.0.jar:lucene-6.6.0/core/
lucene-highlighter-6.6.0.jar:lucene-6.6.0/highlighter/
lucene-memory-6.6.0.jar:lucene-6.6.0/memory/
lucene-queries-6.6.0.jar:lucene-6.6.0/queries/
lucene-queryparser-6.6.0.jar:lucene-6.6.0/queryparser/
2.3 Lucene 架构
首先是信息采集的过程,文件系统、数据库、万维网以及手工输入的文件均可以做为信息采集的对象,也是要搜索的文档的来源,采集万维网上的信息通常使用网络爬虫。完成信息采集以后到 Lucene 层面主要有两个任务:索引文档和搜索文档。
索引文档的过程完成由原始文档到倒排索引的构建过程;
搜索文档用以处理用户查询。而后当用户输入查询关键词,Lucene 完成文档搜索任务,通过分词、匹配、评分、排序等一系列过程以后返回用户想要的文档。
倒排索引(Inverted index),也常被称为反向索引,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射,它是文档检索系统中最经常使用的数据结构,包括词项所在的文章号、词项频率、词项位置等。
3、Lucene 索引详解
3.1 建立实体模型
建立新闻实体类模型
package tup.lucene.index; /** * 新闻实体类 * @author moonxy * */ public class News { private int id;//新闻id private String title;//新闻标题 private String content;//新闻内容 private int reply;//评论数 public News() { } public News(int id, String title, String content, int reply) { super(); this.id = id; this.title = title; this.content = content; this.reply = reply; } // 省略 setter 和 getter 方法 }
3.2 建立索引
Lucene 索引文档须要依靠 IndexWriter 对象,建立 IndexWriter 须要两个参数:一个是 IndexWriterConfig 对象,该对象能够设置建立索引使用哪一种分词器,另外一个是索引的保存路径。IndexWriter 对象的 addDocument() 方法用于添加文档,该方法的参数为 Document 对象,IndexWriter 对象一次能够添加多个文档,最后调用 commit() 方法生成索引。
package tup.lucene.index; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Date; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.FieldType; import org.apache.lucene.document.IntPoint; import org.apache.lucene.document.StoredField; import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.IndexWriterConfig.OpenMode; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import tup.lucene.ik.IKAnalyzer6x; /** * Lucene 建立索引 * @author moonxy * */ public class CreateIndex { public static void main(String[] args) { // 建立3个News对象 News news1 = new News(); news1.setId(1); news1.setTitle("安倍晋三本周会晤特朗普 将强调日本对美国益处"); news1.setContent("日本首相安倍晋三计划2月10日在华盛顿与美国总统特朗普举行会晤时提出加大日本在美国投资的设想"); news1.setReply(672); News news2 = new News(); news2.setId(2); news2.setTitle("北大迎4380名新生 农村学生700多人近年最多"); news2.setContent("昨天,北京大学迎来4380名来自全国各地及数十个国家的本科新生。其中,农村学生共700余名,为近年最多..."); news2.setReply(995); News news3 = new News(); news3.setId(3); news3.setTitle("特朗普宣誓(Donald Trump)就职美国第45任总统"); news3.setContent("当地时间1月20日,唐纳德·特朗普在美国国会宣誓就任,正式成为美国第45任总统。"); news3.setReply(1872); // 开始时间 Date start = new Date(); System.out.println("**********开始建立索引**********"); // 建立IK分词器 Analyzer analyzer = new IKAnalyzer6x();//使用IK最细粒度分词 IndexWriterConfig icw = new IndexWriterConfig(analyzer);
// CREATE 表示先清空索引再从新建立 icw.setOpenMode(OpenMode.CREATE); Directory dir = null; IndexWriter inWriter = null; // 存储索引的目录 Path indexPath = Paths.get("indexdir"); try { if (!Files.isReadable(indexPath)) { System.out.println("索引目录 '" + indexPath.toAbsolutePath() + "' 不存在或者不可读,请检查"); System.exit(1); } dir = FSDirectory.open(indexPath); inWriter = new IndexWriter(dir, icw); // 设置新闻ID索引并存储 FieldType idType = new FieldType(); idType.setIndexOptions(IndexOptions.DOCS); idType.setStored(true); // 设置新闻标题索引文档、词项频率、位移信息和偏移量,存储并词条化 FieldType titleType = new FieldType(); titleType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS); titleType.setStored(true); titleType.setTokenized(true); FieldType contentType = new FieldType(); contentType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS); contentType.setStored(true); contentType.setTokenized(true); contentType.setStoreTermVectors(true); contentType.setStoreTermVectorPositions(true); contentType.setStoreTermVectorOffsets(true); Document doc1 = new Document(); doc1.add(new Field("id", String.valueOf(news1.getId()), idType)); doc1.add(new Field("title", news1.getTitle(), titleType)); doc1.add(new Field("content", news1.getContent(), contentType)); doc1.add(new IntPoint("reply", news1.getReply())); doc1.add(new StoredField("reply_display", news1.getReply())); Document doc2 = new Document(); doc2.add(new Field("id", String.valueOf(news2.getId()), idType)); doc2.add(new Field("title", news2.getTitle(), titleType)); doc2.add(new Field("content", news2.getContent(), contentType)); doc2.add(new IntPoint("reply", news2.getReply())); doc2.add(new StoredField("reply_display", news2.getReply())); Document doc3 = new Document(); doc3.add(new Field("id", String.valueOf(news3.getId()), idType)); doc3.add(new Field("title", news3.getTitle(), titleType)); doc3.add(new Field("content", news3.getContent(), contentType)); doc3.add(new IntPoint("reply", news3.getReply())); doc3.add(new StoredField("reply_display", news3.getReply())); inWriter.addDocument(doc1); inWriter.addDocument(doc2); inWriter.addDocument(doc3); inWriter.commit(); inWriter.close(); dir.close(); } catch (IOException e) { e.printStackTrace(); } Date end = new Date(); System.out.println("索引文档用时:" + (end.getTime() - start.getTime()) + " milliseconds"); System.out.println("**********索引建立完成**********"); } }
执行以后,在控制台输出以下:
**********开始建立索引********** 加载扩展词典:dict/ext.dic 加载扩展中止词典:dict/stopword.dic 加载扩展中止词典:dict/ext_stopword.dic 索引文档用时:1064 milliseconds **********索引建立完成**********
而且在项目中生成以下索引文件:
3.3 Luke 查看索引
索引建立完成之后生成了如上的一批特殊格式的文件,若是直接用工具打开,会显示的都是乱码。可使用索引查看工具 Luke 来查看。
Luke 是开源工具,代码托管在 GitHub 上,项目地址:https://github.com/DmitryKey/luke/releases,此处下载 luke 6.6.0,地址为 https://github.com/DmitryKey/luke/releases/download/luke-6.6.0/luke-6.6.0-luke-release.zip,若是在 Windows 中没法下载,能够在 Linux 中使用 wget 下载,命令为:wget https://github.com/DmitryKey/luke/releases/download/luke-6.6.0/luke-6.6.0-luke-release.zip。
下载后解压,进入 luke 目录,若是是在 Linux 平台,运行 luke.bat 便可启动软件,并在 Path 中输入 index 存储的目录,便可打开索引文件,显示出索引的具体内容。
注意:对于不一样版本的 Lucene,须要选择对应版本的 Luke,不然可能会出现不能正常解析的错误。
3.4 Lucene 查询详解
在 Lucene 中,处理用户输入的查询关键词其实就是构建 Query 对象的过程。Lucene 搜索文档须要先读入索引文件,实例化一个 IndexReader 对象,而后实例化出 IndexSearch 对象,IndexSearch 对象的 search() 方法完成搜索过程,Query 对象做为 search() 方法的对象。搜索结果保存在一个 TopDocs 类型的文档集合中,遍历 TopDocs 集合输出文档信息。
QueryParser 能够搜索单个字段,而 MultiFieldQueryParser 则能够查询多个字段,而且多个字段之间是或的关系,因此在开发中,MultiFieldQueryParser 使用的较多。
package tup.lucene.queries; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.queryparser.classic.QueryParser.Operator; 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.store.Directory; import org.apache.lucene.store.FSDirectory; import tup.lucene.ik.IKAnalyzer6x; /** * 单域搜索 * @author moonxy * */ public class QueryParseTest { public static void main(String[] args) throws ParseException, IOException { // 搜索单个字段 String field = "title"; // 搜索多个字段时使用数组 //String[] fields = { "title", "content" }; Path indexPath = Paths.get("indexdir"); Directory dir = FSDirectory.open(indexPath); IndexReader reader = DirectoryReader.open(dir); IndexSearcher searcher = new IndexSearcher(reader); Analyzer analyzer = new IKAnalyzer6x(false);//最细粒度分词 QueryParser parser = new QueryParser(field, analyzer); // 多域搜索 //MultiFieldQueryParser multiParser = new MultiFieldQueryParser(fields, analyzer); // 关键字同时成立使用 AND, 默认是 OR parser.setDefaultOperator(Operator.AND); // 查询语句 Query query = parser.parse("农村学生");//查询关键词 System.out.println("Query:" + query.toString()); // 返回前10条 TopDocs tds = searcher.search(query, 10); for (ScoreDoc sd : tds.scoreDocs) { // Explanation explanation = searcher.explain(query, sd.doc); // System.out.println("explain:" + explanation + "\n"); Document doc = searcher.doc(sd.doc); System.out.println("DocID:" + sd.doc); System.out.println("id:" + doc.get("id")); System.out.println("title:" + doc.get("title")); System.out.println("content:" + doc.get("content")); System.out.println("文档评分:" + sd.score); } dir.close(); reader.close(); } }
控制台输出以下:
加载扩展词典:dict/ext.dic 加载扩展中止词典:dict/stopword.dic 加载扩展中止词典:dict/ext_stopword.dic Query:+title:农村 +title:村学 +title:学生 DocID:1 id:2 title:北大迎4380名新生 农村学生700多人近年最多 content:昨天,北京大学迎来4380名来自全国各地及数十个国家的本科新生。其中,农村学生共700余名,为近年最多... 文档评分:2.320528
注意,在结果中打印了 DocID 和 id,前者是文档的 ID,是 Lucene 为索引的每一个文档标记,后者是文档自定义的 id 字段。
3.5 Lucene 查询高亮
高亮功能一直都是全文检索的一项很是优秀的模块,在一个标准的搜索引擎中,高亮的返回命中结果,几乎是必不可少的一项需求,由于经过高亮,能够在搜索界面上快速标记出用户的搜索关键字,从而减小了用户本身寻找想要的结果的时间,在必定程度上大大提升了用户的体验性和友好度。
Highlight 包含3个主要部分:
1)段划分器:Fragmenter
2)计分器:Score
3)格式化器:Formatter
package tup.lucene.highlfighter; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.queryparser.classic.ParseException; 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.Fragmenter; import org.apache.lucene.search.highlight.Highlighter; import org.apache.lucene.search.highlight.InvalidTokenOffsetsException; import org.apache.lucene.search.highlight.QueryScorer; import org.apache.lucene.search.highlight.SimpleHTMLFormatter; import org.apache.lucene.search.highlight.SimpleSpanFragmenter; import org.apache.lucene.search.highlight.TokenSources; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import tup.lucene.ik.IKAnalyzer6x; /** * Lucene查询高亮 * @author moonxy * */ public class HighlighterTest { public static void main(String[] args) throws IOException, InvalidTokenOffsetsException, ParseException { String field = "title"; Path indexPath = Paths.get("indexdir"); Directory dir = FSDirectory.open(indexPath); IndexReader reader = DirectoryReader.open(dir); IndexSearcher searcher = new IndexSearcher(reader); Analyzer analyzer = new IKAnalyzer6x(); QueryParser parser = new QueryParser(field, analyzer); Query query = parser.parse("北大学生"); System.out.println("Query:" + query); // 查询高亮 QueryScorer score = new QueryScorer(query, field); SimpleHTMLFormatter fors = new SimpleHTMLFormatter("<span style=\"color:red;\">", "</span>");// 定制高亮标签 Highlighter highlighter = new Highlighter(fors, score);// 高亮分析器 // 返回前10条 TopDocs tds = searcher.search(query, 10); for (ScoreDoc sd : tds.scoreDocs) { // Explanation explanation = searcher.explain(query, sd.doc); // System.out.println("explain:" + explanation + "\n"); Document doc = searcher.doc(sd.doc); System.out.println("id:" + doc.get("id")); System.out.println("title:" + doc.get("title")); Fragmenter fragment = new SimpleSpanFragmenter(score); highlighter.setTextFragmenter(fragment); // TokenStream tokenStream = TokenSources.getAnyTokenStream(searcher.getIndexReader(), sd.doc, field, analyzer);// 获取tokenstream // String str = highlighter.getBestFragment(tokenStream, doc.get(field));// 获取高亮的片断 String str = highlighter.getBestFragment(analyzer, field, doc.get(field));// 获取高亮的片断 System.out.println("高亮的片断:" + str); } dir.close(); reader.close(); } }
控制台输出以下:
加载扩展词典:dict/ext.dic 加载扩展中止词典:dict/stopword.dic 加载扩展中止词典:dict/ext_stopword.dic Query:title:北大学生 title:北大 title:大学生 title:大学 title:学生 id:2 title:北大迎4380名新生 农村学生700多人近年最多 高亮的片断:<span style="color:red;">北大</span>迎4380名新生 农村<span style="color:red;">学生</span>700多人近年最多
4、Tika 文件内容提取
Apache Tika 是一个用于文件类型检测和文件内容提取的库,是 Apache 软件基金会的项目。Tika 能够检测操做 1000 种不一样类型的文档,好比 PPT、PDF、DOC、XLS 等,全部的文本类型均可以经过一个简单的接口被解析,Tika 普遍应用于搜索引擎、内容分析、文本翻译等领域。
Tika 下载的官网地址:http://tika.apache.org/download.html,其历史版本下载地址为:http://archive.apache.org/dist/tika/,此处如今当前最新版 tika-app-1.18.jar
下载下来以后为一个 jar 包,但 Tika 可做为 GUI 工具使用,在 CMD 中先进入下载目录,而后使用以下命令启动 Tika GUI:
java -jar tika-app-1.18.jar -g
java -jar 表示启动 jar 包,后面跟 jar 包的名字,-g(--gui) 参数表示以 GUI 的方式启动 Tika(Start the Apache Tika GUI),固然前提是已经配置好了 java 环境变量。
界面以下:
能够点击 File 菜单中的 Open...来打开一个本地文件或者输入 URL 来打开远程文件,也可直接将本地文件拖入到 Tika,Tika 将会自动识别文件类型并显示文件信息。
默认显示文件的元数据信息,以下:
点击 View 菜单,里面能够选择查看文件的具体内容,如选择其中的 Plain Text,显示以下:
将该 jar 包放入项目 lib 中,调用相应的接口就能够提取不一样文件的内容,主要分为以下提取方法。
方法一:使用 Tika 对象提取文档内容
package tup.tika.demo; import java.io.File; import java.io.IOException; import org.apache.tika.Tika; import org.apache.tika.exception.TikaException; /** * 使用Tika对象自动解析文档,提取文档内容 * @author moonxy * */ public class TikaExtraction { public static void main(String[] args) throws IOException, TikaException { Tika tika = new Tika(); // 新建存放各类文件的files文件夹 File fileDir = new File("files"); // 若是文件夹路径错误,退出程序 if (!fileDir.exists()) { System.out.println("文件夹不存在, 请检查!"); System.exit(0); } // 获取文件夹下的全部文件,存放在File数组中 File[] fileArr = fileDir.listFiles(); String filecontent; for (File f : fileArr) { // 获取文件名 System.out.println("File Name: " + f.getName()); filecontent = tika.parseToString(f);// 自动解析 // 获取文件内容 System.out.println("Extracted Content: " + filecontent); } } }
在工程中新建 Files 目录,放入须要解析的文件:
上述代码中首先新建一个 File 对象指向存放各类文档的文件夹 Files,经过 File 对象的 exists() 方法判断目录路径是否存在,若是路径错误则退出程序,打印提示信息。接下来,经过 listFiles() 方法获取 files 目录下全部的文件,存放在文件数组中。最后新建一个 Tika 对象,调用 parseToString() 方法获取文档内容,该方法的传入参数为 File 对象。
方法二:使用 Parser 接口提取文档内容
package tup.tika.demo; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import org.apache.tika.exception.TikaException; import org.apache.tika.metadata.Metadata; import org.apache.tika.parser.AutoDetectParser; import org.apache.tika.parser.ParseContext; import org.apache.tika.parser.Parser; import org.apache.tika.sax.BodyContentHandler; import org.xml.sax.SAXException; /** * 使用Parser接口自动解析文档,提取文档内容 * @author moonxy * */ public class ParserExtraction { public static void main(String[] args) throws IOException, SAXException, TikaException { // 新建存放各类文件的files文件夹 File fileDir = new File("files"); // 若是文件夹路径错误,退出程序 if (!fileDir.exists()) { System.out.println("文件夹不存在, 请检查!"); System.exit(0); } // 获取文件夹下的全部文件,存放在File数组中 File[] fileArr = fileDir.listFiles(); // 建立内容处理器对象 BodyContentHandler handler = new BodyContentHandler(); // 建立元数据对象 Metadata metadata = new Metadata(); FileInputStream inputStream = null; Parser parser = new AutoDetectParser(); // 自动检测分析器 ParseContext context = new ParseContext(); for (File f : fileArr) { // 获取文件名 System.out.println("File Name: " + f.getName()); inputStream = new FileInputStream(f); parser.parse(inputStream, handler, metadata, context); // 获取文件内容 System.out.println(f.getName() + ":\n" + handler.toString()); } } }
使用 Parse 接口自动提取内容和单一的提取一种文档的区别在于实例化对象不同,AutoDetectParser 是 CompositeParser 的子类,它可以自动检测文件类型,并使用相应的方法把接收到的文档自动发送给最接近的解析器类。
能够参考官方文档,将文档解析为不一样的格式,如上面都是解析为纯文本格式(Plain text),也可解析为 html 格式(Structured text)等,如:
http://tika.apache.org/1.18/examples.html#Parsing_to_XHTML