프로그래밍/ElasticSearch 2016. 8. 30. 08:52
Elasticsearch 의 Decay function 들을 사용해보고 그 과정들을 기록해보고자 한다.
Elasticsearch에서 검색을 하다보면 일반 수치 필드에 대한 scoring 이 필요할 때가 있다. 예를 들면 유투브의 VIEW 수에 따라 높을수록 High score 를 갖게 하는 것이다. 물론 sort 쿼리로 view수를 내림차순으로 하게 할 수도 있지만, 다른 필드와의 복합적인 연산이 어려워진다.
그래서 Elasticsearch 에서 제공하는 decay_function들을 사용할 필요가 생기게 된다. decay_function 을 이용하여 수치적(numerical) 필드에 대한 nomalized scoring을 할 수 있다.
Elastic공식 문서에서 보게 되면 본 목적은 지리적 거리(geographic) 혹은 수치적 데이터에서 검색하려는 수치 인근의 데이터를 높은 순위로 검색하게 하기 위함이다. 또한 0~1 까지로 nomalize score 를 내어주기 때문에 다른 쿼리들과 같이 사용하여 복합 score 를 만들어내기도 좋다. 아래는 그 예시이다.
function_score": {

"DECAY_FUNCTION": {
    "FIELD_NAME": {
          "origin": "11, 12",
          "scale": "2km",
          "offset": "0km",
          "decay": 0.33
    }
}


//
프로그래밍/ElasticSearch 2016. 2. 21. 21:20

얼마전 몇개의 도큐멘트를 갖고 검색환경을 만들었는데, 클러스터 노트의 수가 더 작았음에도 검색 속도는 더 빨랐다는 글을 어디선가 읽었다. 

검색데이터(인덱스의 크기)도 무시 할 수는 없지만, 분산환경에서 특히 무시할 수 없는 것이 커넥션(tcp)비용이다. 그리고 검색이라는 것은 sorting, scoring 에서 많은 파워를 필요하게 된다.

엘라스틱의 경우 쿼리를 받을 경우 두번의 작업을 하게 된다. 해당 키워드들을 찾아 도큐멘트주소(어디노드에 있는지, 어떤 세그멘트에 있는지) 갖고 온 후 각 노드들에 대해 다시 질의한다. 이렇게 두번의 커넥션을 갖게 되는데,  인덱스 도큐멘트가 작은 상태에서 샤드를 여러개로 세팅했다면 당연 모든 노드로 쿼리가 가야하기 때문에 한 샤드에서 가져올때 대비하여 속도에서 더 느린 경험이 될 것 같다.

//
프로그래밍/ElasticSearch 2015. 7. 19. 00:18

elasticsearch 에 analyzer plugin을 만들다보니 offsetAttribue 클래스를 사용하게 되었다

이부분이 잘못되게 되면 하이라이팅이 엉뚱하게 된다. 그래서 token stream에서 나온 토큰들을 짤라서 작업할 때는 

offsetAttribue의 setoffset 메소드를 이용해 offset을 확실히 해주어야 한다. 


//
프로그래밍/ElasticSearch 2015. 7. 15. 21:44
elasticsearch에 인덱스를 만들때 일반 json ES query 로 만들 수 있지만 여기서는 자바로 인덱스를 생성하는 방법을 기록한다. elasticsearch에서는 쉽게 json 하이어라키를 만들어 낼 수 있도록 XcontentBuilder 클래스를 제공한다. startObject로 시작하여 field라는 항목으로 하위 노드에 추가 할 수 있다.
XContentBuilder mappings = null; 
  try {
   mappings = XContentFactory.jsonBuilder();
   mappings.startObject()
    .startObject("test_index")
     .field("dynamic","false")  // dynamic mapping 을 거부한다.
     .startObject("_all").field("enabled",false).endObject()  //모든 필드를 찾지 않도록 한다.
     .startObject("properties")  // 인덱스 내부에 필드들은 properties 아래 만들어져야한다.
       .startObject("document_title")
          .field("type", "string")
          .startObject("norms").field("enabled", false).endObject()
          .field("store", "yes")  //  인덱스에 추가하는 것 뿐 아니라 저장하여 디스플레이 가능하도록 setting
          .field("index", "analyzed")  // 해당 필드 분석하도록 한다. 
          .field("index_analyzer", "name_analyzer")   // 인덱스시에 이용하는 analyzer를 명시할 수 있다.
          .field("search_analyzer", "name_analyzer")
       .endObject()
      .startObject("name_number").field("type", "long").field("store", "yes").endObject()
      .startObject("st_number").field("type", "long").field("store", "yes").endObject()
      .startObject("type_info")
       .field("type", "nested")  // nested 항목으로 만들 수 있다. 이렇게 되면 하나의 document로 만들어지는 듯.
       .startObject("properties")
        .startObject("type_name")
         .field("type", "string")
        .endObject()
        .startObject("type_value")
         .field("type", "double")
        .endObject()
       .endObject()
      .endObject()
     .endObject()
    .endObject()
   .endObject();
  } catch (IOException e) {
   e.printStackTrace();
  }
  
  XContentBuilder indexSettings = null; 
  try {
   indexSettings = XContentFactory.jsonBuilder(); 
   indexSettings.startObject()
       .field("index.number_of_shards", 3)   // 샤드 갯수 지정
       .field("index.number_of_replicas", 3)  // 레플리카 갯수 지정
//       .field("index.store.type", "memory")  // 메모리에 인덱스 데이터 저장 할 지 
       .endObject();
  } catch (IOException e) {
   e.printStackTrace();
  }
  
  CreateIndexRequestBuilder indexCreateRequest = client.admin().indices().prepareCreate(indexName)
    .addMapping("test_index", mappings)  // 메핑 json 데이터를 인덱스 네임파라미터와 함께 setting
    .setSettings(indexSettings);  // index setting 에 관한 정보는 setsettings라는 함수로 만듬
  try{
   indexCreateRequest.execute().actionGet();
  }catch(Exception e){
   e.printStackTrace();
  }
