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

아주 먼 옛날.... 한 6년전쯤? GS모사에 있을때 루씬으로 처음 검색엔진 만들어보고자 했을때 한글 분석기가 없어서 세종프로젝트의 양해를 구해 명사 사전을 받아 단순 탐색+매칭으로 명사추출을 시키고, 스테머, 동의어필터등을 구현해서 만들어왔던 프로젝트가 하나있습니다.


https://github.com/need4spd/lucene-Korean-Analyzer


인데요.. 이게 그로부터 1-2년 후 이수명님께서 한글형태소분석기를 개발해 오픈(http://cafe.naver.com/korlucene)해주시면서 제가 그 형태소분석기를 명사추출 모듈의 하나로 사용하여 필터를 보강하였고, 현재 crescent를 gradle build하면 바로 이 라이브러리를 다운받아 사용하도록 되어있습니다.


최근에 이수명님의 한글형태소분석기가 arirang이라는 이름으로 루씬에 정식으로 contribute가 되어서 아마 정식으로 analyzer 패키지에 build되어 나오는 것으로 알고 있습니다.(짝!짝!짝!) 그래서 이 프로젝트도 그만 접을까 하다가... 루씬의 Analyzer를 공부하기엔 나름 괜찮을듯하여 그냥 남겨두고 대신 루씬 3.X 기반은 더 이상 건드리지 않고 4.X 버전만 조금씩 고쳐보기로 했습니다. 4.X 컨벤션에 맞도록 수정해보는 것도 재미있을것 같고요.


다만, 처음 만들었던 형태가 offset을 증가시키면서 미리 읽어놓은 사전의 키워드와 매칭해서 추출하는 아주 단순한 형태였기 때문에, 색인키워드 추출의 많은 부분을 가급적 이수명님께서 개발하신 형태소분석기와 사전에 의존하도록 수정을 하는 형태로 변경하려고 합니다. 굳이 사전도 2중으로 넣어놓을 필요가 없을듯 하고요..


대신, 복합명사, 사용자명사, 동의어, 불용어 사전만 유지하여 Filter로 사용되도록 기존에 들어있던 사전들을 정리하였습니다.


또한, 테스트케이스들을 새로 정비하여 push하였습니다. :)


저작자 표시 비영리 변경 금지
신고
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 용식

이전에 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 용식

처음 이 회사에 입사하여 매일 손으로 작업하던 업무들을 정리해가던 중 개인적인 시간을 내어

개발을 했던 것이 검색 로그분석기이다. 단순히, 인기검색어등의 분석만이 아니라

쿼리별 평균응답시간부터, remote client ip, 호출 클래스, 정렬조건, 페이지, 색인시간등 

검색 운영에 필요한 전반적인 정보들을 모두 분석해서 웹으로 보려고 했던 것이 이 개인 프로젝트 시작이었다.


제일 초기버전은 로그를 분석 할 파서를 개발하고

이렇게 분석된 정보들을 Java Programm에서 Map을 사용하여 일별로 집계하는 방식이었다.

그리고, 이렇게 집계된 데이터는 Lucene으로 색인하여 저장하고, 조회 또한 Lucene으로 검색하여

조회하는 방식이었다.


이렇게 약 1년을 사용하다보니 이 프로그램을 유지보수해야하는 다른 파트원들이 접근하기가 쉽지 않은 부분이 있었다.


Lucene 라이브러리 자체도 생소하고

통계를 내는 방식도, select sum group by~ 가 아니라 왠 Map에다 넣어놓고 loop로 돌면서 열개가 넘는 항목을 계산하고 있었으니... 


그래서 1년 후에 이 부분을 고쳐보았다.

우선 파서로 로그를 분석하여, 하나의 로그에 대한 모든 정보 (키워드,정렬조건,페이지,아이디,IP,캐시히트여부..)를 Oracle에 저장한다. 그러면 검색로그의 라인수 만큼의 데이터가 생성된다.


그리고 이것을 SQL을 사용하여 각 항목별로 데이터를 select하고 이 데이터를 Lucene으로 색인 - 조회하는 형태였다.


색인하는 부분의 인터페이스만 수정하였고, 데이터를 만드는 것 자체는 익숙한 SQL을 사용하여 마음대로 추가 할 수 있는 구조였다.


이러한 구조로 또 한 2년을 넘게 사용하였는데.. 이제 다른곳에서 좀 문제가 되기 시작했다.


일단 가장 중요한 것이 Lucene으로 색인된 데이터에 대한 핸들링이 어려운 부분이 가장 컸다.


일단 기본적인 CRUD만 해도 각각의 케이스에 대해서 Lucene 라이브러리로 개발하고

서버에 올려서 이걸 사용해서 java로 실행해야 하는 형태였고..

저장된 데이터를 보는 것도 어려웠다. 데이터가 많지 않을때는 인덱스 파일을 로컬로 내려서 

Luke를 사용하기도 했지만 기본적으로 인덱스 파일 자체가 서버에 있다보니, 이걸 local로 내리기도 쉽지 않았다.


전반적으로 관리할 수 있는 페이지를 만들려고도 했지만 왠지 배보다 배꼽이 더 큰 느낌이었다.


그리고 이러한 문제점은 최근에 한번 만들어보려고 했던 실시간 급상승 인기검색어를

추출하는 프로그램을 개발하는데도 많은 제약이 생겼다. 데이터를 좀 바꿔가면서 테스트하는 것 자체가 너무

어려운 상황..


그래서 한번 바꿔야겠다... 하고 생각하던 중 개발그룹에서 Hadoop Infra를 구축해주었고

