소스의 중복도를 체크하는 프로그램의 파일럿을 만들어보려고 새벽 출근한김에 뚝딱거려보았다.


뭐 기본적으로는 루씬으로 색인 후 루씬의 TermFreqVector를 이용해서 코사인유사도를 사용해서 일단 뽑아보는거였는데.. lucene 3.6까지는 TermFreqVector가 있었는데 4.0에서는 그것이 Terms, TermEnum, DocEnum등으로 대체가 된듯 하였다.


그래서 관련한 자료들을 찾아보고

테스트 코드를 만들어가면서 이것저것 실행을 해보고 있었는데

Terms.getSumTotalTermFreq()의 결과가 영~~ 이상하게 나온다. 계속 -1이 나오는것..


IndexReader로부터 하나의 Document에 대한 Terms 인스턴스를 얻어내면

그 Terms의 인스턴스는 딱 하나의 Document가 색인되어있는 역인덱스 파일의 데이터와 같은

형태를 가진다.


예를 들어서,

> Document 1 : "learning perl learning java learning ruby"
> Document 2 : "perl test"


이 두개의 Document가 있을 경우 IndexReader로부터

아래와 같은 코드로 Terms 인스턴스를 얻어낸다..


> IndexReader ir = IndexReader.open(dir);
> Terms terms = ir.getTermVector(0, "f");
>
> System.out.println(terms.getDocCount()); -> 1
> System.out.println(terms.getSumDocFreq()); -> 4
> System.out.println(terms.getSumTotalTermFreq()); -> -1


 옆에 화살표에 표시된 숫자가 결과값인데.. 앞에 이야기하였듯이 terms 인스턴스는 단일 Document가 색인된 역인덱스 파일의 데이터와 같다. 따라서 term이 위와 같이 데이터가 나타나는듯하다.

terms로부터 TermsEnum을 얻고, 이 enum으로부터 각 term에 대한 통계정보를 받을 수 있는 구조이며

위 나열되어있는 terms의 메서드들은 대부분 TermsEnum으로 얻어지는 각 term들의 통계정보에 대한 합이다.


예를들어 getSumDocFreq는 TermsEnum.docFreq()의 합인데, TermsEnum.docFreq()는 

"How many documents contain at least one occurrence of the term in the field" 이다.

즉, 각 term을 가진 Document의 수인데, 단일 Document가 색인된 역인덱스파일에 대한 형태이므로 learning - 1, perl - 1, java - 1, ruby - 1이 되고 이 합인 4가 나오는 형태이다.


물론 이것을 전체 색인 파일을 대상으로 Terms 인스턴스를 뽑아내면 결과는 달라질것이다.

일단 perl에 대한 docFreq()가 1이 아니라 2가 될테니..


아무튼.. -1이 나오는 것이 이상하여..

루씬 메일링 그룹에 질문을 올렸다.. 아 근데 역시 영어가 문제야..ㅠㅠ 

영어로 메일쓰기가 너무 어렵고 보내기 버튼을 누르는 순간까지도 이걸 이 사람들이 질문을 이해할까? 이런 고민만..-.-;;


근데 이 궁금증을 해결할 방법이 없어서 질문을 보냈는데, 루씬 메인 커미터중 한사람인 Michael McCandless로 부터 답을 받았다. 결론은....  현재 색인 포맷이 각 Document별로 저 데이터를 가지고 

있지 않아서 그런것이며 원칙적으로는 수정을 하려고 하고있다.

현재 상태로 -1은 맞는 값이다. 라는 것..


이게 javadoc에 codec이 통계 데이터를 가지고 있지 않으면 -1이 나온다라고 되어있었던것 같은데..

그게 이런 의미로 연결되는 줄은 전혀 몰랐다.. 역시 일단 막 던지고 봐야돼 -_-;


급한거면 지라 이슈를 열어서 패치를 해주겠다고 다시 물어봐오는데..

그럴필요 없다라고.. 다시 메일을 써야하는건 함정 --;


결국 새벽에 만들던 파일럿 프로그램은 3.6을 사용해서 원래 알던 방법으로 구현을 해봤는데..

결과가 영 이상하다..ㅎㅎ 생각해보니 중복 소스를 체크하는 것은 TF-IDF를 이용한 유사도 점수와는 좀 다른 문제인듯하다.



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

회사에서 개발해서 사용 중인 

모니터링 시스템 관련 디버깅하다가

잠깐 짤막한 정리



IndexWriter는 하나로 유지하면서 close 없이 Incremental indexing을 계속 실행하고

IndexSearcher는 IndexWriter로부터 IndexReader를 받아와서 새로 생성하면

변경된 사항을 IndexWriter의 commit 여부 상관없이 적용된 Document의 수정 사항을

가져 올 수 있다.


다만 매번 IndexSearcher를 생성하는 것은 부담이 되기 때문에

IndexReader를 체크하여 changed여부를 확인한다.


이러한 롤을 가지고, Searcher Pool을 운용해도 좋을 것 같다는 생각이 들기도 하고...


Document수가 많지 않으면

그냥 매번 생성하는 것도 나쁘지 않을 것 같고..


IndexWriter가 Incremental indexing의 종료 이벤트를 던져주면

그것으로 Searcher를 새로 생성해도 될 것 같은데..


Incremental indexing 의 주기와 건수

Searcher가 실시간으로 적용 사항을 반영해야 하는 이슈가 중요한지등에 따라서

여러가지 방안이 나올 수 있을 듯...


저작자 표시 비영리 변경 금지
신고
Posted by 용식
TAG 루씬
NearRealTimeSearch를 구현하는데 사용되던 메서드인
IndexReader reopen 메서드가 deprecated되고
static openIfChanged(IndexReader ir) 메서드로 대체 되었습니다.

  IndexReader newReader = IndexReader.openIfChanged(oldReader); 

oldReader가 변경되었다면 새로운 Reader를 반환하고
아니라면 null을 반환합니다.

이때 oldReader와 동일한 타입이 넘어옵니다.

MultiReader라면 MultiReader가 넘어오게 됩니다.


저작자 표시 비영리 변경 금지
신고
Posted by 용식
댓글로 질문을 하신분이 계셔서 그 내용을 확인하고 답변을 드리기 위해서
포스트를 작성합니다.

질문하신 내용은
Custom Analyzer를 만들어서 사용하는데
실제로 원하는대로 작동을 하지 않는다는 것이었습니다. Custom Analyzer의 로직은 아래와 같습니다.



