앞선 포스트에서 FacetSearch에 대해 간단하게 알아보았습니다.


http://devyongsik.tistory.com/679


앞의 예제에서 카테고리를 "java"에서 "programming/java"와 같은 형태로 상위레벨의 카테고리를 같이 표현하면

어떻게 될까요?


한번 테스트를 돌려보면 아래와 같은 exception이 발생합니다.


java.lang.IllegalArgumentException: delimiter character '/' (U+2f) appears in path component "programming/lucene"


"/" 이 캐릭터가 문제가 된다는 것을 대략 알 수 있는데요.. 앞에 예제의 결과에서 색인시 CategoryPath를 생성할때 

CategoryPath cp = new CategoryPath("category", categories[i]);

와 같이 첫번째 파라메터로 넘긴 "category"가 결과에 자동으로 붙어서 "category/lucene"과 같이 나왔던 것을 기억하실겁니다.


결국, 색인시 "category/lucene"으로 색인하고, facet search를 할 때 

new CountFacetRequest(new CategoryPath("category"), 10) 

와 같이 CountFacetRequest를 생성하고 이 결과로 "lucene 3, java 4" 형태의 결과가 나오는 것인데요.


CategoryPath cp = new CategoryPath("category22", categories[i]);


이 과정에서 categories[i]의 값에 '/'가 포함될 경우 Exception이 발생하는 것을 알 수 있습니다.


그럼 일단, 이 부분을 해결하고 검색을 수행하는 샘플 코드를 보겠습니다.



1. category 값의 수정

우선 테스트를 위해 카테고리의 값을 수정하였습니다. 앞에 java혹은 ruby 카테고리를 상위 카테고리로 붙여넣었고 구분자는 "/"를 사용하였습니다. ruby/hadoop이라는 요상한 카테고리가 있지만 테스트를 위해 임의로 넣은 값이니 그냥 넘어가주세요. :)


2. 색인

TaxonomyWriter와 FacetFields를 사용한 추가색인은 동일합니다. 다만, CategoryPath를 생성하는 부분에서 약간의 

차이가 있습니다.

CategoryPath cp = new CategoryPath("category/" + categories[i], '/');

부분인데요..

맨 앞에 facetSearch를 위한 "category/"를 임의로 추가하고, 

맨 마지막 파라메터로 구분자를 넣어주는 형태입니다.


위의 코드는 아래의 코드로 대체될 수 있습니다.

CategoryPath cp = new CategoryPath("category", "java", "lucene");

즉, 구분자를 넘겨주는 것이 아니라 미리 값을 구분해서 넘겨주는 형태입니다.


3. 검색

검색 역시 많은 부분이 동일하지만, 검색을 위한 CategoryPath를 생성하는 부분에서 다소 차이가 있습니다.


CategoryPath drillDownCategoryPath = new CategoryPath("category" + "/" + "java", '/');

CategoryPath drillDownCategoryPath2 = new CategoryPath("category" + "/" + "ruby", '/');


List<FacetRequest> countFacetRequests = Lists.newArrayList();
countFacetRequests.add(new CountFacetRequest(new CategoryPath("category"), 10));
countFacetRequests.add(new CountFacetRequest(drillDownCategoryPath, 10));
countFacetRequests.add(new CountFacetRequest(drillDownCategoryPath2, 10));

를 보시면 되는데요, 간단하게 생각하시면 여기서 생성한 CategoryPath의 바로 다음 레벨까지만 facetSearch가 된다고 생각하시면 됩니다.
예를 들어서 위의 코드를 아래와 같이 수정해보면..

List<FacetRequest> countFacetRequests = Lists.newArrayList();
countFacetRequests.add(new CountFacetRequest(new CategoryPath("category"), 10));

결과는 아래와 같이 나옵니다.

## category: 2
category/java: 4
category/ruby: 2

category의 바로 다음 레벨의 값으로면 facet search가 되었죠. 하지만 우리가 원하는 것은 "java"와 "ruby"밑의 카테고리별의 facet search결과입니다. 때문에 그것을 위해서 위와 같이 CategoryPath를 추가로 생성하여 주고 있습니다.

