본문 바로가기

Lucene

[about Lucene] 루씬으로 검색엔진 개발하기 - Near Realtime Search-

이번 글에서는
루씬인액션에서 소개되고 있는
Near Realtime Search에 대해서 작성해보려고 합니다.

기본적으로 IndexSearcherIndexWriter에 의한 변경 사항을
바로바로 반영하지를 못 합니다. 일반적으로 commit이 된 이후 IndexSearcher를 새로 생성하여야
IndexWriter에 의한 변경된 내용을 반영 할 수 있습니다.

제가 내부적으로 구현하여 사용하고 있는
프로그램에서도 이러한 부분을 Searcher를 새로 만들어서 사용하고 있습니다.

  RealTimeSearch.java
package com.tistory.devyongsik.search;
import java.io.IOException;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.TermVector;
import org.apache.lucene.document.NumericField;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* @author need4spd, need4spd@cplanet.co.kr, 2011. 7. 27.
*
*/
public class RealTimeSearch {
private String[] ids = {"1","2","3"};
private String[] titles = {"lucene in action action","hadoop in action","ibatis in action"};
private String[] contents = {"lucene is a search engine", "hadoop is a dist file system","ibatis is a db mapping tool"};
private int[] prices = {4000, 5000, 2000};
private Directory directory = new RAMDirectory();
private IndexWriter getWriter() throws CorruptIndexException, LockObtainFailedException, IOException {
IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_33, new WhitespaceAnalyzer(Version.LUCENE_33));
IndexWriter indexWriter = new IndexWriter(directory, conf);
return indexWriter;
}
@Before
public void init() throws CorruptIndexException, LockObtainFailedException, IOException {
IndexWriter indexWriter = getWriter();
for(int i = 0; i < ids.length; i++) {
Document doc = new Document();
doc.add(new Field("ids", ids[i], Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("titles", titles[i], Field.Store.YES, Field.Index.ANALYZED));
doc.add(new Field("titles2", titles[i], Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS));
doc.add(new Field("contents", contents[i], Field.Store.YES, Field.Index.ANALYZED, TermVector.YES));
NumericField numField = new NumericField("price", Field.Store.YES, true);
numField.setIntValue(prices[i]);
doc.add(numField);
indexWriter.addDocument(doc);
}
//indexWriter.commit();
indexWriter.close();
}
private void deleteDocument() throws CorruptIndexException, LockObtainFailedException, IOException {
IndexWriter indexWriter = getWriter();
indexWriter.deleteDocuments(new Term("ids", "1"));
indexWriter.commit();
indexWriter.close();
}
private void addDocument() throws CorruptIndexException, LockObtainFailedException, IOException {
IndexWriter indexWriter = getWriter();
Document doc = new Document();
doc.add(new Field("ids", "4", Field.Store.YES, Field.Index.NOT_ANALYZED));
doc.add(new Field("titles", "computer", Field.Store.YES, Field.Index.ANALYZED));
doc.add(new Field("titles2", "computer", Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS));
indexWriter.addDocument(doc);
indexWriter.commit();
indexWriter.close();
}
@Test
public void searchAfterDocumentDeleted() throws CorruptIndexException, IOException {
IndexSearcher indexSearcher = new IndexSearcher(directory);
Term t = new Term("ids", "1");
Query q = new TermQuery(t);
TopDocs docs = indexSearcher.search(q, 10);
Assert.assertEquals(1, docs.totalHits);
//삭제 후 다시 검색 해 본다.
deleteDocument();
docs = indexSearcher.search(q, 10);
Assert.assertEquals(1, docs.totalHits);
}
@Test
public void searchAfterDocumentAdded() throws CorruptIndexException, IOException {
IndexSearcher indexSearcher = new IndexSearcher(directory);
Term t = new Term("ids", "4");
Query q = new TermQuery(t);
TopDocs docs = indexSearcher.search(q, 10);
Assert.assertEquals(0, docs.totalHits);
//추가 후 다시 검색 해 본다.
addDocument();
docs = indexSearcher.search(q, 10);
Assert.assertEquals(0, docs.totalHits);
}
@Test
public void searchAfterDocumentDeleteIndexReaderReopen() throws CorruptIndexException, IOException {
//IndexReader를 얻어온다.
IndexReader indexReader = IndexReader.open(directory);
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
Assert.assertTrue(indexReader.isCurrent());
Term t = new Term("ids", "1");
Query q = new TermQuery(t);
TopDocs docs = indexSearcher.search(q, 10);
Assert.assertEquals(1, docs.totalHits);
//삭제 후 다시 검색 해 본다.
deleteDocument();
Assert.assertFalse(indexReader.isCurrent());
IndexReader newReader = indexReader.reopen();
if(newReader != indexReader) {
indexSearcher = new IndexSearcher(newReader);
indexReader.close();
}
docs = indexSearcher.search(q, 10);
Assert.assertEquals(0, docs.totalHits);
}
@Test
public void searchAfterDocumentAddedIndexReaderReopen() throws CorruptIndexException, IOException {
//IndexReader를 얻어온다.
IndexReader indexReader = IndexReader.open(directory);
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
Assert.assertTrue(indexReader.isCurrent());
Term t = new Term("ids", "4");
Query q = new TermQuery(t);
TopDocs docs = indexSearcher.search(q, 10);
Assert.assertEquals(0, docs.totalHits);
//삭제 후 다시 검색 해 본다.
addDocument();
Assert.assertFalse(indexReader.isCurrent());
IndexReader newReader = indexReader.reopen();
if(newReader != indexReader) {
indexSearcher = new IndexSearcher(newReader);
indexReader.close();
}
docs = indexSearcher.search(q, 10);
Assert.assertEquals(1, docs.totalHits);
}
}
view raw 1.java hosted with ❤ by GitHub