즉, 사용자에게 입력받은 키워드를 별도로 준비된 형태소분석기를 통해서 명사를 추출하고
이 추출된 명사의 위치정보와 기타 루씬의 Filter를 적용하기 위하여 이를 다시 StandardAnalyzer로 분석하는
CustomAnalyzer였습니다.

그런데 테스트를 해보면 
처음 "무궁화 꽃이 피었습니다." 라고 실행하면 정상적으로 "무궁화" , "꽃"이 추출되어 나오는데
그 다음부터는 "무궁화", "꽃이", "피었습니다." 라고 입력된 키워드가 마치 그냥 StandardAnalyzer로 분석되는 것처럼
나온다는 것에 대한 문의 였습니다.

테스트 코드는 아래와 같습니다.


똑같은 문장 "무궁화 꽃이 피었습니다"를 3번 돌면서 분석하는 로직입니다.

보시면 Analyzer를 한번만 생성하여 loop를 돌면서 analyzer.reusableTokenStream을 사용하고 있는 것이 보입니다.

여기서 만약 Analyzer를  loop 돌때마다 new로 생성하여 사용하면 결과는 3번 모두 "무궁화", "꽃"이 분석되어 나옵니다.

문제는 reuseableTokenStream에 있는데요.. 메서드명에서 알 수 있듯이 이는 TokenStream을 재사용하는 메서드입니다.
이 메서드를 실행하게 되면 아래의 메서드가 실행됩니다.


getPreviousTokenStream에 의해서 이전에 저장된 TokenStream을 가져오고 그것이 있으면 그 Stream을 그대로 사용하도록 되어있습니다. 그 TokeStream에 사용자가 입력한 키워드를 주고 분석을 하게 되는 것 입니다.


여기서getPreviouseTokenStream에 의해서 return되는 TokenStream이 바로 CustomAnalyzer에서 만들어내고 있는
 

final Tokenizer source = new StandardTokenizer(Version.LUCENE_33, reader);

Set<String> stopSet = new TreeSet<String>();

return new TokenStreamComponents(source, new StopFilter(Version.LUCENE_33, source, stopSet)); 


이것입니다. 

즉, 첫번째 분석요청에 대해서는 형태소분석을 한 결과를 가지고 위의 TokenStream을 만들어서 적용한 결과가 정상적으로 나왔는데 그 이후의 분석 요청부터는 이미 만들어진 TokenStream이 있기 때문에 CustomAnalyzer에서 형태소분석을 요청하는 부분이 적용되지 않고 사용자가 입력한 키워드를 곧바로 위 StandardAnalyzer를 기본으로 한 TokenStream에 적용해버리기 때문입니다.

TokenStream 자체에 대한 재사용이라고 생각하시면 좋을 것 같습니다.

이를 피하는 방법은..

1. 위에서 언급된 것 처럼 new로 매번 Analyzer의 생성
2. Analyzer.reuseableTokenStream을 사용하지말고 Analyzer.tokeStream 메서드를 사용
3. TokenStream을 재사용하시려면, 실제 형태소분석을 하고 그것을 바탕으로 위치 정보를 뽑아 낼 수 있도록 
형태소분석을 전용으로 하는 Filter를 만드시는 방법

등이 있을 것 같습니다.

위와 같은 경우 "무궁화"와 "꽃"이 정상적으로 추출되는 경우라도 실제 원문과 비교하여 
위치정보등이 틀릴 수도 있습니다. 왜냐하면 위치정보를 추출한 원문이 "무궁화 꽃이 피었습니다." 가 아니라
"무궁화 꽃" 이기 때문입니다. 우연찮게 명사가 같은 위치에 있어서 결과가 제대로 나오는 것 같지만 보통의 경우에는
위치정보를 제대로 사용하실 수 없을 것입니다.

이런저런 이유로 3번이 가장 명확한 방법으로 보이네요.



저작자 표시 비영리 변경 금지
신고
Posted by 용식
TAG 루씬, 질문
이번에는 많은 분들께서 기다리셨을(?) 명사 추출 필터입니다.
사전에 등록되어있는 명사를 기반으로 Tokenizer나 선행 TokenFilter로 부터 넘어온
Token을 탐색하여 명사를 추출해내는 구조입니다.

기대를 하셨다면 죄송스럽게도 굉장히 별거 없는 Filter입니다.
 
형태소 분석이 아닌 단순히 Token을 읽어 사전에 있는 단어를 추출하는 방식입니다.
Token탐색도 그냥 character단위로 쭉.... 합니다.

이걸 다른 자료구조형으로 만들면 속도를 빠르게 할 수도 있겠지만
여기서는 코드의 간결함을 위해서 복잡한 부분은 모두 제외를 시킬 예정입니다.

이 추출의 과정이 형태소분석을 기반으로 알고리즘이 만들어져 있다면 이 Filter가 형태소분석기가 됩니다.
하지만 이 형태소분석이라는 것이 쉬운 것이 아니고..요즘은 웹이 처음 태동 할 당시처럼 형태소 분석으로
명사만 추출해서 되는 상황도 아닙니다. 때문에, 여기서는 사전을 기반으로 Token을 탐색하여
매칭되는 명사들을 그냥 추출해는 정도로 구현을 하려고 합니다.

그리고 가장 중요한 것이 사전으로 형태소분석을 하던 지금과 같은 방식을 사용하던 이 사전은 계속 유지보수가 되어야합니다.
신조어가 계속 생겨나기 때문입니다.

때문에 기본이 되는 사전을 가지고 핵심적인 모듈을 구현하고 그외 신조어/불용어/동의어등 
사용자가 직접 커스터마이즈 할 수 있는 사전을 제공하여 그 rule을 추가적으로 적용하는 방법들도 많이 사용됩니다.

그럼 이 Filter의 테스트케이스부터 보겠습니다.

 

DevysNounFilterTest.java

우선 추출의 대상이 되는 문장이 있고 여기서 추출되어야 하는 단어들을 미리 List에 넣어두고
추출되는 단어들을 비교합니다. 동의어필터 테스트케이스와 마찬가지로 양방향으로 검증을 합니다.

명사가 추출되는 것이 보이시나요?

비록 아주 단순한 로직이지만 이렇게 문장으로부터 원하는 키워드가 추출되기 시작하면 뭔가 기분이 좋아집니다. :)

그럼 Filter의 코드를 보겠습니다.

 
DevysNounEngine.java
DevysNounFilter.java

