본문 바로가기

Lucene

[about Lucene] 루씬으로 검색엔진 개발하기 - Analyzer (3) -

이제 정말로 구현을 해 볼 시간입니다.
실제의 구현은 TDD의 형태를 따라가도록 하겠습니다.

기본적으로 우리가 원하는 결과를 테스트케이스로 만들어 놓고
실제 구현을하여 테스트케이스를 통과하는 형태로 개발을 하겠습니다.

Analyzer가 하나의 Tokenizer 그리고 여러개의 TokenFilter로 이루어져 있다고
말씀드렸는데요, 하나하나에 대해서 테스트 케이스를 먼저 살펴보고
최종적으로 완성된 Analyzer의 테스트 케이스를 확인해보도록 하겠습니다.

들어가기전에 앞 포스트에서 Tokenizer에서 키워드를 추출하면서
색인과 검색에 필요한 Attribute들을 설정하고 이 Attribute들이 모여있는 클래스를
AttributeSource라고 말씀드렸었는데요, TokenFilter에서는 이 AttributeSource의 개념이 상당히
중요하게 적용됩니다.


우선적으로 필요한 것이 입력된 문장으로 가장 기본적인 Token을 추출해내는
Tokenizer입니다. TokenFilter들은 이렇게 추출 된 Token을 가지고 작업을 하기 때문에
제일 먼저 필요 한 것은 Tokenizer입니다.

여기서는 이 TokenizerDevysTokenizer라고 이름을 지으려고 합니다.

Tokenizer는 아래와 같은 rule로 토큰을 만들어 냅니다.

1. 공백이 나오면 추출
2. 앞과 뒤의 캐릭터가 (한글 - 영어/숫자)인 경우 추출
3. 숫자와 "."은 같은 캐릭터로 취급 (3.5등 버전을 위해서)
4. 기타 특수문자는 제외함


예를 들면 "나는 사람이다. 180.5cm이다." 라는 문장이 있다고 했을 때
[나는] [사람이다] [.] [180.5] [cm] [이다] [.]
라고 추출이 됩니다. "."이 좀 눈에 거슬리지만 우선 그냥 놔두고 나중에 TokenFilter를 사용하여 제거 해보도록 하겠습니다.

이것을 기준으로 Tokenizer의 테스트 케이스를 간단하게 작성하여 보겠습니다.

 
DevysTokenizerTest.java

테스트 방식은 아래와 같습니다.
샘플문장이 있고 이 문장으로부터 추출되어야 하는 단어들을 미리 Set에 넣어둡니다.
그리고 Tokenizer를 실행하여 나오는 단어를 Set에 있는 단어들과 비교하여
정상적으로 추출이 되었는지 확인합니다.

그러면, DevysTokenizer를 살펴보겠습니다.

 
DevysTokenizer.java

기본적으로 Tokenizer들은 Tokenizer클래스를 상속받아야 하고, incrementToken 메서드를
반드시 오버라이드 하도록 되어있습니다. 
실제로 문장을 잘라서 Token을 만들어내는 역할을 하는 메서드가 바로 incrementToken 메서드로
내용을 보시면 StringReader를 읽어서 앞서 제시한 룰에 의해서 단어를 잘라내고 그 단어의
위치정보와 위치증가정보등을 각 Attribute에 설정 즉, AttributeSource를 만드는 역할을 합니다.

위 메서드는 2.X 버전까지의 루씬에서 제공되는 CharTokenizerincrementToken 메서드의 로직을
조금 수정하여 구현했던 코드입니다.

한가지 특이한 것이 생성자가 3개가 있습니다.

public DevysTokenizer(Reader in) {
    super(in);
}

public DevysTokenizer(AttributeSource source, Reader input) {
    super(source, input);
}

public DevysTokenizer(AttributeFactory factory, Reader input) {
    super(factory, input);
}



기본적으로 StringReader만을 받는 생성자 하나와 AttributeSource, AttributeFactory를 받는
생성자입니다. 여기서는 기본적인 Attribute만을 사용 할 것이라서 사실 아래 2개의 생성자는
사용하지 않습니다만, 이러한 생성자가 있다는 것을 보여드리기 위해서 코드를 집어 넣었습니다.

