루씬에서 Indexing 작업은 주어진 Text를 Term으로 쪼개서 검색 가능한 최소한의 단위로 만드는 작업이라고 볼수 있습니다.
그리고, 그 단어들을 검색하기 편하도록 거꾸로 정리 놓는거죠. (Inverted index 말입니다.)
이번에는 indexing 작업중에 Text를 쪼개서 Term으로 만드는 방법에 대해서 설명합니다.
이렇게 대상 Text를 잘게 잘라서 term을 만드는 작업은 Analyzer가 수행합니다.
이 글은 "실전비급 아파치 루씬 7"을 참고하였습니다.
자세한 설명 및 다양한 예제는 해당 책을 구매하여 확인하시기 바랍니다.
※ 모든 예제 코드는 Kotlin으로 작성되었습니다. (Lucene library 원본 코드 제외)
크게 나누어 분석은 아래의 단계를 거칩니다.
CharFilter -> Tokenizer -> TokenFilter
CharFilter
- HTMLStripCharFilter: HTML 구문 제거
- MappingCharFilter: 백스페이스, 탭, 개행문자 등을 유니코드로 변경.
TokenStream
AttributeSource
fun main(args: Array) {
val source = "He has children and we love them"
val analyzer = StandardAnalyzer()
val tokenStream = analyzer.tokenStream("Sample", source)
tokenStream.use {
// 속성 등록
// 토큰의 text
val charTermAttr = it.addAttribute(CharTermAttribute::class.java)
// 토큰의 offset 정보
val offsetAttr = it.addAttribute(OffsetAttribute::class.java)
// 토큰이 차지하는 자리의 개수
val positionLengthAttr = it.addAttribute(PositionLengthAttribute::class.java)
// payload 정보를 get/set 할수 있다. (Payload 기반 쿼리르 ㄹ할때 Score에 영향을 준다)
val payloadAttr = it.addAttribute(PayloadAttribute::class.java)
// token의 유형
val TypeAttr = it.addAttribute(TypeAttribute::class.java)
// 현재 읽어야 하는 토큰을 위치 반환. 생략된 토큰이 있다면 샏략된 개수만큼 더해진 값이 써진다.
val positionIncrementAttr = it.addAttribute(PositionIncrementAttribute::class.java)
// token을 키워드로 표시할 때 사용
val keywordAttr = it.addAttribute(KeywordAttribute::class.java)
// Token Stream 초기화
it.reset()
while (it.incrementToken()) {
println("charTerm: $charTermAttr | " +
"offsetAttr: (start=${offsetAttr.startOffset()} end=${offsetAttr.endOffset()}) | " +
"payloadAttr: ${payloadAttr.payload} | " +
"positionLengthAttr: ${positionLengthAttr.positionLength} | " +
"TypeAttribute: ${TypeAttr.type()} | " +
"positionIncrementAttribute:${positionIncrementAttr.positionIncrement} | " +
"keywordAttr:${keywordAttr.isKeyword}"
)
}
}
}
TokenStream에 addAttribute()로 속성을 추가하면 snap shot이 등록되어 그때그대의 token 정보를 볼수 있습니다.
위 예제에서는 incrementToken() 함수로 token을 하나씩 넘기면서 해당 Token의 정보를 출력합니다.
결과는 아래와같습니다.
charTerm: he | offsetAttr: (start=0 end=2) | payloadAttr: null | positionLengthAttr: 1 | TypeAttribute:
이중에서 "and" 는 불용어로 삭제된것을 알수 있습니다. (StopFilter에 의해 걸러짐)
따라서 "we"의 positionIncrementAttribute의 값은 2 입니다.
Tokenizer
- Standard Tokenizer: lucene core에서 제공하는 기본 analyzer로 유니코드 텍스트 분할 알고리즘에 명시된 단어 분리 규칙을 따름.
- Whitespace Tokenizer: 공백 문자로 텍스트를 분할
- NGram Tokenizer: N-Gram 모델을 이용해 분할
- LowerCase Tokenizer: 영문 분할용으로 대문자를 소문자로 바꿔서 분할하며, LetterTokenizer + LowerCaseFitler의 조합
- EdgeNGram Tokenizer: 입력토큰의 시작부분에서 N-Gram을 만듬.
TokenFilter
- StandardFilter: StandardTokenizer로 추출된 토큰을 표준화 합니다.
- LowerCaseFilter: 소문자로 통일 시킵니다.
- StopFilter: 불용어를 제거합니다 ex) 'a', 'the' , 'is', 'and' ...
Analyzer
-> 사용할 Tokenizer와 TokenFitler를 담는 TokenStreamComponents를 return합니다.
org.apache.lucene.analysis.Analyzer.java 내부의 crateCompoenets 정의
/**
* Creates a new {@link TokenStreamComponents} instance for this analyzer.
*
* @param fieldName
* the name of the fields content passed to the
* {@link TokenStreamComponents} sink as a reader
* @return the {@link TokenStreamComponents} for this analyzer.
*/
protected abstract TokenStreamComponents createComponents(String fieldName);
org.apache.lucene.analysis.Analyzer.java 내부에 정의된 TokenStreamComponents 클래스 내용
/**
* This class encapsulates the outer components of a token stream. It provides
* access to the source ({@link Tokenizer}) and the outer end (sink), an
* instance of {@link TokenFilter} which also serves as the
* {@link TokenStream} returned by
* {@link Analyzer#tokenStream(String, Reader)}.
*/
public static class TokenStreamComponents {
/**
* Original source of the tokens.
*/
protected final Tokenizer source;
/**
* Sink tokenstream, such as the outer tokenfilter decorating
* the chain. This can be the source if there are no filters.
*/
protected final TokenStream sink;
/** Internal cache only used by {@link Analyzer#tokenStream(String, String)}. */
transient ReusableStringReader reusableStringReader;
/**
* Creates a new {@link TokenStreamComponents} instance.
*
* @param source
* the analyzer's tokenizer
* @param result
* the analyzer's resulting token stream
*/
public TokenStreamComponents(final Tokenizer source,
final TokenStream result) {
this.source = source;
this.sink = result;
}
/**
* Creates a new {@link TokenStreamComponents} instance.
*
* @param source
* the analyzer's tokenizer
*/
public TokenStreamComponents(final Tokenizer source) {
this.source = source;
this.sink = source;
}
/**
* Resets the encapsulated components with the given reader. If the components
* cannot be reset, an Exception should be thrown.
*
* @param reader
* a reader to reset the source component
*/
protected void setReader(final Reader reader) {
source.setReader(reader);
}
/**
* Returns the sink {@link TokenStream}
*
* @return the sink {@link TokenStream}
*/
public TokenStream getTokenStream() {
return sink;
}
/**
* Returns the component's {@link Tokenizer}
*
* @return Component's {@link Tokenizer}
*/
public Tokenizer getTokenizer() {
return source;
}
}
따라서 Analyzer를 상속받아서 위 함수를 구현하여 직접 custom analyzer를 만들어 사용할 수도 있습니다.
Custom analyzer를 만드는건 추후 Analyzer 분석기 (2/2)에서 포스팅 합니다.
추가적으로 루씬은 core 라이브러리의 기본 분석기 이외에도 lucene-analyzer-common 라이브러리에서 추가적인 Analyzer를 제공합니다.
추가 Analyzer
- Common: 언어나 비지니스 분야에 공통적으로 사용을 위한 분석기
- ICU: 유니코드 표준을 지원하며 ICUInternational compponents for Unicode를 루씬에서 사용하기 위한 모듈
- Kuromoji: 일본어 형태소 분석기
- Phonetic: 비슷한 발음을 검색
- Smart Chinese: 중국어 간체 분석
- Stempel: 폴란드 언어의 동사 원형 추출 알고리즘 제공
- UIMA: 아파치 UIMA(테스트간의 관련성을 찾아내는 비정보화 정보관리 아키텍쳐)와 통합분석 제공
StandardAnalyzer
/**
* Filters {@link StandardTokenizer} with {@link StandardFilter}, {@link
* LowerCaseFilter} and {@link StopFilter}, using a list of
* English stop words.
*/
public final class StandardAnalyzer extends StopwordAnalyzerBase {
...
static {
final List stopWords = Arrays.asList(
"a", "an", "and", "are", "as", "at", "be", "but", "by",
"for", "if", "in", "into", "is", "it",
"no", "not", "of", "on", "or", "such",
"that", "the", "their", "then", "there", "these",
"they", "this", "to", "was", "will", "with"
);
final CharArraySet stopSet = new CharArraySet(stopWords, false);
ENGLISH_STOP_WORDS_SET = CharArraySet.unmodifiableSet(stopSet);
}
...
@Override
protected TokenStreamComponents createComponents(final String fieldName) {
final StandardTokenizer src = new StandardTokenizer();
src.setMaxTokenLength(maxTokenLength);
TokenStream tok = new StandardFilter(src);
tok = new LowerCaseFilter(tok);
tok = new StopFilter(tok, stopwords);
return new TokenStreamComponents(src, tok) {
@Override
protected void setReader(final Reader reader) {
// So that if maxTokenLength was changed, the change takes
// effect next time tokenStream is called:
src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);
super.setReader(reader);
}
};
}
1. StandardTokenizer()를 사용하여 Tokenize 처리
2. StandrardFilter()를 생성 (체인 생성을 위한 Filter - 다른작업은 하지 않음)
3. LowerCaseFilter()를 이용하여 소문자로 변경
4. StopFilter()통하여 불용어 제거
불용어는 클래스 최 상단에 정의되어 있습니다.
"My name is hong gil dong \n I'm a super hero \n Everyone likes me"
위 문구를 분석하면 아래와 같이 Tokenize 됩니다.
my | name | hong | gil | dong | i'm | super | hero | everyone | likes | me |
SimpleAnalyzer
/** An {@link Analyzer} that filters {@link LetterTokenizer}
* with {@link LowerCaseFilter}
**/
public final class SimpleAnalyzer extends Analyzer {
/**
* Creates a new {@link SimpleAnalyzer}
*/
public SimpleAnalyzer() {
}
@Override
protected TokenStreamComponents createComponents(final String fieldName) {
return new TokenStreamComponents(new LowerCaseTokenizer());
}
@Override
protected TokenStream normalize(String fieldName, TokenStream in) {
return new LowerCaseFilter(in);
}
}
분석결과
my | name | is | hong | gil | dong | i | m | a | super | hero | everyone | likes | me |
WhitespaceAnalyzer
/**
* An Analyzer that uses {@link WhitespaceTokenizer}.
**/
public final class WhitespaceAnalyzer extends Analyzer {
/**
* Creates a new {@link WhitespaceAnalyzer}
*/
public WhitespaceAnalyzer() {
}
@Override
protected TokenStreamComponents createComponents(final String fieldName) {
return new TokenStreamComponents(new WhitespaceTokenizer());
}
}
분석 결과
My | name | is | hong | gil | dong | I'm | a | super | hero | Everyone | likes | me |
StopAnalyzer
/**
* Filters {@link LetterTokenizer} with {@link LowerCaseFilter} and {@link StopFilter}.
*/
public final class StopAnalyzer extends StopwordAnalyzerBase {
...
/**
* Creates
* {@link org.apache.lucene.analysis.Analyzer.TokenStreamComponents}
* used to tokenize all the text in the provided {@link Reader}.
*
* @return {@link org.apache.lucene.analysis.Analyzer.TokenStreamComponents}
* built from a {@link LowerCaseTokenizer} filtered with
* {@link StopFilter}
*/
@Override
protected TokenStreamComponents createComponents(String fieldName) {
final Tokenizer source = new LowerCaseTokenizer();
return new TokenStreamComponents(source, new StopFilter(source, stopwords));
}
@Override
protected TokenStream normalize(String fieldName, TokenStream in) {
return new LowerCaseFilter(in);
}
}
createComponents() 내부에서 LowerCaseTokenizer()를 생성하고 StopFilter를 생성하여 TokenStreamComponents를 생성하는 코드를 볼 수 있습니다.
KeywordAnalyzer
**
* "Tokenizes" the entire stream as a single token. This is useful
* for data like zip codes, ids, and some product names.
*/
public final class KeywordAnalyzer extends Analyzer {
public KeywordAnalyzer() {
}
@Override
protected TokenStreamComponents createComponents(final String fieldName) {
return new TokenStreamComponents(new KeywordTokenizer());
}
}
'개발이야기 > Lucene & Solr' 카테고리의 다른 글
[Lucene] 루씬 indexing #5 - N-Gram, 한글 Analyzer (3/3) (0) | 2019.10.16 |
---|---|
[Lucene] 루씬 indexing #4 - Analyzer 동의어 검색(2/3) (0) | 2019.10.15 |
[Lucene] 루씬 Indexing #2 - DocValues란? (0) | 2019.10.11 |
[Lucene] 루씬 indexing #1 - 기본 (0) | 2019.10.10 |
[Lucene] 루씬 - Search and Highlighting 예제 (0) | 2019.10.05 |