본문 바로가기

Lucene

Lucene CustomScoreQuerySample #2 - 응용편 -

우선 이 포스트에 나오는 sample 소스는
http://blog.mono-koubou.net/wp-content/uploads/2008/01/valuesourcequerysample.txt
의 소스를 살짝 수정한 소스 입니다.

앞선 포스트에서는 루씬의 CustomScoreQuery 기초적인 사용법을 보았습니다.

이번에는 이 CustomeScoreQuery를 이용해서 조금 응용을 해보겠습니다.

아래 이미지를 봐주세요.



바로 집을 기준으로 거리가 5이내에 있는 스시 집을 찾아보도록 하겠습니다.
일단, 위와 같은 결과를 얻으려면 일단 스시인 집들을 찾고 그 안에서 각 좌표를 통해 거리를 계산해야 할 것 입니다.
거리를 구하는 공식은.. 피타고라스의 정리를 이용하면 아래와 같이 구해집니다.
(x'-x)^2 + (y' - y)^2 = 거리^2
에서 거리 = sqrt((x'-x)^2 + (y' - y)^2) 가 됩니다.

이것을 이용해 보겠습니다.

일단 코드입니다.


DistanceQuerySample.java
package test;
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.ValueSourceQuery;
import org.apache.lucene.store.RAMDirectory;
/**
* @author need4spd, need4spd@cplanet.co.kr
* @date 2010. 1. 6.
*
*/
public class DistanceQuerySample {
public static void main(String[] args) throws Exception {
DistanceQuerySample sample = new DistanceQuerySample();
sample.makeIndex();
sample.execute("스시",5,5,5);
}
private static final String[][] DATAS = {
{"라면","1,7"},
{"스시","5,2"},
{"라면","6,9"},
{"스시","1,9"},
{"라면","8,3"},
{"스시","7,7"},
{"스시","2,8"},
{"라면","7,4"},
{"라면","3,3"},
{"스시","6,9"}};
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( "position", DATAS[i][1], Store.YES, Index.NOT_ANALYZED ) );
writer.addDocument( doc );
}
}finally{
writer.close();
}
}
public void execute(String value,int x, int y, int distance) throws Exception{
IndexSearcher searcher = new IndexSearcher( dir );
try{
search(searcher,value,x,y,distance);
}finally{
searcher.close();
}
}
@SuppressWarnings("serial")
private void search(Searcher searcher,String value, int x, int y, int distance) throws Exception{
Query tq = new TermQuery(new Term("value",value));
DistanceFunction func = new DistanceFunction("position",x,y,distance);
ValueSourceQuery vq = new ValueSourceQuery(func);
Query 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;
}
};
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( "position" ));
}
}
}
view raw 1.java hosted with ❤ by GitHub

하나하나 살펴보겠습니다. 일단, main 메서드 입니다.

public static void main(String[] args) throws Exception {
DistanceQuerySample sample = new DistanceQuerySample();
sample.makeIndex();
sample.execute("스시",5,5,5);
}
view raw 2.java hosted with ❤ by GitHub

여기서는 "스시"집 중에서 집(5,5)를 기준으로 거리 5이내에 있는 스시집을 찾고자 하는 질의 입니다.
색인 데이터는

private static final String[][] DATAS = {
{"라면","1,7"},
{"스시","5,2"},
{"라면","6,9"},
{"스시","1,9"},
{"라면","8,3"},
{"스시","7,7"},
{"스시","2,8"},
{"라면","7,4"},
{"라면","3,3"},
{"스시","6,9"}};
view raw 3.java hosted with ❤ by GitHub
이렇게 각 가게의 이름과 좌표를 주고 있습니다.

색인하는 메서드는 이전 포스트에 있으니까 넘어가고 바로
search메서드를 살펴보겠습니다.


@SuppressWarnings("serial")
private void search(Searcher searcher,String value, int x, int y, int distance) throws Exception{
Query tq = new TermQuery(new Term("value",value));
DistanceFunction func = new DistanceFunction("position",x,y,distance);
ValueSourceQuery vq = new ValueSourceQuery(func);
Query 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;
}
};
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( "position" ));
//Explanation exp = searcher.explain( query, hits[i].doc );
//System.out.println( exp.toString() );
}
}
view raw 4.java hosted with ❤ by GitHub
좌표와 거리를 받아서 쿼리 구문을 만듭니다.

Query tq = new TermQuery(new Term("value",value));
view raw 5.java hosted with ❤ by GitHub

이 쿼리에 의해서 "스시"집들이 검색이 될 것 입니다.
CustomQuery는 앞에서 말씀드렸지만 기본 질의 쿼리와 valueSource 쿼리를 사용해서 점수를 계산합니다.
기본질의 쿼리의 점수와 valueSourceQuery의 점수를 마음대로 사용 할 수 있지요.

