全文检索之lucene的优化篇--增删改查

本文介绍Lucene的增删改查功能及高亮显示技术。通过具体代码示例展示了如何实现索引的创建、删除、更新和查询,特别关注了查询过程中的高亮效果实现。

主要介绍增删改查索引的功能,并且对于查询到的关键字,返回高亮的结果。高亮的效果,就是将查询出来的结果,在前后加上标签,<font color="red"></font>这样在浏览器显示的就是红色的字体.

        目录效果如上,建立一个com.lucene的包,建立一个IndexDao的类,里面写入索引的增删改查方法;而建立的IndexDaoText类则是对这增删改查的测试;QueryResult则是一个查询结果的类,里面只有2个字段,总记录数和记录集合.

    其中IndexDao类中的代码如下,比较长,其实也只是Search,search长也只是因为查询前要做一些设置,排序,过滤器;查询后取出数据还要做一些高亮和摘要的设置.

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. package com.lucene;  
  2.   
  3. import java.io.IOException;  
  4. import java.util.ArrayList;  
  5. import java.util.HashMap;  
  6. import java.util.List;  
  7. import java.util.Map;  
  8.   
  9. import jeasy.analysis.MMAnalyzer;  
  10.   
  11. import org.apache.lucene.analysis.Analyzer;  
  12. import org.apache.lucene.document.Document;  
  13. import org.apache.lucene.document.NumberTools;  
  14. import org.apache.lucene.index.IndexWriter;  
  15. import org.apache.lucene.index.IndexWriter.MaxFieldLength;  
  16. import org.apache.lucene.index.Term;  
  17. import org.apache.lucene.queryParser.MultiFieldQueryParser;  
  18. import org.apache.lucene.queryParser.QueryParser;  
  19. import org.apache.lucene.search.Filter;  
  20. import org.apache.lucene.search.IndexSearcher;  
  21. import org.apache.lucene.search.Query;  
  22. import org.apache.lucene.search.RangeFilter;  
  23. import org.apache.lucene.search.ScoreDoc;  
  24. import org.apache.lucene.search.Sort;  
  25. import org.apache.lucene.search.SortField;  
  26. import org.apache.lucene.search.TopDocs;  
  27. import org.apache.lucene.search.highlight.Formatter;  
  28. import org.apache.lucene.search.highlight.Fragmenter;  
  29. import org.apache.lucene.search.highlight.Highlighter;  
  30. import org.apache.lucene.search.highlight.QueryScorer;  
  31. import org.apache.lucene.search.highlight.Scorer;  
  32. import org.apache.lucene.search.highlight.SimpleFragmenter;  
  33. import org.apache.lucene.search.highlight.SimpleHTMLFormatter;  
  34.   
  35. public class IndexDao {  
  36.   
  37.     //指定索引路径  
  38.     String indexPath = "F:\\Users\\liuyanling\\workspace\\LuceneDemo\\luceneIndex";  
  39.   
  40.     //指定分词器  
  41.     //Analyzer analyzer = new StandardAnalyzer();  
  42.      Analyzer analyzer = new MMAnalyzer();// 词库分词  
  43.   
  44.     /** 
  45.      * 添加/创建索引 
  46.      *  
  47.      * @param doc 需要创建索引的Document文件 
  48.      */  
  49.     public void save(Document doc) {  
  50.         IndexWriter indexWriter = null;  
  51.         try {  
  52.             indexWriter = new IndexWriter(indexPath, analyzer, MaxFieldLength.LIMITED);  
  53.             indexWriter.addDocument(doc);  
  54.         } catch (Exception e) {  
  55.             throw new RuntimeException(e);  
  56.         } finally {  
  57.             try {  
  58.                 indexWriter.close();  
  59.             } catch (Exception e) {  
  60.                 e.printStackTrace();  
  61.             }  
  62.         }  
  63.     }  
  64.   
  65.     /** 
  66.      * 删除索引 
  67.      *  
  68.      * Term是搜索的最小单位,代表某个 Field 中的一个关键词,如:<title, lucene> 
  69.      * new Term( "title", "lucene" ); //删除标题中带有lucene的索引 
  70.      * new Term( "id", "5" ); 
  71.      * new Term( "id", UUID ); 
  72.      *  
  73.      * @param term 
  74.      */  
  75.     public void delete(Term term) {  
  76.         IndexWriter indexWriter = null;  
  77.         try {  
  78.             indexWriter = new IndexWriter(indexPath, analyzer, MaxFieldLength.LIMITED);  
  79.             indexWriter.deleteDocuments(term);  
  80.         } catch (Exception e) {  
  81.             throw new RuntimeException(e);  
  82.         } finally {  
  83.             try {  
  84.                 indexWriter.close();  
  85.             } catch (Exception e) {  
  86.                 e.printStackTrace();  
  87.             }  
  88.         }  
  89.     }  
  90.   
  91.     /** 
  92.      * 更新索引,也就是先删除后添加 
  93.      *  
  94.      * <pre> 
  95.      * indexWriter.deleteDocuments(term); 
  96.      * indexWriter.addDocument(doc); 
  97.      * </pre> 
  98.      *  
  99.      * @param term 
  100.      * @param doc 
  101.      */  
  102.     public void update(Term term, Document doc) {  
  103.         IndexWriter indexWriter = null;  
  104.         try {  
  105.             indexWriter = new IndexWriter(indexPath, analyzer, MaxFieldLength.LIMITED);  
  106.             indexWriter.updateDocument(term, doc);  
  107.         } catch (Exception e) {  
  108.             throw new RuntimeException(e);  
  109.         } finally {  
  110.             try {  
  111.                 indexWriter.close();  
  112.             } catch (Exception e) {  
  113.                 e.printStackTrace();  
  114.             }  
  115.         }  
  116.     }  
  117.   
  118.     /** 
  119.      * <pre> 
  120.      * totalPage = recordCount / pageSize; 
  121.      * if (recordCount % pageSize > 0) 
  122.      *  totalPage++; 
  123.      * </pre> 
  124.      *  
  125.      * @param queryString  关键字 
  126.      * @param firstResult  从第几条索引开始查 
  127.      * @param maxResults   最多查几条 
  128.      * @return QueryResult 返回查询结果,包括总记录数和所有结果 
  129.      */  
  130.     public QueryResult search(String queryString, int firstResult, int maxResults) {  
  131.         try {  
  132.             // 1,把要搜索的文本解析为 Query  
  133.             String[] fields = { "name""content" };  
  134.             Map<String, Float> boosts = new HashMap<String, Float>();  
  135.             //创建索引时设置相关度,值越大,相关度越高,越容易查出来.name的优先级高于内容  
  136.             boosts.put("name", 3f);  
  137.             boosts.put("content"1.0f); //默认为1.0f  
  138.   
  139.             //构造QueryParser,设置查询的方式,以及查询的字段,分词器和相关度的设置  
  140.             QueryParser queryParser = new MultiFieldQueryParser(fields, analyzer, boosts);  
  141.             //查询关键字queryString的结果  
  142.             Query query = queryParser.parse(queryString);  
  143.               
  144.             //返回查询的结果  
  145.             return search(query, firstResult, maxResults);  
  146.         } catch (Exception e) {  
  147.             throw new RuntimeException(e);  
  148.         }  
  149.     }  
  150.   
  151.     /** 
  152.      * 查询索引 
  153.      * @param query         Query对象 
  154.      * @param firstResult   从第几条索引开始查 
  155.      * @param maxResults    最多查几条 
  156.      * @return QueryResult  返回查询结果,包括总记录数和所有结果 
  157.      */  
  158.     public QueryResult search(Query query, int firstResult, int maxResults) {  
  159.         IndexSearcher indexSearcher = null;  
  160.   
  161.         try {  
  162.             // 2,进行查询  
  163.             indexSearcher = new IndexSearcher(indexPath);  
  164.               
  165.             //查询的设置1:过滤器,只查询出size在200~1000的文件,这个影响效率,不建议使用  
  166.             //由于写成这样new RangeFilter("size","50", "200", true, true);,按理会查出50~200的文件,但是由于字符串的50是>200的,所以                                     //什么也不会查出来,所以要用NumberTools.longToString(),转换一下.  
  167.             Filter filter = new RangeFilter("size", NumberTools.longToString(200),NumberTools.longToString(1000),true,true);  
  168.   
  169.             //查询的设置2:排序,设置根据size从小到大升序排序(不设置也可以的)  
  170.             Sort sort = new Sort();  
  171.             sort.setSort(new SortField("size")); // 默认为升序  
  172.             //sort.setSort(new SortField("size", true));  
  173.   
  174.             //查询出结果  
  175.             TopDocs topDocs = indexSearcher.search(query, filter, 10000, sort);  
  176.   
  177.             //将结果显示出来,收集到总记录数,实例化recordList,收集索引记录  
  178.             int recordCount = topDocs.totalHits;  
  179.             List<Document> recordList = new ArrayList<Document>();  
  180.   
  181.             //准备高亮器,字体颜色设置为red  
  182.             Formatter formatter = new SimpleHTMLFormatter("<font color='red'>""</font>");  
  183.             Scorer scorer = new QueryScorer(query);  
  184.             Highlighter highlighter = new Highlighter(formatter, scorer);  
  185.   
  186.             //设置段划分器,指定关键字所在的内容片段的长度  
  187.             Fragmenter fragmenter = new SimpleFragmenter(50);  
  188.             highlighter.setTextFragmenter(fragmenter);  
  189.   
  190.             // 3,取出当前页的数据  
  191.             //获取需要查询的最后数据的索引号  
  192.             int endResult = Math.min(firstResult + maxResults, topDocs.totalHits);  
  193.             for (int i = firstResult; i < endResult; i++) {  
  194.                 ScoreDoc scoreDoc = topDocs.scoreDocs[i];  
  195.                 int docSn = scoreDoc.doc; // 文档内部编号  
  196.                 Document doc = indexSearcher.doc(docSn); // 根据编号取出相应的文档  
  197.   
  198.                 // 高亮处理,返回高亮后的结果,如果当前属性值中没有出现关键字,会返回 null  
  199.                 // 查询“内容"是否包含关键字,没有则为null,有则加上高亮效果。  
  200.                 String highContent = highlighter.getBestFragment(analyzer, "content", doc.get("content"));  
  201.                 if (highContent == null) {  
  202.                     //如果没有关键字,则设置不能超过50个字符  
  203.                     String content = doc.get("content");  
  204.                     int endIndex = Math.min(50, content.length());  
  205.                     highContent = content.substring(0, endIndex);// 最多前50个字符  
  206.                 }  
  207.                 //返回高亮后的结果或者没有高亮但是不超过50个字符的结果  
  208.                 doc.getField("content").setValue(highContent);  
  209.                    
  210.                 //recordList收集索引记录  
  211.                 recordList.add(doc);  
  212.             }  
  213.   
  214.             // 返回结果QueryResult  
  215.             return new QueryResult(recordCount, recordList);  
  216.         } catch (Exception e) {  
  217.             throw new RuntimeException(e);  
  218.         } finally {  
  219.             try {  
  220.                 indexSearcher.close();  
  221.             } catch (IOException e) {  
  222.                 e.printStackTrace();  
  223.             }  
  224.         }  
  225.     }  
  226. }  

    看完IndexDao的类的增删改查,还有测试这些增删改查的方法.按照之前的做法,IndexDao一个类,就能写完测试方法,但是这里分开了.分开了的话,就算了解耦了,灵活性会好很多.

    下面是IndexDaoTest的测试代码,

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. package com.lucene;  
  2.   
  3. import org.apache.lucene.document.Document;  
  4. import org.apache.lucene.index.Term;  
  5. import org.junit.Test;  
  6.   
  7. import com.lucene.units.File2DocumentUtils;  
  8.   
  9. public class IndexDaoTest {  
  10.     //设置两个需要创建索引的文件  
  11.     String filePath = "F:\\Users\\liuyanling\\workspace\\LuceneDemo\\datasource\\IndexWriter addDocument's a javadoc .txt";  
  12.     String filePath2 = "F:\\Users\\liuyanling\\workspace\\LuceneDemo\\datasource\\小笑话_总统的房间 Room .txt";  
  13.   
  14.     //indexDao,实现具体的增删改查方法  
  15.     IndexDao indexDao = new IndexDao();  
  16.   
  17.     /** 
  18.      * 测试保存方法 
  19.      */  
  20.     @Test  
  21.     public void testSave() {  
  22.         Document doc = File2DocumentUtils.file2Document(filePath);  
  23.         //保存索引的时候设置相关度,相对于两个文件中都有的关键字,doc的相关度会高于doc2,也就是会优先查出来.  
  24.         doc.setBoost(3f);  
  25.         indexDao.save(doc);  
  26.   
  27.         Document doc2 = File2DocumentUtils.file2Document(filePath2);  
  28.         //doc2.setBoost(1.0f);  
  29.         indexDao.save(doc2);  
  30.     }  
  31.   
  32.     /** 
  33.      * 测试删除索引 
  34.      */  
  35.     @Test  
  36.     public void testDelete() {  
  37.         //根据路径,删除所有关于该路径的索引文件  
  38.         Term term = new Term("path", filePath);  
  39.         indexDao.delete(term);  
  40.     }  
  41.   
  42.     /** 
  43.      * 测试更新索引 
  44.      */  
  45.     @Test  
  46.     public void testUpdate() {  
  47.         //将filePath路径下的索引更新,内容都改为"这是更新后的文件内容"  
  48.         Term term = new Term("path", filePath);  
  49.   
  50.         Document doc = File2DocumentUtils.file2Document(filePath);  
  51.         doc.getField("content").setValue("这是更新后的文件内容");  
  52.   
  53.         indexDao.update(term, doc);  
  54.     }  
  55.   
  56.     /** 
  57.      * 测试查询 
  58.      */  
  59.     @Test  
  60.     public void testSearch() {  
  61.         //关键字为IndexWriter,房间和content:绅士(表示只在内容中查询绅士),这里  
  62.         String queryString1 = "IndexWriter";  
  63.         String queryString2 = "房间";  
  64.         String queryString3 = "content:绅士";  
  65.           
  66.         printSearchResult(queryString1, 010);  
  67.         printSearchResult(queryString2, 010);  
  68.         printSearchResult(queryString3, 010);  
  69.     }  
  70.       
  71.     /** 
  72.      * 由于想一次性测试上面的3个查询效果,所以提取了一个打印结果的方法 
  73.      * @param queryString 
  74.      * @param firstResult 
  75.      * @param maxResults 
  76.      */  
  77.     private void printSearchResult(String queryString,int firstResult, int maxResults) {  
  78.         QueryResult qr = indexDao.search(queryString, firstResult, maxResults);  
  79.           
  80.         System.out.println("总共有【" + qr.getRecordCount() + "】条匹配结果");  
  81.         for (Document doc : qr.getRecordList()) {  
  82.             //对于size要改为System.out.println("size =" + NumberTools.stringToLong(doc.get("size")));将大小从字符串转为long  
  83.             File2DocumentUtils.printDocumentInfo(doc);  
  84.         }  
  85.     }  
  86.   
  87. }  

     还有最后一个查询结果类,代码如下.

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. package com.lucene;  
  2.   
  3. import java.util.List;  
  4.   
  5. import org.apache.lucene.document.Document;  
  6.   
  7. /** 
  8.  * 查询结果类,就像实体 
  9.  * @author liu 
  10.  * 
  11.  */  
  12. public class QueryResult {  
  13.     //总记录数  
  14.     private int recordCount;  
  15.     //索引记录  
  16.     private List<Document> recordList;  
  17.   
  18.     //有参构造方法  
  19.     public QueryResult(int recordCount, List<Document> recordList) {  
  20.         super();  
  21.         this.recordCount = recordCount;  
  22.         this.recordList = recordList;  
  23.     }  
  24.   
  25.     //get,set方法  
  26.     public int getRecordCount() {  
  27.         return recordCount;  
  28.     }  
  29.   
  30.     public void setRecordCount(int recordCount) {  
  31.         this.recordCount = recordCount;  
  32.     }  
  33.   
  34.     public List<Document> getRecordList() {  
  35.         return recordList;  
  36.     }  
  37.   
  38.     public void setRecordList(List<Document> recordList) {  
  39.         this.recordList = recordList;  
  40.     }  
  41. }  

          代码看完了,现在看下运行效果.从增查改删依次测起.首先删了现有的索引文件夹

          1.执行添加,效果就是索引文件建立出来了,并且是两个文件都建立好了索引.


          2.查看下结果.照理应该是三条都是有匹配结果的,但是第一条没有,是因为用了过滤器


     IndexDao中的search中配置的filter设置为null,就可以查出结果了.IndexWritersize只有169,正好被过滤了.而且由于content中没有IndexWriter关键字,所以没有高亮,并且被摘要只有50个字符


    3.然后执行以下改的方法,会将IndexWriter中内容修改.修改之后,照理只有一个结果,但是实际上查询结果是有两个记录,之前那条没有删除,然后新建了一条修改了.


        4.删除,会删除indexWriter中的所有索引,还是2条记录,结果不对.