마침 관심이 있던차에 이걸 한번 고쳐봐야겠다... 싶었다. 그래서 손을 댄것이 3주째.. 중간에 한 3번 정도 '아 내가 이걸 왜 손댔지...' 하고 심각히 고민했었음.. --;


큰 틀은 하둡으로 로그를 파싱하고 Map-Reduce를 사용해 분석한 다음 이걸 sqoop을 사용하여

MySQL DB에 각 분석 항목을 테이블별로 밀어넣는 방식으로 잡았고, 조회 페이지 또한 MySQL DB에 접근하여 

조회하는 형태로 잡았다.


현재 상태는 분석->MySQL까지 구현 및 셋팅은 다 되어서 분석을 하고 있고

조회페이지 및 원래 이 수정의 목적이었던.. (하마터면 까먹을뻔한 --) 실시간 급상승 인기검색어 로직을 만들고

구현하면 되는 상황..


작년에 하둡관련 서적을 한권 읽어보긴 하였으나 제대로 사용해보지 못하여 거의 머리에서 날아가 있는 상태에서 

1월에 3일동안 Hadoop 책 한권 읽고 개발을 진행한것이라 거의 초보적인 수준의 Map-Reduce 프로그램이고 

성능 또한 고려하지 않아.. 튜닝의 여지도 많은 상태임에는 분명..

그리고, 일별 데이터가 뭐 수백기가까지는 안되는 상황이라 분석 속도가 크게 빨라지지는 않았지만

일별,월별 그리고 사용자가 임의로 요청한 기간동안의 집계를 하둡을 사용해서 가져오도록 (정확히는 Pig나 뭐 그런거겠지만..) 고려를 하고 있어서 일단 Hadoop의 사용은 괜찮은 것 같다.


초보적인 관점에서 AS-IS, TO-BE의 느낌을 보면..


AS-IS (Oracle+Lucene)

1. 보관된 데이터의 핸들링이 어려움 (CRUD)

2. 일별로 집계되는 데이터를, 특정 기간동안 혹은 월별/ 년도별로 만들기가 조금 난해.

3. 특정 키워드등에 대한 검색은 굉장히 좋음 (원래 검색엔진이니..ㅋㅋ)

4. 스킴이 자유로워서 항목을 추가하기 쉬우나, 그만큼 관리도 어려워짐 (이건 설계가 잘못된 문제일수도..)

5. 파싱된 RAW 데이터가 Oracle DB에 있다보니, 운영을 위한 다른 데이터를 얻기가 쉬움 (N초 이상 쿼리등)

6. 분석되어 있는 데이터로부터 2차 가공된 데이터 혹은 2차 가공을 위한 데이터를 얻어내기가 번거로움


TO-BE (Hadoop+MySQL)

1. 분석 - 조회가 완전히 나눠짐.

2. 분석 항목이 약 2배정도 늘어남 (기존에는 성능문제등으로 하지 못 했던.. 의미있는 데이터인지에 대한 검증은 필요)

3. 하둡으로의 분석 속도도 괜찮으나, 분석된 항목이 많아 MySQL로 데이터를 export 할 때 Sqoop의 효율성이 좀 떨어짐. 분석은 몇분 안걸리는데 export 시간이..--; (다소 데이터가 작고 많은 테이블에 대해서는 효율이 좀 떨어지는듯.. 대량 데이터 전송은 속도가 아주 좋았음. 이건 각 sqoop 실행 shell을 백그라운드로 돌리면 좀 더 나을지 테스트 해보려고 함)

4. 조회 페이지에 대한 유지보수가 간단해짐 (일반적인 DB를 사용한 웹어플리케이션)

5. Map-Reduce 프로그래밍이 처음이라서 로그를 보거나 디버깅하는데 상당히 헤맸음.. 


개발자 입장에서는 꼭 필요한 업무를 사용하고 싶었던 라이브러리등을 사용하여 구축해봤던 부분에서는 만족...


저작자 표시 비영리 변경 금지
신고
Posted by 용식
TAG hadoop, Lucene

루씬 3.X 버전까지는 IndexReader로부터 Term과 TermFreq를 얻기 위해서

아래와 같은 코드를 사용하였습니다.




IndexReader로부터 

TermFreqVector termFreqVector1 = ir.getTermFreqVector(0, "f"); 와 같은 형태로

document 하나에 대한 TermFreqVector를 얻어내고

이를 통해서 term 배열과 freq 배열을 얻어서 조합하는 방식입니다.


4.0에서는 IndexReader.getTermFreqVector 메서드가 사라지고 대신 아래와 같은 방법을 사용합니다.


IndexReader.getTermVector(0, "f") 메서드를 사용합니다.

이때 Terms라는 클래스의 인스턴스를 얻을 수 있는데, 위 코드의 예에서는 doucment 0번에 대한

인스턴스이므로 하나의 document (위의 예에서는 document 0)가 색인된 역인덱스 파일의 통계 정보를

가지고 있는 것과 같은 값을 가지고 있습니다.


예를 들어서, termEnum.docFreq()는 해당 term을 가진 전체 document의 개수이지만, 위의 경우 전체 document가

하나인것과 같은 상태이므로 1을 return하는 형태입니다.


전체 Terms에 대한 통계정보가 아직 제대로 수집이 되지 않는 부분이 있습니다만 (http://devyongsik.tistory.com/577) 곧 수정이 된다고 합니다.


그리고, Terms도 하나의 document가 아니라

전체 혹은 특정 Term이나 필드에 대한 Terms 인스턴스를 얻어내서 그에 대한

통계를 얻을 수 있는 방법도 있을 것 같습니다.


저작자 표시 비영리 변경 금지
신고
Posted by 용식