Lucene系列(三)查询及高亮
系列文章:
<font color="#0066CC">一 准备</font>
<font color="#00CC00">创建项目并添加Maven依赖</font>
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-core --> <!-- Lucene核心库 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>7.2.1</version> </dependency> <!-- Lucene解析库 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-queryparser</artifactId> <version>7.2.1</version> </dependency> <!-- Lucene附加的分析库 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>7.2.1</version> </dependency> <!-- 高亮显示 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-highlighter</artifactId> <version>7.2.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-analyzers-smartcn --> <!-- 中文分词 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-smartcn</artifactId> <version>7.2.0</version> </dependency> </dependencies>
<font color="#0066CC">二 对特定单词查询/模糊查询和查询表达式</font>
<font color="#00CC00">写索引</font>
import java.io.File; import java.io.FileReader; import java.nio.file.Paths; 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.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; public class Indexer { private IndexWriter writer; // 写索引实例 /** * 构造方法 实例化IndexWriter * @param indexDir * @throws Exception */ public Indexer(String indexDir)throws Exception{ Directory dir=FSDirectory.open(Paths.get(indexDir)); Analyzer analyzer=new StandardAnalyzer(); // 标准分词器 IndexWriterConfig iwc=new IndexWriterConfig(analyzer); writer=new IndexWriter(dir, iwc); } /** * 关闭写索引 * @throws Exception */ public void close()throws Exception{ writer.close(); } /** * 索引指定目录的所有文件 * @param dataDir * @throws Exception */ public int index(String dataDir)throws Exception{ File []files=new File(dataDir).listFiles(); for(File f:files){ indexFile(f); } return writer.numDocs(); } /** * 索引指定文件 * @param f */ private void indexFile(File f) throws Exception{ System.out.println("索引文件:"+f.getCanonicalPath()); Document doc=getDocument(f); writer.addDocument(doc); } /** * 获取文档,文档里再设置每个字段 * @param f */ private Document getDocument(File f)throws Exception { Document doc=new Document(); doc.add(new TextField("contents",new FileReader(f))); doc.add(new TextField("fileName", f.getName(),Field.Store.YES)); doc.add(new TextField("fullPath",f.getCanonicalPath(),Field.Store.YES)); return doc; } public static void main(String[] args) { String indexDir="D:\\lucene\\searchindex"; String dataDir="D:\\lucene\\data"; Indexer indexer=null; int numIndexed=0; long start=System.currentTimeMillis(); try { indexer = new Indexer(indexDir); numIndexed=indexer.index(dataDir); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally{ try { indexer.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } long end=System.currentTimeMillis(); System.out.println("索引:"+numIndexed+" 个文件 花费了"+(end-start)+" 毫秒"); } }
<font color="#00CC00">读取索引</font>
import java.nio.file.Paths; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; 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.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.junit.After; import org.junit.Before; import org.junit.Test; public class SearchTest { private Directory dir; private IndexReader reader; private IndexSearcher is; @Before public void setUp() throws Exception { dir=FSDirectory.open(Paths.get("D:\\lucene\\searchindex")); reader=DirectoryReader.open(dir); is=new IndexSearcher(reader); } @After public void tearDown() throws Exception { reader.close(); } }
<font color="#00CC00">对特定单词查询和模糊查询</font>
/** * 对特定单词查询及模糊查询 * * @throws Exception */ @Test public void testTermQuery() throws Exception { String searchField = "contents"; // 所给出的必须是单词,不然差不到 String q = "authorship"; // 一个Term表示来自文本的一个单词。 Term t = new Term(searchField, q); // 为Term构造查询。 Query query = new TermQuery(t); /** * 1.需要根据条件查询 * * 2.最大可编辑数,取值范围0,1,2 * 允许我的查询条件的值,可以错误几个字符 * */ Query query2 = new FuzzyQuery(new Term(searchField,"authorshioo"),1); TopDocs hits = is.search(query, 10); // hits.totalHits:查询的总命中次数。即在几个文档中查到给定单词 System.out.println("匹配 '" + q + "',总共查询到" + hits.totalHits + "个文档"); for (ScoreDoc scoreDoc : hits.scoreDocs) { Document doc = is.doc(scoreDoc.doc); System.out.println(doc.get("fullPath")); } TopDocs hits2 = is.search(query2, 10); // hits.totalHits:查询的总命中次数。即在几个文档中查到给定单词 System.out.println("匹配 '" + "authorshioo"+ "',总共查询到" + hits2.totalHits + "个文档"); for (ScoreDoc scoreDoc : hits2.scoreDocs) { Document doc = is.doc(scoreDoc.doc); System.out.println(doc.get("fullPath")); } }
我们上面查询了单词“authorship”以及模糊查询了单词"authorshioo",结果如下:
可以看到只在LICENSE.txt文档下找到该单词。
那么模糊查询为什么查不到单词"authorshioo"呢?
这是因为我们在这里允许可以错误几个字符为1个,但是我们单词"authorshioo"错误字符个数为2个,所以就查不到。
Query query2 = new FuzzyQuery(new Term(searchField,"authorshioo"),1);
<font color="#00CC00">解析表达式的使用</font>
/** * 解析查询表达式 * * @throws Exception */ @Test public void testQueryParser() throws Exception { // 标准分词器 Analyzer analyzer = new StandardAnalyzer(); String searchField = "contents"; String q = "atomic a atomicReader"; String q2 = "AtomicReader and AtomicReaderContext"; // 建立查询解析器 //searchField:要查询的字段; //analyzer:标准分词器实例 QueryParser parser = new QueryParser(searchField, analyzer); Query query = parser.parse(q); //返回查询到的前10项(查到100个相关内容的话也只会返回10个) TopDocs hits = is.search(query, 10); System.out.println("匹配 " + q + "查询到" + hits.totalHits + "个记录"); for (ScoreDoc scoreDoc : hits.scoreDocs) { Document doc = is.doc(scoreDoc.doc); System.out.println(doc.get("fullPath")); } QueryParser parser2 = new QueryParser(searchField, analyzer); Query query2 = parser2.parse(q2); //返回查询到的前10项(查到100个相关内容的话也只会返回10个) TopDocs hits2 = is.search(query2, 10); System.out.println("匹配 " + q2 + "查询到" + hits2.totalHits + "个记录"); for (ScoreDoc scoreDoc : hits2.scoreDocs) { Document doc = is.doc(scoreDoc.doc); System.out.println(doc.get("fullPath")); } }
我们上面分别查询了:“atomic a atomicReader”和“AtomicReader and AtomicReaderContext”,通过查询结果可以看出即使稍微改变查询内容,也还是可以查询到和我们给出的表达式相关的文档。
<font color="#0066CC">三 中文查询及高亮</font>
<font color="#00CC00">写索引</font>
import java.nio.file.Paths; import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.StringField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; public class Indexer { private String[] ids={"1","2","3"}; private String citys[]={"青岛","南京","上海"}; private String descs[]={ "青岛是一个漂亮的城市。", "南京是一个文化的城市。", "上海是一个繁华的城市。" }; private Directory dir; /** *实例化indexerWriter * @return * @throws Exception */ private IndexWriter getWriter()throws Exception{ //中文分词器 SmartChineseAnalyzer analyzer=new SmartChineseAnalyzer(); IndexWriterConfig iwc=new IndexWriterConfig(analyzer); IndexWriter writer=new IndexWriter(dir, iwc); return writer; } /** * 获取indexDir * @param indexDir * @throws Exception */ private void index(String indexDir)throws Exception{ dir=FSDirectory.open(Paths.get(indexDir)); IndexWriter writer=getWriter(); for(int i=0;i<ids.length;i++){ Document doc=new Document(); doc.add(new StringField("id", ids[i], Field.Store.YES)); doc.add(new StringField("city",citys[i],Field.Store.YES)); doc.add(new TextField("desc", descs[i], Field.Store.YES)); writer.addDocument(doc); } writer.close(); } public static void main(String[] args) throws Exception { new Indexer().index("D:\\lucene\\dataindex2"); System.out.println("Success Indexer"); } }
<font color="#00CC00">中文查询及高亮显示</font>
import java.io.StringReader; import java.nio.file.Paths; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; 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.QueryScorer; import org.apache.lucene.search.highlight.SimpleHTMLFormatter; import org.apache.lucene.search.highlight.SimpleSpanFragmenter; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; /** * * 通过索引字段来读取文档 * @author LXY * */ public class SearchTest { public static void search(String indexDir, String par) throws Exception{ //得到读取索引文件的路径 Directory dir = FSDirectory.open(Paths.get(indexDir)); //通过dir得到的路径下的所有的文件 IndexReader reader = DirectoryReader.open(dir); //建立索引查询器 IndexSearcher searcher = new IndexSearcher(reader); //中文分词器 SmartChineseAnalyzer analyzer=new SmartChineseAnalyzer(); //建立查询解析器 /** * 第一个参数是要查询的字段; * 第二个参数是分析器Analyzer * */ QueryParser parser = new QueryParser("desc", analyzer); //根据传进来的par查找 Query query = parser.parse(par); //计算索引开始时间 long start = System.currentTimeMillis(); //开始查询 /** * 第一个参数是通过传过来的参数来查找得到的query; * 第二个参数是要出查询的行数 * */ TopDocs topDocs = searcher.search(query, 10); //索引结束时间 long end = System.currentTimeMillis(); System.out.println("匹配"+par+",总共花费了"+(end-start)+"毫秒,共查到"+topDocs.totalHits+"条记录。"); //高亮显示start //算分 QueryScorer scorer=new QueryScorer(query); //显示得分高的片段 Fragmenter fragmenter=new SimpleSpanFragmenter(scorer); //设置标签内部关键字的颜色 //第一个参数:标签的前半部分;第二个参数:标签的后半部分。 SimpleHTMLFormatter simpleHTMLFormatter=new SimpleHTMLFormatter("<b><font color='red'>","</font></b>"); //第一个参数是对查到的结果进行实例化;第二个是片段得分(显示得分高的片段,即摘要) Highlighter highlighter=new Highlighter(simpleHTMLFormatter, scorer); //设置片段 highlighter.setTextFragmenter(fragmenter); //高亮显示end //遍历topDocs /** * ScoreDoc:是代表一个结果的相关度得分与文档编号等信息的对象。 * scoreDocs:代表文件的数组 * @throws Exception * */ for(ScoreDoc scoreDoc : topDocs.scoreDocs){ //获取文档 Document document = searcher.doc(scoreDoc.doc); //输出全路径 System.out.println(document.get("city")); System.out.println(document.get("desc")); String desc = document.get("desc"); if(desc!=null){ //把全部得分高的摘要给显示出来 //第一个参数是对哪个参数进行设置;第二个是以流的方式读入 TokenStream tokenStream=analyzer.tokenStream("desc", new StringReader(desc)); //获取最高的片段 System.out.println(highlighter.getBestFragment(tokenStream, desc)); } } reader.close(); } //开始测试 public static void main(String[] args) { //索引指定的路径 String indexDir = "D:\\lucene\\dataindex2"; //查询的字段 String par = "南京"; try { search(indexDir,par); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
结果会把我们查询的“南京”单词给高亮显示,这在我们平时搜索中很常见了。
我们平时搜索中的高亮就像下图:
欢迎关注我的微信公众号(坚持原创,分享美文,分享各种Java学习资源,面试题,以及企业级Java实战项目回复关键字免费领取):
Lucene我想暂时先更新到这里,仅仅这三篇文章想掌握Lucene是远远不够的。另外我这里三篇文章都用的最新的jar包,Lucene更新太快,5系列后的版本和之前的有些地方还是有挺大差距的,就比如为文档域设置权值的setBoost方法6.6以后已经被废除了等等。因为时间有限,所以我就草草的看了一下Lucene的官方文档,大多数内容还是看java1234网站的这个视频来学习的,然后在版本和部分代码上做了改进。截止2018/4/1,上述代码所用的jar包皆为最新。
最后推荐一下自己觉得还不错的Lucene学习网站/博客:
官方网站:Welcome to Apache Lucene
Github:Apache Lucene and Solr