따로 custom analyzer 만들어야 할 일이 생겨 그 과정을 남기고자 한다.
1. 환경 : java, elasticsearch-1.4.6, maven
analyzer 의 구성에는 루씬에서 작동을 따르므로, filter, analyzer, tokenizer 를 각 구현해줘야한다.
밑에 클래스가 index setting 에 적용되어 filter, analyzer, tokenizer를 플러그인에 바인딩한다.
그리고 해당되는 클래스 네임으로 각각 구현해주면 된다.
public class TestAnalysisBinderProcessor extends AnalysisModule.AnalysisBinderProcessor {
@Overrid
public void processAnalyzers(AnalyzersBindings analyzersBindings) {
analyzersBindings.processAnalyzer("test_analyzer", TestAnalyzerProvider.class);
}
@Override
public void processTokenizers(TokenizersBindings tokenizersBindings) {
tokenizersBindings.processTokenizer("test_tokenizer", TestTokenizerFactory.class);
}
@Override
public void processTokenFilters(TokenFiltersBindings tokenFiltersBindings) {
tokenFiltersBindings.processTokenFilter("test_filter", TestTokenFilterFactory.class);
}
}
TestAnalyzerProvider 클래스 구현. AbstractIndexAnalyzerProvider를 상속하고 메인 analyzer클래스를 선언한다.
public class TestAnalyzerProvider extends AbstractIndexAnalyzerProvider {
private final TestAnalyzer analyzer;
@Inject
public TestAnalyzerProvider(Index index, @IndexSettings Settings indexSettings, Environment env, @Assisted String name, @Assisted Settings settings) throws IOException {
super(index, indexSettings, name, settings);
analyzer = new TestAnalyzer(version);
@Override
public TestAnalyzer get() {
return this.analyzer;
}
}
filterfactory 클래스를 만들어 custom filter를 선언한다.
public class TestFilterFactory extends AbstractTokenFilterFactory {
@Inject
public TestFilterFactory(Index index, @IndexSettings Settings indexSettings, @Assisted String name, @Assisted Settings settings) {
super(index, indexSettings, name, settings);
}
@Override
public TokenStream create(TokenStream tokenStream) {
return new TestFilter(tokenStream);
}
}
custom_tokenizer를 선언하는 클래스를 만든다.
잠깐 설명하자면 루씬에서 tokenizer로 분리되어 나오는 것은 TestTokenizer에 incrementToken() 을 오버라이드 구현해서 token을 만들어
그 후 지정된 filter들이 또 다시 increamentToken 함수과정 돌면서 해당 filter 로직에 의해서 tokenstream 을 만들어 낸다.
public class TestTokenizerFactory extends AbstractTokenizerFactory {
@Inject
public TestTokenizerFactory(Index index, @IndexSettings Settings indexSettings, @Assisted String name, @Assisted Settings settings) {
super(index, indexSettings, name, settings);
}
@Override
public Tokenizer create(Reader reader) {
return new TestTokenizer(reader);
}
}
실제 메인으로 엘라스틱 바인딩프로세스에서 호출될 Analyzer 클래스를 만들어준다. TokenStreamComponents 메소드를 구현하여 토크나이져, 필터를 적용시켜 tokenStream을 만들게 된다.
기타 불용어처리를 하기 위해 이 경우 stopwordanalzyer를 기본으로 만든다.
그리고 아래에 사용되는 TestTokenizer, TestFilter 는 따로 구현을 해주어야 한다. 그것은 lucene analyzer구현과 같다.
그 lucene analyzer를 elasticsearch에 바인딩 시키는 것이다.
흐름 :
analyzer가 사용할 filter, tokenizer를 선언한다. 필터는 여러개가 들어갈 수 있다.(tokenStream의 데코레이터 패턴으로 보면된다.)
analyzer로 reader 타입으로 들어오게 되는데 이후 filter class들로 tokensteam형태로 들어가게 되고, 해당 tokenStream을 적절히(원하는 형태로) 처리한다.
public class TestAnalyzer extends StopwordAnalyzerBase {
private Version matchVersion;
public TestAnalyzer(Version version) throws IOException {
// stopword(불용어)를 줄 수 있다. 아래 resource에 파일로 보관했기 때문에 아래와 같이 넣어 준다.
super(version, loadStopwordSet(false, TestAnalyzer.class, "stopwords.txt", "#"));
this.matchVersion = version;
}
@Override
protected TokenStreamComponents createComponents(String fieldName,
Reader reader) {
final Tokenizer source = new TestTokenizer(reader);
TokenStream tok = new LowerCaseFilter(matchVersion, source);
tok = new TestFilter(tok);
tok = new StopFilter(matchVersion, tok, stopwords);
return new TokenStreamComponents(source, tok);
}
}
아래는 TestFilter 이다. tokenizer에서 분리되어진 단어들이 분리된 정보(position, type, offset)와 함께 들어가게 된다. 루씬의 TokenFilter클래스를 상속받아
increamentToken 메소드를 재정의 하면 된다.
increamentToken() 메소드를 돌면서 tokenizer로부터 만들어진 토큰들을 받아 처리한다. 즉 필터는 각 토큰들의 상태(offset, position, type)을 다시 만들어줄 수 있다.
public class TestFilter extends TokenFilter {
private TypeAttribute typeAtt = addAttribute(TypeAttribute.class); // 타입을 나타냄
private CharTermAttribute termAtt = addAttribute(CharTermAttribute.class); // 토큰의 텍스트 자체를 나타냄
private OffsetAttribute offsetAtt = addAttribute(OffsetAttribute.class); // 하이라이팅될 때 그 문자의 offset을 가리킴. es의 highlight쿼리하게 될 때 중요.
private PositionIncrementAttribute posIncrAtt = addAttribute(PositionIncrementAttribute.class); 문자의 거리. token의 index라고 보면됨
public TestFilter(TokenStream input) {
super(input);
}
@Override
public boolean incrementToken() throws IOException {
while(input.incrementToken()) {
termAtt.buffer(); 토큰 텍스트를 set해줌.
offsetAtt.setOffset(텍스트 시작위치, 텍스트 끝 위치 );
typeAtt.setType( 토큰 타입 );
posIncrAtt.setPositionIncrement(토큰 순서);
}
}
}
이클래스에서 위에서 만든 analyzer, filter, tokenizer들을 elasticesearch로 바인딩한다.
public class TestAnalyzerPlugin extends AbstractPlugin {
@Override
public String name() {
return "test-analyzer";
}
@Override
public String description() {
return "test analyzer support";
}
public void onModule(AnalysisModule module) {
module.addProcessor(new TestAnalysisBinderProcessor());
}
}
적용 방법 :
main/resource/es-plugin.properties 파일을 생성 후
plugin=org.elastic.com.TestAnalyzerPlugin
입력 후 저장
이것은 ES에서 따로 설치 과정을 거치지 않고 지정된 디렉토리에서 바로 플러그인을 인식하도록 한다.
mvn package 하여 패키징한다.
elastic plugins 디렉토리 밑에 name 함수에 선언한 "test-analyze" 라는 이름으로 디렉토리를 만들고
밑에 위 소스들을 패키징한 jar를 위치시킨다
인덱스 만들시에 setting 에 아래와 같이 추가해서 인덱스를 만들어준다.
{
"analysis":{
"analyzer":{
"test_analyzer":{
"type":"org.elastic.com.TestAnalyzerPlugin",
"tokenizer":"test_tokenizer",
"filter":["trim","test_filter","lowercase"]
}
}
}
자바의 경우
XContentBuilder indexSettings = XContentFactory.jsonBuilder();
indexSettings.startObject()
.startObject("analysis")
.startObject("analyzer")
.startObject("test_analyzer")
.field("type","com.elastic.plugin.TestAnalyzerProvider")
.field("tokenizer","test_tokenizer")
.field("filter",new String[]{"lowercase","trim","test_filter"})
.endObject()
.endObject()
.endObject()
.endObject();
CreateIndexRequestBuilder indexCreateRequest = client.admin().indices().prepareCreate("test_index_v1)
.setSettings(indexSettings);
indexCreateRequest.execute().actionGet();
토크나이져는 lucene기본으로 있는 whitespacetokenizer 혹은 다른 것들을 쓸 수 있다. 물론 구현체를 넣어서 사용가능(tokenizer 클래스 상속 후 increamentToken() 구현해야함)
정리를 하자면 input -> tokenizer -> tokenfilter -> tokenfilter -> tokenfilter (다수 토큰필터를 적용가능, 물론 코드상 처리순서대로 데이터에 적용됨.)
위의 흐름은 각 사이에 모두 tokenStream 형태로 교환되어 increamentToken()을 통해 문장에서 떨어진 각 단어?토큰 들에 접근이 가능하게 된다.
이런 것들을 이용하여 자체적인 비즈니스 로직이 더해진 색인어를 만들 수 있다.
예를 들면 "게임중에 음료수를 마신다." 를 whitespaceAnalyze 한 다음 형태소 분석을 통해 "게임중에"의 조사를 때고 "게임"으로 바꾼 다던지, 색인이 필요없는 것들은 불용어 처리를 하고,
용언등에 대한 처리를 더하는 등을 할 수 있다.