4. 쿼리
앞선 포스트의 예제해서는 단순히 MatchAllDocsQuery를 사용하여 검색을 하였지만, 이 예제에서는 조금 다릅니다.
DrillDownQuery q = new DrillDownQuery(facetSearchParams.indexingParams, new MatchAllDocsQuery());
와 같이 쿼리를 생성하여 검색을 수행합니다.
MatchAllDocsQuery가 기본 쿼리로 검색을 수행하고, 그 결과셋을 가지고 CategoryPath에 의한 facetSearch가 진행됩니다.

5. 결과
## category: 2
category/java: 4
category/ruby: 2
## category/java: 3
category/java/lucene: 2
category/java/hadoop: 1
category/java/java: 1
## category/ruby: 2
category/ruby/hadoop: 1
category/ruby/ruby: 1



Posted by 용식

오랜만에 Lucene 포스트입니다.

FacetSearch의 샘플 코드인데요.. 기능 자체는 예전 버전부터 제공이 되었던 기능입니다.

FacetSearch란 무엇이냐 하면 간단하게 SQL의 group by와 같은 기능이라고 생각하시면 됩니다.


보통 쇼핑몰에서 검색결과를 카테고리별로 묶어서 해당 카테고리에 몇개의 상품이 검색되어 있는지등을

보여줄때 많이 사용됩니다.


그리고, DB도 그렇지만 성능에 많은 영향을 미치는 기능이기도 하지요..


11번가에서 검색 운영을 할때도 이런저런 성능적인 이슈들이 많았지만 그중 검색결과 페이지의 로딩 속도에 큰 영향을 주는 부분은 바로 이 group by 기능이었습니다.


예전부터 Lucene의 이 기능을 한번 사용해보고 싶었는데 이제서야 학습테스트 코드로나마 작성해보네요.. --ㅋ


깊이있게 들여다보지 못 하고 단순히 색인/검색을 할 수 있는 SampleCode를 보면서 간략히 설명을 드리겠습니다.




1. 준비
우선 가장 눈에 띄는 부분이 taxonomy라는 단어입니다. 

FacetSearch는 검색을 위해 색인하였던 인덱스를 사용하지않고 별도의 디렉토리를 생성합니다.

위 예제에서는 taxonomyDirectory가 그 역할을 하게됩니다. RAMDirectory가 아니라면 실제로는 원래의 색인 디렉토리와는 다른 물리적 디렉토리의 경로가 들어가게 될것입니다.


또한, taxonomyDirectory를 사용해 TaxonomyWriter를 생성합니다.


같은 경로나 혹은 같은 RAMDirectory를 사용하지 않는다는 것을 봐주시면 되고요.. 이 두가지 instance를 만들어내면 색인 준비는 완료가 됩니다.


2. 색인

색인을 할때는 FacetFields라는 클래스의 객체를 생성합니다. 4.0.X 버전에서는 CategoryDocumentBuilder 클래스를 사용했던것 같은데, 4.6.1버전에서는 이 클래스 대신 FacetFields 클래스를 사용합니다.

색인은 색인을 하게 될 모든 Document에 대해서 일반적인 색인외에 아래의 추가적인 과정을 진행합니다.

  1) List<CategoryPath>의 객체를 생성하고..

  2) CategoryPath 객체를 생성합니다. 이때, 필드명과 해당 필드의 value가 생성자로 사용됩니다. ("category", categories[i])

  3) 2)에서 생성한 categoryPath를 1)에서 생성한 List에 add합니다. grouping할 필드가 더 있다면 1),2)를 반복합니다.

  4) 3)에서 만들어진 List를 facetFields에 document와 함께 add합니다.

  5) 모든 Document에 대해 위 작업이 끝나면, taxnomyWriter의 commit()과 close()를 실행합니다.


3. 검색

색인에서 FacetSearch를 위한 추가과정이 있었듯이 검색에서도 추가과정이 있습니다.

우선 IndexSearcher외에 TaxonomyReader를 생성합니다. 

검색을 위한 Query객체를 생성한 후 FacetSearchParams 객체를 생성합니다.

그리고, FacetsCollector를 생성하여, MultiCollector를 사용해 indexSearcher의 search메서드를 실행합니다.

추측컨데 이 MultiCollector에 의해서 한번의 검색으로 TopScoreDocCollector와 FacetsCollector에 검색결과가 수집되는듯 합니다. 



4. 결과

실제 테스트 메서드를 실행한 결과는 아래와 같습니다.

category/hadoop, 2.0

category/lucene, 2.0

category/ruby, 1.0

category/java, 1.0