만약 우리가 추출해내려는 속성 중 추가로 필요한 속성이 있다면
Attribute인터페이스를 상속한 AttributeImpl 클래스를 만들어서 사용 할 수 있는
구조로 되어 있습니다. 예를 들어서 검색이나 색인에 필요한 추가적인 속성이 있다면
아래와 같이 작성 할 수 있습니다.

소스를 보시면 MyAttribute라는 클래스가 보이실건데요
바로 이런식으로 추가적으로 필요한 정보가 있다면 Attribute클래스를 만들어서
활용 하실 수 있습니다.



MyAttribute.java
MyAttributeImpl.java
앞서서 incrementToken 메서드에 대해서 잠깐 이야기 했었는데요
이전까지는 Token클래스를 리턴해주고 이 리턴된 Token을 받아 그 다음 연결되어 있는
TokenFilter가 작업을 하는구조였습니다. 하지만 3.X로 올라오면서 incrementToken메서드는 Token이 아닌
boolean값을 리턴하여 AttributeSource가 추출되었는지를 알려주고, 데코레이터 패턴으로 물려있는
모든 TokenStream들이 (Tokenizer, TokenFilter 들..) 위에서 사용되는 Attribute클래스들 즉 AttributeSource
공유하면서 사용하는 구조입니다.

위에서 보시면 아시겠지만
추출된 정보를 각 Attribute에 set하고 단순히 boolean값을 리턴하는 것으로
Tokenizer의 역할이 끝납니다. 이 다음에 연결되어 있는 TokenFilter에서는 마찬가지로
AttributeSource에서 필요한 Attribute를 꺼내어 가공하고 그 정보를 다시 Attribute에 set하는
구조로 되어 있는 것 입니다.

모든 TokenizerTokenFilterTokenStream을 상속하고 있고 이 TokenStream
AttributeSource를 상속하고 있는 구조에서, 이 AttributeSourceAttribute들을 가지고 있습니다. 이 Attribute들이
매번 생성되는 구조가 아니고 Map에 저장하여 재사용되는 구조로 되어있습니다. 그렇기 때문에
하나의 Analyzer안에서는 TokenStream클래스가 가지고 있는 각 Attribute들을 공통적으로
사용 할 수 있는 것 입니다.

이 부분은 후에 Filter를 구현 할 때 다시 한번 다루게 될 것 입니다.

그리고 두개의 메서드가 더 보이는데요
 

protectedboolean isTokenChar(char c) {
           return (Character.isLetter(c) || Character.isDigit(c) || (c == '.'));
}

protectedchar normalize(char c) {
            return Character.toLowerCase(c);
}


입니다.

이 부분은 StringReadercharacter단위로 읽어가면서 이 character가 추출의 대상인지
아닌지를 결정하는 isTokenChar 메서드와 소문자로 정규화시키는 normalize 메서드가 존재합니다.

이 두개의 메서드도 적절하게 커스터마이즈를 하면 원하는 Token추출 룰을 만들 수 있습니다.

이렇게하여 기본적으로 단어를 추출해내는
Tokenizer를 만들었습니다. 지금까지 루씬이 버전업을 해오면서
1.4부터 굉장히 많은 부분이 바뀌었고, 또 어느정도 하위버전과 호환성을 맞춰오면서 그에 따라
코드의 양도 다소 비대해진 경향이 느껴집니다.

그리고 위처럼 AttributeSourceAttributeFactory등은 저도 실제로 커스터마이즈해서
사용해본적은 없습니다. 우선 준비되어있는 Attribute만으로도 필요한 것을 개발하는데는 큰 문제가 없었습니다.

위 구현 코드를 보시고 이에 대해서 공부를 해보시면
Tokenizer가 하는 일에 대해서 감을 잡으실 수 있을 것 입니다.

https://github.com/need4spd/aboutLucene에서 체크아웃 받으 실 수 있습니다.