그런데, 이상한 클래스가 하나 있습니다.
DistanceFunction 이라는 클래스입니다.
이 클래스는 아래와 같은 모양을 갖습니다.
package test;
import java.io.IOException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.FieldCache;
import org.apache.lucene.search.function.DocValues;
import org.apache.lucene.search.function.FloatFieldSource;
public class DistanceFunction extends FloatFieldSource {
private static final long serialVersionUID = 899237622894172602L;
private final int x_;
private final int y_;
private final int d_;
public DistanceFunction(String field,int x,int y,int d) {
super(field);
x_ = x;
y_ = y;
d_ = d;
}
@Override
public DocValues getCachedFieldValues (FieldCache cache, String field, IndexReader reader) throws IOException {
String[] values = cache.getStrings(reader, field);
for(String value : values) {
System.out.println(" values : " + value);
}
return new DistanceDocValues(values,d_,x_,y_);
}
/**
* 2점 사이의 거리를 구하는 InnerClass
*/
public class DistanceDocValues extends DocValues{
private final String[] values_;
private final int x_;
private final int y_;
private final int d_;
public DistanceDocValues(String[] values,int d,int x,int y){
super();
values_ = values;
x_ = x;
y_ = y;
d_ = d;
}
@Override
public float floatVal(int doc) {
float distance = 0f;
try{
String value = values_[doc];
int px = Integer.parseInt(value.split(",")[0]);
int py = Integer.parseInt(value.split(",")[1]);
distance = (float)(Math.sqrt(Math.pow((px-x_),2)+Math.pow((py-y_),2)));
distance = (float)d_ - distance;
distance = distance > 0f ? distance : 0f;
}catch(Exception ex){
ex.printStackTrace();
}
return distance;
}
@Override
public String toString(int doc) {
return super.toString();
}
}
}
view raw 6.java hosted with ❤ by GitHub

보시면 아시겠지만 이 클래스는 FloatFieldSource를 상속받고 있습니다. 앞에서, CustomScoreQuery를 사용 할 때
FloatFieldSource를 사용해서 ValueSourceQuery를 만들었던 것 기억이 나시나요?


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

위 구문으로 ValueSourceQuery가 던져주는 score는 count필드의 값 그 자체가 되었었습니다.
현재 우리는 특정 필드의 값 자체가 필요한 것이 아니고
그 필드의 값 (좌표)를 통해 거리를 계산해야 하기 때문에 그것을 계산하기 위한 DistanceFunction 클래스를 만들었고
이것을 이용해 ValueSourceQuery를 만들어야하기 때문에 FloatFieldSource를 상속 받았습니다.

그럼 거리를 계산하는 메서드를 보겠습니다.
@Override
public DocValues getCachedFieldValues (FieldCache cache, String field, IndexReader reader) throws IOException {
String[] values = cache.getStrings(reader, field);
for(String value : values) {
System.out.println(" values : " + value);
}
return new DistanceDocValues(values,d_,x_,y_);
}
view raw 8.java hosted with ❤ by GitHub
IndexReader를 통해서 해당 필드의 값을 모두 뽑아 내고 있습니다. 여기서는 좌표값들이 나오겠죠..

그런데 여기서 return값이 계산 된 거리 값일 것 같았는데 DocValues 입니다.
CustomScoreQuery내부적으로는 ValueSourceQuery를 사용해서 ValueSourceQuery의 score를 계산하는데, 이 ValueSourceQuery는
ValueSourceScore를 계산하고 이때 사용 되는 것이 DocValues입니다. 때문에, 위 메서드에서 DocValues를 상속한
DistnaceDocValues를 리턴해주고 있는 것 입니다.

실제적으로 IndexReader를 통해서 뽑아온 좌표들을 사용해서 기준점과 거리를 계산하고 그 값을
리턴해주는 클래스는 아래의 클래스입니다.

public class DistanceDocValues extends DocValues{
private final String[] values_;
private final int x_;
private final int y_;
private final int d_;
public DistanceDocValues(String[] values,int d,int x,int y){
super();
values_ = values;
x_ = x;
y_ = y;
d_ = d;
}
@Override
public float floatVal(int doc) {
float distance = 0f;
try{
String value = values_[doc];
int px = Integer.parseInt(value.split(",")[0]);
int py = Integer.parseInt(value.split(",")[1]);
distance = (float)(Math.sqrt(Math.pow((px-x_),2)+Math.pow((py-y_),2)));
distance = (float)d_ - distance;
distance = distance > 0f ? distance : 0f;
}catch(Exception ex){
ex.printStackTrace();
}
return distance;
}
@Override
public String toString(int doc) {
return super.toString();
}
}
view raw 9.java hosted with ❤ by GitHub
이 클래스에서는 기준점과 대상점을 받아서 거리를 계산하고 있습니다.
public float floatVal(int doc) 메서드의 이름으로 유추해볼때 i번째 검색된 document에 대한 거리 값을 리턴 할 수
있도록 되어 있는 것 같습니다.

distance = (float)d_ - distance;

가 들어가 있는 것은 기준 되는 거리(5) 에서 계산 된 거리를 뺄 경우 가장 가까울수록 큰 값이 나오기 때문입니다.
이 값 그 자체가 score가 되기 때문에 위와 같이 distance를 계산해서 넘겨주면 가장 가까운 가게가 가장 먼저
나오게 됩니다.

distance = distance > 0f ? distance : 0f;

이 부분이 들어가 있는 이유는 5이내의 가게를 찾는 것이기 때문에 거리가 5 이상이 되는 것들은 맨 밑으로 내려버리기 위함입니다.

위와 같이 DocValues를 상속받아 클래스를 만들면 원하는 값으로 점수를 마음대로 산정 할 수 있게 됩니다.

위 클래스 실행 결과는 아래와 같습니다.
1.1985767 : 스시 / 7,7
1.1038789 : 스시 / 5,2
0.48399264 : 스시 / 6,9
0.41801658 : 스시 / 2,8
0.0 : 스시 / 1,9