5. 추가

사실 위 내용에서 약간의 거짓말을 한부분이 있습니다. 색인시 CategoryPath를 만들때 제일 첫번째 파라메터가 groupby할 필드의 필드명이라고 했는데 실은 그렇지는 않습니다.

아래와 같이 작성하셔도됩니다.


CategoryPath cp = new CategoryPath("category22", categories[i]);


다만, 이 경우 검색시에 FacetSearchParams 파라메터를 아래와 같이 생성하여 주시면 됩니다.


FacetSearchParams facetSearchParams = new FacetSearchParams(new CountFacetRequest(new CategoryPath("category22"), 10));


결과는 아래와 같습니다.


category22/hadoop, 2.0

category22/lucene, 2.0

category22/ruby, 1.0

category22/java, 1.0


혹시 뭔가 느껴지시나요? 카테고리앞에 category, 혹은 category2가 붙어있는것을 보실 수 있습니다.

그렇다면 카테고리가 혹시 "programming/java" , "programming/ruby" 등과 같이 depth를 가지고 있다면 어떻게 될까요? 이 부분은 다음에 작성 할 포스트인 DrillDownQuery를 사용하여 해결해보도록 하겠습니다.

Posted by 용식

crescent의 색인관리 페이지를 루씬 4.4로 바꾸면서 

한구님께서 만드셨던 부분을 거의 다 손을 댔는데.. 그중 각 필드의 term 개수를 추출하는 부분에서 이상한 현상이 보였다. 

Long 혹은 Integer 타입의 필드에서 term이 이상하게 추출되는 것이었는데..

현상은 두가지로 나타나고 있었다.


1. 색인된 숫자가 깨져서 보임

2. 색인된 숫자보다 더 많은 term이 색인되어있음



테스트 코드를 만들어서 다시 확인을 해봐도 동일한 현상이었다.

stackoverflow에도 같은 현상으로 질문이 올라온 것도 찾았다.

http://stackoverflow.com/questions/11883066/luke-reveals-unknown-term-values-for-numeric-fields-in-index



위 코드는 모두 수정을 한 내용인데..

일단,1번 문제는 색인된 Term을 BytesRef라는 이름의 클래스를 사용해 뽑아내면서 색인된 숫자에 대해서는

NumericUtils.prefixCodedToInt 혹은 ToLong 메서드를 사용해야 정상적인 숫자를 볼 수 있는듯하고... (lucene user group에 질문 올리고 답변 받음.. 근데 왜 이래야하는지는... 아직 잘 모르겠음..;;;)


2번 문제는 루씬에서 숫자형식의 필드를 색인하는 방법에서 발생하는 차이인것으로 보인다.

위 stackoverlfow에도 있는 글에서도..

Within Lucene, each numeric value is indexed as a trie structure, where each term is logically assigned to larger and larger pre-defined brackets (which are simply lower-precision representations of the value). The step size between each successive bracket is called the precisionStep, measured in bits. Smaller precisionStep values result in larger number of brackets, which consumes more disk space in the index but may result in faster range search performance.

위 내용이 핵심인듯하다. 기본적으로 int가 32bit인데반해, 루씬에서의 NumberType Field의 기본적인 precisionStep이 4로 발생하는 문제였던것 같다. 그래서, 위 소스에서 precisionStep을 32로 셋팅을 해서 돌려보면 색인한 value만 딱 나오는 것이 확인된다.


루씬 document에서도 (2.9 기준이긴 하지만..)

range쿼리를 사용하지 않을거라면 precisionStep을 Integer.MAX_VALUE로 설정해도 괜찮다고 되어있다.

If you only need to sort by numeric value, and never run range querying/filtering, you can index using a precisionStep of Integer.MAX_VALUE. This will minimize disk space consumed.


하지만.. 굳이 numbertype field를 쓰면서.. rangeQuery를 안 쓰는 경우가 많지는 않을듯...


암튼.. 이 내용을 바탕으로 다시 수정해봐야겠다.

Posted by 용식

https://builds.apache.org/job/Lucene-Artifacts-4.x/javadoc/core/org/apache/lucene/index/package-summary.html#stats


예전에 루씬4.0이 처음 나왔을무렵에..

인덱스 파일에 대한 통계 정보를 가지고 오는 것을 한번 정리하려고 관련 자료만 스크랩을 해두었었는데요