기본적으로는 동의어필터의 SynonymEngine과 같은 인터페이스를 구현하고있습니다.
하나의 Token으로부터 여러개의 Token이 파생되는 경우에는 대부분 Stack<AttributeSource>로 활용이
가능하기 때문에 이를 이용해서 인터페이스를 만들고 이용합니다.

내부로직은 각 엔진이 가지고 있게 됩니다.

Token을 하나씩 인덱스를 증가시켜가며 단어를 추출하고
이를 사전과 비교하여 사전에 있는 단어라면 이를 Stack에 저장해 넘겨줍니다.

여기서 word 타입만을 명사 추출 대상으로 삼는데
동의어의 경우 기본적으로 명사를 추출 할 대상이 아니게 되고 기타 불필요한 로직을 타지 않게 하기 위합니다.
따라서 동의어필터와 명사필터의 우선순위를 봤을 때 추출된 명사의 동의어를
추출해야하므로 명사필터가 동의어필터보다 앞에 와야함을 알 수 있습니다.

사전/탐색/비교등의 로직을 더 빠르고 확실하게 구현한다면 그래도 꽤 쓸만한 명사 추출 필터가 되지 않을까 합니다.
다만 여러방법으로 명사를 추출하도록 하는 것 예를들면 사전과 매칭되는 가장 긴 명사를 추출한다던가 하는 부분이 추가되면
더 좋을 것 같습니다.

이 필터에서 한가지 중요한 것은 원문 토큰도 리턴을 해준다는 것 입니다.

즉 , "하둡을" 이라는 Token에서 "하둡"뿐만이 아니라 
"하둡"+"하둡을"Token이 추출되는 것 입니다.

이는 나중에 사용자가 "하둡을 사용하는 방법"이라고 검색을 했다고 하면
기본적으로 사용자가 입력한 키워드에 더 많은 가중치를 두어서
검색 결과의 우선순위를 높이는데 사용하기 위함입니다.

"하둡을" * BOOST! + "하둡" + "사용하는" + "방법" 와 같은 형태의 쿼리를 사용 할 수도 있을 것 입니다.

또한 위 로직에서는 빠져있지만 추출된 명사의 OFFSET을 제대로 계산하여
Attribute에 설정해주어야 합니다. 나중에 매치된 키워드에 하이라이팅을 해주기 위해서는
OFFSET정보가 필요하기 때문입니다.

이렇게해서 루씬을 사용한 개발에서 가장 중요하고 어려운 부분 중 하나인
Analyzer에 대한 이야기를 우선 마칠 수 있을 것 같습니다. 어렵고 중요한 부분인데 설명이나 코드의 부족함이
많았던 것 같아서 아쉬움이 남기도 하고 걱정도 됩니다.

공유해드린 소스를 사용하여 이런저런 테스트를 해보신다면
더 쉽게 Analyzer에 접근 하실 수 있을 것이라 생각됩니다.

이후에는 색인과 검색, 그리고 쿼리등에 대해서 간단한 코드를 사용하여
설명을 드리려고 합니다.

아직 코드를 작성해두지 않아 조금 시간이 걸릴지도 모르겠네요..^^
 
https://github.com/need4spd/aboutLucene
에서 체크아웃 받으 실 수 있습니다. 


저작자 표시 비영리 변경 금지
신고
Posted by 용식
로그분석/조회 프로그램에 대한 루씬쪽 약간의 변경작업..

1. 전체 document 약 2억6천만개
 - 하루 약 40만개씩 증가 
 - 색인파일 연도별로 분리
 - MultiSearcher 사용

2. 검색속도 개선을 위한 작업
 - date필드를 int형 필드로 마이그레이션
 - value 필드를 int형 필드로 마이그레이션
 - rangeQuery를 Filter로 변경하여 검색

3. 리소스 개선을 위한 작업
 - long타입을 int로 마이그레이션 

까지는 완료... 

4. 더 할 수 있는 작업
 - 색인 조건 최적화
 - TermVector, Norm값등은 필요 없으므로 가지고 가지 않도록

5. replication 기능 개발

역시 책을 읽어야해...;;

1판에 있던 내용도 지금 다시 읽으니 참 새롭다.
저작자 표시 비영리 변경 금지
신고
Posted by 용식
TAG 루씬
이번에는 동의어 필터를 만들어보겠습니다. 동의어의 역할은 다 아시겠지만 특정한 키워드에 대해서 확장된 검색을 지원 할 수 있도록 해줍니다. 이 예제도 앞선 예제들과 마찬가지로 루씬인액션의 책에 있는 예제를 보고 우리 상황에 맞춰 다시 만든 코드들입니다. 먼저 테스트 케이스를 보겠습니다. DevysSynonymFilterTest.java
테스트 케이스는 양방향으로 검사를 하고 있습니다. 
동의어 목록을 미리 지정해두고 TokenFilter로부터 나오는 Token을 리스트와 비교.. 
그리고 이 Token을 따로 저장해두고 다시 동의어 목록을 가지고 비교합니다. 

 예를 들어서 동의어가 "노트북, notebook, 노트북PC"로 등록되어 있다면 3개 중 하나의 키워드가 
검색쿼리로 들어온다고 해도 동의어로 등록된 3개의 키워드 전부로 검색이 되는 효과를 줄 수 있습니다. 
이걸로 어느정도 오타에 대한 커버도 가능하겠죠. 보통 동의어 사전을 저런식으로 모두 동등한 키워드로 
등록하는 것과 노트북:notebook, 노트북PC 이런식으로 대표어를 설정하여 등록하는 방식 두가지를 
생각해볼 수 있겠습니다. 

관리적인 면에서는 후자가 편하겠지만 확장성에서는 전자가 편하다고 생각되므로 
전자의 방식으로 동의어 필터를 구현하겠습니다. 

이 동의어의 확장을 검색시에 수행 할 것이냐 색인시에 수행 할 것이냐도 
조금 생각 해 볼 만한 문제입니다. 검색시에 확장을 하게 된다면 "노트북"이라는 검색 쿼리에 대해서 
실제로 검색엔진에서 "노트북" or "notebook" or "노트북PC"와 같은 형태로 쿼리 자체가 확장 될 수도 있겠습니다. 

하지만 루씬에서의 동의어 검색은 위와 같은 방법이 아니라 색인시에 동의어를 사용하여 같이 색인을 하고 동의어가 어떤 것이 들어오던 검색이 가능하도록 해주고 있습니다. 

특히 이 소스에서는 동의어를 루씬 RamDirectory를 사용 색인하여 두고 활용하기 때문에 동의어들에 대해서 어느 동의어가 들어오더라도 똑같은 기능을 하도록 구현하였습니다. 기본적인 컨셉은 이렇습니다. 

 루씬에서 하나의 Document에는 같은 이름의 필드를 여러개 등록 할 수 있습니다. 
