본문 바로가기

Lucene

Lucene CustomScoreQuerySample #1

우선 아래의 소스는
http://blog.mono-koubou.net/wp-content/uploads/2008/01/valuesourcequerysample.txt
이 소스를 참고하여 좀 더 쉬운 내용으로 재구성 했음을 알려드립니다.

짐승님을 통해 알게 된 CustomScoreQuery. 이게 보면 볼 수록 매력적이다.

내일 공휴일이기도 하고..
이런거 한번 삘 받았을 때 좀 파보고 싶어서 위 링크의 소스를 보고
간단하게 이것 저것 테스트를 해보기 위한 소스를 만들어 보았다.

2개의 필드를 가진 데이터를 RamDirectory에 색인 해 놓고
검색 테스트를 하면서 score가 어떻게 변하는지 보았다.

위 예제에서는 ValueSource등을 거리 계산 값으로 계산하기 위한 Inner Class들이 몇 개 나오지만
일단 여기서는 필드에 있는 숫자로 정렬하기 위해서 기본적인 API만을 사용해 보았다.


package test;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.IndexWriter.MaxFieldLength;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.search.function.CustomScoreQuery;
import org.apache.lucene.search.function.FloatFieldSource;
import org.apache.lucene.search.function.ValueSourceQuery;
import org.apache.lucene.store.RAMDirectory;
public class CustomScoreQuerySample {
private Log logger = LogFactory.getLog(CustomScoreQuerySample.class);
public static void main(String[] args) throws Exception {
CustomScoreQuerySample sample = new CustomScoreQuerySample();
sample.makeIndex();
sample.execute("야구");
}
private static final String[][] DATAS = {
{"야구","981"},
{"축구","672"},
{"야구","521"},
{"축구","124"},
{"야구","908"},
{"축구","652"},
{"축구","872"},
{"야구","278"},
{"야구","485"},
{"축구","372"}};
private RAMDirectory dir = new RAMDirectory();
private Analyzer analyzer = new SimpleAnalyzer();
public void makeIndex() throws Exception{
IndexWriter writer = new IndexWriter( dir, analyzer , true, MaxFieldLength.UNLIMITED);
try{
for(int i=0;i<DATAS.length;i++){
Document doc = new Document();
doc.add( new Field( "value", DATAS[i][0], Store.YES, Index.NOT_ANALYZED ) );
doc.add( new Field( "count", DATAS[i][1], Store.YES, Index.NOT_ANALYZED ) );
writer.addDocument( doc );
}
}finally{
writer.close();
}
}
public void execute(String value) throws Exception{
IndexSearcher searcher = new IndexSearcher( dir );
try{
search(searcher,value);
}finally{
searcher.close();
}
}
private void search(Searcher searcher,String value) throws Exception{
Query tq = new TermQuery(new Term("value",value));
FloatFieldSource source = new FloatFieldSource("count");
ValueSourceQuery vq = new ValueSourceQuery(source);
//FieldScoreQuery vq = new FieldScoreQuery("count",FieldScoreQuery.Type.INT);
CustomScoreQuery query = new CustomScoreQuery(tq,vq) {
@Override
public float customScore(int doc, float subQueryScore, float valSrcScore) {
float result = subQueryScore * valSrcScore;
System.out.println("doc:"+doc+" / subQueryScore:"+subQueryScore+" / valSrcScore:"+valSrcScore + " / result:"+result);
return valSrcScore;
}
};
query.setStrict(true);
TopScoreDocCollector collector = null;
collector = TopScoreDocCollector.create(5 * 5, false);
searcher.search(query, collector);
ScoreDoc[] hits = collector.topDocs().scoreDocs;
int count = hits.length;
for( int i = 0; i < count; i++ ){
Document doc = searcher.doc(hits[i].doc);
float score = hits[i].score;
System.out.println( score + " : " + doc.get( "value" )+" / "+ doc.get( "count" ));
//Explanation exp = searcher.explain( query, hits[i].doc );
//System.out.println( exp.toString() );
}
}
}
view raw 1.java hosted with ❤ by GitHub