파일명은 RealTimeSearch이지만 사실 RealTimeSearch에 대한 예제는 아닙니다.

지금까지 루씬에서 IndexWriter에 의한 변경을 IndexSearcher에서 반영하기 위해 사용했던
방식을 테스트케이스 형태로 구현해둔 것 입니다.

우선 init()메서드를 통해 메모리에 문서를 색인하여 놓습니다.
그리고 private으로 deleteDocument, addDocument 메서드를 구현하였습니다.
이를 사용하여 IndexWriter에 의한 변경 내용이 어떻게 반영이 되는지 확인해 볼 것 입니다.

첫번째 테스트케이스 searchAfterDocumentDeleted 에서는
ids가 1인 문서를 찾고...  deleteDocument 메서드를 실행하여 ids가 1인 문서를 삭제합니다.
그리고 다시 ids가 1인 문서를 찾고 있습니다. deleteDocument에서 commit까지 하였지만 이미 생성되어 있던
IndexSearcher는 이 내용을 반영하지 못 합니다.

searchAfterDocumentDeleteIndexReaderReopen 메서드를 보시면
마찬가지로 ids가 1인 문서를 삭제를 한 후 ids가 1인 문서를 검색하는데 그 전에 IndexReader를 다시 생성하는
부분이 있습니다.

IndexReader newReader = indexReader.reopen();
if(newReader != indexReader) {
indexSearcher = new IndexSearcher(newReader);
indexReader.close();
}


바로 이 부분이 지금까지 사용했던 IndexWriter에 의한 변경 내용을 반영하는 방법이었습니다.
이렇게 구현을 해도 기능은 이상없이 잘 적용이 됩니다. 하지만 책에서는 IndexWritercommit 작업이 디스크 IO를 유발하고
기타 부하를 주는 작업이라고 하네요.. 그래서 commit을 하지 않아도 IndexSearcher가 변경된 내용을 반영 할 수 있는
NearRealTime Search가 바로 이번에 루씬인액션에서 소개되었습니다.