(http://devyongsik.tistory.com/576)

이번에 관련 스크랩 정보를 사용해서, Lucene 4.4에서 한번 테스트를 해보았습니다.


Junit을 사용한 학습 테스트를 작성하였지만, 시간상 Assert는 사용하지 않고 그냥 값들을 System.out.println으로 출력만 하도록 하였습니다. 이 부분은 나중에 다시 손을 좀 봐야겠습니다. 아무래도 field랑  term을 loop돌면서 처리되는 것들이 많다보니.... 


일단 작성한 테스트 케이스 코드는 아래와 같습니다.



메서드는 4개입니다.

모드 인덱스 파일의 정보를 가져오는 메서드인데 단위가 가장 작은것부터 가져오도록 구성을 해보았습니다.

작은것부터 TermsEnum, Terms, DocsEnum, Segment입니다. 보통 하위 데이터의 sum을 상위 클래스에서 가져올 수 있는 구조로 되어있습니다.


테스트에 사용된 문서는 아래와 같습니다.


doc1

 - contents : sir lion

 - contents2 : the hadoop


doc2

 - contents : tiger

 - contents2 : hadoop drive


doc3

 - contents : the wolf lion

 - contents2 : tool


메서드별로 출력결과와 함께 간단한 설명을 붙여보도록 하겠습니다.


1. commonStatisticsFromTermsEnum

출력결과

fieldName : contents

doc Freq of [lion] : 2

total term Freq of [lion] : 2

------------------------------------

doc Freq of [sir] : 1

total term Freq of [sir] : 1

------------------------------------

doc Freq of [the] : 1

total term Freq of [the] : 1

------------------------------------

doc Freq of [tiger] : 1

total term Freq of [tiger] : 1

------------------------------------

doc Freq of [wolf] : 1

total term Freq of [wolf] : 1

------------------------------------


fieldName : contents2

doc Freq of [drive] : 1

total term Freq of [drive] : 1

------------------------------------

doc Freq of [hadoop] : 2

total term Freq of [hadoop] : 2

------------------------------------

doc Freq of [the] : 1

total term Freq of [the] : 1

------------------------------------

doc Freq of [tool] : 1

total term Freq of [tool] : 1

------------------------------------


##########################################


2개의 메서드가 제공되는데 하나는 TermsEnum.docFreq이고 하나는 TermsEnum.totalTermFreq입니다.

docFreq는 해당 Term이 그 필드에 존재하는 document의 개수입니다. [hadoop]을 보시면 docFreq가 2인것이 확인 가능하지만, [the]에 대해서는 각 필드별로 1씩 나눠져 나오는것을 보실 수 있습니다. 필드가 다르기 때문에 나타나는 결과입니다.

totalTermFreq는 전체 document의 필드에서 해당 Term이 나오는 횟수입니다. 마찬가지로 필드별로 계산됩니다.


2. commonStatisticsFromTerms

출력결과

size : 5

doc count : contents : 3

sum of doc freq : contents : 6

sum of total term freq : contents : 6

------------------------------------

size : 4

doc count : contents2 : 3

sum of doc freq : contents2 : 5

sum of total term freq : contents2 : 5

------------------------------------

4개의 메서드를 사용했습니다. Terms.size, Terms.getDocCount, Terms.getSumDocFreq, Terms.getSumTotalTermFreq 입니다.


Terms.size는 중복되는 Term을 제외한 그 필드의 Term 개수입니다. contents에서는 [the]가 contents2 [hadoop] term이 중복이며 그로인해 각각 5와 4의 값을 갖습니다.


Terms.getDocCount는 해당 필드에 term이 하나라도 있는 document의 개수입니다. 전체 document가 3개이고 각 document가 모두 필드에 term을 가지고 있으므로 동일하게 3의 값을 갖습니다.


Terms.getSumDocFreq는 해당 필드에서 TermsEnum.docFreq의 합입니다. 모든 term의 freq의 합을 뜻하게 되지요.. 여기서는 contents는 6, contents2는 5를 갖습니다. 이는, 첫번째 테스트에서 나온 TermsEnum.docFreq의 합과 같습니다.


Terms.getSumTotalTermFreq는 해당 필드에서 TermsEnum.totalTermFreq의 합입니다. 즉, 전체 document에서 이 필드에 전체 term의 freq 합입니다. TermsEnum이 term별 freq였다면 이건 이 term별 freq를 합한 값이 되겠죠..


3. commonStatisticsFromDocEnums

출력결과

freq from docsEnum : 1

freq from docsEnum : 1

##########################################

메서드는 하나입니다. DocsEnum.freq...

다만, DocsEnum을 받아올때의 메서드를 보면 

DocsEnum docsEnumForContents = MultiFields.getTermDocsEnum(directoryReader, MultiFields.getLiveDocs(directoryReader), "contents2", new BytesRef("hadoop"));

이렇게 되어있는데요... 필드와 term을 파라메터로 넘겨주고있습니다. 즉, 이 테스트에서는 전체 document에서 contents2 필드에 hadoop term을 가진 document들의 DocsEnum을 넘겨받게 되고... 2개의 document가 존재하므로 위와 같은 결과가 나오게 됩니다. 


만약 doc2의 contents2가 hadoop drive hadoop이라면 결과가 조금 다르겠죠..

아래와 같이 나올것입니다.

freq from docsEnum : 1

freq from docsEnum : 2

##########################################



4. commonStatisticsFromSegment

실행결과

max doc : 3

num docs : 3

deleted docs : 0

sum of doc freq : 6

sum of total term freq : 6

doc freq having this term in field : 2

##########################################

비교적 명료하게 때문에 자세한 설명은 생략하고... 대신 freq를 가져올 때 필드명을 지정한다는 것과 맨 마지막 docFreq 메서드에서는 Term 객체를 같이 넘겨준다는 것만 잘 보시면 될것 같습니다.


그외에도 몇가지가 더 있는것 같은데... 필요할때마다 찾아봐야할것 같습니다.


특히, FieldInvertState 클래스로 역인덱스 파일의 통계값을 많이 가져올 수 있는것 같은데 이걸 어떻게 얻어내는지 잘 모르겠네요... 더 공부가 필요할것 같습니다.


자세한 내용은 http://lucene.apache.org/core/4_4_0/core/org/apache/lucene/index/package-summary.html#termstats 에서도 확인 가능합니다.


테스트코드는 아래의 경로에 있습니다.

https://github.com/need4spd/aboutLucene/blob/master/lucene-4.x/src/test/java/com/tistory/devyongsik/indexing/IndexStatisticsTest.java



Posted by 용식

오픈되어 있는 소스중에 이미지로부터 50자리의 0과 1로 이루어진 hashcode를 추출해주는 소스가 있다.

(https://code.google.com/p/ironchef-team21/source/browse/ironchef_team21/src/ImagePHash.java?r=75856e07bb89645d0e56820d6e79f8219a06bfb7)


이미지 프로세싱에 대해서 잘 알고 있지 못 하기 때문에 정확하게 내용을 알고있지는 못 하지만

이 50자리의 해시코드가 완전히 같은 경우 동일한 이미지로 인식이 된다. 

소스중 distance를 구하는 메서드가 있는데 0~49 index까지 charAt 메서드를 사용하여 같은 숫자를 가지고 있는지 

파악하여, 다른 숫자의 개수를 return해 주는 메서드이다. 이 distance가 0이면 완전히 같은 이미지이고, distance의 값이 클수록 (거리가 멀수록) 다른 이미지가 된다.


이런저런 테스트를 해본결과 동일 이미지에 약간의 배너마크가 들어가거나 한 경우 5이내의 distance를 보였고, 특이한것은 이미지의 색감이 완전히 달라도 모양(패턴)이 비슷하면 distance의 값이 작게 나왔다.


위 소스를 보면 hashcode 추출시 흑백이미지로 변환시키는 부분이 있는데 그 부분에 의해서 나오는 현상이지 않을까 싶다.


그냥 짐작하건데 이미지를 50분할해서 각 부분의 해시를 추출하는게 아닐까 싶기도...


아무튼... 완전히 distance가 0이 아니더라도 5이내의 distance 값 안에서는 비슷한 (모양)패턴의 이미지가 찾아지기도 한다는 것을 확인하게 된 이후 (정말 운 좋게 절묘한 sample test에서 찾아냈다..) 티셔츠, 신발등 비슷한 모양의 이미지 검색과 동일한 이미지 검색을 사용 할 수 있겠다.. 싶은 생각이 들었다. 


완전히 같은 해시코드만을 select하여 동일 이미지를 찾는 것은 DB로도 충분히 가능하겠으나, distance가 5이내의 이미지 검색... 과 같은 형태가 된다면 Lucene의 CustomScoreQuery를 사용하면 좋지 않을까 싶은 생각이 들어서... 구현을 해보았다.


옛날에 작성했던 포스트를 참조해서 구현하려고 하는데.. Lucene 4.4는 예전과는 또 형태가 완전히 바뀌어서 ..

그리고 옛날에 작성했던 포스트의 내용이 제대로 기억나지 않아서 겨우겨우... 모양만 작성하였다 . -.-


일단, 이를 위해서 FunctionQuery와 CustomScoreQuery를 작성하였는데, FunctionQuery에서는 이미지의 hash코드를 사용하여 distance를 계산해 이를 float 값으로 리턴하여준다. 그러면, CustomScoreQuery에서는 이 값을 받아서 정렬을 위해 음수로 변환한 후 return하도록 간단히 구현하였다.


1. ImageHashDistanceFunction.java




ValueSource와 String을 생성자에서 인자로 받는다. 여기서 이 두개의 값을 사용하여 각 hash의 distance를 계산하게 되는데, String hash는 검색시 넘겨주는 hash값이다. (즉, 검색 쿼리가 되는 이미지의 hash 값이 되겠다.)

그리고 ValueSource로는 IndexReader로부터 색인되어 있는 이미지들의 hash 값을 가지고와서 이를 가지고 검색 쿼리로 들어온 hash코드와 distance 계산을 하게 된다.


그리고 그 값을 FloatDocValues의 인스턴스로 넘겨주고 있다.




2. ImageHashDistanceScore.java


customScore 메서드가 핵심인데.. 앞의 ImageHashDistanceFunction에서 넘어온 FloatDocValues의 값이 이 메서드의 subQueryScore로 넘어오게 된다. 이 메서드를 사용해서 예전에 포스팅한것처럼 여러가지 형태의 검색을 구현 할 수 있다. 여기서는 단순히 distance가 작을수록 높은 점수를 받게 하도록 하기 위해서 넘어온 FloatDocValues의 값을 음수로 만들어주는 형태로만 간단하게 구현을 하였다.


3. Test



searchFunctionDistance 메서드를 보면, 위에서 만든 두개의 클래스를 사용해서 Query를 만들고 이를 사용해서 검색을 하고있다. 예제에서는 "12345"와 가장 비슷한 hash값을 갖는 문서들을 순서대로 검색하게 된다.


hashCode : 12345, str1 : 12345, distance  : 0.0

fieldValue : 12345, subQueryScore : 0.0

hashCode : 12345, str1 : abcde, distance  : 5.0

fieldValue : abcde, subQueryScore : 5.0

hashCode : 12345, str1 : 12345, distance  : 0.0

fieldValue : 12345, subQueryScore : 0.0


id : null, hash : 12345, score : -0.0

id : null, hash : 12345, score : -0.0

id : null, hash : abcde, score : -5.0

위 결과를 보면 맨 마지막 3줄에서 같은 hash를 갖는 이미지들이 상위로 노출됨을 알 수 있다.


아직 제대로 파악을 하지 못 했을 수도 있지만 ImageHashDistanceFunction 클래스를 통해서 특정 distance이상의 이미지를 제거하지 못하였다. 값을 0을 줘도 음수를 줘도 어쨎든 값이 ImageHashDistanceScore로 넘어가버린다. 일단 이 클래스로 넘어오게 되면 필터링이 아니라 정렬순서에만 영향을 주기 때문에 그 이전에 Filtering 시키는 방법이 필요할듯한다. 


그 방법으로 Filter와 Collector 클래스를 확장하는 예제가 있는것 같은데.. Filter는 아직 학습이 부족하여 제대로 구현하지 못 하였다. Collector로는 아주 단순하게 한번 구현을 해보았는데..


4. HashDistanceCollector.java



collect 메서드가 그 기능에 맞춰서 구현된 메서드이다. 나머지는 루씬에서 구현된 부분들을 그대로 가지고왔다. 점수가 하드코딩 되어있기는 하지만, 특정 점수 이상(혹은 이하)의 경우에만 collect가 되도록 수정한 것이다.


5. Test


맨 마지막 메서드가 테스트 메서드인데.. 돌려보면 아래와 같은 결과가 나온다.

hashCode : 12345, str1 : 12345, distance  : 0.0

fieldValue : 12345, subQueryScore : 0.0

doc : 0, score : -0.0

hashCode : 12345, str1 : abcde, distance  : 5.0

fieldValue : abcde, subQueryScore : 5.0

doc : 1, score : -5.0

hashCode : 12345, str1 : 12345, distance  : 0.0

fieldValue : 12345, subQueryScore : 0.0

doc : 2, score : -0.0

id : null, hash : 12345, score : -0.0

id : null, hash : 12345, score : -0.0

-1보다 큰 점수를 갖는 이미지만 collect하도록 되어있기 때문에, 위와는 달리 -5.0을 갖는 이미지는 검색에서 제외되었음을 알 수 있다. 또한, 로그를 통해서 FunctionQuery와 CustomScoreQuery, Collector가 어떤 순서로 작동하는지도 대략적으로 확인이 가능하다.




Posted by 용식

이전에 IndexCommit에 대해서 잠깐 확인을 해보았었는데요..

이번에는 IndexDeletionPolicy에 대해서 확인해보려고 합니다.


기본적으로 제공되는 IndexDeletionPolicy의 구현 클래스로는 

KeepOnlyLastCommitDeletionPolicy와 SnapShotDeletionPolicy가 있습니다.

KeepOnlyLastCommitDeletionPolicy가 기본이구요.


commit을 할때마다 commit의 정보를 가지고있는 IndexCommit 객체가 생성되는데 이 IndexCommit을

어떻게 할것이냐의 정책이라고 보시면 좋을 것 같습니다.


우선 기본적인 IndexDeletionPolicy에서 제공되는 메서드가 2개가 있습니다. 하나는, onInit 메서드 하나는 onCommit 메서드입니다. KeepOnlyLastCommitDeletionPolicy에서는 oninit 메서드에서는 commit list가 commit 할때마다 넘어오게되고 

이를 onCommit 메서드로 넘겨서 제일 마지막 Commit남 남기고 삭제하도록 되어있습니다. 



위 예가 그것을 보여주고 있는데요, 로깅을 추가하기위해서 MyPolicy라고 내부 클래스를 하나 만들어서 적용해보았습니다. 로직은 KeepOnlyLastCommitDeletionPolicy와 동일합니다.



이런형태로 IndexDeletionPolicy를 상속하면 commit시의 행동을 결정 할 수도 있습니다.

이를 이용해서 replication도 적용이 가능하겠구요.. 백업도 가능하겠구요...


두번째는 SnapShotDeletionPolicy인데요, 이것은 사용자가 key로 현재의 IndexCommit을 저장해두는 (스냅샷을 만드는) 기능을 제공합니다. 인덱스파일들이 백업이 되어있다면 특정 시점으로의 복구나 이런것들도 가능할것 같습니다.



Posted by 용식

IndexDeletionPolicy에 대한 내용을 보기전에 IndexCommit에 대한 학습예제를 조금 

작성해보았습니다.

lucene 소스에 있는 IndexCommit 테스트케이스에는 두개의 IndexCommit에 대한 비교만

들어가 있네요.. 실제 commit시에 어떠한 값을 가지고 있는지 확인해보고 싶어서 아래와 같이

출력만하는 학습테스트 클래스를 작성하였습니다.



결과를 보시면..

1. commit이 일어날때마다 generation을 계속 증가합니다.

2. commit이 일어날때마다 segmentcount 역시 증가합니다.

3. segmentcount의 경우 위 예제에서의 결과만으로 보면, 문서를 하나씩 indexWriter에 add 후 commit을 하였기 때문에 문서 하나가 하나의 세그먼트에 들어갔고, 그 결과 "java" term으로 삭제하고 commit을 했을 때 generation을 4로 증가하였지만, segmentcount는 문서 하나가 삭제 되면서 2로 줄어든것이 확인 가능합니다.

만약, document1,2를 한번에 add 후 commit을 하였다면 위 결과는 조금 다를 것입니다. 그 코드는 밑에 다시 붙여넣을게요..

4. segmentFileName역시 segment_N의 형태로 나타납니다.

5. commit후 남아있는 파일 리스트를 fileName으로 받아올 수 있습니다.

6. isDeleted의 값은 이 테스트에서는 어디에 사용되고 무엇을 뜻하는지 잘 알 수 없네요..



위 3번에서 말씀드린 예제를 보시면..



위와 같이 나오네요... "java" term으로 삭제시에 segementCount가 2로 유지되는것을 보실 수 있습니다.


이 내용을 바탕으로 IndexDeletionPolicy 부분을 좀 살펴봐야겠습니다.


Posted by 용식

예전에 루씬으로 색인한 인덱스파일로부터 Term Freq를 뽑는 포스팅을 한적이 있는데요..


http://devyongsik.tistory.com/577

http://devyongsik.tistory.com/578


그 당시에는 인덱스리더로부터 TermVector를 하나 들고와서 TermVector로부터 Terms 객체를 얻어서

getSumTotalTermFreq()를 실행했었고 이 값이 -1이 나왔었습니다. 이건 전체 Term의 Freq의 합이죠..


IndexReader ir = IndexReader.open(dir);

Terms terms1 = ir.getTermVector(0, "f");


System.out.println(terms1.getSumTotalTermFreq());


당시 루씬 메일링 리스트에 질문을 해서 위와 같은 케이스에서의 -1은 정상이라는 답변도 받았었습니다.


이번에 페이스북에서 '김호연'님하고 같이 이야기를 하다가 알게된 사이트에서

http://sujitpal.blogspot.kr/2012/12/analyzing-enron-data-frequency.html


MultiFields로부터 TermFreq를 얻는 방법이 있어서, 이것을 자바로 변환해보았습니다.


결과를 보시면 전체 색인된 document로부터 같은 필드명을 갖는 필드에 대한 모든 Term Freq 정보를 가져올 수 있네요. 위에서 -1이 나오던 getSumTotalTermFreq도 해당 필드에서 추출된 전체 Term의 Freq의 합으로 잘 나옵니다.


위 예에서는 13.. 왠지 전체 필드에 대한 Term을 가지고 위와 비슷한 작업을 할 수 있을것도 같은데요.. 흠..


Posted by 용식

Lucene에서 indexWriter.updateDocument는 

Term에 해당하는 document를 바로 update하는 것이 아니고

delete 후 insert 로직으로 작동합니다.


즉, id=2인 document를 update 하려고 했을때, 인덱스에 id=2인 document가 없다면

업데이트를 하려고했던 document가 insert가 됩니다.






간단한 테스트케이스를 보시면..

처음에 id=1인 문서를 색인하고(addDocument) 그 이후에 id=1인 term query로 update를 합니다.


update 후 id=1로 검색을 한 결과를 보면 아래와 같이

Assert.assertTrue(topDocs.totalHits == 1);

Assert.assertEquals("document 1 update", indexSearcher.doc(topDocs.scoreDocs[0].doc).get("title"));


update된 문서를 보실 수 있습니다.

그 이후에 id=2인 document를 id=2인 term query로 update를 하게 되면 기존 인덱스에는 id=2인 term으로 검색되어 나오는 document가 없기 때문에, id=2인 document가 insert가 됩니다.


id=2로 검색을 해보면 아래와 같은 결과를 얻습니다.

Assert.assertTrue(topDocs.totalHits == 1);

Assert.assertEquals("document 2 update", indexSearcher.doc(topDocs.scoreDocs[0].doc).get("title"));


그리고 당연히 전체 document 개수는 2개가 되어있겠죠.

topDocs = indexSearcher.search(new MatchAllDocsQuery(), 10);

Assert.assertTrue(topDocs.totalHits == 2);

Posted by 용식

저와 같이 Crescent 프로젝트를 진행해주고 계시는 강한구님과

저에게 항상 많은 영감과 자극을 주시는 진성주님의 두개 포스트를 링크합니다.


1. http://blog.softwaregeeks.org/archives/816

2. http://dol9.tistory.com/217


우선 1번의 진성주님 블로그를 보시고 프로젝트 import까지 진행하시고

2번의 강한구님의 블로그를 보시면서, codec, core, test-framework를 ant build 후

클래스패스에 추가해주신다음에 테스트 케이스를 돌려보면 잘 되실거에요.


저 같은 경우는 core는 이미 클래스패스에 추가가 되어있기는 했었는데요..

아무튼 빠진것들을 넣어주시면 될 것 같습니다.


라이브러리를 익히는데 테스트 케이스를 보면서

직접 이것저것 해보는 것만큼 좋은게 없죠~ :)

Posted by 용식
TAG llucene