메서드별로 하나씩 살펴 보겠습니다.

public static void main(String[] args) throws Exception {
CustomScoreQuerySample sample = new CustomScoreQuerySample();
sample.makeIndex();
sample.execute("야구");
}
view raw 2.java hosted with ❤ by GitHub
main 메서드에서는 샘플 데이터를 RamDirectory를 사용해 인덱싱 하고 "야구"라는 검색어로 검색을 수행 합니다.


샘플 데이터입니다.
private static final String[][] DATAS = {
{"야구","981"},
{"축구","672"},
{"야구","521"},
{"축구","124"},
{"야구","908"},
{"축구","652"},
{"축구","872"},
{"야구","278"},
{"야구","485"},
{"축구","372"}};
view raw 3.java hosted with ❤ by GitHub
[0][i]의 값은 주제라고 가정하고, [i][1]의 값은 해당 주제에 대한 조회수라고 가정하겠습니다.
즉, main 메서드에서 "야구"라고 검색을 할 경우 주제가 "야구"인 것들이 검색이 되는데 이것을 조회수 많은 순으로
검색을 하려고 하는 것 입니다.


private RAMDirectory dir = new RAMDirectory();
private Analyzer analyzer = new SimpleAnalyzer();
public void makeIndex() throws Exception{
IndexWriter writer = new IndexWriter( dir, analyzer , true, MaxFieldLength.UNLIMITED);
try{
for(int i=0;i<DATAS.length;i++){
Document doc = new Document();
doc.add( new Field( "value", DATAS[i][0], Store.YES, Index.NOT_ANALYZED ) );
doc.add( new Field( "count", DATAS[i][1], Store.YES, Index.NOT_ANALYZED ) );
writer.addDocument( doc );
}
}finally{
writer.close();
}
}
view raw 4.java hosted with ❤ by GitHub

makeIndex() 메서드에서는위 샘플 데이터를 value와 count라는 필드로 색인을 합니다.
딱히 특별한 것은 없습니다.


public void execute(String value) throws Exception{
IndexSearcher searcher = new IndexSearcher( dir );
try{
search(searcher,value);
}finally{
searcher.close();
}
}
view raw 5.java hosted with ❤ by GitHub
검색어를 받아서 search 메서드를 통해 검색을 수행합니다.


private void search(Searcher searcher,String value) throws Exception{
Query tq = new TermQuery(new Term("value",value));
FloatFieldSource source = new FloatFieldSource("count");
ValueSourceQuery vq = new ValueSourceQuery(source);
//FieldScoreQuery vq = new FieldScoreQuery("count",FieldScoreQuery.Type.INT);
CustomScoreQuery query = new CustomScoreQuery(tq,vq);
query.setStrict(true);
TopScoreDocCollector collector = null;
collector = TopScoreDocCollector.create(5 * 5, false);
searcher.search(query, collector);
ScoreDoc[] hits = collector.topDocs().scoreDocs;
int count = hits.length;
for( int i = 0; i < count; i++ ){
Document doc = searcher.doc(hits[i].doc);
float score = hits[i].score;
System.out.println( score + " : " + doc.get( "value" )+" / "+ doc.get( "count" ));
//Explanation exp = searcher.explain( query, hits[i].doc );
//System.out.println( exp.toString() );
}
}
view raw 6.java hosted with ❤ by GitHub
핵심 메서드인데요... 여기서 이것저것 바꿔보고 테스트를 해보려고 합니다.

일단, 위 소스를 그대로 실행하면 아래와 같은 결과가 나옵니다.

1482.12 : 야구 / 981
1371.8297 : 야구 / 908
787.14014 : 야구 / 521
732.7504 : 야구 / 485
420.00952 : 야구 / 278
특별히 정렬에 대한 로직이 없어도 원하던대로 count의 역순으로 정렬되어 나오게 됩니다.


FloatFieldSource source = new FloatFieldSource("count");
ValueSourceQuery vq = new ValueSourceQuery(source);
CustomScoreQuery query = new CustomScoreQuery(tq,vq);
view raw 7.java hosted with ❤ by GitHub