NearRealTimeTest.java
package com.tistory.devyongsik.search;
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.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;
import org.junit.Assert;
import org.junit.Test;
/**
* @author need4spd, need4spd@cplanet.co.kr, 2011. 7. 27.
*
*/
public class NearRealTimeTest {
@Test
public void testNearRealTime() throws Exception {
Directory dir = new RAMDirectory();
@SuppressWarnings("deprecation")
IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(Version.LUCENE_30), IndexWriter.MaxFieldLength.UNLIMITED);
for (int i = 0; i < 10; i++) {
Document doc = new Document();
doc.add(new Field("id", "" + i, Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
doc.add(new Field("text", "aaa", Field.Store.NO, Field.Index.ANALYZED));
writer.addDocument(doc);
}
@SuppressWarnings("deprecation")
IndexReader reader = writer.getReader();
IndexSearcher searcher = new IndexSearcher(reader);
Query query = new TermQuery(new Term("text", "aaa"));
TopDocs docs = searcher.search(query, 1);
Assert.assertEquals(10, docs.totalHits);
Assert.assertEquals(1,docs.scoreDocs.length);
writer.deleteDocuments(new Term("id", "7"));
Document doc = new Document();
doc.add(new Field("id", "11", Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
doc.add(new Field("text", "bbb", Field.Store.NO, Field.Index.ANALYZED));
writer.addDocument(doc);
IndexReader newReader = reader.reopen();
Assert.assertFalse(reader == newReader);
reader.close();
searcher = new IndexSearcher(newReader);
TopDocs hits = searcher.search(query, 10);
Assert.assertEquals(9, hits.totalHits);
query = new TermQuery(new Term("text", "bbb"));
hits = searcher.search(query, 1);
Assert.assertEquals(1, hits.totalHits);
newReader.close();
writer.close();
}
@Test
public void testNearRealTimeRemoveDeprecated() throws Exception {
Directory dir = new RAMDirectory();
IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_33, new WhitespaceAnalyzer(Version.LUCENE_33));
IndexWriter writer = new IndexWriter(dir, conf);
for (int i = 0; i < 10; i++) {
Document doc = new Document();
doc.add(new Field("id", "" + i, Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
doc.add(new Field("text", "aaa", Field.Store.NO, Field.Index.ANALYZED));
writer.addDocument(doc);
}
IndexReader reader = IndexReader.open(writer, true);
IndexSearcher searcher = new IndexSearcher(reader);
Query query = new TermQuery(new Term("text", "aaa"));
TopDocs docs = searcher.search(query, 1);
Assert.assertEquals(10, docs.totalHits);
Assert.assertEquals(1,docs.scoreDocs.length);
writer.deleteDocuments(new Term("id", "7"));
Document doc = new Document();
doc.add(new Field("id", "11", Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
doc.add(new Field("text", "bbb", Field.Store.NO, Field.Index.ANALYZED));
writer.addDocument(doc);
IndexReader newReader = reader.reopen();
Assert.assertFalse(reader == newReader);
reader.close();
searcher = new IndexSearcher(newReader);
TopDocs hits = searcher.search(query, 10);
Assert.assertEquals(9, hits.totalHits);
query = new TermQuery(new Term("text", "bbb"));
hits = searcher.search(query, 1);
Assert.assertEquals(1, hits.totalHits);
newReader.close();
writer.close();
}
@Test
public void testNearRealTimeRemoveDeprecatedDelete() throws Exception {
Directory dir = new RAMDirectory();
IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_33, new WhitespaceAnalyzer(Version.LUCENE_33));
IndexWriter writer = new IndexWriter(dir, conf);
for (int i = 0; i < 10; i++) {
Document doc = new Document();
doc.add(new Field("id", "" + i, Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
doc.add(new Field("text", "aaa", Field.Store.NO, Field.Index.ANALYZED));
writer.addDocument(doc);
}
IndexReader reader = IndexReader.open(writer, true);
IndexSearcher searcher = new IndexSearcher(reader);
Query query = new TermQuery(new Term("text", "aaa"));
TopDocs docs = searcher.search(query, 1);
Assert.assertEquals(10, docs.totalHits);
Assert.assertEquals(1,docs.scoreDocs.length);
writer.deleteDocuments(new Term("id", "7"));
Document doc = new Document();
doc.add(new Field("id", "11", Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
doc.add(new Field("text", "bbb", Field.Store.NO, Field.Index.ANALYZED));
writer.addDocument(doc);
IndexReader newReader = reader.reopen();
Assert.assertFalse(reader == newReader);
reader.close();
searcher = new IndexSearcher(newReader);
TopDocs hits = searcher.search(query, 10);
Assert.assertEquals(9, hits.totalHits);
query = new TermQuery(new Term("text", "bbb"));
hits = searcher.search(query, 1);
Assert.assertEquals(1, hits.totalHits);
//삭제 후
writer.deleteDocuments(new Term("text","bbb"));
IndexReader newReader2 = newReader.reopen();
Assert.assertFalse(newReader == newReader2);
newReader.close();
query = new TermQuery(new Term("text", "bbb"));
searcher = new IndexSearcher(newReader2);
hits = searcher.search(query, 1);
Assert.assertEquals(0, hits.totalHits);
newReader2.close();
writer.close();
}
}
view raw 2.java hosted with ❤ by GitHub

코드가 좀 길지만 맨 위 메서드가 책에서 나온 예제이고..
그 아래 두개의 메서드는 첫번째 메서드에서 deprecated된 메서드의 사용을 제가 수정한 메서드 두개입니다.

내용의 핵심은 IndexWrtierIndexReader를 얻어서 이를 사용하여 IndexSearcher를 생성하는 것 입니다.
기본적으로 IndexReader를 다시 open하는 작업은 동일하지만 IndexWritercommit을 하지 않아도
변경 된 내용을 IndexSearcher가 반영 할 수 있다는 부분이 다른 점 입니다.

IndexSearcher는 아래와 같이 생성합니다. 

IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(Version.LUCENE_30), IndexWriter.MaxFieldLength.UNLIMITED);
IndexReader reader = writer.getReader();
IndexSearcher searcher = new IndexSearcher(reader);


다만, 현재 3.3 버전에서는 IndexWriter.getReader 메서드가 deprecated처리가 되었기 때문에
이 부분을 아래와 같이 수정하였습니다.

IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_33, new WhitespaceAnalyzer(Version.LUCENE_33));
IndexWriter writer = new IndexWriter(dir, conf);
IndexReader reader = IndexReader.open(writer, true);
IndexSearcher searcher = new IndexSearcher(reader);


테스트케이스를 보시면 commit을 하지 않아도 문서가 추가되고 삭제된 내용이 IndexSearcher에 의해서
반영되고 있는 것을 확인 하실 수 있습니다.

https://github.com/need4spd/aboutLucene
에서 체크아웃 받으 실 수 있습니다.