이번에 공개소프트웨어 개발자 대회에 출품하였다가
1회전 낙방한 한글명사 추출 Analyzer입니다.

형태소 분석 알고리즘은 들어있지 않고
String Scan 형식으로 사전과 Match되는 명사를 추출하고
동의어사전, 복합명사사전, 불용어사전등을 사용하여
루씬의 Analyzer와 Filter의 사용으로 여러 기능을 구현하고 있습니다.

형태소분석까지는 무리더라도
Scan을 좀 빠른 속도와 효율적인 알고리즘, 자료구조로 바꿔보고 싶은
욕심은 있습니다.

형태소분석 기능이 들어가면 더할나위 없이 좋겠지만
제가 형태소분석에 대한 지식이 없어서 구현은 하지 못 하였습니다.

소스는 github를 사용하여 관리하고 있습니다.

한글명사 추출의 기능적인 면보다도 어쩌면 Analyzer와 Filter의 사용에 대한
공부에 더 좋을지도 모르겠네요 :)

com/tistory/devyongsik/sample 패키지를 보시면
이 Analyzer를 활용한 예도 있으니 참고해보세요.


https://github.com/need4spd/lucene-korean-analyzer

래도 조금 기대했었는데
아직 실력이 많이 부족한 것 같습니다. 참여자가 많지 않다고 하여
너무 쉽게 생각했을지도 모르겠습니다. :)

내년에 다시 한번 도전해보고 싶네요. 

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

예전에 작성해 놓은 Analyzer와 Filter 포스트는 1.4.X 버젼대의 소스입니다.
예전에는 Analyzer의 형식이

public TokenStream tokenStream(String fieldName, Reader content) {
  return new GSKoreanSynonymFilter(
      new GSKoreanStopWordFilter(
       new GSKoreanSeperatorWordFilter(
        new GSKoreanSeperatorNameWordFilter(
         new GSKoreanTokenizer(content){} //GSKoreanTokenizer
        ) // GSKoreanSeperatorNameWordFilter
       ) //GSKoreanSeperatorWordFilter
     ),
    engine);
 }

이런 식이었는데 새버젼 소스를 받아서 열어보니 많이 바뀌었습니다.
아직 기존의 Analyzer를 새 버젼에 맞춰서 변경시키지는 못 했는데, 일단 어느 부분이 바뀌었는지 간략하게나마
알아봤으면 좋겠습니다.

간단한 모양을 하고 있는 StopAnalyzer를 열어보았습니다.

  public TokenStream tokenStream(String fieldName, Reader reader) {
    return new StopFilter(new LowerCaseTokenizer(reader), stopWords);
  }

기존에 사용하던 방식의 tokenStream 메서드도 있습니다. 버젼이 올라갔다고 이걸 그냥 없애버리면 아무래도 버젼을 업그레이드 하기 어렵겠죠.. ^^

그리고 새로운 메서드와 내부클래스가 눈에 들어옵니다.

  private class SavedStreams {
    Tokenizer source;
    TokenStream result;
  };

  public TokenStream reusableTokenStream(String fieldName, Reader reader) throws IOException {
    SavedStreams streams = (SavedStreams) getPreviousTokenStream();
    if (streams == null) {
      streams = new SavedStreams();
      streams.source = new LowerCaseTokenizer(reader);
      streams.result = new StopFilter(streams.source, stopWords);
      setPreviousTokenStream(streams);
    } else
      streams.source.reset(reader);
    return streams.result;
  }


reusableTokenStream 이 메서드인데요, 그냥 한번 훑어보니 뭔가 tokenStream을 저장해놓고 그것을 재 사용하는 방식을 쓰는 것 같습니다.

일단은 최상위 추상 클래스인 Analyzer에 정의 되어 있는 getPreviousTokenStream(); 메서드를 호출하여
뭔가 예전의 TokenStream을 가져옵니다. 위 메서드는 아래와 같이 되어 있습니다.

 private ThreadLocal tokenStreams = new ThreadLocal();

  protected Object getPreviousTokenStream() {
    return tokenStreams.get();
  }

  protected void setPreviousTokenStream(Object obj) {
    tokenStreams.set(obj);
  }

ThreadLocal 이라는 녀석이 저의 짧은 지식으로는 수행되는 쓰레드별로 별도의 저장소를 만들어
쓰레드당 고유의 저장소에서 객첼르 저장하고 사용하는 방식을 제공하는 그런거로 알고 있는데 일단 이것은 좀 더 공부가 필요 할 듯 하다. ㅠㅠ

아무튼, 싱글턴 이라는 것을 기억해두고 계속 진행해 보면, 그렇게해서 저장되어 있는 (공유되고 있는) SavedStream이 없을 경우에는 새로운 SavedStream을 생성 시킵니다.
그리고 tokenizer를 SavedStreams.source에 set하고
SavedStreams.result 에 TokenStream (FilterClass들) 들을 set 한 후 1.4.X 버젼과 비슷한 방식으로 사용을 하게 됩니다. 그리고 마지막에는
setPreviousTokenStream(streams);
메서드를 통해 생성했던 SavedStreams 객체를 ThreadLocal에 넣어놓습니다.

