周六花了整整一下午,将Lucene5中有关索引的常见操作进行了简单封装,废话不多说,上代码:
package com.yida.framework.lucene5.util; import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; 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.LockObtainFailedException; /** * Lucene索引读写器/查询器单例获取工具类 * @author Lanxiaowei * */ public class LuceneManager { private volatile static LuceneManager singleton; private volatile static IndexWriter writer; private volatile static IndexReader reader; private volatile static IndexSearcher searcher; private final Lock writerLock = new ReentrantLock(); //private final Lock readerLock = new ReentrantLock(); //private final Lock searcherLock = new ReentrantLock(); private LuceneManager() {} public static LuceneManager getInstance() { if (null == singleton) { synchronized (LuceneManager.class) { if (null == singleton) { singleton = new LuceneManager(); } } } return singleton; } /** * 获取IndexWriter单例对象 * @param dir * @param config * @return */ public IndexWriter getIndexWriter(Directory dir, IndexWriterConfig config) { if(null == dir) { throw new IllegalArgumentException("Directory can not be null."); } if(null == config) { throw new IllegalArgumentException("IndexWriterConfig can not be null."); } try { writerLock.lock(); if(null == writer){ //如果索引目录被锁,则直接抛异常 if(IndexWriter.isLocked(dir)) { throw new LockObtainFailedException("Directory of index had been locked."); } writer = new IndexWriter(dir, config); } } catch (LockObtainFailedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { writerLock.unlock(); } return writer; } /** * 获取IndexReader对象 * @param dir * @param enableNRTReader 是否开启NRTReader * @return */ public IndexReader getIndexReader(Directory dir,boolean enableNRTReader) { if(null == dir) { throw new IllegalArgumentException("Directory can not be null."); } try { if(null == reader){ reader = DirectoryReader.open(dir); } else { if(enableNRTReader && reader instanceof DirectoryReader) { //开启近实时Reader,能立即看到动态添加/删除的索引变化 reader = DirectoryReader.openIfChanged((DirectoryReader)reader); } } } catch (IOException e) { e.printStackTrace(); } return reader; } /** * 获取IndexReader对象(默认不启用NETReader) * @param dir * @return */ public IndexReader getIndexReader(Directory dir) { return getIndexReader(dir, false); } /** * 获取IndexSearcher对象 * @param reader IndexReader对象实例 * @param executor 如果你需要开启多线程查询,请提供ExecutorService对象参数 * @return */ public IndexSearcher getIndexSearcher(IndexReader reader,ExecutorService executor) { if(null == reader) { throw new IllegalArgumentException("The indexReader can not be null."); } if(null == searcher){ searcher = new IndexSearcher(reader); } return searcher; } /** * 获取IndexSearcher对象(不支持多线程查询) * @param reader IndexReader对象实例 * @return */ public IndexSearcher getIndexSearcher(IndexReader reader) { return getIndexSearcher(reader, null); } }
package com.yida.framework.lucene5.util; import java.io.IOException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import org.ansj.lucene5.AnsjAnalyzer; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.TextField; 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.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.InvalidTokenOffsetsException; 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.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; /** * Lucene工具类(基于Lucene5.0封装) * @author Lanxiaowei * */ public class LuceneUtils { private static final LuceneManager luceneManager = LuceneManager.getInstance(); private static Analyzer analyzer = new AnsjAnalyzer(); /** * 打开索引目录 * * @param luceneDir * @return * @throws IOException */ public static FSDirectory openFSDirectory(String luceneDir) { FSDirectory directory = null; try { directory = FSDirectory.open(Paths.get(luceneDir)); /** * 注意:isLocked方法内部会试图去获取Lock,如果获取到Lock,会关闭它,否则return false表示索引目录没有被锁, * 这也就是为什么unlock方法被从IndexWriter类中移除的原因 */ IndexWriter.isLocked(directory); } catch (IOException e) { e.printStackTrace(); } return directory; } /** * 关闭索引目录并销毁 * @param directory * @throws IOException */ public static void closeDirectory(Directory directory) throws IOException { if (null != directory) { directory.close(); directory = null; } } /** * 获取IndexWriter * @param dir * @param config * @return */ public static IndexWriter getIndexWrtier(Directory dir, IndexWriterConfig config) { return luceneManager.getIndexWriter(dir, config); } /** * 获取IndexWriter * @param dir * @param config * @return */ public static IndexWriter getIndexWrtier(String directoryPath, IndexWriterConfig config) { FSDirectory directory = openFSDirectory(directoryPath); return luceneManager.getIndexWriter(directory, config); } /** * 获取IndexReader * @param dir * @param enableNRTReader 是否开启NRTReader * @return */ public static IndexReader getIndexReader(Directory dir,boolean enableNRTReader) { return luceneManager.getIndexReader(dir, enableNRTReader); } /** * 获取IndexReader(默认不启用NRTReader) * @param dir * @return */ public static IndexReader getIndexReader(Directory dir) { return luceneManager.getIndexReader(dir); } /** * 获取IndexSearcher * @param reader IndexReader对象 * @param executor 如果你需要开启多线程查询,请提供ExecutorService对象参数 * @return */ public static IndexSearcher getIndexSearcher(IndexReader reader,ExecutorService executor) { return luceneManager.getIndexSearcher(reader, executor); } /** * 获取IndexSearcher(不支持多线程查询) * @param reader IndexReader对象 * @return */ public static IndexSearcher getIndexSearcher(IndexReader reader) { return luceneManager.getIndexSearcher(reader); } /** * 创建QueryParser对象 * @param field * @param analyzer * @return */ public static QueryParser createQueryParser(String field, Analyzer analyzer) { return new QueryParser(field, analyzer); } /** * 关闭IndexReader * @param reader */ public static void closeIndexReader(IndexReader reader) { if (null != reader) { try { reader.close(); reader = null; } catch (IOException e) { e.printStackTrace(); } } } /** * 关闭IndexWriter * @param writer */ public static void closeIndexWriter(IndexWriter writer) { if(null != writer) { try { writer.close(); writer = null; } catch (IOException e) { e.printStackTrace(); } } } /** * 关闭IndexReader和IndexWriter * @param reader * @param writer */ public static void closeAll(IndexReader reader, IndexWriter writer) { closeIndexReader(reader); closeIndexWriter(writer); } /** * 删除索引[注意:请自己关闭IndexWriter对象] * @param writer * @param field * @param value */ public static void deleteIndex(IndexWriter writer, String field, String value) { try { writer.deleteDocuments(new Term[] {new Term(field,value)}); } catch (IOException e) { e.printStackTrace(); } } /** * 删除索引[注意:请自己关闭IndexWriter对象] * @param writer * @param query */ public static void deleteIndex(IndexWriter writer, Query query) { try { writer.deleteDocuments(query); } catch (IOException e) { e.printStackTrace(); } } /** * 批量删除索引[注意:请自己关闭IndexWriter对象] * @param writer * @param terms */ public static void deleteIndexs(IndexWriter writer,Term[] terms) { try { writer.deleteDocuments(terms); } catch (IOException e) { e.printStackTrace(); } } /** * 批量删除索引[注意:请自己关闭IndexWriter对象] * @param writer * @param querys */ public static void deleteIndexs(IndexWriter writer,Query[] querys) { try { writer.deleteDocuments(querys); } catch (IOException e) { e.printStackTrace(); } } /** * 删除所有索引文档 * @param writer */ public static void deleteAllIndex(IndexWriter writer) { try { writer.deleteAll(); } catch (IOException e) { e.printStackTrace(); } } /** * 更新索引文档 * @param writer * @param term * @param document */ public static void updateIndex(IndexWriter writer,Term term,Document document) { try { writer.updateDocument(term, document); } catch (IOException e) { e.printStackTrace(); } } /** * 更新索引文档 * @param writer * @param term * @param document */ public static void updateIndex(IndexWriter writer,String field,String value,Document document) { updateIndex(writer, new Term(field, value), document); } /** * 添加索引文档 * @param writer * @param doc */ public static void addIndex(IndexWriter writer, Document document) { updateIndex(writer, null, document); } /** * 索引文档查询 * @param searcher * @param query * @return */ public static List<Document> query(IndexSearcher searcher,Query query) { TopDocs topDocs = null; try { topDocs = searcher.search(query, Integer.MAX_VALUE); } catch (IOException e) { e.printStackTrace(); } ScoreDoc[] scores = topDocs.scoreDocs; int length = scores.length; if (length <= 0) { return Collections.emptyList(); } List<Document> docList = new ArrayList<Document>(); try { for (int i = 0; i < length; i++) { Document doc = searcher.doc(scores[i].doc); docList.add(doc); } } catch (IOException e) { e.printStackTrace(); } return docList; } /** * 返回索引文档的总数[注意:请自己手动关闭IndexReader] * @param reader * @return */ public static int getIndexTotalCount(IndexReader reader) { return reader.numDocs(); } /** * 返回索引文档中最大文档ID[注意:请自己手动关闭IndexReader] * @param reader * @return */ public static int getMaxDocId(IndexReader reader) { return reader.maxDoc(); } /** * 返回已经删除尚未提交的文档总数[注意:请自己手动关闭IndexReader] * @param reader * @return */ public static int getDeletedDocNum(IndexReader reader) { return getMaxDocId(reader) - getIndexTotalCount(reader); } /** * 根据docId查询索引文档 * @param reader IndexReader对象 * @param docID documentId * @param fieldsToLoad 需要返回的field * @return */ public static Document findDocumentByDocId(IndexReader reader,int docID, Set<String> fieldsToLoad) { try { return reader.document(docID, fieldsToLoad); } catch (IOException e) { return null; } } /** * 根据docId查询索引文档 * @param reader IndexReader对象 * @param docID documentId * @return */ public static Document findDocumentByDocId(IndexReader reader,int docID) { return findDocumentByDocId(reader, docID, null); } /** * @Title: createHighlighter * @Description: 创建高亮器 * @param query 索引查询对象 * @param prefix 高亮前缀字符串 * @param stuffix 高亮后缀字符串 * @param fragmenterLength 摘要最大长度 * @return */ public static Highlighter createHighlighter(Query query, String prefix, String stuffix, int fragmenterLength) { Formatter formatter = new SimpleHTMLFormatter((prefix == null || prefix.trim().length() == 0) ? "<font color=\"red\">" : prefix, (stuffix == null || stuffix.trim().length() == 0)?"</font>" : stuffix); Scorer fragmentScorer = new QueryScorer(query); Highlighter highlighter = new Highlighter(formatter, fragmentScorer); Fragmenter fragmenter = new SimpleFragmenter(fragmenterLength <= 0 ? 50 : fragmenterLength); highlighter.setTextFragmenter(fragmenter); return highlighter; } /** * @Title: highlight * @Description: 生成高亮文本 * @param document 索引文档对象 * @param highlighter 高亮器 * @param analyzer 索引分词器 * @param field 高亮字段 * @return * @throws IOException * @throws InvalidTokenOffsetsException */ public static String highlight(Document document,Highlighter highlighter,Analyzer analyzer,String field) throws IOException { List<IndexableField> list = document.getFields(); for (IndexableField fieldable : list) { String fieldValue = fieldable.stringValue(); if(fieldable.name().equals(field)) { try { fieldValue = highlighter.getBestFragment(analyzer, field, fieldValue); } catch (InvalidTokenOffsetsException e) { fieldValue = fieldable.stringValue(); } return (fieldValue == null || fieldValue.trim().length() == 0)? fieldable.stringValue() : fieldValue; } } return null; } /** * @Title: searchTotalRecord * @Description: 获取符合条件的总记录数 * @param query * @return * @throws IOException */ public static int searchTotalRecord(IndexSearcher search,Query query) { ScoreDoc[] docs = null; try { TopDocs topDocs = search.search(query, Integer.MAX_VALUE); if(topDocs == null || topDocs.scoreDocs == null || topDocs.scoreDocs.length == 0) { return 0; } docs = topDocs.scoreDocs; } catch (IOException e) { e.printStackTrace(); } return docs.length; } /** * @Title: pageQuery * @Description: Lucene分页查询 * @param searcher * @param query * @param page * @throws IOException */ public static void pageQuery(IndexSearcher searcher,Directory directory,Query query,Page<Document> page) { int totalRecord = searchTotalRecord(searcher,query); //设置总记录数 page.setTotalRecord(totalRecord); TopDocs topDocs = null; try { topDocs = searcher.searchAfter(page.getAfterDoc(),query, page.getPageSize()); } catch (IOException e) { e.printStackTrace(); } List<Document> docList = new ArrayList<Document>(); ScoreDoc[] docs = topDocs.scoreDocs; int index = 0; for (ScoreDoc scoreDoc : docs) { int docID = scoreDoc.doc; Document document = null; try { document = searcher.doc(docID); } catch (IOException e) { e.printStackTrace(); } if(index == docs.length - 1) { page.setAfterDoc(scoreDoc); page.setAfterDocId(docID); } docList.add(document); index++; } page.setItems(docList); closeIndexReader(searcher.getIndexReader()); } /** * @Title: pageQuery * @Description: 分页查询[如果设置了高亮,则会更新索引文档] * @param searcher * @param directory * @param query * @param page * @param highlighterParam * @param writerConfig * @throws IOException */ public static void pageQuery(IndexSearcher searcher,Directory directory,Query query,Page<Document> page,HighlighterParam highlighterParam,IndexWriterConfig writerConfig) throws IOException { IndexWriter writer = null; //若未设置高亮 if(null == highlighterParam || !highlighterParam.isHighlight()) { pageQuery(searcher,directory,query, page); } else { int totalRecord = searchTotalRecord(searcher,query); System.out.println("totalRecord:" + totalRecord); //设置总记录数 page.setTotalRecord(totalRecord); TopDocs topDocs = searcher.searchAfter(page.getAfterDoc(),query, page.getPageSize()); List<Document> docList = new ArrayList<Document>(); ScoreDoc[] docs = topDocs.scoreDocs; int index = 0; writer = getIndexWrtier(directory, writerConfig); for (ScoreDoc scoreDoc : docs) { int docID = scoreDoc.doc; Document document = searcher.doc(docID); String content = document.get(highlighterParam.getFieldName()); if(null != content && content.trim().length() > 0) { //创建高亮器 Highlighter highlighter = LuceneUtils.createHighlighter(query, highlighterParam.getPrefix(), highlighterParam.getStuffix(), highlighterParam.getFragmenterLength()); String text = highlight(document, highlighter, analyzer, highlighterParam.getFieldName()); //若高亮后跟原始文本不相同,表示高亮成功 if(!text.equals(content)) { Document tempdocument = new Document(); List<IndexableField> indexableFieldList = document.getFields(); if(null != indexableFieldList && indexableFieldList.size() > 0) { for(IndexableField field : indexableFieldList) { if(field.name().equals(highlighterParam.getFieldName())) { tempdocument.add(new TextField(field.name(), text, Field.Store.YES)); } else { tempdocument.add(field); } } } updateIndex(writer, new Term(highlighterParam.getFieldName(),content), tempdocument); document = tempdocument; } } if(index == docs.length - 1) { page.setAfterDoc(scoreDoc); page.setAfterDocId(docID); } docList.add(document); index++; } page.setItems(docList); } closeIndexReader(searcher.getIndexReader()); closeIndexWriter(writer); } }
package com.yida.framework.lucene5.util; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.apache.lucene.document.Document; import org.apache.lucene.search.ScoreDoc; public class Page<T> { /**当前第几页(从1开始计算)*/ private int currentPage; /**每页显示几条*/ private int pageSize; /**总记录数*/ private int totalRecord; /**总页数*/ private int totalPage; /**分页数据集合[用泛型T来限定集合元素类型]*/ private Collection<T> items; /**当前显示起始索引(从零开始计算)*/ private int startIndex; /**当前显示结束索引(从零开始计算)*/ private int endIndex; /**一组最多显示几个页码[比如Google一组最多显示10个页码]*/ private int groupSize; /**左边偏移量*/ private int leftOffset = 5; /**右边偏移量*/ private int rightOffset = 4; /**当前页码范围*/ private String[] pageRange; /**分页数据*/ private List<Document> docList; /**上一页最后一个ScoreDoc对象*/ private ScoreDoc afterDoc; /**上一页最后一个ScoreDoc对象的Document对象ID*/ private int afterDocId; public void setRangeIndex() { int groupSize = getGroupSize(); int totalPage = getTotalPage(); if(totalPage < 2) { startIndex = 0; endIndex = totalPage - startIndex; } else { int currentPage = getCurrentPage(); if(groupSize >= totalPage) { startIndex = 0; endIndex = totalPage - startIndex - 1; } else { int leftOffset = getLeftOffset(); int middleOffset = getMiddleOffset(); if(-1 == middleOffset) { startIndex = 0; endIndex = groupSize - 1; } else if(currentPage <= leftOffset) { startIndex = 0; endIndex = groupSize - 1; } else { startIndex = currentPage - leftOffset - 1; if(currentPage + rightOffset > totalPage) { endIndex = totalPage - 1; } else { endIndex = currentPage + rightOffset - 1; } } } } } public int getCurrentPage() { if(currentPage <= 0) { currentPage = 1; } else { int totalPage = getTotalPage(); if(totalPage > 0 && currentPage > getTotalPage()) { currentPage = totalPage; } } return currentPage; } public void setCurrentPage(int currentPage) { this.currentPage = currentPage; } public int getPageSize() { if(pageSize <= 0) { pageSize = 10; } return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public int getTotalRecord() { return totalRecord; } public void setTotalRecord(int totalRecord) { this.totalRecord = totalRecord; } public int getTotalPage() { int totalRecord = getTotalRecord(); if(totalRecord == 0) { totalPage = 0; } else { int pageSize = getPageSize(); totalPage = totalRecord % pageSize == 0 ? totalRecord / pageSize : (totalRecord / pageSize) + 1; } return totalPage; } public void setTotalPage(int totalPage) { this.totalPage = totalPage; } public int getStartIndex() { return startIndex; } public void setStartIndex(int startIndex) { this.startIndex = startIndex; } public int getEndIndex() { return endIndex; } public void setEndIndex(int endIndex) { this.endIndex = endIndex; } public int getGroupSize() { if(groupSize <= 0) { groupSize = 10; } return groupSize; } public void setGroupSize(int groupSize) { this.groupSize = groupSize; } public int getLeftOffset() { leftOffset = getGroupSize() / 2; return leftOffset; } public void setLeftOffset(int leftOffset) { this.leftOffset = leftOffset; } public int getRightOffset() { int groupSize = getGroupSize(); if(groupSize % 2 == 0) { rightOffset = (groupSize / 2) - 1; } else { rightOffset = groupSize / 2; } return rightOffset; } public void setRightOffset(int rightOffset) { this.rightOffset = rightOffset; } /**中心位置索引[从1开始计算]*/ public int getMiddleOffset() { int groupSize = getGroupSize(); int totalPage = getTotalPage(); if(groupSize >= totalPage) { return -1; } return getLeftOffset() + 1; } public String[] getPageRange() { setRangeIndex(); int size = endIndex - startIndex + 1; if(size <= 0) { return new String[0]; } if(totalPage == 1) { return new String[] {"1"}; } pageRange = new String[size]; for(int i=0; i < size; i++) { pageRange[i] = (startIndex + i + 1) + ""; } return pageRange; } public void setPageRange(String[] pageRange) { this.pageRange = pageRange; } public Collection<T> getItems() { return items; } public void setItems(Collection<T> items) { this.items = items; } public List<Document> getDocList() { return docList; } public void setDocList(List<Document> docList) { this.docList = docList; } public ScoreDoc getAfterDoc() { setAfterDocId(afterDocId); return afterDoc; } public void setAfterDoc(ScoreDoc afterDoc) { this.afterDoc = afterDoc; } public int getAfterDocId() { return afterDocId; } public void setAfterDocId(int afterDocId) { this.afterDocId = afterDocId; if(null == afterDoc) { this.afterDoc = new ScoreDoc(afterDocId, 1.0f); } } public Page() {} public Page(int currentPage, int pageSize) { this.currentPage = currentPage; this.pageSize = pageSize; } public Page(int currentPage, int pageSize, Collection<T> items) { this.currentPage = currentPage; this.pageSize = pageSize; this.items = items; } public Page(int currentPage, int pageSize, Collection<T> items, int groupSize) { this.currentPage = currentPage; this.pageSize = pageSize; this.items = items; this.groupSize = groupSize; } public Page(int currentPage, int pageSize, int groupSize, int afterDocId) { this.currentPage = currentPage; this.pageSize = pageSize; this.groupSize = groupSize; this.afterDocId = afterDocId; } public static void main(String[] args) { Collection<Integer> items = new ArrayList<Integer>(); int totalRecord = 201; for(int i=0; i < totalRecord; i++) { items.add(new Integer(i)); } Page<Integer> page = new Page<Integer>(1,10,items,10); page.setTotalRecord(totalRecord); int totalPage = page.getTotalPage(); for(int i=0; i < totalPage; i++) { page.setCurrentPage(i+1); String[] pageRange = page.getPageRange(); System.out.println("当前第" + page.currentPage + "页"); for(int j=0; j < pageRange.length; j++) { System.out.print(pageRange[j] + " "); } System.out.println("\n"); } } }
package com.yida.framework.lucene5.util; /** * @ClassName: HighlighterParam * @Description: 高亮器参数对象 * @author Lanxiaowei * @date 2014-3-30 下午12:22:08 */ public class HighlighterParam { /**是否需要设置高亮*/ private boolean highlight; /**需要设置高亮的属性名*/ private String fieldName; /**高亮前缀*/ private String prefix; /**高亮后缀*/ private String stuffix; /**显示摘要最大长度*/ private int fragmenterLength; public boolean isHighlight() { return highlight; } public void setHighlight(boolean highlight) { this.highlight = highlight; } public String getFieldName() { return fieldName; } public void setFieldName(String fieldName) { this.fieldName = fieldName; } public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getStuffix() { return stuffix; } public void setStuffix(String stuffix) { this.stuffix = stuffix; } public int getFragmenterLength() { return fragmenterLength; } public void setFragmenterLength(int fragmenterLength) { this.fragmenterLength = fragmenterLength; } public HighlighterParam(boolean highlight, String fieldName, String prefix, String stuffix, int fragmenterLength) { this.highlight = highlight; this.fieldName = fieldName; this.prefix = prefix; this.stuffix = stuffix; this.fragmenterLength = fragmenterLength; } public HighlighterParam(boolean highlight, String fieldName, int fragmenterLength) { this.highlight = highlight; this.fieldName = fieldName; this.fragmenterLength = fragmenterLength; } public HighlighterParam(boolean highlight, String fieldName, String prefix, String stuffix) { this.highlight = highlight; this.fieldName = fieldName; this.prefix = prefix; this.stuffix = stuffix; } public HighlighterParam() { } }
工具类对IndexWriter,IndexReader,IndexSearcher,Analyzer,QueryParser等Lucene这些常用操作对象的获取进行了封装,其中IndexWriter采用了单例模式,确保始终只有一个对象实例,因为Lucene限制了索引写操作是阻塞的,即同一时刻只能有一个IndexWriter在执行写操作,直到indexWriter释放lock,而索引读的操作是可以并发进行的。