后来,发现原来

         Lucene在删除索引时,经常会出现代码成功执行,但索引并未正直删除的现象,总结一下,要注意以下因素:

1.在创建Term时,注意Term的key一定要是以"词"为单位,否则删除不成功,例如:添加索引时,如果把"d:\doc\id.txt"当作要索引的字符串索引过了,那么在删除时,如果直接把"d:\doc\id.txt"作为查询的key来创建Term是无效的,应该用Id.txt(但这样会把所有文件名为Id.txt的都删除,所以官方建议最好用一个能唯一标识的关键字来删除,比如产品编号,新闻编号等(我的猜想:每个document都加入一个id字段作为唯一标识(可用系统当前时间值作为id的值),每当要删除包含某关键字的文档索引时,先将这些文档搜索出来,然后获取它们的id值,传给一个List,然后再用List结合id字段来删除那些文档的索引......不过这个方法的效率可能会低了一点,因为每个文档都要搜两遍);

2.要删除的“词”,在创建索引时,一定要是Tokened过的,否则也不成功;

3.IndexReader,IndexModifer,IndexWriter都提供了DeleteDocuements方法,但建议用IndexModifer来操作,原因是IndexModifer内部做了很多线程安全处理;(PS:IndexModifer已经过期了)

4.删除完成后,一定要调用相应的Close方法,否则并未真正从索引中删除。

   以上是网上查找的原因,最后实验出来为什么lucene的索引删不掉还是花了一点时间。首先是看下File2DocumentUtils中的索引字段的设置。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Document doc = new Document();  
  2. //将文件名和内容分词,建立索引,存储;而内容通过readFileContent方法读取出来。  
  3. doc.add(new Field("name",file.getName(),Store.YES,Index.ANALYZED));  
  4. doc.add(new Field("content",readFileContent(file),Store.YES,Index.ANALYZED));  
  5. //将文件大小存储,但建立索引,但不分词;路径则不需要建立索引  
  6. //doc.add(new Field("size",String.valueOf(file.length()),Store.YES,Index.NOT_ANALYZED));  
  7. doc.add(new Field("size",NumberTools.longToString(file.length()),Store.YES,Index.NOT_ANALYZED));  
  8. doc.add(new Field("path", file.getAbsolutePath(), Store.YES, Index.NO));  

        其中,namecontentANALYZED,sizepath分别是NOT_ANALYZEDNO.上面说要可以要Tokened的字段才能用,但是Tokened过期了,ANALYZED替换了.而对于"d:\doc\id.txt",我开始搞不清楚是什么意思.后来发现是指如我的路径:F:\datasource\IndexWriter addDocument'sa javadoc .txt,按照正常思路,写成这样Term term = new Term("path","F:\\datasource\\IndexWriteraddDocument's a javadoc .txt");,但是term不认,写成这样反而认Term term = new Term("path","IndexWriter addDocument's a javadoc .txt");我开始以为是这样,后来发现也不认。写成这样它才认Term term = new Term("path","indexwriter");可以看出IndexWriter变成小写了,因为大写也不认。这就是上面说的key一定要是以"词"为单位.

         对于Term到底是认哪种的写法,感到很奇怪.设想可能跟分词器有关,正好我之前我有用名字文本测试分词器,效果如下,但是我用的语句是这样的"IndexWriter addDocument's a javadoc.txt",所以,我把文件名由"IndexWriter addDocument's a javadoc .txt"改成了"IndexWriter addDocument's ajavadoc.txt".

      Term term = new Term("name","s");写成这样,测试发现MMAnalyzer分词器认这种写法,可以查出结果,但是StandardAnalyzer不认.所以对于极易分词器,写上indexwriter,adddocument,s,javadoc.txt都是可以的.


    将删除和更新的方法重新写一下,term写成这样

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Term term = new Term("name""indexwriter");  

    进行测试,删除索引,重新创建索引,更新效果如下,只有一个语句了.

    而删除效果,则全部删除了,一条不留.


最后,还没完,还有下篇《全文检索之lucene的优化篇--查询篇》,介绍lucene中的各种查询方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值