그리고, 만약 streams != null 이면
streams.source.reset(reader); 를 통해서 contents를 reset하고
TokenStream을 리턴합니다.

쓰레드가 살아있는 동안 TokenStream을 새로 생성 할 필요없이..
다시 가져와 사용 할 수 있다.. 라는 정도로 이해하면 무리가 없을지 잘 모르겠습니다. ^^
파싱하려는 내용이 변경되어도 streams.source.reset(reader); 을 통해서 contents를 reset해주고 있기
때문에 문제는 없을 것 같구요.


그 다음에 살펴 볼 부분이 next(final Token token) 메서드입니다.
2.4.0으로 넘어오면서 final 이 붙어버렸습니다.

일단 쉬운 비교를 위해서 예전과 소스를 비교해보겠습니다.

예전:

public final Token next(Token result) throws IOException {
    // return the first non-stop word found
    int skippedPositions = 0;
    while((result = input.next(result)) != null) {
      if (!stopWords.contains(result.termBuffer(), 0, result.termLength)) {
        if (enablePositionIncrements) {
          result.setPositionIncrement(result.getPositionIncrement() + skippedPositions);
        }
        return result;
      }
      skippedPositions += result.getPositionIncrement();
    }
    // reached EOS -- return null
    return null;
  }


 

  public final Token next(final Token reusableToken) throws IOException {
    assert reusableToken != null;
    // return the first non-stop word found
    int skippedPositions = 0;
    for (Token nextToken = input.next(reusableToken); nextToken != null; nextToken = input.next(reusableToken)) {
      if (!stopWords.contains(nextToken.termBuffer(), 0, nextToken.termLength())) {
        if (enablePositionIncrements) {
          nextToken.setPositionIncrement(nextToken.getPositionIncrement() + skippedPositions);
        }
        return nextToken;
      }
      skippedPositions += nextToken.getPositionIncrement();
    }
    // reached EOS -- return null
    return null;
  }

bold 처리 된 부분이 바뀐 부분인데요
파라메터가 final로 변경되면서 그 값을 변경하지 못 하기 때문에
내부적으로 nextToken이라는 로컬 객체를 하나 더 생성하여 사용하고 있습니다. reusableToken은 그냥 다음 필터클래스나 토크나이저 클래스에 넘어갈 뿐입니다.

한가지 궁금한 것은.. reusableToken이라는 것이 어디에 사용되는가 하는 건데요...;;;

사실 각각의 필터에서 조작하고 작업하는 대상이 되는 Token은 nextToken일 것인데..
reusableToken은 무엇 때문에 final로 선언되어 파라메터로 넘어가고 있는건지 아직은 잘 모르겠습니다..

나중에는 Tokenizer에서 추출된 Term과 포지션 정보를 set해서 넘겨주는데 사용하고 로직을 보면 새로운 Token 객체의 생성없이 계속 처음에 생성된 그 Token을 재사용하면서 쓰고 있긴한데..

그외 다른 목적에 대해서
나중에 알게 되면 다시 정리하거나.. 혹시 알고 계신분이 있으시면
댓글로라도 답변을 좀 부탁드리고 싶네요. ^^
저작자 표시
신고
Posted by 용식
루씬에서 사용 할 색인어 추출기..
컨셉을 잡아봤습니다.

명사만 뚝뚝 추출 할 것이냐, 문장 그대로를 검색 시킬 것이냐..

예를 들면

"쇼핑몰 포인트를 어디서 받을 수 있을까요" 라는 검색어가 들어올 경우

키워드만 추출하는 (쇼핑몰/ 포인트)가 아닌

"어디서 받을 수 있을까요" 라는 문장만으로도 검색이 가능하게 하는

그런것이 트렌드인듯 합니다. 네이버도 그렇고.. 구글도 그렇고...

일단.. 이렇게 가려고 합니다.

Tokenizer를 상속받은 KoreanTokenizer 클래스를 만들어서

어절분리(띄어쓰기..), 특수문자제거, 한글-영어/숫자 분리(삼성mp3 -> 삼성, mp3),
소문자 정규화를 시키고

명사사전을 사용하여 명사를 분리하는 Filter 클래스를 만들고..

필요없는 어미를 제거하는 Filter 클래스 만들고

불용어 제거 하는 Filter와 (이건 추가 할지 말지...)

동의어사전을 사용하여 동의어를 뽑아내는 Filter.

이렇게 만들어보려고 합니다.


형태소분석 기반이 아닌지라..결과가 상당히 제멋대로 일 수 있을 것 같아요..

명사사전을 뒤져본다해도 .. 어떻게 처리할지..