이것을 활용하여 루씬의 RamDirectory를 사용하여 색인하여 활용 하려고 합니다. 
동의어 사전이 위와 같이 ROW단위로 등록이 되어 있다고 할 때 각 ROW를 읽어 ","로 split을 한 후 하나의 Document에 동일 필드명(syn)으로 집어 넣어 루씬의 RamDirectory를 활용하여 색인합니다. 

 그리고 검색 쿼리가 들어 올 경우 그 쿼리로 색인된 동의어 리스트에서 검색을 하여 나온 Document에서 syn필드의 값들을 꺼내 그것으로 검색을 하도록 하는 것 입니다. 


대부분의 사전을 이렇게 색인을 활용 할 수도 있을 것 입니다. 
이렇게 추출된 동의어의 리스트를 실제 필터에서 어떻게 활용하는지 보겠습니다. 
이번에 만들어질 동의어 필터는 SynonymEngine 이라는 클래스를 내부적으로 사용하고 있습니다. 
 이 SynonymEngine은 Engine 인터페이스를 상속한 것으로써 Stack를 리턴해주는 getAttributeSources 메서드를 하나 가지고 있습니다. 
 이 SynonymEngine은 위 예제처럼 색인과 검색을 통하여 동의어의 리스트를 받아와서 이를 AttributeSource에 저장하여 이것의 State를 Stack에 저장합니다.


DevysSynonymFilter.java
이제 State에 대한 이야기를 해야 할 것 같습니다.

필터 중 이렇게 Tokenizer로부터 추출 된 하나의 Token으로부터 여러개의 Token을 추가적으로 추출해내는 필터들이 있습니다. 
 동의어 필터라던가 명사 추출 필터 같은 경우 하나의 Token으로부터 추가적인 Token들을 추출해내죠. 
 이런 경우에 필터는 원본 Token을 저장해두고 추가적으로 추출된 Token을 Collection 혹은 Stack에 넣어두고 그 Token들을 리턴한 후 맨 마지막에 원본 Token을 리턴하여 작업을 끝내는 방식을 사용 할 수 있습니다. 
(보통 한번 사용 된 Token은 사라져야 하기 때문에 Stack등이 많이 사용됩니다.) 

 이때 사용되는 것이 State입니다. 

 2.X에서 3.X로 넘어오면서 Token의 각 속성에 대해 Attribute라는 클래스가 생긴 것과 이 State가 생긴 것이 Analyzer의 내부 구현에서 가장 큰 변화라고 생각합니다. 
 처음에 이 개념을 이해하는데 조금 애를 먹었는데요 공부했던 것이 오래전이라서 저도 다시금 기억을 되살려가며 설명 드리려 합니다. 

 앞에서 언급했지만 Analyzer의 Tokenizer와 모든 TokenFilter들은 TokenStream 클래스를 상속하고 있습니다. 
 이 TokenStream클래스는 내부적으로 AttributeSource 클래스를 상속받고 있고 이 AttributeSource 클래스가 이전 Token에서의 속성들인 CharTermAttribute등 여러 종류의 Attribute들을 가지고 있는 구조입니다. 

 때문에 데코레이터패턴으로 물려있는 Tokenizer와 TokenFilter들이 모두 이 속성을 공유하여 작업 할 수 있는 것 입니다. 그렇다면 만약 A라는 단어로부터 B,C,D라는 동의어가 추출된다고 할 때 예전 같으면 이 추출된 키워드를 가진 Token 클래스를 다음 Filter에게 넘겨주면 되는 구조였지만, 

이제는 B,C,D 각 동의어들의 속성을 가진 AttributeSource를 만들어내어 다음 Filter가 사용 할 수 있도록 해줘야 하는 것 입니다. 

하지만 3.X의 루씬에서 incrementToken 메서드는 더 이상 Token을 리턴해주는 것이 아니라 단순히 boolean 값을 리턴해줍니다. 위에서 몇번 언급을 하였듯이 TokenSteam들은 AttributeSource를 공유하여 사용하는 구조입니다.

여기서 바로 AttributeSource의 captureState와 restoreState 메서드가 사용됩니다. 

기본적으로 추출된 키워드를 포함한 Attribute들이 모두 AttributeSource에 들어있는 것이기 때문에 이 AttributeSource 자체를 저장해뒀다가 꺼내서 사용하는 개념입니다. 

선행 Tokenizer나 TokenFilter로부터 넘어온(실제로는 AttributeSource에 저장해둔) AttributeSource를 복사하여 이것을 가지고 동의어에 대한 속성을 만들어서 Stack에 그 State를 저장하여 둡니다. 왜 복사를 하냐면 우리는 원본에 대한 정보도 필요하기 때문입니다. 원본에 대해서 뭔가 조작을 해버리면 원본 속성을 잃어버리기 때문에 복사를해서 작업을 하는 구조로 되어있는 것 입니다.
SynonymFilter의 incrementToken 메서드를 보면

synonyms = engine.getAttributeSources(input.cloneAttributes());


이런 부분이 있습니다. 동의어를 추출 할 SynonymEngine에게 현재 Token의 정보를 가지고 있는 AttributeSource를 복사하여 넘겨주고 있습니다. 그러면 SynonymEngine에서는 동의어 추출을 위해 필요한 정보들을 이 AttributeSource로부터 얻어 올 수 있습니다.
SynonymEngine에서는 이 복사된 AttributeSource를 사용하여 동의어에 대한 속성들을 Set한 후 이 AttributeSource의 State를 Stack에 저장합니다.

synonymStack.push(attributeSource.captureState());


그리고 incrementToken 메서드에서는 Stack의 size를 체크하여 Stack이 채워져있으면 그 Stack으로부터 State를 받아와서 현재 TokenStream의 AttributeSource를 Stack에 넣어졌던 State(동의어 정보)로 변경합니다.

restoreState(synState);


그리고 true를 리턴하지요. 그러면 후행 TokenStream에서는 추출 된 동의어에 대해서 작업이 가능합니다.

물론 incrementToken 메서드를 보시면 Stack에 비어있는 경우 맨 끝에서 true를 리턴해주기 때문에 동의어가 없는 경우나 동의어를 모두 추출한 이후 원본 AttributeSource도 후행 TokenStream에서 사용 할 수 있게 되어있습니다. 