이 3개 문장을 잘 봐주세요...
CustomScoreQuery를 사용해서 IndexSearcher를 통해 검색을 하게 되면, 검색되는 scoresDoc들의 score가 CustomScoreQuery의 customScore에 의해서 계산되고, 이 메서드는
public float customScore(int doc, float subQueryScore, float valSrcScore) {
   return subQueryScore * valSrcScore;
}
view raw 8.java hosted with ❤ by GitHub
이렇게 되어 있습니다.

이때, doc은 검색 된 document의 id이고, subQueryScore는 루씬에서 원래 검색 쿼리 (위 예제에서는 value:야구)에 의한
TF/IDF 점수입니다.
그리고, valSrcScore가 바로 ValueSourceQuery에서 source로 지정 된 FloatFieldSource의 값입니다.

위의 경우에는 각각의 데이터에 대해서 count 필드의 값인 981,908,521 등의 값이 됩니다.
기본적인 메서드의 구현이 subQueryScore * valSrcScore로 되어 있기 때문에 위 결과에서 score가 1482점 , 1371점으로
나오고 있는 것 입니다.

그럼 한번 coustomScore 메서드를 오버라이드 해보겠습니다.
CustomScoreQuery query = new CustomScoreQuery(tq,vq) {
@Override
public float customScore(int doc, float subQueryScore, float valSrcScore) {
float result = subQueryScore * valSrcScore;
System.out.println("doc:"+doc+" / subQueryScore:"+subQueryScore+" / valSrcScore:"+valSrcScore + " / result:"+result);
return valSrcScore;
}
};
//CustomScoreQuery query = new CustomScoreQuery(tq,vq);
view raw 9.java hosted with ❤ by GitHub

이제 customScore 메서드에서는 기본 쿼리에 의한 정확도 점수(TF/IDF)와 FloatFieldSource의 count 필드 값을
곱한 결과 값과 각각의 subQueryScore와 valSrcScore 값을 찍어내고 있습니다.
그리고 실제 리턴 값은 count 필드의 값을 리턴하고 있습니다.

이렇게 수정해서 실행 한 결과는 아래와 같습니다.

doc:0 / subQueryScore:1.5108256 / valSrcScore:981.0 / result:1482.12
doc:2 / subQueryScore:1.5108256 / valSrcScore:521.0 / result:787.14014
doc:4 / subQueryScore:1.5108256 / valSrcScore:908.0 / result:1371.8297
doc:7 / subQueryScore:1.5108256 / valSrcScore:278.0 / result:420.00952
doc:8 / subQueryScore:1.5108256 / valSrcScore:485.0 / result:732.7504

981.0 : 야구 / 981
908.0 : 야구 / 908
521.0 : 야구 / 521
485.0 : 야구 / 485
278.0 : 야구 / 278

보면 각 검색 결과 된 document의 score는 count 필드의 값으로 대체 되었으며, 이 값 자체가 점수가 되어 결국 count 필드의 값
역순으로 정렬되어 나오게 됩니다. 결과에서 result부분을 보면, 처음에 실행한 결과의 score와 같은 것을 확인 할 수 있습니다.

subQueryScore의 점수는 모두 동일한데 , value 필드에 "야구"라는 단어 하나만 들어가 있기 때문에
TF/IDF에 의한 점수는 같을 수 밖에 없습니다.

여기서 몇가지 더 보도록 하겠습니다.

일단, FloatFieldSource인데요.. 이건 Type별로 ByteField.., Int등의 클래스가 더 존재하는데
필드의 값 크기에 따라서 골라서 사용하시면 됩니다. 이 클래스의 사용은 데이타 byte 크기 * maxDoc bytes 만큼의 ram이 필요하기 때문에 크기별로 클래스를 나누어 놓은 것 같습니다.

그리고,
FloatFieldSource source = new FloatFieldSource("count");
ValueSourceQuery vq = new ValueSourceQuery(source);
view raw 10.java hosted with ❤ by GitHub
이 구문은
FieldScoreQuery vq = new FieldScoreQuery("count",FieldScoreQuery.Type.FLOAT);
view raw 1.java hosted with ❤ by GitHub
으로 변경 될 수 있습니다.
Type에 따라서 내부적으로 FieldSource 클래스를 취사선택 하도록 되어있습니다.

