프로그래밍/ElasticSearch 2015. 5. 28. 14:33
Elasticsearch에서는 기본 analyzer 이외에도 따로 analyzer를 만들어서 쓸 수 있다. 
따로 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 한 다음 형태소 분석을 통해 "게임중에"의 조사를 때고 "게임"으로 바꾼 다던지, 색인이 필요없는 것들은 불용어 처리를 하고,
용언등에 대한 처리를 더하는 등을 할 수 있다.

참고 : http://jjeong.tistory.com/818


//