이 개념이 조금 어려울실 수도 있겠지만 AttributeSource를 중심으로 생각하시면 조금은 쉽게 이해가 되실 것 입니다.
그리고 동의어 필터에서는 원본 Token으로부터 파생된 동의어들의 위치정보값을 모두 0으로 셋팅하여주고 있습니다.  

이것은 이전 Token(여기서는 원본 Token)과의 위치거리가 0이라는 뜻으로 결국 원본 Token과 같은 위치에 있는 동일한 Token이라는 의미로 사용됩니다. phraseQuery에서는 중요한 개념이지만 저희는 직접 QueryParser를 만들어 사용 할 것이기 때문에 크게 중요하게 사용되는 속성은 아닙니다. 

 하지만 꼭 알고 넘어가야하는 속성이기도 합니다.
PositionIncrementAttribute positionAttr = savedAttributeSource.addAttribute(PositionIncrementAttribute.class); //원본 AttributeSource의 Attribute를 받아옴 positionAttr.setPositionIncrement(0);
https://github.com/need4spd/aboutLucene 에서 체크아웃 받으 실 수 있습니다.


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


글 쓰다가 좀 애매한 부분이 있어서.. 3년전에 공부 할 때 읽었던 루씬인액션 초판 번역본을 꺼내보았다.
루씬 1.4를 기반으로 쓰여진 루씬인액션 초판 번역본이다. 

책장에서 책을 꺼냈는데 책에 붙어있는 인덱스들이 눈에 들어왔다.

책을 열어보니 여기저기.. 지금보면 나는 또 모를 얘기들이
연필이고 볼펜으로 적혀있었다. 

나는 3년전 그때 당시 팀장님과 추경돈대리님의 명(?)을 받고
검색에 "검"자와 자바의 "자"자도 모르면서
뭘 만들어보겠다고 이렇게나 열심히 공부했을까.. 싶은 생각이 든다.

이 책 번역해주신 이문호님께서는 가끔 블로그에 좋은 댓글을 남겨주시고.. 내가 작성한 포스트에 오류가 있으면
지적해주셔서 수정 할 수 있게 도와주시기도 한다. 
또 한분의 역자이신 강철구님과는 루씬 user group에서 메일로 질문남기다가
알게 되어 이곳 페북에서까지 친구(?)맺고 지내고있고.. 난 이분께서 번역하신 책을 3권 정도 가지고 있다. ㅋㅋ

좋은책이 한글로 번역되어 있다는 것이 정말 좋은 것이구나 라는 생각을 이때 처음 했던 것 같다.

그리고 이 두분과 또 한분의 공역자분은 내 첫 회사에서 같이 근무하셨던
(나는 파견나가 있어서 실제로 많이 뵙진 못 했었지만) 분들이다. 세상이 참 좁은 정도가 아니라 이럴 수가 있나 싶기도..ㅎㅎ

아무튼 나는 그 당시 뭔가 해보라는 명은 떨어졌고... 믿을 것은 이 책 밖에 없었고
그래서 죽어라고 이 책만 읽으며 소스열어보고 테스트해보고 정말 이것저것 맨땅에 헤딩하면서
해봤던 기억이 있다. 꽤 절실하게...
 
덕분에 이 책으로 루씬과 검색에 대해서 공부하게 되었고, 자리를 옮기면서 풍주형님께는 자바를 배울 수 있었다. 

약 5년전에 검색운영업무를 처음 시작한 이후로 지금까지 계속 검색업무를 해오고 있기는 하지만
사실 3년전에 루씬을 접하게 되면서 루씬에 대한 공부와 함께 풍주형님에게 자바에 대한 많은 것들을
배울 수 있었던 것이 내 개발자 로드맵에 있어서 굉장히 큰 전환점이 되었던 것 같다. 

그리고 이것 덕분에  비록 온라인상에서이지만 다른 실력 좋으신 분들도 알게 되었다. 

우연찮게 옛날 책을 꺼내보다가 
책에 무수히 붙어있는 인덱스를 보며 왠지 감회가 새로워 블로그에 그냥 두서없이 끄적여본다...

근데 갑자기 딸래미가 울어대서 흥이 깨졌다. 이만~~~
 
저작자 표시 비영리 변경 금지
신고
Posted by 용식
그러면
이제 루씬에서의 Analyzer에 대해서 살펴보도록 하겠습니다.

이제부터 정말 작성하기가 조심스러워지네요... 저도 루씬을 잘 이해하고 있는 상태는 아니라서.... ㅎㅎ 
더군다나 제가 공부하며 만들었던 루씬 버전이 2.4 정도 버전인데 현재 3.3 버전은 기본적인 개념은 비슷하겠지만
내부적은 구현 방식은 완전히 바뀌었더라구요...

앞으로 내부적인 루씬 소스에대한 설명보다는 주로 사용법/커스터마이즈 방법을 소개해드리게 될 것 같습니다. 

우선 Analyzer가 어떻게 사용되는지 보도록 하겠습니다.


AnalyzerUsageSampleTest.java
우선 분석 할 문장을 StringReader로 생성하고, 이 문장을 분석 할 Analyzer를 생성합니다.
여기서는 WhitespaceAnalyzer를 이용하였습니다.

StringReader stringReader = new StringReader("집에서 블로그를 작성합니다.");

Analyzer analyzer = new WhitespaceAnalyzer(Version.LUCENE_32);


루씬에서 Raw데이터를 분석하기 위해서 쓰여지는 타입이
StringReader입니다. Raw데이터가 웹페이지던, 혹인 DB에 있는 데이터던
StringReader타입으로 변환해주면 얼마든지 루씬을 사용하여 색인/검색을 할 수 있습니다.
Analyzer에는 많은 종류가 있습니다. 여기서 사용한 WhitespaceAnalyzer
문장을 "공백"을 기준으로 분리하여 키워드를 추출하는 Analyzer입니다.
이 외에도 KeywordAnalyzer, SimpleAnalyzer, StandardAnalyzer등 많은 Analyzer가 존재합니다.
대략 이름만봐도 어떻게 키워드를 추출 할지 짐작이 되실 것 입니다.
WhitespaceAnalyzer의 생성자에서 사용되는 VersionAnalyzer 내부적으로
Api를 선택적으로 사용하기 위한 방법으로 보입니다. 아마 Lucene 4.0 정도가 되면
전부 정리가 되지 않을까 싶네요. 여기서는 크게 신경쓰지 않겠습니다.
그리고 문장과 필드(여기서는 title)를 가지고 TokenStream을 생성합니다.