사전은 회사 내부에서 사용을 하기에

내부에서 명사로 가장 많이 사용되는 단어들을 사전으로 만들어 보려고 합니다.

신고
Posted by 용식
루씬을 사용하기 위해
상품명 색인어 추출을 위한 Analyzer를 제작중입니다.

맨땅에서 할 실력이 안되어서
루씬인액션 책에 있는 StemFilter를 참고하여
텍스트 사전 기반의 StemFilter를 하나 만들고

CharTokenizer를 참고하여 영문/숫자와 한글을 분리해내는
Tokenizer를 하나 만들어서 시작 해볼 생각입니다.

형태소 분석이라기 보다는 말 그대로 색인어 추출에 가깝죠..;;

국어도 못 하고
형태소 분석하기 위해서 국어 공부와 알고리즘 등등을 할 엄두가 나지 않아
일단 부딫혀 봤습니다.

빡세네요..^^;;

그래도 저한테는 많은 도움이 될 것 같습니다..

정리되는대로 소스도 올려보고 해서 많은 도움 받았으면 좋겠네요..웅


일단
정품 아베크롬비 프리티 베드포드 핫팬츠/반바지[착불], 4KUS 16배속 DVD±RW(DRW-3S167)[16X16]+블랙베젤+FANTASY2.0(동영상편집프로그램)
를 넣으면
[정품][아베크롬비][프리티][베드포드][핫팬츠][반바지][착불][4kus][16][배속][dvd][rw][drw][3s167][16x16][블랙베젤][fantasy2][0][동영상편집프로그램]

MP3엠피3 BYR24098 패션[style] usb1gb/충전기 패션style usb1gb충전기
를 넣으면
[mp3][엠피][3][byr24098][패션][style][usb1gb][충전기][패션][style][usb1gb][충전기]

MP3P폰, 세닉스 MP3플레이어 DMP-860 (256MB) 패션 스타일 MP3 Player 유혹FL-350 1GB+USB충전아답터
를 넣으면
[mp3p][폰][세닉스][mp3][플레이어][dmp][860][256mb][패션][스타일][mp3][player][유혹][fl][350][1gb][usb][충전아답터]

요기까지 되는데.. 보고있으면 어디부터 손대야 할지 ㅠㅠ...

발전되가는 모습을 계속 적어나갈 수 있었으면 좋겠습니다 ^^
신고
Posted by 용식
PerFieldAnalayzerWrapper analyzer = new PerFieldAnalyzerWrapper(new SimpleAnalyzer());

analyzer.addAnalyzer("field_name", new KeywordAnalyzer());

Query query = QueryParser.parse("filed_name:S12 AND SPACE", "descrption", analyzer);

Hits hits = searcher.search(query);

...

신고
Posted by 용식

Analyzer는 텍스트를 일련의 토큰 (TokenStream)으로 변환한다. TokenStream 클래스는 Token 클래스를 차례로 반환한다.

Token은 문자열, 시작위치, 끝 위치, 토큰 종류, 위치 증가값등을 갖고 텀은
단어와 위치 증가값만을 사용하여 변환되며, 텀이 바로 색인에 저장되는 단위이다.

TokenStream에는 Tokenier와 TokenFilter가 있고
Tokenizer는 글자 단위로 토큰을 만들고, TokenFilter는 토큰 단위로 처리를 한다.

Tokenizer가 TokenStream을 반환하면, TokenFilter는 TokenStream을 입력 받아 TokenStream이 토큰을 뽑아내면 그 토큰에 새로운 토큰을 더하거나 삭제 할 수 있다. (분석기 확장 용이)

이를 염두에 두고 StopAnalyzer의 구조를 보면..

public class StopAnalyzer extends Analyzer {
  private Set stopWords;

  public StopAnalyzer() {
    stopWords = StopFilter.makeStopSet(StopAnalyzer.ENGLISH_STOP_WORDS);
  }

  public StopAnalyzer(String[] stopWords) {
    this.stopWords = StopFilter.makeStopSet(stopWords);
  }

  public TokenStream tokenStream(String fieldName, Reader reader) {
    return new StopFilter(
                    new LowerCaseTokenizer(reader, stopTable);
  }
}

일단 Tokenizer가 소문자로 변환한 토큰 스트림을 반환하면
StopFilter가 받아서 불용어 토큰을 삭제한다.

일반적으로 인자로 입력받은 input Stream을 사용해 token을 얻어와 자기자신 (filter)을
적용시켜 넘겨준다.

  public TokenStream tokenStream(String fieldName, Reader reader) {
    TokenStream result = new SynonymFilter(
                          new StopFilter(
                            new LowerCaseFilter(
                              new StandardFilter(
                                new StandardTokenizer(reader))),
                            StandardAnalyzer.STOP_WORDS),
                          engine
                         );
    return result;
  }

신고
Posted by 용식