Lucene
[Lucene] MultiSearcher의 사용 (3.0.3 기준) -2-
용식
2011. 2. 12. 23:12
IndexSearcher는 Index의 변경 내용을 그 상태로는 인지 하지 못 합니다.
주로 IndexWriter에 의해서 추가적인 색인이 되거나
IndexReader에 의한 삭제등이 될텐데요..
제가 구현하고 있는 프로그램도 하루에 한번 로그를 분석 후 색인을 추가하기 때문에
색인 파일 변경에 대한 감지를 하여 이를 처리 할 수 있어야 했습니다.
MultiSearcher를 사용했기 때문에
각 Searcher들이 바라보고 있는 Index파일들을 모두 체크하여
인덱스 파일이 변경되었을 경우 Index파일을 다시 열어서 IndexSearcher를 다시 생성하는
방법을 사용하기로 하였습니다.
각 Searcher별로 가지고 있는 IndexReader를 MultiSearcher로 부터는 직접적으로 얻을 수 없었기 때문에
IndexReader를 List에 담아 보관하고, IndexSearcher를 사용 할 때 마다 IndexReader로 Index파일을 체크하여
만약 하나라도 변경된 사항이 있으면, 현재 만들어진 MultiSearcher를 날려버리고 전부 새로 IndexSearcher를 생성하여
다시 MultiSearcher를 생성하는 방식을 사용했습니다.
세부적은 성능등은 고려하지 않았는데
사용자가 극 소수이고 트래픽도 많지 않았기 때문에 이정도로도 충분하더군요...
또한 IndexSearcher는 재사용 하는 것이 좋기 때문에 , MultiSearcher를 생성 후
이를 Map에 담아두고 return해주는 방식으로 Manager 클래스를 구현하였습니다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.tistory.devyongsik.search.searcher; | |
import java.io.File; | |
import java.io.IOException; | |
import java.text.SimpleDateFormat; | |
import java.util.ArrayList; | |
import java.util.Calendar; | |
import java.util.Collections; | |
import java.util.GregorianCalendar; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.concurrent.ConcurrentHashMap; | |
import org.apache.commons.logging.Log; | |
import org.apache.commons.logging.LogFactory; | |
import org.apache.lucene.index.IndexReader; | |
import org.apache.lucene.search.IndexSearcher; | |
import org.apache.lucene.search.MultiSearcher; | |
import org.apache.lucene.search.Searcher; | |
import org.apache.lucene.store.Directory; | |
import org.apache.lucene.store.FSDirectory; | |
import com.tistory.devyongsik.common.properties.StatisticProperties; | |
public class IndexSearcherManager { | |
private Log logger = LogFactory.getLog(IndexSearcherManager.class); | |
private static IndexSearcherManager instance = new IndexSearcherManager(); //#1. 싱글턴 패턴을 위한 instance생성 | |
private Map<String,MultiSearcher> searcherCache = new ConcurrentHashMap<String,MultiSearcher>(); //#2. MultiSearcher를 담고 있는 Map | |
private List<Searcher> searchers = Collections.synchronizedList(new ArrayList<Searcher>()); //#3. MultiSearcher를 생성하는데 사용했던 Searcher들 | |
private List<IndexReader> indexReaders = Collections.synchronizedList(new ArrayList<IndexReader>()); //#4. Searcher를 생성하는데 사용했던 IndexReader들 | |
private int startYear = 2009; //연도별로 인덱스 파일을 나누는 시작연도 | |
private IndexSearcherManager() { | |
init(); //#5. 초기화 | |
} | |
private void init() { | |
try { | |
if(logger.isDebugEnabled()) | |
logger.debug("init IndexSearcher..."); | |
indexReaders.clear(); //#6. IndexReader와 IndexSearcher 제거 | |
searchers.clear(); | |
SimpleDateFormat format = new SimpleDateFormat("yyyy"); | |
Calendar calendar = new GregorianCalendar(); | |
int nowYear = Integer.parseInt(format.format(calendar.getTime())); //#7. 현재 연도를 구함 | |
for(startYear = 2009 ;startYear <= nowYear; startYear++) { | |
if(logger.isDebugEnabled()) | |
logger.debug("startYear : " + startYear); | |
Directory dir = FSDirectory.open(new File("d:/index/" + String.valueOf(startYear))); //#8. 디렉토리 생성 | |
if(logger.isDebugEnabled()) | |
logger.debug("FSDirectory : " + dir); | |
IndexReader reader = IndexReader.open(dir, true); | |
IndexSearcher searcher = new IndexSearcher(reader); //#9. IndexSearcher 를 생성 | |
searchers.add(searcher); //#10. Searcher를 List에 add | |
indexReaders.add(reader); //#11. IndexReader를 List에 add | |
} | |
MultiSearcher multiSearcher = new MultiSearcher(searchers.toArray(new Searcher[0])); //#12. 각 인덱스 디렉토리별로 IndexSearcher를 생성후 MultiSearcher 생성 | |
searcherCache.put("default", multiSearcher); //#13. Map에 default라는 이름으로 저장해둔다. | |
} catch (IOException e) { | |
e.printStackTrace(); | |
throw new RuntimeException(); | |
} | |
} | |
protected static IndexSearcherManager getSearcherManager() { | |
return instance; | |
} | |
protected MultiSearcher getSearcher() { //#14. Searcher를 요청하는 메서드로.. | |
checkIndexReader(); //#15. 요청시 IndexReader를 통해 현재 인덱스 파일을 체크함 go to #18 | |
if(searcherCache.containsKey("default")) { //#16. 키가 있으면 저장된 MultiSearcher return | |
if(logger.isDebugEnabled()) | |
logger.debug("contains default key...."); | |
return searcherCache.get("default"); | |
} else { | |
if(logger.isDebugEnabled()) | |
logger.debug("not contains default key...."); | |
init(); //#17. 없으면 생성 후 return | |
return searcherCache.get("default"); | |
} | |
} | |
private void checkIndexReader() { | |
boolean isIndexReaderClosed = false; | |
try { | |
for(IndexReader indexReader : indexReaders) { //#18. 각 IndexReader를 통해 | |
if(!indexReader.isCurrent()) { //#19. 변경유무를 체크하고, 변경이 있는 경우 | |
if(logger.isDebugEnabled()) | |
logger.debug(indexReader.directory() + " is not current"); | |
indexReader.directory().close(); //#20. IndexReader를 close후 | |
indexReader.close(); | |
searcherCache.remove("default"); //#21. 생성되어 있는 MultiSearcher를 제거한다. 각 IndexReader에 대해서 중복으로 동작함. | |
searcherCache.clear(); | |
isIndexReaderClosed = true; | |
} else { | |
if(logger.isDebugEnabled()) | |
logger.debug(indexReader.directory() + " is current"); | |
} | |
} | |
if(isIndexReaderClosed) { //#22. MultiSearcher가 제거 되었으므로 다시 새로운 Idnex File로 IndexSearcher를 생성 | |
init(); | |
} | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
protected void closeIndexSearcher() throws IOException { //#23. Searcher close.. 외부에서 색인 후 명시적으로 Searcher를 제거 할 때 사용. Map에서 키가 제거 되었으며로 다음 요청시 init()에 의해 재생성 됨 | |
if(logger.isDebugEnabled()) | |
logger.debug("IndexSearcher close..."); | |
MultiSearcher multiSearcher = searcherCache.get("default"); | |
multiSearcher.close(); | |
searcherCache.remove("default"); | |
searcherCache.clear(); | |
} | |
} |
주요내용은 주석으로 적었습니다만.. 주요 골격은 Index 디렉토리로부터 Searcher들을 생성하여 이를 통해 MultiSearcher를 생성합니다. 그리고 재사용을 위해 Map에 저장하여 이것을 사용해 검색을 수행합니다.
현재로서는 default 라는 이름의 키만을 사용하게 되어있지요..
기본적으로 잘 다듬어진 코드는 아니지만 일단, 이정도로 Searcher를 사용하여 생성하고 있습니다.
만약, 구현하려는 부분에 짧은 주기의 색인 반영이나 Index 파일의 변경이 필요하다면
더 디테일한 구현이 필요 할 것 입니다.
이것을 테스트해보기 위해서
테스트 케이스를 작성해보았습니다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.tistory.devyongsik.search.searcher; | |
import java.io.File; | |
import java.io.IOException; | |
import org.apache.lucene.analysis.WhitespaceAnalyzer; | |
import org.apache.lucene.analysis.standard.StandardAnalyzer; | |
import org.apache.lucene.document.Document; | |
import org.apache.lucene.document.Field; | |
import org.apache.lucene.document.NumericField; | |
import org.apache.lucene.index.CorruptIndexException; | |
import org.apache.lucene.index.IndexWriter; | |
import org.apache.lucene.queryParser.ParseException; | |
import org.apache.lucene.queryParser.QueryParser; | |
import org.apache.lucene.search.MultiSearcher; | |
import org.apache.lucene.search.Query; | |
import org.apache.lucene.search.ScoreDoc; | |
import org.apache.lucene.store.Directory; | |
import org.apache.lucene.store.FSDirectory; | |
import org.apache.lucene.store.LockObtainFailedException; | |
import org.apache.lucene.util.Version; | |
import org.junit.Assert; | |
import org.junit.BeforeClass; | |
import org.junit.Test; | |
import com.tistory.devyongsik.common.properties.StatisticProperties; | |
/** | |
* @author need4spd, need4spd@cplanet.co.kr, 2010. 12. 8. | |
* | |
*/ | |
public class IndexSearcherManagerTest { | |
@BeforeClass | |
public static void indexing() throws CorruptIndexException, LockObtainFailedException, IOException { | |
StatisticProperties prop = StatisticProperties.getProperties(); | |
Directory indexStoreA = FSDirectory.open(new File("d:/2009")); | |
Directory indexStoreB = FSDirectory.open(new File("d:/2010")); | |
Directory indexStoreC = FSDirectory.open(new File("d:/2011")); | |
Document lDoc = new Document(); | |
lDoc.add(new Field("luceneSection", "total_request", Field.Store.YES, Field.Index.NOT_ANALYZED)); | |
lDoc.add(new Field("logDate", "20101213", Field.Store.YES, Field.Index.NOT_ANALYZED)); | |
lDoc.add(new Field("keyword", "나이키", Field.Store.YES, Field.Index.ANALYZED)); | |
lDoc.add(new Field("gubun", "COUNT", Field.Store.YES, Field.Index.ANALYZED)); | |
lDoc.add(new NumericField("statisticValue", 1, Field.Store.YES, Field.Index.NOT_ANALYZED.isIndexed()).setIntValue(1305)); | |
Document lDoc2 = new Document(); | |
lDoc2.add(new Field("luceneSection", "total_request", Field.Store.YES, Field.Index.NOT_ANALYZED)); | |
lDoc2.add(new Field("logDate", "20101214", Field.Store.YES, Field.Index.NOT_ANALYZED)); | |
lDoc2.add(new Field("keyword", "아디다스", Field.Store.YES, Field.Index.ANALYZED)); | |
lDoc2.add(new Field("gubun", "COUNT", Field.Store.YES, Field.Index.ANALYZED)); | |
lDoc2.add(new NumericField("statisticValue", 1, Field.Store.YES, Field.Index.NOT_ANALYZED.isIndexed()).setIntValue(2305)); | |
Document lDoc3 = new Document(); | |
lDoc3.add(new Field("luceneSection", "total_request", Field.Store.YES, Field.Index.NOT_ANALYZED)); | |
lDoc3.add(new Field("logDate", "20101215", Field.Store.YES, Field.Index.NOT_ANALYZED)); | |
lDoc3.add(new Field("keyword", "청바지", Field.Store.YES, Field.Index.ANALYZED)); | |
lDoc3.add(new Field("gubun", "COUNT", Field.Store.YES, Field.Index.ANALYZED)); | |
lDoc3.add(new NumericField("statisticValue", 1, Field.Store.YES, Field.Index.NOT_ANALYZED.isIndexed()).setIntValue(520)); | |
Document lDoc4 = new Document(); | |
lDoc4.add(new Field("luceneSection", "total_request", Field.Store.YES, Field.Index.NOT_ANALYZED)); | |
lDoc4.add(new Field("logDate", "20101216", Field.Store.YES, Field.Index.NOT_ANALYZED)); | |
lDoc4.add(new Field("keyword", "반바지", Field.Store.YES, Field.Index.ANALYZED)); | |
lDoc4.add(new Field("gubun", "COUNT", Field.Store.YES, Field.Index.ANALYZED)); | |
lDoc4.add(new NumericField("statisticValue", 1, Field.Store.YES, Field.Index.NOT_ANALYZED.isIndexed()).setIntValue(5920)); | |
IndexWriter writerA = new IndexWriter(indexStoreA, new StandardAnalyzer(org.apache.lucene.util.Version.LUCENE_30), true, IndexWriter.MaxFieldLength.LIMITED); | |
IndexWriter writerB = new IndexWriter(indexStoreB, new StandardAnalyzer(org.apache.lucene.util.Version.LUCENE_30), true, IndexWriter.MaxFieldLength.LIMITED); | |
IndexWriter writerC = new IndexWriter(indexStoreC, new StandardAnalyzer(org.apache.lucene.util.Version.LUCENE_30), true, IndexWriter.MaxFieldLength.LIMITED); | |
writerA.addDocument(lDoc); | |
writerA.addDocument(lDoc2); | |
writerA.addDocument(lDoc3); //#1. 첫번째 IndexWriter를 통해 3개의 문서를 색인 | |
writerA.optimize(); | |
writerA.close(); | |
writerB.addDocument(lDoc4); //#2. 두번째 IndexWrtier를 통해 1개의 문서를 색인 | |
writerB.optimize(); | |
writerB.close(); | |
writerC.close(); | |
} | |
private void addDocument() throws IOException { //#3. 첫번째 IndexWriter에 추가로 문서 색인을 하기 위한 메서드 | |
StatisticProperties prop = StatisticProperties.getProperties(); | |
Directory indexStoreA = FSDirectory.open(new File(prop.getProperty("INDEX_DIR")+"/2009")); | |
Document lDoc = new Document(); | |
lDoc.add(new Field("luceneSection", "total_request", Field.Store.YES, Field.Index.NOT_ANALYZED)); | |
lDoc.add(new Field("logDate", "20101212", Field.Store.YES, Field.Index.NOT_ANALYZED)); | |
lDoc.add(new Field("keyword", "반바지", Field.Store.YES, Field.Index.ANALYZED)); | |
lDoc.add(new Field("gubun", "COUNT", Field.Store.YES, Field.Index.ANALYZED)); | |
lDoc.add(new NumericField("statisticValue", 1, Field.Store.YES, Field.Index.NOT_ANALYZED.isIndexed()).setIntValue(52920)); | |
IndexWriter writerA = new IndexWriter(indexStoreA, new StandardAnalyzer(org.apache.lucene.util.Version.LUCENE_30), false, IndexWriter.MaxFieldLength.LIMITED); | |
writerA.addDocument(lDoc); | |
writerA.optimize(); | |
writerA.close(); | |
} | |
@Test | |
public void multiSearcherManagerTest() throws IOException { | |
//#4. 첫번째 테스트는 IndexSearcherManager에 대한 테스트로 Searcher의 return이 정상적으로 이루어지는지 | |
//그리고, Searcher를 close한 후에도 Searcher가 return이 되는지를 체크합니다. | |
IndexSearcherManager indexSearcherManager = IndexSearcherManager.getSearcherManager(); | |
Assert.assertNotNull(indexSearcherManager); | |
Assert.assertNotNull(indexSearcherManager.getSearcher()); | |
indexSearcherManager.closeIndexSearcher(); | |
Assert.assertNotNull(indexSearcherManager.getSearcher()); | |
} | |
@Test | |
public void multiSearcherManagerSearchTest() throws IOException, ParseException { | |
//#5. 실제 데이터로 테스트를 합니다. | |
IndexSearcherManager indexSearcherManager = IndexSearcherManager.getSearcherManager(); | |
MultiSearcher searcher = indexSearcherManager.getSearcher(); | |
QueryParser parser = new QueryParser(Version.LUCENE_30, "luceneSection", new WhitespaceAnalyzer()); | |
Query query = parser.parse("total_request"); | |
System.out.println("query : " + query); | |
ScoreDoc[] hits = searcher.search(query, null, 1000).scoreDocs; | |
Assert.assertEquals(4, hits.length); //#6. 처음에 4개의 문서를 색인하였으므로, 4개가 나와야 합니다. | |
addDocument(); //#7. 문서 추가 색인 | |
searcher = indexSearcherManager.getSearcher(); | |
hits = searcher.search(query, null, 1000).scoreDocs; | |
Assert.assertEquals(5, hits.length); //#8. 문서가 추가되었으므로 5개의 결과가 나와야 합니다. | |
} | |
} |
상당히 간단한 테스트케이스입니다.
실제 정렬이라던가하는 부분은 실제 정렬 필드를 생성하여 검색을 수행하는 클래스에서 하기 때문에
저 같은 경우는 따로 테스트케이스를 만들어서 사용하고 있습니다.
이 테스트케이스는 정말로 Manager 클래스에 대한 테스트이지요.....
루씬으로 단일 색인 파일로는 15기가정도의 파일까지는 사용을 해보았지만, 다수의 Index 파일은
처음 사용하려 하는 것이고, 개발이 끝난 상태가 아니라서 사실 더 많은 정보를 얻지는 못 하였습니다.
대략적으로 개발이 어느정도 끝나가고 있어서 (주요 모듈들..)
기존에 사용하던 단일 Index 파일로부터 새롭게 사용 할 분석연도별 인덱스 파일을
생성하기 위해 마이그레이션을 진행하였는데 2009년도가 7기가, 2010년도가 12기가 정도
나오는 것 같았습니다.
이정도면 사용하면서 어느정도 성능이라던가
추가적은 내용에 대한 공부가 될 수 있을 것 같네요...
대략적으로 MultiSearcher의 사용에 대해서 적어보았습니다.
루씬에 처음 다가서시려는 분들께 조금이나마 도움이 되었으면 좋겠네요...