TokenStream tokenStream = analyzer.tokenStream("title", stringReader);

여기서 "title"은 이 예제에서는 사용되지는 않습니다.
그리고 TokenStream으로부터 3가지의 Attribute를 받아옵니다.

CharTermAttribute termAtt = tokenStream.getAttribute(CharTermAttribute.class);

PositionIncrementAttribute posIncrAtt = tokenStream.addAttribute(PositionIncrementAttribute.class);

OffsetAttribute offsetAtt = tokenStream.addAttribute(OffsetAttribute.class);


바로 CharTermAttribute, PositionIncrementAttribute, OffsetAttribute 입니다.
이 3가지는 사실 루씬에서는 실제로 Token이라는 클래스가 사용되며 이 Token이라는 클래스안에 필드데이터로 들어있던 값들 입니다.

다만 3.X부터는 내부적으로 Token이 아니라 위 Attribute를 사용하도록 전체적으로 수정이 된 것 같습니다. 물론 Token 클래스도 여전히 존재합니다.
 
CharTermAttribute은 추출된 키워드를,
PositionIncrementAttribute 는 키워드가 바로 이전에 추출된 키워드로 부터 얼마나 떨어져있는지를,
OffsetAttribute 은 키워드가 문장에서 어느 위치에 있는지를 나타내는 속성입니다.

그리고 이 속성들은 모두 TokenStream이 상속하고 있는 AttributeSource 클래스에
속한 속성들입니다. 이후 AttributeSource라고 말씀드리면 이런 모든 속성들을 가지고 있는
클래스라고 생각해주시면 좋을 것 같습니다.
 
특히 CharTermAttribute를 대량의 데이터를 색인 할 때 리소스 활용과 속도를 위해서
char[]를 사용하여 재사용성을 최대로 높이고 있습니다.

위 3가지 속성은 색인/검색에 있어서 상당히 중요한 속성입니다.

CharTermAttriute는 추출된 키워드 그 자체를 나타내기에 당연히 중요한 속성이고
PositionIncrementAttribute는 검색을 할 때 "나이키와 운동화의 사이에 단어가 3 이하인 문장" 이러한식으로
검색을 할 때 사용됩니다.
OffsetAttriute는 제가 사용 했을 때는 검색된 결과에서 Match된 부분을 Highlight하여 보여줄 때
사용되었었습니다. 이 부분을 잘 이해하고 있어야 동의어에 의한 검색시에도 제대로 Match된 키워드에 Highlight하여 보여 줄 수 있습니다.

결국 Analyzer가 하는 역할은 색인 및 검색 대상이 되는 원문으로부터 키워드를 추출하고,
검색과 부가 정보 표시에 필요한 각종 속성들을 생성하여 넘겨주는 역할을 한다라고 볼 수 있을 것 입니다. 

더 자세한 내용은 책을 참고하시면 좋겠죠. ^^

그 후 TokenStream으로부터
추출되어 나오는 키워드가 없을 때 까지 loop를 돌면서
각 속성의 값을 추출하는 형식입니다.

-- 결과 --

text : 집에서

postIncrAttr : 1

startOffSet : 0

endOffSet : 3

text : 블로그를

postIncrAttr : 1

startOffSet : 4

endOffSet : 8

text : 작성합니다.

postIncrAttr : 1

startOffSet : 9

endOffSet : 15


이제 대략적인 사용법이 눈에 들어오시나요?
이렇게 TokenStream으로부터 추출되는 Attribute들을 사용하여 키워드를 색인하고 검색하는 것 입니다.

앞으로 Token이라는 것은 "키워드","위치정보"등이 포함된 데이터라고 생각해주시면 됩니다.
(위 3가지 속성의 합이라고 생각해주셔도 됩니다.) 
 

실제로 2.X 버전까지는 Token이라는 클래스를 TokenStream으로부터 받아서 사용하였지만
3.X로 올라오면서 Token을 사용하지 않고 위 예제처럼 TokenStream으로부터 필요한 Attribute를 얻어서
사용하는 방식으로 변경되었습니다.


Analyzer는 데코레이터 패턴으로 구현이 되어있습니다.
Analyzer는 크게 TokenizerTokenFilter를 사용하여 구성되어 있고
이 두 클래스는 모두 TokenStream을 상속하여 구현되어 있습니다. 실제 소스를 열어보시면 더 많은 클래스들이 관여하고 있고
3.X 버전에서 보면 구현하는 형태도 많이 변경되었지만 기본적인 개념은 TokenStream 클래스를 사용한 데코레이터 패턴입니다.

Tokenizer는 문장을 읽어서 자기가 가진 룰대로 문자를 잘라 Token으로 만들어내는 역할을하며,
이 
Tokenizer의 결과를 TokenFilter가 받아서 사용하고 또 그 결과를 TokenFilter가.. 어쩌구 저쩌구..
이런 형태로 구현이 되어있습니다.

new TokenFilter(new TokenFilter( new Tokenizer()));

//3.X에서는
TokenStreamComponents를 이용하여 구현 형태를 다소 다르게 하고 있습니다.
Tokenizer source = new Tokenizer();
new TokenStreamComponents(source , new TokenFilter(new TokenFilter(source)));

왜 이런형태가 되었는지는 저도 좀 살펴봐야 할 것 같네요..  


당연히 제일 안쪽에는 Tokenizer가 들어가야 할 것 입니다. 기본적으로 문장으로부터 Token이 추출되어야
Token을 가지고 Filter가 일을 할 수 있을테니까요.

실제로 불용어 리스트를 가지고 추출되는 Token이 해당 불용어와 일치하는 경우
Token을 버리는 StopAnalyzer는 아래와 같이 작성되어 있습니다.

final Tokenizer source = new WhitespaceTokenizer(matchVersion, reader);

return new TokenStreamComponents(source, new StopFilter(matchVersion, source, stopwords));


new TokenStreamComponents는 크게 신경쓰지마세요. 결국 우리가 사용하게 되는 것은
TokenStream입니다. 위 예제에서 analyzer.tokenStream()은 내부에서 결국
new TokenStreamComponents().getTokenStream()으로 연결됩니다. StopFiltersource를 생성자 파라메터로
받고 있는 것을 보실 수 있습니다.
위 예제에서 analyzer.tokenStream() TokenStreamComponents.getTokenStream()에 의해서
StopFilter
return됩니다. 따라서 결국 tokenStream.incrementToken() StopFilter incrementToken()
호출 되고 이 메서드 내부에서 StopFilter가 생성자 파라메터로 받은 source 즉, Tokenizer incementToken()
메서드가 선 호출되어 결과를 받아 처리하는 형태로 되어 있습니다. 

