이번 포스트에서는 루씬에서 사용되는 Query와 Filter의 기본적은 사용예제를
보여드리려고 합니다.
다시 예전에 작성했던 IndexSearcherTest의 메서드들을 살펴보면
4가지 쿼리 타입에 대해서 테스트 케이스가 작성되어 있는 것을 보실 수 있습니다.
IndexSearcherTest.java
각 메서드를 간단하게 살펴보면 우선 searchByTerm 메서드에서는
Term을 사용하여 Query를 만들고 있습니다.
위 쿼리는 ids 필드에서 값이 1인 Document를 검색하고 싶다는 뜻입니다.
searchByBooleanQuery 메서드에서는 Term을 사용한 Query 2개를 BooleanQuery를 사용하여
연결하고 있습니다.
위 예제는 ids가 1 이거나 contents에 ibatis 키워드가 들어가 있는 Document를 찾겠다는 뜻 입니다.
두개의 쿼리를 연결 할 때 Occur.SHOULD가 사용되는데 이것은 두 쿼리를 OR로 연결하겠다는 뜻 입니다.
이외에 MUST (and) 와 MUST NOT(and not) 등이 있습니다.
searchByTermRangeQuery 메서드에서는 TermRangeQuery를 사용한 예입니다.
titles2가 h ~ j 사이 (첫 알파벳 기준)에 있는 Document를 검색합니다. 이때 대상 filed는 Indexing은 되어야 하고
Analyze는 되어있지 않아야 합니다.
searchByNumericRangeQuery 메서드는 NumericRangeQuery를 사용한 예 입니다.
여기서는 price가 2000 ~ 4000 사이의 Document를 검색합니다. 맨 끝 boolean 파라메터 2개는 2000과 4000을 포함 할 것인지여부입니다. true가 포함하는 것 입니다.
NumericRangeQuery 쿼리의 static 메서드는 newIntRange말고도 LongRange등 여러 종류의 메서드가 제공되는데
한가지 주의 하실 점이 이 쿼리를 사용하시려면 색인시에 해당 필드의 타입을 맞춰주어야 합니다.
위 예제로 보면 IntRange를 사용하고 있기 때문에 색인시에 위 필드는 아래와 같이 색인 되어야 합니다.
물론, TermRangeQuery처럼 Indexing은 되어 있어야하고, Analyze는 되어있지 않아야 합니다. (숫자이니 딱히 Analyze 할 건 없겠죠..)
추가적으로 Term쿼리와 Boolean쿼리에 대해서 말씀을 드리려고 합니다.
BuildQueryTest.java
FilterTest.java
보여드리려고 합니다.
다시 예전에 작성했던 IndexSearcherTest의 메서드들을 살펴보면
4가지 쿼리 타입에 대해서 테스트 케이스가 작성되어 있는 것을 보실 수 있습니다.
IndexSearcherTest.java
This file contains 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; | |
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.IndexWriter; | |
import org.apache.lucene.index.IndexWriterConfig; | |
import org.apache.lucene.index.Term; | |
import org.apache.lucene.search.BooleanClause.Occur; | |
import org.apache.lucene.search.BooleanQuery; | |
import org.apache.lucene.search.IndexSearcher; | |
import org.apache.lucene.search.NumericRangeQuery; | |
import org.apache.lucene.search.Query; | |
import org.apache.lucene.search.ScoreDoc; | |
import org.apache.lucene.search.TermQuery; | |
import org.apache.lucene.search.TermRangeQuery; | |
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; | |
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); | |
} | |
} |
각 메서드를 간단하게 살펴보면 우선 searchByTerm 메서드에서는
Term을 사용하여 Query를 만들고 있습니다.
Term t = new Term("ids", "1");
Query q = new TermQuery(t);
위 쿼리는 ids 필드에서 값이 1인 Document를 검색하고 싶다는 뜻입니다.
searchByBooleanQuery 메서드에서는 Term을 사용한 Query 2개를 BooleanQuery를 사용하여
연결하고 있습니다.
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);
위 예제는 ids가 1 이거나 contents에 ibatis 키워드가 들어가 있는 Document를 찾겠다는 뜻 입니다.
두개의 쿼리를 연결 할 때 Occur.SHOULD가 사용되는데 이것은 두 쿼리를 OR로 연결하겠다는 뜻 입니다.
이외에 MUST (and) 와 MUST NOT(and not) 등이 있습니다.
searchByTermRangeQuery 메서드에서는 TermRangeQuery를 사용한 예입니다.
titles2가 h ~ j 사이 (첫 알파벳 기준)에 있는 Document를 검색합니다. 이때 대상 filed는 Indexing은 되어야 하고
Analyze는 되어있지 않아야 합니다.
Query q = new TermRangeQuery("titles2", "h", "j", true, true);
TopDocs docs = indexSearcher.search(q, 10);
searchByNumericRangeQuery 메서드는 NumericRangeQuery를 사용한 예 입니다.
Query q = NumericRangeQuery.newIntRange("price", 2000, 4000, true, true);
여기서는 price가 2000 ~ 4000 사이의 Document를 검색합니다. 맨 끝 boolean 파라메터 2개는 2000과 4000을 포함 할 것인지여부입니다. true가 포함하는 것 입니다.
NumericRangeQuery 쿼리의 static 메서드는 newIntRange말고도 LongRange등 여러 종류의 메서드가 제공되는데
한가지 주의 하실 점이 이 쿼리를 사용하시려면 색인시에 해당 필드의 타입을 맞춰주어야 합니다.
위 예제로 보면 IntRange를 사용하고 있기 때문에 색인시에 위 필드는 아래와 같이 색인 되어야 합니다.
NumericField numField = new NumericField("price", Field.Store.YES, true);
numField.setIntValue(prices[i]);
물론, TermRangeQuery처럼 Indexing은 되어 있어야하고, Analyze는 되어있지 않아야 합니다. (숫자이니 딱히 Analyze 할 건 없겠죠..)
추가적으로 Term쿼리와 Boolean쿼리에 대해서 말씀을 드리려고 합니다.
색인시 whiteanalyzer에 의해서 "lucene in action"의 문장이 [lucene]+[in]+[action]으로 분석되어
색인이 되었는데 검색시 Term t = new Term("title", "lucene in action"); 과 같은 형식으로
쿼리가 만들어지게 되면 절대로 검색이 되지 않을 것 입니다.
일반적으로 사용자가 입력 한 키워드를 스페이스로 구분하여 두개의 TermQuery를 생성하고
이를 BooleanQuery의 AND 조건으로 조합하면 키워드에 대해서는 괜찮은 쿼리식을 만들 수
있습니다. 다만, 띄어쓰기를 하지 않는 사용자도 있기 때문에 이 부분에 대해서는
Analyzer를 활용하여 쿼리를 만들어 볼 수도 있습니다.
This file contains 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.query; | |
import java.io.IOException; | |
import java.io.StringReader; | |
import junit.framework.Assert; | |
import org.apache.lucene.analysis.Analyzer; | |
import org.apache.lucene.analysis.TokenStream; | |
import org.apache.lucene.analysis.WhitespaceAnalyzer; | |
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; | |
import org.apache.lucene.index.Term; | |
import org.apache.lucene.search.BooleanClause.Occur; | |
import org.apache.lucene.search.BooleanQuery; | |
import org.apache.lucene.search.Query; | |
import org.apache.lucene.search.TermQuery; | |
import org.apache.lucene.util.Version; | |
import org.junit.Test; | |
/** | |
* @author need4spd, need4spd@cplanet.co.kr, 2011. 7. 27. | |
* | |
*/ | |
public class BuildQueryTest { | |
private String keyword = "lucene in action"; | |
@Test | |
public void buildQueryWithAnalyzer() throws IOException { | |
Analyzer a = new WhitespaceAnalyzer(Version.LUCENE_33); | |
TokenStream stream = a.tokenStream("not use", new StringReader(keyword)); | |
CharTermAttribute term = stream.getAttribute(CharTermAttribute.class); | |
BooleanQuery resultQuery = new BooleanQuery(); | |
while(stream.incrementToken()) { | |
String t = term.toString(); | |
Query q = new TermQuery(new Term("field", t)); | |
resultQuery.add(q, Occur.MUST); | |
} | |
System.out.println(resultQuery); | |
Assert.assertEquals("+field:lucene +field:in +field:action", resultQuery.toString()); | |
} | |
} |
보시면 사용자가 입력 한 키워드에 대해서 Analyzer로 분석을 하여 Boolean 쿼리를
생성하고 있습니다. 이런 방식으로 키워드를 추출하여 검색 쿼리를 만드는 방법도 있으니
참고하시면 좋을 것 같습니다.
검색쿼리에서 Boolean쿼리를 사용하여 원하는 결과 값만을 가져 올 수 있지만
루씬에서는 Filter 클래스를 사용하여 마찬가지로 원하는 결과를 추려낼 수 있습니다.
This file contains 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; | |
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.IndexWriter; | |
import org.apache.lucene.index.IndexWriterConfig; | |
import org.apache.lucene.search.FieldCacheTermsFilter; | |
import org.apache.lucene.search.Filter; | |
import org.apache.lucene.search.IndexSearcher; | |
import org.apache.lucene.search.MatchAllDocsQuery; | |
import org.apache.lucene.search.NumericRangeFilter; | |
import org.apache.lucene.search.Query; | |
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 FilterTest { | |
private String[] ids = {"1","2","3"}; | |
private String[] titles = {"lucene in action action","hadoop in action","ibatis in action"}; | |
private String[] contents = {"java search lucene is a search engine made by java", "hadoop is a dist file system with java","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 filterByTerm() throws CorruptIndexException, IOException { | |
IndexSearcher indexSearcher = new IndexSearcher(directory); | |
Query allQuery = new MatchAllDocsQuery(); | |
Filter f = new FieldCacheTermsFilter("ids", "2"); | |
TopDocs docs = indexSearcher.search(allQuery, f, 10); | |
Assert.assertEquals(1, docs.totalHits); | |
} | |
@Test | |
public void filterByPriceRange() throws CorruptIndexException, IOException { | |
IndexSearcher indexSearcher = new IndexSearcher(directory); | |
Query allQuery = new MatchAllDocsQuery(); | |
Filter f = NumericRangeFilter.newIntRange("price", 2000, 4000, true, true); | |
TopDocs docs = indexSearcher.search(allQuery, f, 10); | |
Assert.assertEquals(2, docs.totalHits); | |
} | |
} |
메서드가 2개가 있는데요 하나는 Term에 의한 필터링
하나는 Range를 사용한 필터링입니다. Boolean 쿼리에 의한 필터링과 가장 큰 차이점은
필터는 점수에 대한 관여를 하지 않는다는 것 입니다.
관련 내용은
http://stackoverflow.com/questions/3721020/lucene-what-is-the-difference-between-query-and-filter
여기를 보시면 아실 수 있을 것 입니다.
필터를 사용 할 때 주의 할 부분이 있는데요
Term에 의한 필터링을 할 때는 해당 필드의 데이터가 NOT_ANALYZED로
되어 있어야 값이 정상적으로 나옵니다.
그리고 Range에 의한 필터는
해당 필드가 같은 NumericType으로 색인이 되어있어야
정상적으로 결과를 가져 올 수 있습니다.
RangeFilter에서 맨 끝의 boolean 파라메터 두개는
경계값을 가져 올 것인지 말 것인지에 대한 boolean 값입니다. true가 값을
포함하여 가져오도록 되어있습니다.
책을 보시면 CustomFilter를 직접 개발하여 사용하는 예제도 있으니
관심이 있으시다면 꼭 한번 보시기를 권해드립니다.
쿼리와 필터를 적절히 사용하시면 검색 식을 구현하는데 큰 무리가 없으실 것이구요..
검색에 있어서 가장 중요한 것은 아무튼 색인시에 얼마나 키워드가 잘 추출되었는가와
사용자가 입력한 키워드로부터 색인시 추출 되었던 키워드를 얼마나 잘 추출하여 쿼리식을
구성하는지 입니다.
보시면 아시겠지만 API 자체가 워낙 잘 구성이 되어있기 때문에
실제 사용에 있어서 큰 어려움은 없으실 것이라 생각됩니다.
예제는
https://github.com/need4spd/aboutLucene
예제는
https://github.com/need4spd/aboutLucene
에서 체크아웃 받으 실 수 있습니다.