마지막으로 또 한가지는
CustomScoreQuery 에는 setStrict라는 메서드가 있습니다.

기본적으로 이 값은 false로 되어 있는데 이는
FieldSource의  값에 대해 쿼리 정규화를 적용하느냐 안 하느냐 입니다.
false일 경우 정규화를 하고 아닐 경우는 그 값을 그대로 사용합니다.

위 같은 경우 count 필드의 값이 그대로 점수로 노출 되는 이유는
query.setStrict(true);
이 구문 때문입니다.

이 구문을 주석처리 하면 결과는

doc:0 / subQueryScore:1.2598537 / valSrcScore:541.4526 / result:682.15106
doc:2 / subQueryScore:1.2598537 / valSrcScore:287.56046 / result:362.28412
doc:4 / subQueryScore:1.2598537 / valSrcScore:501.161 / result:631.3896
doc:7 / subQueryScore:1.2598537 / valSrcScore:153.43916 / result:193.3109
doc:8 / subQueryScore:1.2598537 / valSrcScore:267.6906 / result:337.251

541.4526 : 야구 / 981
501.161 : 야구 / 908
287.56046 : 야구 / 521
267.6906 : 야구 / 485
153.43916 : 야구 / 278

이렇게 바뀝니다.

내부적으로 어떤 계산식에 의해서 정규화가 진행 되는 것을 알 수 있습니다...

또 한가지..

search 메서드 제일 하단에 주석으로 막혀있는
Explanation exp = searcher.explain( query, hits[i].doc );
System.out.println( exp.toString() );
view raw 2.java hosted with ❤ by GitHub
이 부분을 주석으로 풀어주면

541.4526 : 야구 / 981
682.15106 = (MATCH) custom(value:야구, float(count)), product of:
  682.15106 = custom score: product of:
    1.2598537 = (MATCH) weight(value:야구 in 0), product of:
      0.83388424 = queryWeight(value:야구), product of:
        1.5108256 = idf(docFreq=5, maxDocs=10)
        0.5519394 = queryNorm
      1.5108256 = (MATCH) fieldWeight(value:야구 in 0), product of:
        1.0 = tf(termFreq(value:야구)=1)
        1.5108256 = idf(docFreq=5, maxDocs=10)
        1.0 = fieldNorm(field=value, doc=0)
    541.4526 = (MATCH) float(count), product of:
      981.0 = float(count)=981.0
      1.0 = boost
      0.5519394 = queryNorm
  1.0 = queryBoost
이런식으로 하나의 검색 결과에 대한 점수 계산을 보실 수 있습니다.

위 bold 처리 한 부분의 queryNorm 부분을 보시면 0.5519394 라고 되어 있는데 바로 쿼리 정규화가 적용된 것 입니다.
이 부분은 CustomScoreQuery.setStrict 메서드를 통해 true로 바꿔주면
아래와 같이 변경 됩니다.

981.0 : 야구 / 981
1482.12 = (MATCH) custom(value:야구, float(count)) STRICT, product of:
  1482.12 = custom score: product of:
    1.5108256 = (MATCH) fieldWeight(value:야구 in 0), product of:
      1.0 = tf(termFreq(value:야구)=1)
      1.5108256 = idf(docFreq=5, maxDocs=10)
      1.0 = fieldNorm(field=value, doc=0)
    981.0 = (MATCH) float(count), product of:
      981.0 = float(count)=981.0
      1.0 = boost
      1.0 = queryNorm
  1.0 = queryBoost

더 자세한 것은 더 공부해봐야 겠지만..
제일 위 link 걸어놓은 예제를 보면 예전에 termVector를 사용해서 계산 하던 것을
간단하게 거리 공식을 계산해서 만들어 놓은 것을 보실 수 있습니다.

이 다음에는 어떤 것을 가지고 실험을 해볼까.. 또 고민해봐야겠습니다. ㅎㅎ