Tokenizer는 문장에서 기본적으로 키워드를 추출해내는 룰을 가지고 있습니다.
WhitespaceAnalyzerWhitespaceTokenizer를 사용하여 키워드를 추출하도록 되어 있고
WhitespaceTokenizer는 공백을 기준으로 하나의 Token를 생성하여 넘겨주도록 되어있습니다. 
즉, Tokenizer를 직접 구현하면 문장에서 추출되는 Token을 우리가 원하는대로 만들어 낼 수 있습니다.

단순히 공백이 아니라, 
 

mp3 -> [mp] + [3],

ipad사용자 -> [ipad] + [사용자]


등으로 한글과 영어/숫자를 분리하여 Token으로 추출해
낼 수도 있습니다.
잠깐 다른 이야기를 하면 영어,숫자와 한글은 기본적으로 분리를 하는 것이 좋기는 하지만 "넘버3" 같은 영화제목을 생각하면
붙이는 것이 맞을 수도 있습니다.

그래서 앞선 포스트에서 말씀드렸듯이 비지니스와 언어등에 따라서 추출되는 방법이 다 달라질 수 있다는 것 입니다.

그리고 TokenFilter가 있습니다. 이 TokenFilterTokenizer를 감싸는 형식 (데코레이터 패턴이라고 말씀드렸었죠?)으로
사용됩니다. 일반적으로 Tokenizer로부터 추출 된 Token에 뭔가 추가적인 작업을 하는 형태로 구현이 됩니다.
즉, 위 예제 코드에 있는 문장의 경우 공백을 기준으로 "집에서" 라는 키워드가 하나의 CharTermAttribute로 추출된다고 하면
여기서 "에서"라는 어미를 제거하여 최종적으로 "집"이라는 키워드를 추출한다던가 "집에서"라는 키워드 자체를
불용어로 등록하여 아예 아무런 키워드를 추출하지 않도록 하는 작업등을 TokenFilter가 하게 됩니다.

그러면 여기까지의 내용을 가지고 앞으로 우리가 무엇을 만들어야 할지 한번 살펴보겠습니다.
우선 주어진 문장으로부터 Token을 추출해야 합니다. Tokenizer를 구현해야 합니다.
하지만 공백을 기준으로 Token을 추출하면 한글에서는 큰 가치가 없는 Token일 경우가 많습니다.
따라서 우리는 이 Token으로 부터 의미있는 키워드를 추출해야 합니다.
Tokenizer에서 공백을 기준으로 Token을 만들어 넘겨주면 키워드를 추출하는 작업은 TokenFilter를 사용해서 진행합니다.
저희는 추가적으로 공백 + (한글과 영어/숫자)를 분리하여 넘겨주도록 해보겠습니다. 
기본적으로 "어미사전"으로부터 어미를 제거하는 StemFilter
"동의어 사전"으로부터 동의어를 추출하는 SynonymFilter
"명사사전"으로부터 명사를 추출하는 NounExtractFilter
"불용어사전"으로부터 불용어 처리를 하는 StopFilter
"복합명사사전"으로부터 주어진 키워드를 사전에 정의된 명사들로 추출하는 CompoundsNounExtractFilter
등을 생각해 볼 수 있겠습니다.
각각의 Filter들은 실제로 구현을 해보면서 Analyzer를 완성시켜보도록 하겠습니다.
가장 간단한 형식으로 구현 할지 모든 내용을 다 담아서
구현을 할지 아직 결정을 하지 못 했네요...
설명을 어떻게 해야할지도 중요한 문제라서 모든 내용을 다 담아서 전부 구현을 하게 되면.. 그것만으로도
글의 분량이 어마어마해질 것 같아서... 고민이네요...

포스트에 사용된 예제는https://github.com/need4spd/aboutLucene에서 체크아웃 받으 실 수 있습니다. 
저작자 표시 비영리 변경 금지
신고
Posted by 용식
앞에 데모 프로그램을 보셔서 아시겠지만
검색이라는 하나의 작업을 위해서는 
크게 색인과 검색이라는 2가지 프로세스를 거쳐야 합니다.

이중 색인과 검색에 모두 관여하는 것이 하나 있는데
그것이 바로 키워드 추출.. 즉, 흔히 이야기하는(하지만 너무나 어려운)
형태소분석입니다. (이후에는 키워드 추출이라고 하도록 하겠습니다.)

잠깐 루씬을 떠나서
검색이론에 대해서 이야기 해 보겠습니다.

흔히 검색에서 이야기하는 색인 파일은 대부분 "역색인(Inverted Index)" 구조로 되어있습니다.

이게 어떤 구조인가하면.... 책의 맨 뒤 부록에 나오는
"단어"를 기준으로 페이지를 나열한 형태의 색인 구조라고 보시면 됩니다.

나이키 - Doc1, Doc2, Doc3
운동화 - Doc1, Doc5
청바지 - Doc6, Doc7


이런식으로 키워드를 기준으로 그 키워드가 포함된 문서들을
정의해 놓은 형태입니다.

"나이키"라는 검색어가 들어오면 Doc1, Doc2, Doc3을 찾아서 화면에
보여주는 것이지요.

"나이키와 운동화"라는 검색어가 들어오면
"나이키"에 연결된 Doc1, Doc2, Doc3
"운동화"에 연결된 Doc1, Doc5 를 찾아서
And 연산으로 Doc1을 찾아서 결과로 보여주는 구조입니다.

Doc1, Doc2, Doc3
&
Doc1, Doc5
= Doc1 


물론 재검색의 Boolean 조건이 OR라면 Doc1, Doc2, Doc3, Doc5가 전부 보여질 것 입니다.

보시면 아시겠지만 "키워드" 기반의 검색에 최적화 된 구조라고 볼 수 있습니다.

이러한 색인구조에는 미리 스코어를 지정 해 둘 수
없는 구조이기 때문에 Ranking이 그만큼 중요해지는 구조이기도 합니다.

그리고, 위의 예만 보시더라도
Doc1~Doc7까지의 문서에서 나이키, 운동화, 청바지등 사용자들에게
혹은 비지니스안에서 의미가 있는 키워드를 추출해내는 것과
사용자가 입력한 키워드인 "나이키와 운동화"에서 "나이키", "운동화"를 추출해내는 것이
얼마나 중요한 작업인가도 알 수 있습니다.

이 키워드가 추출되지 않으면
검색을 할 수가 없는 것 입니다.

