본문 바로가기

Lucene

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

앞선 포스트에서 IndexWriter를 사용한 색인을 알아보았습니다.
이번에는 IndexSearcher를 사용한 Document 검색과 검색을 할 때 사용되는
Query에 대해서 알아보고 더불어..Filter까지도 함께 알아보도록 하겠습니다.

우선 간단한 IndexSearcher의 테스트 케이스입니다.
 
 
IndexSearcherTest.java
package com.tistory.devyongsik.search;
import java.io.IOException;
public class IndexSearcherTest {
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();
}
@Test
public void searchByTerm() 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);
t = new Term("titles", "action");
q = new TermQuery(t);
docs = indexSearcher.search(q, 10);
Assert.assertEquals(3, docs.totalHits);
ScoreDoc[] hits = docs.scoreDocs;
for(int i = 0; i < hits.length; i++) {
System.out.println(hits[i].doc);
System.out.println(hits[i].score);
Document resultDoc = indexSearcher.doc(hits[i].doc);
System.out.println(resultDoc.get("titles"));
}
}
@Test
public void searchByBooleanQuery() throws CorruptIndexException, IOException {
IndexSearcher indexSearcher = new IndexSearcher(directory);
BooleanQuery resultQuery = new BooleanQuery();
Term t = new Term("ids", "1");
Query q = new TermQuery(t);
resultQuery.add(q, Occur.SHOULD);
Term t2 = new Term("contents", "ibatis");
Query q2 = new TermQuery(t2);
resultQuery.add(q2, Occur.SHOULD);
TopDocs docs = indexSearcher.search(resultQuery, 10);
Assert.assertEquals(2, docs.totalHits);
}
@Test
public void searchByTermRangeQuery() throws CorruptIndexException, IOException {
IndexSearcher indexSearcher = new IndexSearcher(directory);
Query q = new TermRangeQuery("titles2", "h", "j", true, true);
TopDocs docs = indexSearcher.search(q, 10);
Assert.assertEquals(2, docs.totalHits);
}
@Test
public void searchByNumericRangeQuery() throws CorruptIndexException, IOException {
IndexSearcher indexSearcher = new IndexSearcher(directory);
Query q = NumericRangeQuery.newIntRange("price", 2000, 4000, true, true);
TopDocs docs = indexSearcher.search(q, 10);
Assert.assertEquals(2, docs.totalHits);
}
}
view raw 1.java hosted with ❤ by GitHub


특별히 어려운 코드는 없을 것 입니다.
테스트 케이스가 4가지가 있는데요 나중에 이야기 할
Query 종류에 따른 사용을 보여드리기 위해서 각각 테스트 케이스를 작성하였습니다.

IndexSearcher를 사용 할 때의 주의 할 점은 여러가지가 있겠지만
그 중 가장 중요한 것이 IndexSearcher의 생성은 비용이 많이 들어가는 부분이기 때문에
가급적 단일 인스턴스를 여러 스레드가 사용 하는 형식으로 구현을 하는 것이 좋고
IndexWriter에 의한 변경 내용을 바로 IndexSearcher가 확인 할 수 없기 때문에 이에 대한 처리를
고민해봐야 합니다. (이 부분은 나중에.. 다시 언급 할 예정입니다.) 

IndexSearcher는 기본적으로 Query 클래스와 검색을 해 올 Document의 숫자를
지정하도록 되어있습니다.

TopDocs docs = indexSearcher.search(Query, 10);


Query는 나중에 따로  이야기를 할 예정이고 일반적으로 최초 검색 결과 페이지에서
보여 줄 정도의 개수만을 검색하고 이후 페이징에 따라서 검색 해올 개수를 늘려가는
방식으로 구현합니다. 

 이렇게 받아온 TopDocs 클래스로부터 ScoreDoc[] 을 얻어냅니다.

ScoreDoc[] hits = docs.scoreDocs;

//이 ScoreDoc은 실제 검색 된 Document의 내부 ID와 점수를 가지고 있습니다.

for(int i = 0; i < hits.length; i++) {
               System.out.println(hits[i].doc);
               System.out.println(hits[i].score);
          }


위 주석에도 작성하였지만 이 ScoreDoc은 자기 자신의 docID와 (색인 파일내에서 유일한 ID 값입니다.) 점수를 가지고 있습니다.

그리고 검색 된 내용을 가지고 오고 싶을 때는 아래와 같이 구현합니다. 

 Document resultDoc = indexSearcher.doc(hits[i].doc);
System.out.println(resultDoc.get("titles"));


실제 Document로부터 색인시 필드의 옵션 중 Store 옵션을 YES 설정 한 필드에 대해서는
String으로 된 필드의 내용을 얻어 올 수 있습니다.

앞에서 잠깐 언급하였지만 IndexSearcherIndexWriter에 의한 변경 내용을 자동으로 반영하여 보여주지 못 합니다.
이 부분에 대한 처리를 같이 살펴보고.. 2.9부터 지원이 되는걸로 생각이 되는 Near Real Time Search에 대해서
살펴보고 그 후에 쿼리들에 대한 이야기를 해보려고 합니다.

예제 소스들은https://github.com/need4spd/aboutLucene
에서 체크아웃 받으 실 수 있습니다.