'facet search'에 해당되는 글 2건

  1. 2014.02.06 [Lucene] DrillDown Facet Search in Lucene 4.6.1
  2. 2014.02.05 [Lucene] FacetSearch in Lucene 4.6.1

앞선 포스트에서 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 용식