이것이 검색에 있어서 키워드 추출(형태소 분석)이고
이 역할을 해주는 것이 루씬의 Analyzer입니다.

사실 루씬으로 검색엔진을 만든다고 할 때
가장 먼저 부딫히게 되는 장벽이 바로 이 Analyzer의 구현이고
또 가장 어려운 부분중에 하나가 역시 이 Analyzer입니다.

검색이라는 그 행동 자체는
어디서나 동일하겠지만 (랭킹의 구현은 다르겠지요..) 키워드 추출이라는 것은
나라별로, 언어별로 심지어 비지니스(1)별로 원하는 추출 방식이 모두 다르고, 추출해야하는
방법도 모두 다르기 때문입니다.

본격적으로 들어가기전에 키워드 추출에 대해서 조금 더 살펴보도록 하겠습니다.

초반에는 문장에서 (혹은 웹 페이지에서) 의미있는 키워드라는 것이 대부분 "명사"에 한정되는 구조였습니다.
전반적으로 색인 대상이 되는 문서가 많지 않았기 때문에 문장에 들어있는 중요한 명사들만
추출해서 색인을 해도 큰 문제가 없었울 것 입니다.

하지만 요즘에는 이렇게 명사만을 추출하는 것은 
큰 의미가 없을 것 같습니다.(2)

"사랑합니다"라는 노래를 찾아보기 위해 구글에서 "사랑"이라는 키워드로 검색을 해보세요.

"사랑"이 들어간 무수한 페이지들이 나올 것 입니다. 정말로 내가 원하는 것을 찾기 위해서는
"사랑"이라는 단어 하나만으로는 많이 부족합니다. 

최근에는 단순히 키워드만을 추출하는 것이 아닌, 점점 자연어에 근접한 키워드로 검색이 가능하도록
해주는 것이 중요해지고 있습니다. 물론 이러한 수준까지 이번 프로젝트(?)를 통해서
구현하지는 못 할 것 입니다. 사실 형태소 분석도 구현하기가 아주 어려워요..--;
참고로 이수명님께서 만드신 루씬용 형태소 분석기가 있습니다.

이번에는 미리 만들어진 사전을 기반으로 하여
문장과 입력된 키워드로부터 단어를 추출하는 수준으로
구현을 해 볼 예정입니다.

그리고 또 하나는 색인 할 때의 키워드 추출과 검색어로 들어온 키워드를 분석 할 때에는
약간의 차이가 있어야 한다는 것 입니다.

Doc1 : "삼성은 우리나라에서 가장 큰 기업이다. 전자부문을 비롯하여...." 
Doc2 : "삼성전자는 이번에 갤럭시S를 발표하였다."


라는 두개의 문장이 있다고 가정을 해보겠습니다.

Doc1에서의 키워드는 "삼성" , "우리나라" , "기업", "전자", "부문", "전자부문" 등이..
Doc2에서의 키워드는 "삼성" , "전자", "삼성전자", "갤럭시S", "발표" 등이 나온다고 볼 수 있을 것 입니다.

이렇게 색인이 되어 있는 상태에서
한명의 사용자가 검색어로 "삼성전자"를 입력하였습니다.

위 문장에서 키워드가 추출된 결과를 참고해보면 "삼성전자"로부터 "삼성"+"전자"
추출이 되는 것이라고 생각해 볼 수 있습니다.
그렇다면, 사용자가 "삼성전자"와 관련된 문서를 찾고 싶어서 입력 한 검색어를 "삼성""전자"
분석하여 검색이 되도록 해야 할까요? 그렇다면 Doc1과 Doc2과 모두 검색이 될 것 입니다.

하지만, Doc1은 사실 삼성전자와는 별 관련이 없는 글입니다.

오히려 사용자가 입력한 "삼성전자"라는 검색어를 특별히 분석하지 않고
그대로 검색이 되도록 한다면 Doc2만이 검색이 될 것이고 이것이 더 좋은
결과 값이라고 생각이 될 수도 있습니다.

사실 이러한 부분은 recall(재현율)과 precision(정확도) 중 어느 곳에
더 비중을 두느냐 혹은 위와 같은 상황에서 아예 Doc1을 검색 결과에서 제외를 시킬 것이냐
Ranking으로 해결 할 것이냐등에 따라서 정답이 없는 이야기이지만
키워드 추출이라는 것이 단어를 무조건 많이 추출한다는 것이
답은 아니라는 것을 보여드리기 위해서 언급을 해보았습니다.

이번에 직접 구현 할 Analyzer에서는 위 기준에 맞춰서
색인용과 검색어 분석용으로 나누어 구현을 할 예정입니다.

작성을 하고 이번 포스트를 마무리 하려고 하니..
이번 내용에서는 루씬에 대해서는 전혀 내용이 없네요..--;

하지만 검색 엔진을 만든다고하면 반드시 꼭 집고 넘어가야 하는
부분이기도 합니다. 최근의 검색 엔진들은 자연어 검색도 지원해주고 있기 때문에
여기서 이야기된 많은 부분들은 이미 검색엔진이 태동 될 초창기때
모두 한번씩은 언급이 되었던 내용들이라고 생각합니다.

하지만 뒤따라가는 입장에서
이미 지나간 이슈라고 그냥 넘어 갈 수는 없겠죠..^^

다음 포스트에서 
루씬에서 구현되어 있는 Analyzer의 구조와 가장 간단한 Analyzer하나를 뜯어보고
이를 바탕으로 저희만의 "사전기반의 명사 추출" Analyzer를 
만들어 나가보도록 하겠습니다.

하루써서 하루 올리는 입장이라서
글 올라오는 속도가 매우 들쑥날쑥 할 것 같습니다. 보통 애 재우고나야 
앉아서 뭘 쓸 시간이 생겨서요...ㅎㅎ

이해부탁드릴게요..^^

그리고 저도 검색/루씬 초짜입니다. 또 구현된 소스를
보시다보면 구현 자체에 문제가 있을 수도 있습니다.

내용 중 오류나 잘못된 부분들이 있으면
언제든 말씀해주세요.

감사합니다.

(1) - 쇼핑몰에서의 키워드 추출과 포털에서의 키워드 추출이 완전히 같을 수는 없을 것 입니다.
(2) - 쇼핑몰등에서는 조금 다릅니다. 명사/모델명등 단어만 추출하는 것이 충분히 의미가 있을 수 있습니다. 
(3) - 이 카페에 가시면 관련 내용들을 보실 수 있습니다.

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