//
프로그래밍/ElasticSearch 2015. 7. 9. 10:02

maven 패키징 & 카피 후에

elastic_path/bin/plugin --url file:///absolute/path/to/elasticsearch-cardinality-plugin-0.0.1.jar --install plugin_name

주의점 : file:// 를 꼭 붙여줘야한다는 것. 그냥 file path 예시인줄...


다른방법.

plugins 밑에 디렉토리에 플러그인 네임과 같은 디렉토리를 만들고 jar를 위치시킨다.

그리고 ES를 재시작한다. 

//
프로그래밍/ElasticSearch 2015. 7. 9. 09:59

analyzer plugin을 수정하고 올리고 나서 몇번의 리스타트 후에 이런 에러를 만낫다.

마스터 노드의 로그상에서는 StringOutofIndex  에러만 나게 되는데 어디서 나는지에 대한 것은 찾아볼 수 없다.

그러다가 analyzer에서 token을 substring 하는 부분에서 테스트적으로 넣엇던 것이 실수로 머지 되어 패키징 했다는 것을 알고 수정.

결론 : elasticsearch의 로그가 생각보다 친절 하지 않다.

//
프로그래밍/ElasticSearch 2015. 6. 13. 10:28

루씬을 사용하는 ES는 루씬 스코어링 과정을 따른다. 따로 analyzer를 구현해서 사용하는 터라 특정 필드에 매칭되서 얼마의 scoring이 되는가를 확인하기 위해서 explain 을 사용할 수 있다. RDB의 explain과 비슷하게 scoring 절차, 결과를 볼 수 있다.


query 을 활용 한 curl 쿼리

curl -XGET 'localhost:9200/twitter/tweet/1/_explain' -d '{ "query" : { "term" : { "message" : "search" } } }'


query_string을 활용 한 curl 쿼리

curl -XGET 'localhost:9200/twitter/tweet/1/_explain?q=message:search'
"query_string": { "query": "content:this AND available:yes" }

이런식도 가능
"query_string": { "query": "content:this OR that" }


Json query

{ "explain": true, "query" : { "term" : { "user" : "kimchy" } }


결과 json

{ "matches" : true, "explanation" : { "value" : 0.15342641, "description" : "fieldWeight(message:search in 0), product of:", "details" : [ { "value" : 1.0, "description" : "tf(termFreq(message:search)=1)" }, { "value" : 0.30685282, "description" : "idf(docFreq=1, maxDocs=1)" }, { "value" : 0.5, "description" : "fieldNorm(field=message, doc=0)" } ] } }

//
프로그래밍/ElasticSearch 2015. 6. 13. 00:22

엘라스틱서치 분석기를 통해 쪼개지는 term 을 확인할 수 있다.  rest api 로 제공하는데 analyzer의 작동을 확인해볼수 있다.

analyzer 부분을 생략하면  es  기본 분석기가 사용된다. 플러그인타입으로 자체 개발한 analyzer를 테스트 중에

웹브라우저에서 바로 분석 결과를 확인할 수 있는 api  있길래..

analyzer 파라미터로 분석기이름을 넣어주고  text 값으로 테스트할 텍스트를 넣어준다.


http://host:9200/index_name/_analyze?analyzer=test_analyzer&text=오늘은 뭐해요


//
프로그래밍/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


//
프로그래밍/ElasticSearch 2015. 5. 28. 11:32

PUT /index_name { "settings" : { "number_of_shards" : 3, "number_of_replicas" : 1 } }



//