반응형

각 컴포넌트 설치

elasticsearch & logstash & filebeat 설치

설치는 간단하다. 자세한 설명은 하지 않겠다. elastic 공식 사이트로 접속하여 스테이블한 tar파일을 받아서 로컬의 적절한 경로에 압축 해제 하면 된다.


각 컴포넌트 구동 및 설정

elasticsearch 구동

결과적으로 elasticsearch에 데이터를 저장하므로 먼저 elasticsearch를 구동한다. 
가장 심플하게 설치디렉토리로 가서 아래의 커맨드로 구동한다.

./elasticsearch

ingestion with filebeat

filebeat는 파일의 변경을 수집하여 logstash로 전달하는 역할을 담당한다. 
가장 filebeat를 압축해제한 디렉토리를 보면 filebeat.yml 설정파일이 존재하는데 다음과 같은 내용을 추가해보자.

첫번째 크롤한 패스를 적절한 패스를 아래와 변경하자.

- input_type: log

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /daum/logs/sdb-admin-homo-new/*

다음은 멀티 라인 옵션을 변경하자. 멀티라인옵션이 없을 경우 new line 기준으로 아이템을 구분한다. 
아래의 예는 로그에서 [yyyy-mm-dd 형태로 시작할 경우 item을 구분하겠다는 의미이다.

  multiline.pattern: '^\[[0-9]{4}-[0-9]{2}-[0-9]{2}'
  multiline.negate: true
  multiline.match: after 

그리고 전송할 logstash의 주소를 입력한다.

#------------------------ Logstash output ---------------------------
output.logstash:
  # The Logstash hosts
  hosts: ["localhost:5043"]

마지막으로 아래와 같은 커맨드로 실행한다.

./filebeat -e -c filebeat.yml -d "publish"

아직 입력을 처리할 logstash가 설정되지 않아서 아래와 같은 로그가 발생할 것이다.

2017/08/25 05:11:42.786091 single.go:140: ERR Connecting error publishing events (retrying): dial tcp 127.0.0.1:5043: getsockopt: connection refused
2017/08/25 05:12:04.341011 metrics.go:34: INFO No non-zero metrics in the last 30s
2017/08/25 05:12:34.340861 metrics.go:34: INFO No non-zero metrics in the last 30s

processing with logstash

아래와 같이 가장 간단한 형태의 설정파일을 만들어보자. 예제에서는 이름을 first-pipeline.conf라고 하겠다. 
5043 포트로 filebeat의 송신을 listen하고 그 결과를 엘라스틱 서치에 전달한다.

input {
        beats {
                host => "localhost"
                port => "5043"
                tags => ["sdb-homo-admin-new"]
        }
}

output {
        elasticsearch {
                hosts => [ "localhost:9200" ]
        }
}

마지막으로 아래와 같은 커맨드로 logstash를 기동한다.

./logstash -f first-pipeline.conf --config.reload.automatic

logstash가 정상적으로 기동되면 filebeat에는 다음과 같은 로그가 보일 것이다.

2017/08/25 05:29:44.044839 sync.go:70: DBG  Events sent: 1148
2017/08/25 05:29:44.044969 client.go:214: DBG  Publish: {
  "@timestamp": "2017-08-25T05:02:34.358Z",
  "beat": {
    "hostname": "sonjeong-ug-ui-MacBook-Pro.local",
    "name": "sonjeong-ug-ui-MacBook-Pro.local",
    "version": "5.5.2"
  },
  "input_type": "log",
  "message": "[2017-08-25 09:54:17,950] [localhost-startStop-2] INFO  o.s.w.c.s.XmlWebApplicationContext - Closing WebApplicationContext for namespace 'admin-servlet-servlet': startup date [Thu Aug 24 18:36:38 KST 2017]; parent: Root WebApplicationContext",
  "offset": 501734,
  "source": "/daum/logs/sdb-admin-homo-new/undefined.log.log",
  "type": "log"
}
2017/08/25 05:29:44.044995 output.go:109: DBG  output worker: publish 1 events
2017/08/25 05:29:44.053219 sync.go:70: DBG  Events sent: 1
2017/08/25 05:29:44.053257 sync.go:70: DBG  Events sent: 1

search in elasticsearch

elasticsearch에 생성되는 인덱스명은 logstash-$DATE 형식이다. 아래와 같은 쿼리로 조회를 해보자.

curl "http://localhost:9200/logstash-2017.08.25/_search?pretty&q=response=200"


반응형
반응형

부모-자식 관계는 본질적으로 내포된 모델과 비슷하다. 그러나 차이가 있다면 내포된 오브젝트는 하나의 도큐먼트에 저장되는 반면, 부모-자식에서는 부모와 자식이 완전하게 다른 도큐먼트로 저장된다는 것이다.


부모-자식 기능은 하나의 도큐먼트 타입이 다른 것과 연관되게 해준다. 내포된 오브젝트와 비교한 이점은 다음과 같다.


자식 도큐먼트의 리인덱싱 없이 부모 도큐먼트를 업데이트 할 수 있다.

부모와 다른 자식의 영향 없이 자식 도큐먼트를 추가 변경 삭제할 수 있다. 이것은 특히 자식 도큐먼트들이 다수이고 자주 변경되고 추가될때 유용하다.

자식 도큐먼트는 검색의 결과로 리턴될 수 있다.

엘라스틱서치는 부모와 연관된 자식을 매핑을 유지하고 있어서 쿼리타임에 빠르게 조인할 수 있다. 그러나 이 경우 부모 자식 관계의 한계도 존재하는데 부모 도큐먼트와 자식 도큐먼트는 같은 샤드상에 존재해야 한다.


Parent-Child Mapping


부모-자식 관계를 설정하기 위해 자식 타입의 parent에 도큐먼트 타입을 명시해야 한다. 이것은 인덱스 생성 시점에 수행되거나 자식 타입이 생성되기 전에 update-mapping API가 수행되어야 한다.


예를들어 많은 도시의 지점을 가진 회사가 있다고 가정해보자. 우리는 각 지점에 근무하는 종업원들을 찾기를 원할 수 있다. 물론 부모-자식관계 대신에 application-side-joins이나 데이터 비정규화를 사용할 수 있다. 그러나 우리의 목적은 부모-자식관계를 이용하여 데모를 해보는 것이다.


엘라스틱서치에 종업원 타입에 부모가 지점 타입이라고 알려줘야 한다.


PUT /company

{

  "mappings": {

    "branch": {},

    "employee": {

      "_parent": { (1)

        "type": "branch" 

      }

    }

  }

}

(1) 종업원 타입의 도큐먼트는 지점 타입의 자식이다.


Indexing Parents and Children


부모 도큐먼트를 인덱싱하는 것은 다른 도큐먼트랑 차이가 없다. 부모는 그들의 자식에 대해서 알 필요가 없기 때문이다.


POST /company/branch/_bulk

{ "index": { "_id": "london" }}

{ "name": "London Westminster", "city": "London", "country": "UK" }

{ "index": { "_id": "liverpool" }}

{ "name": "Liverpool Central", "city": "Liverpool", "country": "UK" }

{ "index": { "_id": "paris" }}

{ "name": "Champs Élysées", "city": "Paris", "country": "France" }


자식 도큐먼트를 인덱싱할 때에서는 연관된 부모 도큐먼트의 ID를 반드시 명시해야 한다.


PUT /company/employee/1?parent=london 

{

  "name":  "Alice Smith",

  "dob":   "1970-10-24",

  "hobby": "hiking"

}



직원 도큐먼트는 런던 지점의 자식이다.


이들의 부모 ID를 제공하는 것은 두가지 목적이 있다. 하나는 부모와 자식간의 링크를 생성하는 것이고, 두번째는 부모와 자식을 같은 샤드에 저장하기 위함이다.


샤드의 도큐먼트를 탐색할때 우리는 엘라스틱서치에게 어떤 라우팅 값을 사용할지를 설명한다. 디폴트 값은 도큐먼트의 _id이며 이는 도큐먼트에 딸린 샤드를 판단하기 위해서이다. 라우팅 값은 단순한 수식으로 표현된다.


shard = hash(routing) % number_of_primary_shards


하지만 만약 부모ID를 명시했다면 본인의 id 대신에 부모ID를 라우팅 값으로 사용하게 된다. 이 말은 부모와 자식이 같은 라우팅 값을 사용하게 된다는 것을 의미하며 이것은 또한 같은 샤드에 값이 저장된다는 것을 의미한다.


부모 ID는 모든 싱글 도큐먼트 요청에 명시되어야 한다. 자식 도큐먼트에 조회하는 GET 요청 또는 자식 도큐먼트를 인덱싱하고, 업데이트하고 삭제할 때 항상 전달 되어야 한다. 그러나 검색 요청과 같이 모든 샤드에 적용되는 요청이 아닐 경우에는 요청은 도큐먼를 가지고 있는 하나의 샤드로 전송된다.


부모아이디도 물론 벌크 API를 사용할때도 지정할 수 있다.


POST /company/employee/_bulk

{ "index": { "_id": 2, "parent": "london" }}

{ "name": "Mark Thomas", "dob": "1982-05-16", "hobby": "diving" }

{ "index": { "_id": 3, "parent": "liverpool" }}

{ "name": "Barry Smith", "dob": "1979-04-01", "hobby": "hiking" }

{ "index": { "_id": 4, "parent": "paris" }}

{ "name": "Adrien Grand", "dob": "1987-05-11", "hobby": "horses" }


Warning


만약 자식 도큐먼트의 부모를 변경하기를 원한다면 자식 도큐먼트를 리인덱싱하거나 업데이트 하는 것으로는 불충분하다. 새로운 부모 도큐먼트가 다른 샤드에 저장되어 있을 수 있기 때문이다. 대신에 우리는 오래된 자식을 삭제하고 새로운 자식을 인덱싱해야 한다.


Finding Parents by Their Children


has_child 쿼리 및 필터는 자식 컨텐츠를 기반으로 부모 도큐먼트를 찾을 수 있게 해준다. 예를 들어 1980년 이후에 태어난 직원이 근무하는 지점을 찾기 위한 쿼리는 다음과 같다.


GET /company/branch/_search

{

  "query": {

    "has_child": {

      "type": "employee",

      "query": {

        "range": {

          "dob": {

            "gte": "1980-01-01"

          }

        }

      }

    }

  }

}


내포된 쿼리와 같이 has_child 쿼리는 몇몇의 자식 도큐먼트들과 매치과 될 수 있다. 그리고 각각의 다른 연관 스코어를 가진다. 하지만 이러한 스코어는 싱글 스코어로 감소하여하면 이는 부모 도큐먼트의 scrore_mode 파라미터에 의존한다. 디폴트 세팅은 none이고 이것은 자식 스코어를 무시하고 부모에게 1.0에 스코어를 부여한다. 그러나 avg, min, max 그리고 sum과 같은 세팅을 할 수 잇다.


다음 쿼리는 런던과 리버풀을 리턴한다. 그러나 런던은 더 좋은 스코어를 가지게 된다. 이유는 Alice Smith가 Barry Smith보다 더 잘 매치가 되기 때문이다.


GET /company/branch/_search

{

  "query": {

    "has_child": {

      "type":       "employee",

      "score_mode": "max",

      "query": {

        "match": {

          "name": "Alice Smith"

        }

      }

    }

  }

}


Tip

디폴트 스코어 모드가 확실리 다른 모드 보다 빠르다. 이유는 엘라스틱서치가 각 자식 도큐먼트에 대한 계산을 할 필요가 없기 때문이다. 다른 모드로 세팅한다면 스코어를 잘 관리해야 할 것 이다.


min_children and max_children


has_child 쿼리와 필터는 min_children과 max_children 파라미터를 적용할 수 있다. 특정한 범위에 해당하는 자식의 숫자에 해당하는 도큐먼트에 리턴한다.


이 쿼리는 적어도 두명의 직원이 근무하는 저점의 매치 될 것이다.


GET /company/branch/_search

{

  "query": {

    "has_child": {

      "type":         "employee",

      "min_children": 2, 

      "query": {

        "match_all": {}

      }

    }

  }

}



has_child Filter


has_child 필터는 has_child 쿼리와 동일하게 동작한다. 그러나 score_mode 파라미터를 적용할 수 없다. 이것은 오직 필터링을 위해서만 사용되기 때문이다.

has_child 필터의 결과는 캐싱되지 않는 반면 has_child 내의 필터에는 일반적은 캐싱 룰이 적용된다.


Finding Children by Their Parents


내포된 쿼리는 항상 하나의 루트 도큐먼트를 하나의 결과로 리턴하지만, 부모와 자식 관계의 도큐먼트의 독립적이며 각각 독립적으로 쿼리가 가능하다. has_child 쿼리는 그들의 자식을 기초로하여 부모는 찾는 쿼리가 가능했으며, has_parent 쿼리는 부모를 기반으로 자식 데이터를 리턴하는 것이 가능하다.


이것은 has_child 쿼리와 매우 유사하다. 다음 예제는 UK에 근무하는 직원을 리턴한다.


GET /company/employee/_search

{

  "query": {

    "has_parent": {

      "type": "branch", 

      "query": {

        "match": {

          "country": "UK"

        }

      }

    }

  }

}


has_parent 쿼리는 score_mode를 지원하는데 두가지 세팅만 할 수 있다. : none, socre. 각 자식은 하나의 부모만 가질 수 있으므로, 자식을 위해 다수의 스코어를 하나의 스코어로 만들 필요는 없다. 선택은 단순할 것이다.


has_parent Filter


has_parent 필터는 has_parent 쿼리와 동일하게 동작한다. 그러나 역시 score_mode를 지원하지 않는다. 단지 필터링을 위해서만 사용된다

has_parent 필터의 결과는 캐싱되지 않는 반면 has_parent 필터내의 필터에는 일반적인 캐싱룰이 적용된다.


Children Aggregation


부모-자식 관계에서는 Nested Aggregation에서 논의된 것 처럼 자식 어그리게이션이 지원된다. 부모 어그리게이션은 지원되지 않는다.


다음 예제는 각 나라의 우리 직원들이 어떻게 구성되지는 확인 할 수 있다.


GET /company/branch/_search

{

  "size" : 0,

  "aggs": {

    "country": {

      "terms": { 

        "field": "country"

      },

      "aggs": {

        "employees": {

          "children": { 

            "type": "employee"

          },

          "aggs": {

            "hobby": {

              "terms": { 

                "field": "employee.hobby"

              }

            }

          }

        }

      }

    }

  }

}


Grandparents and Grandchildren


부모-자식 관계는 한 세대를 넘어 확장 될 수 있다. 그리고 이러한 도큐먼트들은 모든 같은 샤드에 저장된다.


이제 지점 타입의 부모로 국가 타입을 만들어 보자.


PUT /company

{

  "mappings": {

    "country": {},

    "branch": {

      "_parent": {

        "type": "country" 

      }

    },

    "employee": {

      "_parent": {

        "type": "branch" 

      }

    }

  }

}


국가와 지점은 단순한 부모-자식 관계를 가지고 있다. 그래서 우리가 했던 똑같은 방식으로 부모와 자식을 인덱싱할 수 있다.


POST /company/country/_bulk

{ "index": { "_id": "uk" }}

{ "name": "UK" }

{ "index": { "_id": "france" }}

{ "name": "France" }

POST /company/branch/_bulk

{ "index": { "_id": "london", "parent": "uk" }}

{ "name": "London Westmintster" }

{ "index": { "_id": "liverpool", "parent": "uk" }}

{ "name": "Liverpool Central" }

{ "index": { "_id": "paris", "parent": "france" }}

{ "name": "Champs Élysées" }


부모 아이디는 각 지점 도큐먼트가 부모 국가 도큐먼트와 같은 샤드의 경로에 위치함을 보장해준다. 하지만 자식 손자에 똑같은 기술을 적용했을때 어떠한 일이 발생하는지 살펴보자.


PUT /company/employee/1?parent=london

{

  "name":  "Alice Smith",

  "dob":   "1970-10-24",

  "hobby": "hiking"

}



직원 도큐먼트의 샤드 경로는 부모 아이디 london에 의해 결정된다. 그러나 런던 도큐먼트의 그의 부모아이디인 UK에 의해서 결정된다. 결국엔 부모와 부모자식이 다른 샤드에 위치하는 일이 발생할 수 있으며 같은 샤드에 부모자식 매핑이 위치해야 된다는 것이 위반될 수 있다.


대신에 우리는 추가적인 라우팅 파라미터가 필요한다. 세 세대를 같은 샤드에 위치하기 위해선 다음과 같이 인덱스를 수행해야 한다.


PUT /company/employee/1?parent=london&routing=uk 

{

  "name":  "Alice Smith",

  "dob":   "1970-10-24",

  "hobby": "hiking"

}


라우팅이 값은 부모 값을 오버라이딩한다.


부모 파라미터는 각 부모의 도큐먼트 링크를 위해 사용되며, 라우팅 파라미터는 그의 부모 세대들과 같은 샤드에 위치하게 하는 것을 보장한다. 라우팅 값은 모든 싱글 도큐먼트 요청에 필요하다.


쿼리나 어그리게이션 연산에는 각 세대에 걸친 스텝이 필요하다. 예를들어 하이킹을 즐기는 지구언들이 근무하는 국가를 찾기 위해서는 우리는 국가와 지점, 지점과 직원간의 조인을 수행할 필요가 있다.


GET /company/country/_search

{

  "query": {

    "has_child": {

      "type": "branch",

      "query": {

        "has_child": {

          "type": "employee",

          "query": {

            "match": {

              "hobby": "hiking"

            }

          }

        }

      }

    }

  }

}


Practical Considerations


Parent-child 조인은 인덱스 타임 성능이 검색 타임 성능보다 중요할때 관계를 관리하기 위해 유용한 기술이다. 그러나 명백하게 비용이 많이 든다. 부모-자식 쿼리는 내포된 쿼리보다 5배에서 10배 정도 느리다.


Memory Use


부모 자식의 아이디 매핑은 메모리에 유지된다. 이것들은 도큐먼트의 값을 사용하는 대신에 매핑을 바꾸기 위한 계획이다. 모든 부모 도큐먼트 아이디를 자식 도큐먼트의 유지해야 하기 때문에 각 8 바이트의 메모리가 요구된다. 실제로는 압축이 되지만 이것은 러프하게 생각해본것이다.


우리는 얼마나 많은 메모리가 자식-부모 관계 캐쉬로 사용되는지를 확인할 수 있다. 이 때 indices-stats API 또는 node-stats API를 활용할 수 있다.


GET /_nodes/stats/indices/id_cache?human 


사람이 읽기 쉬운 형태로 노드에 의해 사용되고 있는 ID 캐쉬의 메모리 사용이 리턴된다.


Global Ordinals and Latency


부모-자식은 조인 속도를 높이기 위해 글로벌 서수를 사용한다. 부모-자식 맵이 인메모리에 있던 디스크에 있던 상관없이 글로벌 서수는 인덱스에 대한 어떤 변경에 의해 재구성될 필요가 있다.


샤드에 더 많은 부모가 있을 수록 더 많은 글로벌 서수가 빌드를 의해 사용된다. 부모-자식은 각 부모에 많은 지식이 있는 경우가 많은 부모에 적은 자식이 있는 경우보다 더 잘 맞는 상황이다.


기본적으로 글로벌 서수는 레이지 빌드 된다. 리프레쉬 후 첫번째 부모-자식 쿼리 또는 어그리게이션이 글로벌 서수를 빌드하는 트리거가 된다. 이것은 유저들에게 명확하게 지연되고 있다는 것을 느끼게 할 것이다. 우리는 eager_global_ordinals를 이용하여 변경 타임에 글로벌 서수를 빌드하는 비용을 사용하게 할 수 있다.


PUT /company

{

  "mappings": {

    "branch": {},

    "employee": {

      "_parent": {

        "type": "branch",

        "fielddata": {

          "loading": "eager_global_ordinals" 

        }

      }

    }

  }

}


_parent 필드를 위한 글로벌 서수는 새로운 세그먼트가 검색에 노출되기 전에 빌드될 것이다.


많은 부모가 있다면 글로벌 서수는 빌드하는데 수초를 소요한다. 이 경우 refresh_interval을 지정할 수 있는데 이것을 지정하면 리프레쉬 횟수를 줄어들게 할 수 있다. 이는 매초 글로벌 서수를 리빌딩하는데 드는 비용을 확실하게 감소시킬수 있다.


Multigenerations and Concluding Thoughts


다수 세대에 의한 조인은 매력적이게 들릴지 모르나 다음과 같은 비용이 수반된다.


더 많이 조인할수록 성능은 나빠질 것이다.

각 세대는 부모의 _id 필드를 저장하게 됨으로 많은 수의 메모리를 사용하게 된다.


관계 스키마를 부모-자식 관계를 사용하기로 했다면 다음과 같은 사항들을 고려해야 한다.


부모-자식 관계를 절약해서 사용하라. 부모가 많은 자식을 가질 경우에만 사용하라.

하나의 쿼리에서 다수의 부모-자식 관계의 조인을 피하라.

has_child 필터를 사용할 때 정렬하지 말고, has_child 관계를 사용할때 score_mode를 none으로 세팅하라.

부모 ID를 짧게 유지하라. 메모리를 절약할 수 있을 것이다.

부모-자식 관계를 사용하기 전에 항상 먼저 다른 관계를 사용하는 것을 고려하라.


반응형
반응형

엘라스틱서치에서 하나의 도큐먼트에 대한 생성, 삭제, 업데이트는 atomic하다. 이 의미는 같은 도큐먼트내에 저장된 엔티티에도 동일하다. 예를 들어 우리는 블로그포스트와 그의 코멘트를 하나의 도큐먼트로 저장할 수 있다. 


PUT /my_index/blogpost/1

{

  "title": "Nest eggs",

  "body":  "Making your money work...",

  "tags":  [ "cash", "shares" ],

  "comments": [ 

    {

      "name":    "John Smith",

      "comment": "Great article",

      "age":     28,

      "stars":   4,

      "date":    "2014-09-01"

    },

    {

      "name":    "Alice White",

      "comment": "More like this please",

      "age":     31,

      "stars":   5,

      "date":    "2014-10-22"

    }

  ]

}


만약 다이나믹 매핑을 이용한다면 코멘트 필드는 오브젝트 필드로서 자동생성될 수 있다.


모든 내용이 같은 도큐먼트에 있기 떄문에 쿼리타임에 블로그 포스트와 코멘트를 조인할 필요가 없다. 그러므로 성능이 향상될 것이다.


문제는 도큐먼트가 다음과 같은 쿼리에 매치될 수 있다.


GET /_search

{

  "query": {

    "bool": {

      "must": [

        { "match": { "name": "Alice" }},

        { "match": { "age":  28      }} 

      ]

    }

  }

}



Alice는 31 이다 28이 아니다.


이유는 cross-object 매칭 때문인데 내부 오프젝트의 배열을 논할때 아래와 같은 단순한 키-값 형식으로 인덱싱을 하기 때문이다.


{

  "title":            [ eggs, nest ],

  "body":             [ making, money, work, your ],

  "tags":             [ cash, shares ],

  "comments.name":    [ alice, john, smith, white ],

  "comments.comment": [ article, great, like, more, please, this ],

  "comments.age":     [ 28, 31 ],

  "comments.stars":   [ 4, 5 ],

  "comments.date":    [ 2014-09-01, 2014-10-22 ]

}


Alice와 31, John과 2014-09-01의 일치는 잃어버리기 때문이다. 타입 오브젝트의 필드는 검색의 관점에서 하나의 오브젝트로 저장되는게 유용하기 때문이다. 


이러한 문제를 풀기 위해 nested objects가 디자인되었다. 각 내포된 오브젝트는 보이지않는 분리된 도큐먼트로 인덱스되는데 다음과 같다.


{ (1)

  "comments.name":    [ john, smith ],

  "comments.comment": [ article, great ],

  "comments.age":     [ 28 ],

  "comments.stars":   [ 4 ],

  "comments.date":    [ 2014-09-01 ]

}

{ (2)

  "comments.name":    [ alice, white ],

  "comments.comment": [ like, more, please, this ],

  "comments.age":     [ 31 ],

  "comments.stars":   [ 5 ],

  "comments.date":    [ 2014-10-22 ]

}

{ (3)

  "title":            [ eggs, nest ],

  "body":             [ making, money, work, your ],

  "tags":             [ cash, shares ]

}


(1) First nested object

(2) Second nested object

(3) The root or parent document


각각의 내포된 오브젝트들은 분리하여 인덱싱됨으로 오브젝트내에 필드들은 그들의 관계를 유지하게 된다. 우리는 같은 내포된 오브젝트내에서 매치가 발생하는지 쿼리를 수행할 수 있다.


이 뿐만 아니라 내포된 오브젝트들은 인덱싱되었으므로, 쿼리 타임에 빠르게 루트 도큐먼트와 조인할 수 있다.


이러한 추가적인 내포된 오브젝트들은 숨겨져있다. 이는 직접 접근할 수 없다는 것이다. 업데이트, 추가 삭제를 하기 위해서는 전체 도큐먼트를 다시 인덱스해야 한다. 이는 또한 중요한 점이 내포된 오브젝트 만으로 검색 결과로 리턴될 수 없다.


Nested Objects Mapping


내포된 필드를 세팅하는 것은 단순하다. 


PUT /my_index

{

  "mappings": {

    "blogpost": {

      "properties": {

        "comments": {

          "type": "nested", 

          "properties": {

            "name":    { "type": "string"  },

            "comment": { "type": "string"  },

            "age":     { "type": "short"   },

            "stars":   { "type": "short"   },

            "date":    { "type": "date"    }

          }

        }

      }

    }

  }

}


내포된 필드는 타입 오브젝트의 필드로서 같은 파라미터를 받아들인다.


이것으로 충분하다. 이제 어떤 코멘트 오브젝트도 분리된 내포 오브젝트로 인덱싱된다.


Querying a Nested Object


내포된 오브젝트들은 분리된 히든 도큐먼트로 인덱싱되기 때문에 그들에 직접 쿼리르 할 수 없다. 대신에 그들을 접근하기 위해서는 nested 쿼리를 이용해야 한다.


GET /my_index/blogpost/_search

{

  "query": {

    "bool": {

      "must": [

        { "match": { "title": "eggs" }}, (1)

        {

          "nested": {

            "path": "comments", (2)

            "query": {

              "bool": {

                "must": [ 

                  { "match": { "comments.name": "john" }},(3)

                  { "match": { "comments.age":  28     }}

                ]

        }}}}

      ]

}}}



(1) 타이틀 절은 루트 도큐먼트에 대해 수행된다.

(2) nested 절은 내포된 코멘트 필드로 스텝 다운된다. 더이상 루트 도큐먼트에 접근하지 않고 어떠한 내포된 도큐먼트의 필드에 접근한다. .

(3) comments.name와 comments.age 절은 동일한 내포 도큐먼트에 수행된다. 


Tip

내포 필드는 다른 내포 필드를 포함할 수 있다. 비슷하게 내포 쿼리는 내포 쿼리를 포함할 수 있다.


물론 내포 쿼리는 몇몇의 내포 도큐먼트와 매치될 수 있다. 각 매칭된 내포 도큐먼트는 자신의 연관된 스코어를 가지나 이러한 다수의 스코는 하나의 스코어로 감소될 필요가 있다. 


기본적으로 내포된 도큐먼트의 매칭 스코어는 평균이다. 이것은 score_mode의 파라미터로 조정될수 있는데 . avg, max, sum, none이 그것이다.


GET /my_index/blogpost/_search

{

  "query": {

    "bool": {

      "must": [

        { "match": { "title": "eggs" }},

        {

          "nested": {

            "path":       "comments",

            "score_mode": "max", 

            "query": {

              "bool": {

                "must": [

                  { "match": { "comments.name": "john" }},

                  { "match": { "comments.age":  28     }}

                ]

        }}}}

      ]

}}}


가장 잘 매칭된 내포 오브젝트로부터 _score를 루트 오브젝트에 준다.


내포된 필터에 의한 결과 자신은 캐쉬되지 않지만 일반적은 캐싱 룰에 의해 내포 필터 내부에 필터에 의해 적용된다.


Sorting by Nested Fields


내포된 필드의 값으로 정렬하는 것이 가능하다. 결과를 더욱 재밌게 만들어보자. 


PUT /my_index/blogpost/2

{

  "title": "Investment secrets",

  "body":  "What they don't tell you ...",

  "tags":  [ "shares", "equities" ],

  "comments": [

    {

      "name":    "Mary Brown",

      "comment": "Lies, lies, lies",

      "age":     42,

      "stars":   1,

      "date":    "2014-10-18"

    },

    {

      "name":    "John Smith",

      "comment": "You're making it up!",

      "age":     28,

      "stars":   2,

      "date":    "2014-10-16"

    }

  ]

}


우리는 10월에 받은 코멘트를 가진 블로그를 검색하기를 원한다. star 갯수가 작은순으로 각 블로그 포스트에 정렬되기를 원한다. 요청은 다음과 같을 것이다.


GET /_search

{

  "query": {

    "nested": { (1)

      "path": "comments",

      "filter": {

        "range": {

          "comments.date": {

            "gte": "2014-10-01",

            "lt":  "2014-11-01"

          }

        }

      }

    }

  },

  "sort": {

    "comments.stars": { (2)

      "order": "asc",   (3)

      "mode":  "min",  (4)

      "nested_filter": { (5)

        "range": {

          "comments.date": {

            "gte": "2014-10-01",

            "lt":  "2014-11-01"

          }

        }

      }

    }

  }

}


(1) 내포된 쿼리는 블로그 포스트의 조회 결과를 10월에 코멘트를 받은 것으로 한정한다.

(2) 결과는 comments.stars 필드의 가장 작은 값으로 오름차순 정렬된다.

(3) 소트 절의 내포된 필터는 메인 쿼리 절의 필터와 동일하다.


왜 우리는 내포된 필터의 쿼리 조건을 반복해야 될까? 이유는 쿼리가 수행된 이후에 정렬이 발생하기 때문이다. 10월에 코멘트를 받은 블로그 포스트가 매치되고, 결과로 블로그 포스트 도큐먼트가 리턴된다. 만약 내포 필터절을 포함하지 않았다면 블로그 포스트가 받은 전체 코멘트에 대한 정렬이 발생하게 될 것이다.


Nested Aggregations


같은 방식으로 우리는 서치 타임에 내포된 오브젝트에 접근하기 위해 특별한 내포 쿼리를 사용할 필요가 있다.


GET /my_index/blogpost/_search

{

  "size" : 0,

  "aggs": {

    "comments": { (1)

      "nested": {

        "path": "comments"

      },

      "aggs": {

        "by_month": {

          "date_histogram": { (2)

            "field":    "comments.date",

            "interval": "month",

            "format":   "yyyy-MM"

          },

          "aggs": {

            "avg_stars": {

              "avg": { (3)

                "field": "comments.stars"

              }

            }

          }

        }

      }

    }

  }

}


(1) 내포된 어그리게이션을 내포된 코멘트 오브젝트로 스텝다운 한다.

(2) comments.date 필드에 기초한 달에 따라 코멘트 버킷에 담는다.

(3) stars 평균값을 계산하여 각 버킷에 담늗다.


결과는 다음과 같은 어그리게이션으로 나타난다.


...

"aggregations": {

  "comments": {

     "doc_count": 4, 

     "by_month": {

        "buckets": [

           {

              "key_as_string": "2014-09",

              "key": 1409529600000,

              "doc_count": 1, 

              "avg_stars": {

                 "value": 4

              }

           },

           {

              "key_as_string": "2014-10",

              "key": 1412121600000,

              "doc_count": 3, 

              "avg_stars": {

                 "value": 2.6666666666666665

              }

           }

        ]

     }

  }

}

...

  

네개의 코멘트가 있으며 하나는 9월 세개는 10월이다.


reverse_nested Aggregation


내포된 어그리게이션은 내포된 도큐먼트의 하나의 필드에만 접근할 수 있다. 이는 루트 도큐먼트나 다른 내포된 도큐먼트의 필드를 볼수 없다는 것이다. 하지만 reverse_nested 어그리게이션을 통해 부모 스코프로 돌아갈 수 있다.


예를 들어 코멘트 나이에 기초한 코멘트의 태그의 관심을 보인다고 가정해보자. comment.age는 내포된 필드인 반면 tags는 루트 도큐먼트에 있다.


GET /my_index/blogpost/_search

{

  "size" : 0,

  "aggs": {

    "comments": {

      "nested": { (1)

        "path": "comments"

      },

      "aggs": {

        "age_group": {

          "histogram": { (2) 

            "field":    "comments.age",

            "interval": 10

          },

          "aggs": {

            "blogposts": {

              "reverse_nested": {}, (3)

              "aggs": {

                "tags": {

                  "terms": { (4)

                    "field": "tags"

                  }

                }

              }

            }

          }

        }

      }

    }

  }

}


(1) 내포된 agg은 코멘트 오브젝트로 스텝 다운한다.

(2) 히스토그램 agg은 comments.age 필드를 그룹핑하고 각 버킷은 10 years이다.

(3) reverse_nested agg는 루트 도큐먼트로 스탭 백한다.

(4) terms agg는 단어들이 나타난 객수로 코멘트를 카운트 한다.


연관된 결과는 다음과 같이 나타난다.


..

"aggregations": {

  "comments": {

     "doc_count": 4, (1)

     "age_group": {

        "buckets": [

           {

              "key": 20, (2)

              "doc_count": 2, 

              "blogposts": {

                 "doc_count": 2, (3)

                 "tags": {

                    "doc_count_error_upper_bound": 0,

                    "buckets": [ (4)

                       { "key": "shares",   "doc_count": 2 },

                       { "key": "cash",     "doc_count": 1 },

                       { "key": "equities", "doc_count": 1 }

                    ]

                 }

              }

           },

...



(1) 4개의 코멘트가 있다. 

(2) 두 개의 코멘트는 20대가 쓴 것이다.

(3) 두 블로그 포스트가 이러한 코멘트에 연관되어 있다.

(4) 이러한 블로그 포스트의 인기 태그는 shares, cash, equlities이다.


When to Use Nested Objects


내포된 오브젝트는 하나의 메인 오브젝트와 적당한 수의 연관된 중요한 엔티티가 있을때 유용한다. 블로그 포스트에 적당한 코멘트에 있을 경우가 그렇다.


내포된 모델의 단점은 다음과 같다.


내포된 도큐먼트를 추가, 변경, 삭제하기 위해서는 전체 도큐먼트를 다시 인덱싱해야 한다. 

검색결과는 전체 도큐먼트가 리턴되며 내포된 도큐먼트만 매칭할수는 없다.

때때로 메인 도큐먼트와 연관된 엔티티를 완전히 분리할 필요가 있는데 이러한 분리를 위해 parent-child 관계가 제공된다.




반응형
반응형

엘라스틱서치는 다른 종류의 종이다. 특히 당신이 SQL의 세상으로부터 왔다면. 엘라스틱서치는 많은 이점이 있다. : 성능, 확장성, NRT 서치, 대용량 데이터 분석 등. 또한 쉽게 이러한 일들을 수행할 수 있다. 


그러나 마법같이 모든 것을 해결해줄수는 없다. 이에 우리는 어떻게 동작하고 당신의 요구사항을 어떻게 동작하게 만들수 있는지를 이해해야 한다.


엔티티간의 관계를 처리하는 것은 관계를 지정할 수 있는 스토어처럼 명백하진 않다. 데이터 정규화를 엘라스틱서치에 적용할 수 없다. Handling Relationships, Nested Objects 그리고  Parent-Child Relationship을 통해 우리는 가능한 접근법들의 특성과 제약에 대해서 알아볼 것이다. 


Handling Relationships


실제 세상에서 관계란 : 블로그 포스트는 코멘트를 가지고, 은행 계좌는 거래를 포함하고, 고객은 은행 계좌를 가지고, 디렉토리는 파일과 서브 디렉토리를 가진다.


관계형 데이터베이스는 이러한 관계를 명확하게 관리하기 위해 설계되었다.

  • 각 엔티티는 주키에 의해 식별할 수 있다.
  • 엔티티는 정규화된다. 유니크한 엔티티는 하나만 저장되고 그들의 저장된 주키로 엔티티의 관계를 연결한다. 엔티티 데이터의 변경은 한 곳에서만 일어난다.
  • 엔티티는 쿼리시 조인될 수 있다.
  • 하나의 엔티티의 변경은 ACID하다.
  • 대부분의 관계형 데이터베이스는 다수 엔티티의 ACID 트랜잭션을 지원한다.


각 관계형 데이터베이스는 한계점을 가지고 있다. 그리고 그들은 full-text 서치를 지원하지 않는다. 쿼리 타임에 엔티티를 조인하는 것은 비용이 매우 비싸다. 엔티티간 조인을 수행하는 것은 일반적으로 하드웨어 간에 수행된다면 더욱 비싸지면 이러한 이유로 데이터를 하나의 서버에 저장한다.


엘라스틱서치는 대부분의 NoSQL 데이터베이스와 마찬가지로 실데이터를 flat하게 처리한다. 인덱스는 독립적인 도큐먼트의 플랫한 컬렉션이다. 하나의 도큐먼트는 서치 요청에 대응되는 모든 정보를 포함해야 한다


하나의 도큐먼트의 변경에 대해서 ACID 하지만 다수의 도큐먼트에 대해서는 그렇지 않다. 이렇기 때문에 트랜잭션 실패가 발생했을 경우 전체를 롤백할 수 없으므로, 부분 실패가 발생할 수 있다.


이러한 FlatWorld는 다음과 같은 이전이 있다.

  • 인덱싱은 빠르고 락에서 자유롭다.
  • 서칭은 빠르고 락에서 자유롭다.
  • 대용량 데이터를 다수의 노드에 분산시킬수 있다. 각 도큐먼트는 독립적이기 때문이다.


그러나 관계 측면에서 문제가 발생할 수 있는데 엘라스틱 서치에서 관계형 데이터를 관리하는 방법은 다음과 같다.

  • Application-side joins
  • Data denormalization
  • Nested objects
  • Parent/child relationships


종종 최종 해결책은 이러한 기술을 섞어서 사용하는 것이다.


Application-side joins


우리는 조인을 구현함으로써 관계형 데이터베이스의 조인을 에뮬레이트할 수 있다. 예를 들어 우리가 user와 blog post를 인덱싱하고 있다가 가정하다. 실제 예를 보면 다음과 같을 것이다.


PUT /my_index/user/1 

{

  "name":     "John Smith",

  "email":    "john@smith.com",

  "dob":      "1970/10/24"

}


PUT /my_index/blogpost/2 

{

  "title":    "Relationships",

  "body":     "It's complicated...",

  "user":     1 

}


인덱스, 타입 및  각 도큐먼트는 주키에 대한 함수로서 각 도큐먼트에 저장된다.


블로그 포스트는 저장된 user id에 의해 user와 연결된다. 인덱스와 타입은 어플리케이션에서 하드코딩될 필요가 없다.


ID가 1인 블로그 포스트를 찾는 일은 쉽다.


GET /my_index/blogpost/_search

{

  "query": {

    "filtered": {

      "filter": {

        "term": { "user": 1 }

      }

    }

  }

}


John이라고 불리는 유저의 블로그 포스트를 찾기 위해선 두가지 쿼리을 수행해야 한다. 첫번째는 존이라고 불리는 유저를 찾는 것이고, 두번째는 그들의 ID를 추출해 해당하는 블로그포스트를 찾는 것이다.


GET /my_index/user/_search

{

  "query": {

    "match": {

      "name": "John"

    }

  }

}


GET /my_index/blogpost/_search

{

  "query": {

    "filtered": {

      "filter": {

        "terms": { "user": [1] }  

      }

    }

  }

}


어플리케이션 사이드 조인의 주 이점은 데이터가 정규화 될 수 있다는 것이다. 한쪽에서 유저의 이름을 변경하면 된다는 것이다. 반면 안좋은 점은 서치 타임에 도큐먼트를 조인하기 위해 추가적인 쿼리를 발생해야 한다는 점이다.


이 예제는 하나의 유저가 첫번째 쿼리에 매치되었지만 실제 세계에서는 쉽게 존이라는 수백만의 유저가 나타날 수 있다. 이럴 경우 두번째 쿼리는 수백만의 단어를 찾게 될 수 있다.


이러한 접근법은 첫번째 엔티티가 작은 수의 도큐먼트를 가지고 있고, 되도록이면 변경되지 않을떄 적합하다. 이럴 경우 어플리케이션에서 결과를 캐싱하여 첫번째 쿼리가 자주 실행되는 것을 피할 수 있다.


Denormalizing Your Data


엘라스틱 서치에서 최고의 성능을 내는 방법은 인덱스 타입에 데이터를 비정규화하는 것이다. 각 데이터의 중복을 허용하는 함으로써 조인할 필요가 없게 된다.


만약 글쓴이의 이름으로 블로그 포스트를 찾기를 원한다면 블로그 포스트 도큐먼트에 글쓴이의 이름을 포함시키는 것이다.


PUT /my_index/user/1

{

  "name":     "John Smith",

  "email":    "john@smith.com",

  "dob":      "1970/10/24"

}


PUT /my_index/blogpost/2

{

  "title":    "Relationships",

  "body":     "It's complicated...",

  "user":     {

    "id":       1,

    "name":     "John Smith" 

  }

}


유저 데이터의 일부분은 비정규화되어 블로그포스트 도큐먼트에 포함되었다.


이제 우리는 한번의 쿼리에서 John이라고 불리는 유저의 블로그 포스트를 찾을 수 있다.


GET /my_index/blogpost/_search

{

  "query": {

    "bool": {

      "must": [

        { "match": { "title":     "relationships" }},

        { "match": { "user.name": "John"          }}

      ]

    }

  }

}


데이터 비정규화의 이점은 속독이다. 각 도큐먼트가 쿼리에 필요한 정보를 모두 포함하기 때문에 비싼 조인을 수행할 필요가 없다.


Field Collapsing


공통적인 요구사항중 하나는 특정한 필드로 그룹핑하여 서치 결과를 표현하는 것이다. 우리는 user명으로 그룹핑된 블로그 포스트를 리턴받기를 원할 수 있다. 이름으로 그룹핑한다는 것은 단어를 aggregation할 필요가 있다는 것을 내포한다. 유저의 전체 이름을 그룹핑하기 위해서 이름 필드는 not_analyzed 형식이여야 하며 자세한 내용은 Aggregations and Analysis를 살펴보자.


PUT /my_index/_mapping/blogpost

{

  "properties": {

    "user": {

      "properties": {

        "name": { 

          "type": "string",

          "fields": {

            "raw": { 

              "type":  "string",

              "index": "not_analyzed"

            }

          }

        }

      }

    }

  }

}


user.name 필드는 full-text 서치를 위해 사용될 수 있다.


The user.name.raw 필드는 terms aggregation을 위해 사용될 것이다.


몇가지 데이터를 추가해보자.


PUT /my_index/user/1

{

  "name": "John Smith",

  "email": "john@smith.com",

  "dob": "1970/10/24"

}


PUT /my_index/blogpost/2

{

  "title": "Relationships",

  "body": "It's complicated...",

  "user": {

    "id": 1,

    "name": "John Smith"

  }

}


PUT /my_index/user/3

{

  "name": "Alice John",

  "email": "alice@john.com",

  "dob": "1979/01/04"

}


PUT /my_index/blogpost/4

{

  "title": "Relationships are cool",

  "body": "It's not complicated at all...",

  "user": {

    "id": 3,

    "name": "Alice John"

  }

}


이제 존이라고 불리는 유저와 관계된 블로그 포스트를 찾고 유저 이름에 따른 결과를 그룹핑해보자.


GET /my_index/blogpost/_search

{

  "size" : 0, 

  "query": { 

    "bool": {

      "must": [

        { "match": { "title":     "relationships" }},

        { "match": { "user.name": "John"          }}

      ]

    }

  },

  "aggs": {

    "users": {

      "terms": {

        "field":   "user.name.raw",      

        "order": { "top_score": "desc" } 

      },

      "aggs": {

        "top_score": { "max":      { "script":  "_score"           }}, 

        "blogposts": { "top_hits": { "_source": "title", "size": 5 }}  

      }

    }

  }

}


블로그 포스트 어그리개이션이 반환되는 우리가 관심 있는 블로그 포스트는 size를 0로 세팅함으로써 일반적인 서치 히트를 비활성화 할 수 있다.


쿼리는 존이라는 이름을 가진 유저와 관계가 있는 블로그 포스트를 리턴한다.


terms aggregation은 user.name.raw라는 값을 위한 bucket를 생성한다.


top_score aggregation읜 순서는 각 버켓의 top-scoring document의 유저 어그리개이션의 단어 순서이다.


top_hits aggregation은 각 유저의 다섯개의 가장 관계 있는 블로그 포스트의 title 필드가 리턴한다.


요약된 결과는 아래와 같다.


...

"hits": {

  "total":     2,

  "max_score": 0,

  "hits":      [] 

},

"aggregations": {

  "users": {

     "buckets": [

        {

           "key":       "John Smith", 

           "doc_count": 1,

           "blogposts": {

              "hits": { 

                 "total":     1,

                 "max_score": 0.35258877,

                 "hits": [

                    {

                       "_index": "my_index",

                       "_type":  "blogpost",

                       "_id":    "2",

                       "_score": 0.35258877,

                       "_source": {

                          "title": "Relationships"

                       }

                    }

                 ]

              }

           },

           "top_score": { 

              "value": 0.3525887727737427

           }

        },

...


hits 배열은 비어있다. 이유는 size 0으로 세팅했기 때문이다. 


각 유저를 위한 top results 버켓이 있다.


각 유저의 blogspots.hits 배열은 유저를 위한 top results를 포함한다.


유저 버킷은 가장 연관된 블로그 포스트에 의해 정렬된다. 


top_hits aggregation의 사용은 가장 관계있는 블로그 포스트의 유저명을 리턴받는 쿼리를 수행하는 것과 같으며, 이는 각 유저를 위해 그들의 베스트 블로그 포스트를 가지고오기 위한 쿼리를 수행하는 것과 같다. 


각 버킷의 top hits 결과는 원래 메인 쿼리의 기반한 작은 가볍고 작은 쿼리를 수행 결과 이다. 이러한 기능은 페이징이나 하이라이팅에 유용하다.


Denormalization and Concurrency


물론 데이터 비정규화는 약점을 가지고 있다. 첫번째 약점은 인덱스 자체의 용량이 커진다는 것이다. 그러나 이것은 큰 문제는 아니다. 디스크에 데이터는 압축될 것이며, 디스크 용량은 싸다. 엘라스틱서치는 추가적인 데이터에 잘 대처한다.


더 중요한 이슈는 유저의 이름이 변경되었을때, 그의 블로그 포스트를 모두 업데이트해야 된다는 점이다. 다행이도 이름은 잘 바뀌지 않는다. 만약 변경된다면 수천의 블로그 포스트가 변경되어야 하지만 이것 또한 bulk API로 1초내에 수행할 수 있다. 


하지만 더 복잡한 시나리오를 고려해보자. 여기서는 변경은 일반적이며, 멀리 떨어져 있으며, 더욱 중요하고, 동시에 수행된다.


이런 예제를 위해 엘라스틱 서치에서 파일시스템의 디렉토리 트리를 에뮬레이트해보겠다.


우리가 특정 디렉토리에 파일들을 조회 한다면 다음과 같이 수행될 될것이다.


grep "some text" /clinton/projects/elasticsearch/*


이러한 요구사항에 맞는 정보를 등록해보자.


PUT /fs/file/1

{

  "name":     "README.txt", 

  "path":     "/clinton/projects/elasticsearch", 

  "contents": "Starting a new Elasticsearch project is easy..."

}


  • 파일명
  • 파일을 저장하고 있는 디렉토리의 전체 경로

우리는 또한 특정 디렉토리 하위에 디렉토리에 파일이 존재하는지를 검사하기도 원한다.


grep -r "some text" /clinton


이것을 지원하기 위해 path 계층구조를 인덱스할 필요도 있다. 


/clinton

/clinton/projects

/clinton/projects/elasticsearch


이러한 hierarchy를 주어진 path 필드로 자동적으로 생성될 수 있는데 이는 아래와 같다.


PUT /fs

{

  "settings": {

    "analysis": {

      "analyzer": {

        "paths": { 

          "tokenizer": "path_hierarchy"

        }

      }

    }

  }

}


커스텀 패스 analyzer는 기존 세팅으로 path_hierarchy tokenizer를 사용한다. 


파일 타입을 위한 매핑은 다음과 같다.


PUT /fs/_mapping/file

{

  "properties": {

    "name": { 

      "type":  "string",

      "index": "not_analyzed"

    },

    "path": { 

      "type":  "string",

      "index": "not_analyzed",

      "fields": {

        "tree": { 

          "type":     "string",

          "analyzer": "paths"

        }

      }

    }

  }

}


name 필드는 정확한 이름을 포함한다.


path 필드는 정확한 디렉토리 명이 포함될 것이며 path.tree 필드에 path hierarchy가 포함될 것이다.


인덱스에 세팅되고 파일이 인덱싱되면, 우리는 elasticsearch가 포함된 파일을 /clinton/projects/elasticsearch 디렉토리에서 다음과 같이 서치할 수 있다.


GET /fs/file/_search

{

  "query": {

    "filtered": {

      "query": {

        "match": {

          "contents": "elasticsearch"

        }

      },

      "filter": {

        "term": { 

          "path": "/clinton/projects/elasticsearch"

        }

      }

    }

  }

}


Find files in this directory only.


/clinton 디렉토리 하위의 모든 파일에 대한 검사를 수행할려면 다음과 같다.


GET /fs/file/_search

{

  "query": {

    "filtered": {

      "query": {

        "match": {

          "contents": "elasticsearch"

        }

      },

      "filter": {

        "term": { 

          "path.tree": "/clinton"

        }

      }

    }

  }

}


Renaming Files and Directories


파일명을 수정하는 것은 쉽다. 


PUT /fs/file/1?version=2 

{

  "name":     "README.asciidoc",

  "path":     "/clinton/projects/elasticsearch",

  "contents": "Starting a new Elasticsearch project is easy..."

}


버젼 넘버는 변경이 적용되었다는 것을 확실히 할뿐 아니라 인덱스의 도큐먼트는 이 같은 버젼넘버를 가지게 된다.


디렉토리명을 변경하는 것도 가능하다. 하지만 이는 패스 계층에 포함된 모든 디렉토리 및 파일을 변경해야 한다. 이는 빠를수도 느릴수도 있는데 이는 변경되어야 하는 파일수에 의존적이다. 


Solving Concurrent Issues


위의 문제에서 가장 큰 문제는 동시에 여러명의 사람이 파일이나 디렉토리 이름을 변경할 때 온다. 디렉토리 하위에는 수천개의 파일이 존재할 수 있으며, 여러명이 동시에 변경을 한다면 정보의 파편화가 발생할 수 있다.


다음과 같은 경우중 하나가 발생할 수 있다.


버젼 넘버를 사용하기로 했다면 버젼 충돌이 발생하면서 변경에 실패할 수 있다..

버젼을 사용하지 않는다면 당신의 변경이 다른 유저의 변경으로 업데이트 될 수 있다.


엘라스틱 서치의 문제는 ACID 트랜잭션을 지원하지 않는다는 것이다. 개별적인 문서의 변경은 ACID하나 다수의 도큐먼트의 변경은 지원하지 않는다.


만약 메인 데이터 스토어는 관계형 데이터베이스이고, 엘라스틱 서치이 검색엔진으로 사용되고 있다면 데이터베이스에서의 변경이 엘라스틱 서치에 복제된다면 별다른 문제가 없을 것이다. 이러한 방법은 database의 ACID 트랜잭션의 이점을 사용할 수 있기때문에 바른 순서로 엘라스틱서치에 변경이 발생할 것이다. 동시성은 관계형 데이터베이스에서 처리된다. 


만약 관계형 데이터베이스를 사용하지 않는다면, 동시성 이슈를 엘라스틱서치 레벨에서 사용해야 한다. 다음과 같은 세가지 실제적인 솔루션이 있다.


  • Global Locking
  • Document Locking
  • Tree Locking


Global Locking


우리는 특정 시간에 하나의 프로세스만 변경을 할 수 있도록 함으로써 동시성 이슈를 완벽하게 해결할 수 있다. 대부분의 변경은 몇개의 파일일 것이며, 매우 빨리 완료될 것이다. 


도큐먼트 단위의 ACID만을 지원하기 떄문에 글로벌 락으로써 도큐먼트의 존재 및 삭제를 사용할 수 있다.


PUT /fs/lock/global/_create

{}


만약 conflict가 발생한다면 다른 프로세스가 글로벌 락을 획득한 것이며, 다음에 다시 시도를 하면 된다. 만약 성공한다면 글로벌 락의 오너가 되며 변경을 계속 수행하면 된다. 만약 변경이 끝나면 글로벌 락 도큐먼트를 삭제함으로써 락을 릴리이즈 해야 한다.


DELETE /fs/lock/global


변경이 얼마나 자주일어나고 얼마나 오래 걸리냐에 따라서 시스템의 성능은 현저히 떨어짛수가 있다. 병렬처리를 증대하기위해 락킹을 더욱 정교하게 만들 필요가 있다.


Document Locking


모든 파일 시스템에 락킹을 하는 대신에 같은 기술을 이용해 개별적인 도큐먼트에 락키을 수행할 수 있다. 변경되고 생성될 파일에 대한 scrolled search를 수행할 수 있다.


PUT /fs/lock/_bulk

{ "create": { "_id": 1}} 

{ "process_id": 123    } 

{ "create": { "_id": 2}}

{ "process_id": 123    }


락 도큐먼트의 아이디는 락킹될 파일의 아이디와 같다.


process_id는 유니크하며 변경이 수행될 프로세스를 표현한다.


만약 몇몇의 파일이 이미 락킹이 되어 있다면 bulk 요청은 실패할 것이며 다음에 다시 수행하면 된다.


만약 모든 파일에 대한 락을 다시 요청한다면 명백하게 실패할 것이다. 이는 우리에 의해 락이 잡힌 파일이 존재하기 때문이다. 이에 우리는 단순한 create 명령 대신에 upsert 파라미터와 아래 스크립트와 함께 업데이트 명령을 날려야 한다.


if ( ctx._source.process_id != process_id ) { 

  assert false;  

}

ctx.op = 'noop'; 



process_id는 파라미터로 스크립트에 전달되어야 한다.


assert false은 exception을 발생시키고 업데이트를 실패하게 만든다


Changing the op from update to noop prevents the update request from making any changes, but still returns success.


전체 업데이트 리퀘스트는 다음과 같다.


POST /fs/lock/1/_update

{

  "upsert": { "process_id": 123 },

  "script": "if ( ctx._source.process_id != process_id )

  { assert false }; ctx.op = 'noop';"

  "params": {

    "process_id": 123

  }

}


만약 도큐먼트가 이미 존재하지 않는다면 upsert 도큐먼트는 insert될 것이다. 하지만 도큐먼트가 존재한다면 저장된 도큐먼트의 process_id를 확인한 후 자기 자신이라면 아무런 일을 하지 않고 다르다면 오류가 발생하면서 락을 획득하는데 실패할 것이다.


만약 모든 락이 성공적으로 생성되면 변경을 수행하면 된다.


더 중요한점은 락을 꼭 해제해주어야 한다는 것인데 bulk delete를 수행함으로써 락을 해제할 수 있다.


POST /fs/_refresh 


GET /fs/lock/_search?scroll=1m 

{

    "sort" : ["_doc"],

    "query": {

        "match" : {

            "process_id" : 123

        }

    }

}


PUT /fs/lock/_bulk

{ "delete": { "_id": 1}}

{ "delete": { "_id": 2}}


refresh 요청은 모든 락 도큐먼트가 서치 요청에 보이도록 확실하게 해준다.


하나의 검색 요청에 많은 수의 결과를 조회하기를 원한다면 scroll 쿼리를 사용할 수 있다.


도큐먼트 레벨 락킹은 잘 정제된 접근 제어를 가능하게 한다. 그러나 수백만의 락파일을 생성하는 것은 비용이 많이 든다. 몇몇의 경우 tree-scenario가 더 좋은 락킹의 메커니즘으로 나타날때도 있다.


Tree Locking


우리는 파일이나 디렉토리에 배타적인 접근이 필요하다. 이럴 경우 exclusive lock 도큐먼트를 획득함으로써 가능한다.


{ "lock_type": "exclusive" }


어떤 부모 디렉토리에 shared 락이 필요하다면 다음과 같이 생각할 수 있다.


{

  "lock_type":  "shared",

  "lock_count": 1 

}


lock_count 레코드는 shared lock을 획득하고 있고 프로세스의 수이다.


/clinton/projects/elasticsearch/README.txt의 파일명을 변경하고 싶은 프로세스는 파일에 대한 배타적인 락을 획득해야 되고, /clinton, clinton/projects, clinton/projects/elasticsearch의 디렉토리에 공유 락을 획득해야 된다.


배타적인 락을 획득할 요청은 다음과 같으며, shared lock에 대한 카운트를 올리는 것은 다음과 같은 로직으로 가능하다.


if (ctx._source.lock_type == 'exclusive') {

  assert false; 

}

ctx._source.lock_count++ 


만약 락 타입이 배타적이면 assert 문장은 예외를 발생할 것이며 update 문장은 실패할 것이다.


그렇지 않을 경우 lock_count를 증가시킬 것이다.


또한 락 도큐먼트가 존재하지 않을때 처리를 위한 upsert 문장도 표현되어야 한다. 전체 update 요청 스크립트는 다음과 같다.


POST /fs/lock/%2Fclinton/_update 

{

  "upsert": { 

    "lock_type":  "shared",

    "lock_count": 1

  },

  "script": "if (ctx._source.lock_type == 'exclusive')

  { assert false }; ctx._source.lock_count++"

}


도큐먼트의 ID는 /clinton이며, URL 인코딩을 하면 %2fclinton과 같다.


upsert 도큐먼트는 도큐먼트가 존재하지 않으면 인서트 된다.


parent 디렉토리에 공유 락을 획득하면 파일 자체게 exclusive 락을 생성해야 한다.O


PUT /fs/lock/%2Fclinton%2fprojects%2felasticsearch%2fREADME.txt/_create

{ "lock_type": "exclusive" }


이제 누군가가 /clinton 디렉토리의 이름을 변경하기 원한다면 그들은 path 상에 배타적인 락을 획득해야 한다.


PUT /fs/lock/%2Fclinton/_create

{ "lock_type": "exclusive" }


이 요청은 실패할 것이다. 이유는 같은 ID의 락 도큐먼트가 존재하기 때문이다. 다른 유저는 우리의 오퍼레이션이 끝날때 까지 기다리고 우리가 락을 릴리이즈하면 락을 획득할 수 있다. 배타적인 락은 단순히 삭제를 하면 된다.


DELETE /fs/lock/%2Fclinton%2fprojects%2felasticsearch%2fREADME.txt


공유락은 lock_count를 감소해야 되며 카운트가 0가되면 락 도큐먼트를 삭제해야 한다.


if (--ctx._source.lock_count == 0) {

  ctx.op = 'delete' 

}


lock_count가 0이 되면 ctx.op는 update에서 delete로 바뀐다. 


이 업데이트 요청은 각 부모 디렉토리로 긴것에서 짧은순으로 역순으로 수행되어야 한다


POST /fs/lock/%2Fclinton%2fprojects%2felasticsearch/_update

{

  "script": "if (--ctx._source.lock_count == 0) { ctx.op = 'delete' } "

}


트리 락킹은 우리에게 작은 노력으로 잘 정의된 동시성 제어를 제공한다. 물론 모든 솔루션에 적합하지는 않다. 


프로세스가 비정상적으로 종료한다면 두가지 문제가 발생할 것이다.


죽은 프로세스에 대한 락은 누가 어떻게 릴리이즈를 할 것인가?

죽은 프로세스를 어떻게 클린징할 것인가?


이러한 범위는 이 책에서 벗어난다. 그러나 만약 락킹을 사용하기로 생각했다면 이러한 해답을 만들 필요가 있다.


비정규화는 많은 프로젝트에서 좋은 결정이다. 하지만 락킹에 대한 처리를 위한 복잡한 스키마와 구현이 추가될 수 있다. 이에 엘라스틱 서치는 연관 엔티티를 처리하기 위한 두가지 모델을 제시한다.

반응형
반응형

매핑은 검색 엔진에게 어떻게 도큐먼트들이 매핑될 수 있는지를 정의하는 단계이다. 필드가 검색 대상이 되는지 어떻게 토큰나이즈 되는지 등이 예이다. 엘라스틱 서치에서는 인덱스는 다른 매핑 타입의 도큐먼트들을 저장할 수 있다. 엘라스틱 서치는 각 매핑 타입을 위한 1:N 매핑을 적용할 수 있다.


명시적인 매핑은 index/type 레벨에서 정의된다. 기본적으로 명시적인 매핑을 정의할 필요는 없다. 새로운 타입이나 새로운 필드가 소개될때 퍼포먼스 저하 없이 자동으로 새성되고 등록되기 때문이다. 기본값이 오버라이드 될 필요가 있을때에만 매핑 정의는 제공되어야만 한다.


Mapping types


매핑 타입은 하나의 인덱스내에 도큐먼트들을 논리적인 그룹으로 분리하는 방법이다. 데이터베이스의 테이블로 생각하면 유사하다. 타입간은 분리는 완전한 분리를 의미하는 것은 아니다. (같은 Lucene 인덱스내에 모든 도큐먼트들이 저장되기 때문이다.)


같은 이름의 필드 명은 같은 타입과 같은 매핑 특성을 가지기 때문에 주로 추천되는 타입이다.  my_type.my_field와 같은 타입 접두사를 이용해 필드를 명시적으로 선택하지만 이것은 완벽하지 않다. 필드의 faceting과 같은 영역이 지정되었을때 절대 수행되지 않을 것이다.


Mapping API


매핑을 생성하기 위해 Put Mapping API를 사용할 필요가 있다. 또는 인덱스를 생성할때 다양한 매핑을 추가할 수 있다.


Global Settings


index.mapping.ignore_malformed 글로벌 세팅은 모든 매핑 타입에서 malformed 컨텐츠를 무시하기 위한 인덱스 레벨을 세팅할 수 있다. (malformed 컨텐츠의 예는 텍스트 문자열 값을 숫자 타입으로 인덱스 하는 것이다.)


index.mapping.coerce 글로벌 세팅은 모든 타입에 숫자 타입 컨텐츠로 지정하기 위한 인덱스 레벨이다. 기본 세팅은 true이고 숫자로 구성된 문자열은 숫자 타입으로 지정할려고 시도할 것 이다. 변환에 실패하면 값은 malformed로 지정되고, ignore_malformed 세팅을 따를 것이다.


Fields


각 매핑은 연관된 많은 필드를 가지고 있다. 도큐먼트 메타데이터를 인덱스하기 위해서 사용될 수 있다.


_uid


인덱스된 각 도큐먼트의 id와 type은 연관관계가 있다. 내부적인 _uid 필드는 index내에서 도큐먼트의 유일한 식별자이며, 이것은 타입과 id의 조합이다. 타입기반 필터링을 수행하기 위한 _type은 색인되지 않고 _id도 색인되지 않을때 _uid 필드가 자동적으로 사용된다.










반응형
반응형

엘라스틱 REST API는 HTTP를 통해 JSON을 이용하여 노출되어 있다. 


이번에 볼 리스트된 컨벤션은 REST API를 통해 적용할 수 있다.


Multiple Indices


대부분은 API는 다수의 인덱스를 통한 실행을 지원하기 위해 test1,test2,test3과 같은 식으로 인덱스를 기술할 수 있다. 또한 wildcards를 지원해서 test* 라던지 +test*, -test3 등의 문법도 지원한다.


모든 멀티풀 인덱스 API는 다음과 같은 쿼리 파라미터를 지원한다.


ignore_unavailable - 특정 인덱스가 사라졌거나 닫혀서 이용가능하지 않을때 무시할 지 여부를 컨트롤 한다. true 나 false로 지정할 수 있다.

allow_no_indices - 와일드카드 인덱스가 없다면 실패로 판단할 지 여부를 컨트롤 한다. true/false로 지정할 수 있다. 예를 들어 와일드카드 표현식 foo*가 명시되었는데 foo로 시작하는 이용가능한 인덱스가 없다면 세팅에따라 실패를 리턴한다. 

expand_wildcards - 어떤 종류의 인덱스에 와일드카드 익스프레션을 적용할 지 여부를 컨트롤 한다. 만약 open이라고 명시되었다면 open 인덱스에만 접근하고 closed라고 명시하면 closed 인덱스에 접근한다. open,closed라고 둘다 명시 되어 있으면 모든 인덱스에 적용한다. none이라고 지정되면 와일드카드 표현식은 이용할 수 없으며 all이라고 지정하면 모든 인덱스에 확장될 것이다. 


위 파라미터의 디폴트 세팅은 api에 특성에 따라 다르다.


Single-index alias API 및 Document API와 같은 싱글 인덱스와 멀티풀 indices를 지원하지 않는다.


Common options


다음 옵션들은 모든 REST API에 적용할 수 있는 옵션들이다.


Pretty Results


?pretty=true 이라고 어떤 요청에라도 붙이되면 결과는 반환되는 JSON은 pretty 포맷으로 될 것이다. 또다른 옵션으로 ?format=yaml 이라고 붙인다면 결과는 yaml 포맷으로 반환될 것이다.


Human readable outputs


통계치가 사람이 읽기 쉬운 형태로 반환된다.  (for humans (eg "exists_time": "1h" or "size": "1kb"), for computers (eg "exists_time_in_millis": 3600000 or "size_in_bytes": 1024)). ?human=false 쿼리 스트링에 다음과 같은 내용을 붙임으로써 모니터링 툴과 같은 시스템이 사용하기에 더욱 의미있는 통계치가 나올 것이다. 기본값은 false이다.


Flat Settings


flat settings 플래그는 리스트의 렌더링에 영향을 준다. flat_settings flag가 true이면 리턴된 결과는 다음과 같다.


{

  "persistent" : { },

  "transient" : {

    "discovery.zen.minimum_master_nodes" : "1"

  }

}


false이면 더 사람이 인식하기 좋은 형태로 나타난다.


{

  "persistent" : { },

  "transient" : {

    "discovery" : {

      "zen" : {

        "minimum_master_nodes" : "1"

      }

    }

  }

}


기본값은 false이다.


Parameters


REST 파라미터는 언더스코어 형식의 규칙을 따른다.


Boolean Values


모든 REST API의 파라미터는 "false"를 제공한다. "false"는 false, 0, no 혹은 off를 의미한다. 또한 "true"를 제공한다. 


Number Values 


모든 REST API는 JSON 숫자 타입 지원과 더불어 String을 숫자 파라미터를 사용할 수 있도록 지원한다.


Time units


기간을 명시할 필요가 있을때, 예를 들어 타임아웃과 같은 것이다. 2d는 2일을 의미하며 지원되는 단위는 다음과 같다.


y - Year

M - Month

w - Week

d - Day

h - Hour

m - Minute

s - Second


Distance Units


거리를 명시할 필요가 있을때 명시를 하지 않으면 기본적으로 meter를 사용한다. 다른 단위들도 명시할 수 있으며 1km 또는 2mi등이다.


Mile - mi or miles

Yard - yd or yards

Feet - ft or feet

Inch - in or inch

Kilometer - km or kilometers

Meter - m or meters 

Centimeter - cm or centimeters

Millimeter - mm or millimeters

Nautical mile - NM, nmi or nauticalmiles


Geohash Cell Filter의 정밀도 파라미터는 거리로 인식되며 명시를 하지 않아도 된다.


Fuzziness


몇몇 쿼리와 API는 정확하지 않은 fuzziness 파라미터를 통해서 fuzzy 매칭을 지원한다. Fuzziness 파라미터는 context sensitive하며 이 의미는 쿼리의 필드의 타입에 의본성이 강하다.


Numeric, date and IPv4 fields


숫자, 날짜, IPv4 필드를 쿼리할 떄 fuzziness는 +/- 마진을 해석한다. 동작은 범위 쿼리와 비슷하게 행동한다.


-fuzziness <= field value <= +fuzziness


퍼지 파라미터는 숫자형으로 세팅될 수 있고 예를 들자면 2 또는 2.0이다. 날짜 필드는 밀리세컨드가 기본이지만 타임 값을 가진 1h같은 형태일 수도 있다. ip는 long 값으로 인식되거나 또다른 IPv4 어드레스로 조합할 수 있다.


String fields


문자열 필드가 쿼리될때 퍼지는 Levenshtein Distance로 해석된다. 


The fuzziness parameter can be specified as:


0, 1, 2

최대 허용되는 Levenshtein Distance


AUTO

단어의 길이에 따라 자동으로 거리가 결정된다.


 0..2 -무조건 같아야 함

 3..5 - 하나 정도 틀려도 됨

 >5 - 두개 정도 틀려되 됨


AUTO가 퍼지 검색에 가장 적합하다.


0.0..1.0

수식을 이용하여 distance를 교정한다. length(term) * (1.0 - fuzziness), eg 0.6 퍼지는 길이가 10자인 단어의 교정거리는 4이다.

Note: all APIs except for the Fuzzy Like This Query, the maximum allowed edit distance is 2.


Result Casing


모든 REST API는 케이스 파라미터를 허용한다. camelCase로 세팅하면 모든 결과 필드명이 camel case로 리턴되며, underscore로 세팅도 사용할 수 있다.

인덱스된 소스 도큐먼트에는 적용할 수 없다는 것에 주의하자.


JSONP


황성화 되면 모든 REST API는 JSONP 결과의 콜백 파라미터 결과를 받는다. config.yaml을 수정하면 된다.


http.jsonp.enable: true


활성화하면 엘라스틱 서치 아키텍처로 인해 보안 이슈가 발생할 수 있다. 


Request body in query string


소스 쿼리 파라미터 대신에 request body를 이용하여 쿼리 문자열을 전달할 수 있다.


URL-based access control


많은 유저들이 URL 기반 접속 제어를 프록시를 이용하여 엘라스틱 서치 인덱스에 보안 접근을 수행한다. 멀티 서치, 멀티 연산 및 벌크 요청을 위해 유저는 URL에 특정한 인덱스를 지정한다. 


특정 URL의 인덱스를 유저에 의해 오버라이딩되는 것을 방지하기 위해 config.yml에 대해 다음과 같은 세팅을 추가할 수 있다.


rest.action.multi.allow_explicit_index: false


기본값은 true이나 false로 세팅하면 요청 바디에 특정한 인덱스를 명시한 요청은 거부될 것이다.


반응형
반응형

이제 우리는 기초적인 내용을 잠깐 보았다. 이제 좀더 실제적인 데이터셋과 동작을 해보자. 고객 은행 계좌 정보를 표현한 샘플을 준비하였다. 각 도큐먼트는 다음과 같은 스키마를 가진다.


{

    "account_number": 0,

    "balance": 16623,

    "firstname": "Bradshaw",

    "lastname": "Mckenzie",

    "age": 29,

    "gender": "F",

    "address": "244 Columbus Place",

    "employer": "Euron",

    "email": "bradshawmckenzie@euron.com",

    "city": "Hobucken",

    "state": "CO"

}


이 데이터는 www.json-generator.com/로 부터 생성한 데이터이다. 이 사이트는 랜덤하게 실제 데이터를 무시하되 의미있는 데이터를 생성해준다.


Loading the Sample Dataset


샘플 데이터셋을 다운로드 받고 디렉토리에 압축을 해제한 다음 아래의 명령으로 데이터를 로딩해보자.


curl -XPOST 'localhost:9200/bank/account/_bulk?pretty' --data-binary @generated.json


그리고 인덱스에 로딩된 데이터를 살펴보자.


curl 'localhost:9200/_cat/indices?v'

health index pri rep docs.count docs.deleted store.size pri.store.size

yellow bank    5   1       1000            0    424.4kb        424.4kb


1000개의 도큐먼트가 bank 인덱스에 성공적을 벌크 로딩 되었다는 것을 의미한다.


The Search API


이제 몇가지 간단한 검색을 시작해보자. 검색을 수행하기 위한 두가지 간단한 방법은 다음과 같다. 하나는 REST URI를 통해 검색 파라미터를 전송하는 방법이고, 다른 방법은 REST 요청 본문에 검색 파라미터를 전송하는 방식이다. request 본문에 검색 파라미터를 전달하는 방식이 더 표현하기 쉽고 더 읽기도 쉬울 것이다. 그러나 우리는 이번 튜토리얼에서는 URI를 통해 파라키터를 전송할 것이다.


검색을 위한 REST API는 _search 엔드포인트를 통해 접근가능하다. 이번 예제는 bank 인덱스의 모든 도큐먼트를 반환하는 것이다.


curl 'localhost:9200/bank/_search?q=*&pretty'


첫번째 검색 요청을 분석해보자. 우리는 bank 인덱스를 조회할 것이고, q=* 라고 파라미터를 지정하였는데 이 의미는 인덱스에 모든 도큐먼트를 매칭하겠다는 의미이다.


결과는 다음과 같다. (중략)


curl 'localhost:9200/bank/_search?q=*&pretty'

{

  "took" : 63,

  "timed_out" : false,

  "_shards" : {

    "total" : 5,

    "successful" : 5,

    "failed" : 0

  },

  "hits" : {

    "total" : 1000,

    "max_score" : 1.0,

    "hits" : [ {

      "_index" : "bank",

      "_type" : "account",

      "_id" : "1",

      "_score" : 1.0, "_source" : {"account_number":1,"balance":39225,"firstname":"Amber","lastname":"Duke","age":32,"gender":"M","address":"880 Holmes Lane","employer":"Pyrami","email":"amberduke@pyrami.com","city":"Brogan","state":"IL"}

    }, {

      "_index" : "bank",

      "_type" : "account",

      "_id" : "6",

      "_score" : 1.0, "_source" : {"account_number":6,"balance":5686,"firstname":"Hattie","lastname":"Bond","age":36,"gender":"M","address":"671 Bristol Street","employer":"Netagy","email":"hattiebond@netagy.com","city":"Dante","state":"TN"}

    }, {

      "_index" : "bank",

      "_type" : "account",


응답으로 부터 다음과 같은 부분을 발견할 수 있다.


took – 엘라스틱 서치가 검색을 수행하는데 걸린 시간 (milliseconds)

timed_out – 검색에 타임아웃이 발생했는지 안했는지 여부

_shards – 얼마나 많은 샤드에서 검색했는지와 성공/실패 샤드 수

hits – 검색 결과

hits.total – 검색 조건에 매칭된 도큐먼트 수

hits.hits – 검색 결과 실제 배열 (디폴트로 처음 10개의 도큐먼트만 보인다.)

_score and max_score - 이번에는 무시하자.


여기 똑같은 검색을 요청 본문에서  처리해보자.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": { "match_all": {} }

}'


URI에 q=*를 전달하는 대신에 검색을 위해 JSON 스타일의 요청 본문을 전송하였다. 다음 섹션에서 JSON 쿼리에 대해서 논의해보도록 하자.


결과는 같을 것이다.


이해해야할 중요한 점은 한번 검색 결과가 전달 되면 엘라스틱서치는 더 이상 어떤 종류의 서버 사이드 리소스나 결과에 대한 커서를 열지 않는다는 것이다. 이것은 SQL기반의 다른 플랫폼에서는 완전한 제약조건이며, 이로서 전체 쿼리 결과에 따른 패치를 계속 서버로 와서 수행할 수 있게 되는 것이다. 이럴 경우 스테이트풀한 서버 사이드 커서를 이용한다.


Introducing the Query Language


엘라스틱 서치는 JSON 스타일에 DSL을 쿼리를 수행할 수 있게 제공한다. 쿼리 언어는 꽤 이해하기 쉽다. 


지난 예제로 돌아가서 쿼리를 수행해보자.


{

  "query": { "match_all": {} }

}


위의 내용를 해석해보면 쿼리 부분은 우리에서 쿼리 정의가 어떻게 되었는지 알려주고 match_all 파트는 단순히 우리가 수행하고 싶은 쿼리의 타입이다. match_all 쿼리는 단순히 특정 인덱스에 있는 모든 도큐먼트를 검색한다.


쿼리 파라미터에 추가적으로 검색 결과에 영향을 주는 다른 파라미터를 정의할 수 있다. 예를 들어 다음은 모든 매치되는 문서중 단 하나의 결과만 반환해 달라는 의미이다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": { "match_all": {} },

  "size": 1

}'


사이즈를 명시하지 않으면 기본적으로 10이다.


다음 예제는 11 부터 20번째 문서를 리턴해달라는 것이다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": { "match_all": {} },

  "from": 10,

  "size": 10

}'


첫번째 도큐먼트는 0이고 from 파라미터는 몇번째 도큐먼트부터 인지를 지정하고 size는 얼마나 많은 도큐먼트를 from 파라미터로부터 반환할 것인가를 의미한다. 이러한 기능을 검색결과의 페이징을 구현할 때 유용한다. 만약 from을 명시하지 않으면 기본값은 0이다.


이번 예제는 account 잔액에 따라 내림차순 정렬을 한 다음 상위 10개의 도큐먼트만 반환하는 예제이다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": { "match_all": {} },

  "sort": { "balance": { "order": "desc" } }

}'


Executing Searches


몇가지 기본적인 검색 파라미터에 대해서 살펴 보았다. 이제 쿼리 DSL에 대해서 조금더 살펴보자. 첫번째는 반환되는 도큐먼트 필드에 대해서 살펴보겠다. 기본적으로 모든 검색의 결과로 풀 JSON 도큐먼트가 반환된다. 이것은 source로 표현된다. 만약 전체 소스 도큐먼트를 리턴받기를 원하지 않는다면 source내의 특정 필드를 지정할 수 있다.


소스내의 account_number와 balance만 반환받는 예제이다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": { "match_all": {} },

  "_source": ["account_number", "balance"]

}'


만약 SQL 지식이 있다면 위의 예제는 SELECT FROM의 필드리스트와 유사하다는 것을 알 수 있을 것이다.


이제 쿼리 부분으로 돌아가자. 이전에 match_all은 모든 도큐먼트를 매칭할때 사용된다고 했다. 이제 match라고 불리는 새로운 쿼리를 소개하겠다. match 검색 쿼리의 가장 기본이라고 생각하면 되겠다.


이번 예제는 account number 가 20인것을 리턴한다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": { "match": { "account_number": 20 } }

}'


이번 예제는 mill이라는 단어를 포함한 주소를 가진 모든 계좌를 반환한다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": { "match": { "address": "mill" } }

}'


이번 예제는 주소가 mill 또는 lane을 포함하는 모든 계좌를 반환한다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": { "match": { "address": "mill lane" } }

}'


이번 예제는 mill lane 이라는 구문이 포함된 계좌를 반환한다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": { "match_phrase": { "address": "mill lane" } }

}'


이제 불린 쿼리를 소개하겠다. 불린 쿼리는 불린 로직을 이용해 작은 쿼리들을 큰 쿼리로 변환 기능을 제공한다.


다음 예제는 두개의 매치 쿼리를 이용해 mill 과 lane이 포함된 주소를 가진 모든 계좌를 번환한다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": {

    "bool": {

      "must": [

        { "match": { "address": "mill" } },

        { "match": { "address": "lane" } }

      ]

    }

  }

}'


위의 예에서 must 절은 모든 쿼리의 결과를 만족하는 도큐먼트만을 제공해달라는 의미이다.


반대로 mill이나 lane이 주소에 포함된 모든 계좌를 리턴받기 위해선 아래와 같이 명령을 수행하면 된다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": {

    "bool": {

      "should": [

        { "match": { "address": "mill" } },

        { "match": { "address": "lane" } }

      ]

    }

  }

}'


위의 예제에서 should 절은 리스트의 쿼리 중 어떤 것이라고 참이되면 매칭된 것으로 간주한다는 의미이다.


이번 예제는 mill 이나 lane이 모두 포함되지 않는 모든 계좌를 반환한다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": {

    "bool": {

      "must_not": [

        { "match": { "address": "mill" } },

        { "match": { "address": "lane" } }

      ]

    }

  }

}'


위의 예제에서 must_not 절은 쿼리 리스트 중 참이 되면 매칭의 결과로 고려하지 않겠다는 의미이다.


must, should, must_not은 불린 쿼리내에 동시에 적용할 수 있다. 


이번 예제는 나이가 40인 사람들의 계좌를 리턴하는데 ID 주에 살지 않는 사람을 리턴해달라는 의미이다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": {

    "bool": {

      "must": [

        { "match": { "age": "40" } }

      ],

      "must_not": [

        { "match": { "state": "ID" } }

      ]

    }

  }

}'


Executing Filters


이전 섹션에서 우리는 도큐먼트 score라고 불리는 상세 정보를 스킵했었다. 스코어는 숫자이고 우리가 명시한 검색 쿼리에 도큐먼트가 얼마나 잘 맞는지를 나타내는 상대적인 수치이다. 더 높은 스코어는 상대적으로 더 신뢰할 수 있는 도큐먼트이고 낮은 스코어는 약간 덜 신뢰할 수 있는 도큐먼트라고 생각할 수 있다. 


엘라스틱 서치는 모든 쿼리는 스코어 계산을 수행한다. 어떤 경우 우리는 스코어가 필요없을 수 있는데 엘라스틱 서치는 필터라는 형태의 또다른 쿼리 기능을 제공한다.  필터는 쿼리에서 제외하는 컨셉이며, 더 빠른 수행속도를 위한 두가지 주요한 특징을 가지고 있다. 


필터는 쿼리보다 더 빠르게 수행하기 위해 스코어를 계산하지 않는다.

필터는 반복적인 쿼리가 훨씬 빨리 수행될 수 있데 메모리에 캐쉬될 수 있다. 


필터를 이해하기 위해선 첫번째 필터 쿼리를 소개하겠다. 예제는 range 필터이며 range값에 따라 도큐먼트를 필터링 한다. 일반적으로 숫자나 날짜를 필터링하기 위해 사용된다.


이번 예제는 잔액 20000에서 30000 사이인 모든 계좌를 리턴하는 필터 쿼리이다. 


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "query": {

    "filtered": {

      "query": { "match_all": {} },

      "filter": {

        "range": {

          "balance": {

            "gte": 20000,

            "lte": 30000

          }

        }

      }

    }

  }

}'


위를 분석해 보면 필터 쿼리는 match_all 쿼리를 포함하고 range filter가 적용되어 있다. 위의 경우 range 필터는 range안에 들어온 결과가 의미가 있으므로, 더 이상 도큐먼트는 다른 것들과 연관성을 고려할 필요가 없다.


일반적으로 필터를 사용할 지 쿼리를 사용할 지 여부는 연관 스코어가 필요한지 아닌지 여부이다. 스코어가 중요하지 않다면 필터를 쓰고, 그렇지 않다면 쿼리를 사용해라. 만약 SQL 지식이 있다면 쿼리와 필터는 SELECT WHERE 절과 비슷하다.


게다가 match_all, match, bool, filtered 그리고 range query외에 다양한 query/filter 타입이 존재한다. 이번에는 기초만 이해하기 위해서 자세한 설명을 생략한다.


Executing Aggregations


데이터에 대한 통계를 그룹짓고 추출하기 위해 집합 기능을 제공한다. 집합연산에 대해 이해하기 쉬운 간단한 방식은 SQL의 GROUP BY와 다른 집합연산들과 어느 정도 비슷하다고 생각하면 된다. 엘라스틱 서치에서는 서치를 수행하고 동시에 집합연산의 결과를 한 reponse에 받을 수 있는 기능을 제공한다. 이는 매우 강력하고 효율적이다. 한번에 오퍼레이션을 수행하기 때문에 네트워크 라운드트립을 피할 수 있다.


모든 계좌의 주를 그룹핑하는 예제를 수행해보자. 카운트 별로 내림차순되어 상위 10개의 주가 반환된다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "size": 0,

  "aggs": {

    "group_by_state": {

      "terms": {

        "field": "state"

      }

    }

  }

}'


위의 집합연산의 컨셉은 SQL의 아래 쿼리와 비슷하다.


SELECT COUNT(*) from bank GROUP BY state ORDER BY COUNT(*) DESC


결과는 다음과 같다.(partially shown):


  "hits" : {

    "total" : 1000,

    "max_score" : 0.0,

    "hits" : [ ]

  },

  "aggregations" : {

    "group_by_state" : {

      "buckets" : [ {

        "key" : "al",

        "doc_count" : 21

      }, {

        "key" : "tx",

        "doc_count" : 17

      }, {

        "key" : "id",

        "doc_count" : 15

      }, {

        "key" : "ma",

        "doc_count" : 15

      }, {

        "key" : "md",

        "doc_count" : 15

      }, {

        "key" : "pa",

        "doc_count" : 15

      }, {

        "key" : "dc",

        "doc_count" : 14

      }, {

        "key" : "me",

        "doc_count" : 14

      }, {

        "key" : "mo",

        "doc_count" : 14

      }, {

        "key" : "nd",

        "doc_count" : 14

      } ]

    }

  }

}


AL주에 있는 21개 계좌, TX주에 17개 계좌등이 있다는 것을 확인할 수 있다.


size=0의 의미는 서치 결과를 보여주지 말라는 의미이다. 단지 집합의 결과만 보겠다는 의미이다.


이번 예제는 주별로 평균 계좌 잔액을 계산하는 예제이다. (이번에도 카운트별로 내림차순 정렬되어 상위 10개의 상태만이 나타난다.):


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "size": 0,

  "aggs": {

    "group_by_state": {

      "terms": {

        "field": "state"

      },

      "aggs": {

        "average_balance": {

          "avg": {

            "field": "balance"

          }

        }

      }

    }

  }

}'


어떻게 group_by_state 집한 연산내에 average_balance 집한연산이 수행되었느지를 주의 깊게 살펴 보자. 이것은 모든 집합연산에 공통적인 패턴이다. 당신은 임의의 집합연산내에 다른 집합연산을 내포할 수 있다.


이제 average balance에 따라 내림차순으로 정렬해보자.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "size": 0,

  "aggs": {

    "group_by_state": {

      "terms": {

        "field": "state",

        "order": {

          "average_balance": "desc"

        }

      },

      "aggs": {

        "average_balance": {

          "avg": {

            "field": "balance"

          }

        }

      }

    }

  }

}'


이번 예제는 나이대에 따라 그룹핑을 한 다음 성별에 따라 그룹핑을 하고 마지막으로 각 그룹의 balance를 평균을 내보겠다.


curl -XPOST 'localhost:9200/bank/_search?pretty' -d '

{

  "size": 0,

  "aggs": {

    "group_by_age": {

      "range": {

        "field": "age",

        "ranges": [

          {

            "from": 20,

            "to": 30

          },

          {

            "from": 30,

            "to": 40

          },

          {

            "from": 40,

            "to": 50

          }

        ]

      },

      "aggs": {

        "group_by_gender": {

          "terms": {

            "field": "gender"

          },

          "aggs": {

            "average_balance": {

              "avg": {

                "field": "balance"

              }

            }

          }

        }

      }

    }

  }

}'


더 많은 집합연산 기능을 제공하지만 여기서는 더 이상 다루지 않겠다. 더 많은 연산이 필요하면 aggregation 가이드를 참고하기 바란다. 


Conclusion


엘라스틱 서치는 단순하면서도 복잡한 프로덕트이다. 우리는 어느 정도 기초에 대해서 배웠다. 이 튜토리얼이 엘라스틱 서치를 이해하는데 많은 도움이 되었기를 바란다.

반응형

'Programming > Elasticsearch' 카테고리의 다른 글

Elasticsearch - Mapping  (0) 2015.05.28
Elasticsearch - API Conventions  (1) 2015.05.28
Elasticsearch - Modifying Your Data  (0) 2015.05.27
Elasticsearch - Exploring Cluster  (2) 2015.05.22
Elasticsearch - 시작하기  (1) 2015.05.20
반응형

엘라스틱 서티는 NRT상의 데이터 조작과 검색 기능을 제공한다. 기본적으로 데이터를 입력/변경/삭제하고 조회 결과에 나타날때까지 1초의 딜레이가 예상된다. 이것은 트랜잭션이 완료되면 즉시 조회가 가능한 SQL 기반의 다른 플랫폼과의 주요한 차이점이다.


Indexing/Replacing Documents


하나의 도튜먼트를 인덱싱 해보기 위해 다시 아래와 같은 커맨드를 수행해보자,


curl -XPUT 'localhost:9200/customer/external/1?pretty' -d '

{

  "name": "John Doe"

}'


다시 customer 인덱스, external 타입에 ID 1로 도큐먼트가 인덱싱 되었다. 만약 다른 도큐먼트로 위의 커맨드를 다시 실행한다면 엘라스틱 서치는 ID 1에 존재하는 도큐먼트를 새로운 도큐먼트로 대체할 것이다.


curl -XPUT 'localhost:9200/customer/external/1?pretty' -d '

{

  "name": "Jane Doe"

}'


ID 1에 저장된 도큐먼트의 이름이 "John Doe" 에서 "Jane Doe"로 변경 될 것이다. 만약 다른 아이디를 사용한다면 새로운 도큐먼트가 인덱스되고 기존의 이미 존재하는 도큐먼트는 변경되지 않을 것이다.


curl -XPUT 'localhost:9200/customer/external/2?pretty' -d '

{

  "name": "Jane Doe"

}'


위의 명령의 결과로 ID 2에 새로운 도큐먼트가 인덱스 될 것이다. 


인덱싱을 할때 ID 부분은 옵션이다. 만약 명시하지 않는다면 엘라스틱 서치는 도큐먼트를 인덱스하기 위해 랜덤한 ID를 생성할 것이다. 엘라스틱 서치가 생성한 아이디는 인덱스 API 호출결과로 반환할 것이다.


다음 예제는 명시적인 아이디 없이 도큐먼트를 인덱스하는 것이다.


curl -XPOST 'localhost:9200/customer/external?pretty' -d '

{

  "name": "Jane Doe"

}'


위의 케이스에서 ID를 명시하지 않기 때문에 PUT절 대신 POST절을 사용해야 한다.


Updating Documents


추가적으로 도큐먼트를 인덱스하거나 대체하는 것 외에 또한 도큐먼트에 대한 업데이트를 수행할 수 있다. 그러나 실제적으로 엘라스틱서치에서는 내부적으로 in-place 업데이트를 수행하지는 않는다. 업데이트를 수행하면 엘라스틱 서치는 오래된 도큐먼트를 지우고 업데이트된 새로운 도큐먼트를 인덱스 한다.


이번 예제에서는 어떻게 기존 도큐먼트를 업업데이트 하는지를 보여준다.


curl -XPOST 'localhost:9200/customer/external/1/_update?pretty' -d '

{

  "doc": { "name": "Jane Doe" }

}'


이번 예제는 기존 도큐먼트를 이름을 변경하고 age를 추가하는 업데이트를 수행한 예이다.


curl -XPOST 'localhost:9200/customer/external/1/_update?pretty' -d '

{

  "doc": { "name": "Jane Doe", "age": 20 }

}'


업데이트는 간단한 스크립트를 이용하여 수행될 수 있다. 다음과 같은 다이나믹 스크립트는 1.4.3에서는 비활성화 되어 있다. 자세한 내용은 스크립트 관련 자료를 자세히 살펴 보도록 하자. 이번 예제는 스크립트를 이용해 나이를 5 증가 시키는 예제이다.


curl -XPOST 'localhost:9200/customer/external/1/_update?pretty' -d '

{

  "script" : "ctx._source.age += 5"

}'


위의 예에서 ctx._source는 업데이트를 하기 위해 현재 문서를 레퍼런스 한다.


현재 시점에서 업데이트는 한번에 하나의 도큐먼트에 대해서만 수행할 수 있다. 그러나 미래에는 엘라스틱 서치에서는 주어진 쿼리 조건에 따라 다수의 문서를 업데이트할 수 있는 기능을 제공할 것이다.  (like an SQL UPDATE-WHERE statement)


Deleting Documents


도큐먼트 삭제는 완전히 단반향이다.


curl -XDELETE 'localhost:9200/customer/external/2?pretty'


우리는 또한 쿼리 조건에 의해 다수의 도큐먼트를 삭제할 수 있다. 이번 예제는 이름에 John을 포함하고 있는 모든 고객을 삭제하는 것이다. 


curl -XDELETE 'localhost:9200/customer/external/_query?pretty' -d '

{

  "query": { "match": { "name": "John" } }

}'


위의 URI가 /_query로 변경된 것에 주의하자. 그리고 본문에 삭제 쿼리 기준이 명시되었다. 그리고 삭제를 위해 DELETE 절이 여전히 사용되고 있다. 쿼리 문법에 대한 내용은 나중에 자세히 살펴보겠다.


Batch Processing


개별적인 도큐먼트를 인덱스, 업데이트 및 삭제를 수행하는 것 외에 엘라스틱 서치에서는 _bulk API를 사용해서 배치 오퍼레이션을 수행할 수 있다. 이 기능은 중요한데 네트워크 라운드 트립을 감소시켜 더 빠르게 연산을 가능하게 해준다.


간단한 예로 두개의 도큐먼트를 하나의 오퍼레이션으로 벌크 처리를 수행해보겠다. 


curl -XPOST 'localhost:9200/customer/external/_bulk?pretty' -d '

{"index":{"_id":"1"}}

{"name": "John Doe" }

{"index":{"_id":"2"}}

{"name": "Jane Doe" }

'


이번 예제는 첫번째 도큐먼트를 업데이트 하고 두번째 도큐먼트를 삭제하는것을 하나의 벌크 오퍼레이션에서 수행하는 것이다.


curl -XPOST 'localhost:9200/customer/external/_bulk?pretty' -d '

{"update":{"_id":"1"}}

{"doc": { "name": "John Doe becomes Jane Doe" } }

{"delete":{"_id":"2"}}

'


삭제를 위한 액션을 위해서는 삭제를 하기 위한 ID정보를 명시한 도큐먼트만 있으면 된다는 점을 주의하자.


벌크 API는 모든 액션을 순서에 따라 시퀀셜하게 수행한다. 만약 어떤 이유에서건 하나의 액션이 실패하면 그 후에 액션들은 계속 수행될 것이다. 벌크 API의 결과가 반환될 때 각 액션에 대한 상태가 제공될 것이다.

반응형

'Programming > Elasticsearch' 카테고리의 다른 글

Elasticsearch - Mapping  (0) 2015.05.28
Elasticsearch - API Conventions  (1) 2015.05.28
Elasticsearch - Exploring Your Data  (0) 2015.05.27
Elasticsearch - Exploring Cluster  (2) 2015.05.22
Elasticsearch - 시작하기  (1) 2015.05.20
반응형

REST API


이제 우리는 우리의 노드를 가지게 되었고, 실행하였다. 다음은 노드와 어떻게 커뮤니케이션을 하는 것인가이다. 엘라스틱 서치에서는 매우 이해하기 쉽고 파워풀한 REST API를 제공한다. API를 통해서 수행할 수 있는 작업은 다음과 같다.


클러스터, 노드와 인덱스 헬스, 상태, 통계 체크

클러스터, 모드 , 인덱스 데이터 및 메타데이터 관리

인덱스에 CRUD 수행 및 검색 수행

페이징, 소팅, 필터링, 스크립팅, faceting, aggregation등 진화된 검색 수행


Cluster Health


기본적인 헬스 체크부터 시작해보자. 우리는 curl를 사용할 것이다. 하지만 당신이 원한다면 HTTP/REST 콜을 생성하는 다른 툴을 사용해도 좋다. 명령을 수행하기 위해 다른 쉘 윈도우를 열자.


클러스터 헬스를 체크하기 위해 _cat API를 사용할 것이다. 우리 노드의 HTTP 엔드포인트는 9200번이라고 가정하자.


curl "localhost:9200/_cat/health?v"


결과는 다음과 같다. 


epoch      timestamp cluster       status node.total node.data shards pri relo init unassign

1394735289 14:28:09  elasticsearch green           1         1      0   0    0    0        0


우리의 클러스터명이 "elasticsearch" 이고 상태가 green이라는 것을 알 수 있다.


클러스터 헬스 체크를 수행했을때 green, yellow 또는 red가 결과로 받을 수 있다. Green의 의미는 모든 것이 좋다는 의미이며, yellow는 모든 데이터가 이용가능하나 몇몇의 복제본이 아직 할당 되지 않았다는 것을 의미한다. Red의 의미는 어떤 데이터는 어떤 이유에 의해 유효하지 않다는 것을 의미한다. 만약 클러스터의 상태가 red라도 부분적으로 이용가능하다. 그러나 데이터를 유실할 수 있기 때문에 최대한 빨리 고쳐야 한다. 


또한 위의 결과로부터 우리는 하나의 노드에 0개의 샤드임을 확인할 수 있는데 이는 아직 데이터가 없기 때문이다. 또 클러스터 명이 기본으로 elasticsearch인데 엘라스틱 서치는 멀티캐스트 네트워크를 이용해 다른 노드를 찾기 때문에 사고로 하나의 클러스터로 조인될 수 있을을 주의하자.


우리 클러스터에 존재하는 모든 노드를 리스트를 가지고 와보자.


curl 'localhost:9200/_cat/nodes?v'


결과는 다음과 같다.


host         ip        heap.percent ram.percent load node.role master name

mwubuntu1    127.0.1.1            8           4 0.00 d         *      New Goblin


여기 우리는 "New Goblin"으로 명명된 하나의 노드가 있음을 알 수 있다.


List All Indexes


이제 우리의 인덱스들을 조회해보자.


curl 'localhost:9200/_cat/indices?v'


결과는 다음과 같다.


health index pri rep docs.count docs.deleted store.size pri.store.size


아직 클러스터에는 인덱스가 없다는 것을 의미한다.


Create an Index


이제 "customer"라는 이름의 인덱스를 생성하고 다시 모든 인덱스를 조회해보자.


curl -XPUT 'localhost:9200/customer?pretty'

curl 'localhost:9200/_cat/indices?v'


첫번째 커맨드는 customer로 명명된 인덱스를 생성하는 것이다. 뒤에 붙인 pretty는 JSON 결과를 반환할때 pretty-print 해달라는 의미이다. 


결과는 다음과 같다.


curl -XPUT 'localhost:9200/customer?pretty'

{

  "acknowledged" : true

}


curl 'localhost:9200/_cat/indices?v'

health index    pri rep docs.count docs.deleted store.size pri.store.size

yellow customer   5   1          0            0       495b           495b


두번째 명령의 결과는 우리는 customer라는 하나의 인덱스를 가지고 있으며 5개의 프라이머리 샤드와 1개의 복제본을 가지고 있으며 0개의 도큐먼트가 포함되어 있다는 것이다. 


또한 customer 인덱스가 강태가 yellow라고 나타나는데 앞에서 이 상태는 몇몇의 복제본이 아직 할당되어 있지 않다는 것을 의미한다고 했었다. Customer 인덱스에 대해 이러한 결과가 발생하는 이유는 이 인덱스를 위해 하나의 복제본을 기본적으로 생성하기 때문이다. 우리는 하나의 노드만 실행했기때문에 고가용성을 위한 하나의 복제본은 아직 할당 되지 않았다. 세컨드 노드에 복제본이 할당되면 상태는 green으로 변경될 것이다.


Index and Query a Document


이제 클러스터 인덱스에 무언가를 넣어보자. 도큐먼트를 인덱싱하기 위해 우리는 엘라스틱 서치에게 인덱스에 타입을 알려야 한다.


단순한 customer 도큐먼트를 "external" 타입으로 인덱스하고 ID는 1로 할당하자.


우리의 JSON 도큐먼트는 다음과 같다.


{"name:"John Doe"}


다음과 같은 커맨드를 수행하자.


curl -XPUT 'localhost:9200/customer/external/1?pretty' -d '

{

  "name": "John Doe"

}'


결과는 다음과 같다.


{

  "_index" : "customer",

  "_type" : "external",

  "_id" : "1",

  "_version" : 1,

  "created" : true

}


위의 결과로 부터 우리는 새로운 customer 도큐먼트가 customer 인덱스의 external type에 성공적으로 생성되었음을 알 수 있다. 도큐먼트는 인덱싱 타임에 명시한 내부적인 id 1을 가진다.


엘라스틱 서치의 중요한 점은 도큐먼트를 인덱싱하기 저네 명시적으로 인덱스를 생성할 필요가 없다. 앞의 예제에서 만약 인덱스가 존재하지 않는다면 엘라스틱 서치는 자동적으로 인덱스를 생성한다.


이제 인덱스된 도큐먼트를 검색해보자.


curl -XGET 'localhost:9200/customer/external/1?pretty'


결과는 다음과 같다.


curl -XGET 'localhost:9200/customer/external/1?pretty'

{

  "_index" : "customer",

  "_type" : "external",

  "_id" : "1",

  "_version" : 1,

  "found" : true, "_source" : { "name": "John Doe" }

}


특별한 점 없이 잘 조회되는 것을 알 수 있다.


Delete an Index


이제 인덱스를 삭제하고 다시 모든 인덱스를 조회해보자.


curl -XDELETE 'localhost:9200/customer?pretty'

curl 'localhost:9200/_cat/indices?v'


결과는 다음과 같다.


{

  "acknowledged" : true

}



health index pri rep docs.count docs.deleted store.size pri.store.size


이것은 모든 인덱스가 성공적으로 삭제되었다는 것을 의미한다. 


이전의로 돌아가 다시 하나의 도큐먼트를 입력해보자. 인덱스를 삭제해보자.


curl -XPUT 'localhost:9200/customer'

curl -XPUT 'localhost:9200/customer/external/1' -d '

{

  "name": "John Doe"

}'

curl 'localhost:9200/customer/external/1'

curl -XDELETE 'localhost:9200/customer'


지금 까지 공부한 것을 생각해보면 엘라스틱 서치에는 데이터에 접근하는 다음과 같은 패턴이 있다는 것을 알 수 있다. 패턴을 요약해보면 아래와 같다.


curl -X<REST Verb> <Node>:<Port>/<Index>/<Type>/<ID>


이러한 REST 접근 패턴은 기억하기 쉽고 이해하기 쉬워서 엘라스틱 서치를 처음 학습할 때 편할 것이다.


반응형

'Programming > Elasticsearch' 카테고리의 다른 글

Elasticsearch - Mapping  (0) 2015.05.28
Elasticsearch - API Conventions  (1) 2015.05.28
Elasticsearch - Exploring Your Data  (0) 2015.05.27
Elasticsearch - Modifying Your Data  (0) 2015.05.27
Elasticsearch - 시작하기  (1) 2015.05.20
반응형

Getting Started


엘라스틱 서치는 고성능의 확장가능한 오픈소스 풀텍스트 검색 및 분석 엔진이다. 준실시간으로 대량의 데이터의 빠르게 저장할 수 있고 검색할 수 있고, 분석할 수 있다. 


다음은 엘라스틱 서치를 사용할 만한 유스케이스들이다.


  • 당신의 판매할 상품들을 위한 고객들이 검색할 수 있게 온라인 웹스토어를 실행킨다. 이런 경우 엘라스틱 서치에 모든 제품의 카탈로그를 저장하고, 상품들의 자동완성 추천을 할 수 있다.
  • 트렌드나 통계, 합산 등을 위해 로그 및 트랜잭션 데이터를 수집하기를 원한다. 이러한 경우 Logstash를 사용할 수 있다. 한번 데이터가 엘라스틱 서치에 저장되면 그것들을 검색하거나 조합할 수 잇다.
  • 가격 알림 플랫폼을 실행할 수 있다. 특정 가격 이하로 떨어진 상품을 추천하는 것이 예이다. 이러한 경우 엘라스틱 서치에 데이터를 저장하고 Percolator를 이용하여 리버스 서치를 이용할 수 있다. 
  • 빠르게 투자하고, 분석하고, 살펴보고 대량의 데이터에 즉답을 요청하여 분석 및 비즈니스에 활용하고 싶다면 이러한 경우 엘라스틱서치에 데이터를 저장하고 Kibana를 사용해서 커스텀 대시보드를 빠르게 만들 수 있다.

Basic Concepts

엘라스틱 서치에 핵심이 되는 몇가지 컨셉이 있다. 이러한 컨셉들을 이해하면 비약적으로 프로세스를 배우기가 쉬워질 것이다.

Near Realtime (NRT)

엘라스틱 서치는 NRT 검색 플랫폼이다. 약간 지연이 있다는 것을 의미한다. 일반적으로 1초 내외이며 검색되기 전에 도큐먼트를 인덱스하는 시간이 소요된다.

Cluster

클러스터는 전체 데이터를 하나 또는 그 이상의 노드의 집합에서 유지하고 있다는 것을 의미한다. 이는 통합 인덱싱 및 모든 노드를 통한 검색 기능을 제공한다는 것을 의미한다. 클러스터는 유니크한 이름으로 식별되며 기본은 "elasticsearch" 이다. 이름은 매우 중요하다. 노드는 하나의 클러스터의 부분이며, 만약 노드가 셋업 된다면 이름으로 클러스터에 조인한다. 가장 좋은 방법은 프로덕션의 클러스터명을 명시적으로 지정하는 것이다. 

Node

노드는 클러스터의 구성이 되는 싱글 서버 이다. 데이터를 저장하고, 클러스터 인덱싱에 참여하며 검색 기능을 제공한다. 클러스터와 마찬가지로 노드도 이름으로 식별된다. 기본은 랜덤 마블 캐릭터명이 시작시에 할당된다. 만약 기본값을 원하지 않는다면 어떤 이름도 지정할 수 있다. 이름은 관리 목적상 중요하다.

노드는 클러스터명에 의해 특정한 클러스터에 조인한다. 기본으로 각 노드는 elasticsearch라는 클러스터명으로 조인할려고 시도할 것이다. 만약 네트워크상에 기본값의 클러스터가 존재한다면 곤란하므로 클러스터명은 꼭 지정하도록 하자.

Index

인덱스는 비슷한 특성을 가진 도큐먼트의 집합이다. 예를 들어 고객 데이터를 위한 인덱스를 가질 수 있고, 프로덕트 카탈로그를 위한 인덱스를 가질 수 있다. 인덱스는 이름으로 식별되며 이름은 인덱스를 수행하거나, 검색하거나, 업데이트하거나 삭제를 수행할때 사용할 수 있다.

Type

인덱스내에 하나 혹은 몇몇의 타입을 정의할 수 있다. 타입은 인덱스의 논리적인 카테고리 또는 파티션이다. 일반적으로  도큐먼트를 위해 정의된 타입은 공통 필드의 집합을 가지고 있다. 예를 들면 블로그 플랫폼이 있고 하나의 인덱스에 데이터에 모든 데이터들이 저장된다고 가정하자. 이 인덱스에 유저 데이터를 위한 타입을 정의할 수 있고, 블로그 데이터를 위한 또 다른 타입을 정의할 수도 있으며 코멘트 데이터를 위한 또다른 타입을 정의할 수도 있다.

Document

도큐먼트는 인덱싱 될 수 있는 정보의 단위이다. 예를 들어 한명의 고객을 위한 도큐먼트가 있고, 하나의 제품을 위한 또다른 도큐먼트가 있을 수 있다. 도큐먼트는 JSON으로 표현된다.

인덱스나 타입내에 많은 도큐먼트들이 저장될 수 있다. 물리적으로는 인덱스내에 도큐먼트가 저장되지만 인덱스내에 타입으로 도큐먼트는 인덱스되거나 할당 될 수 있다.

Shard & Replicas

인덱스는 잠재적으로 싱글 노드의 하드웨어 용량을 초과하는 대량의 데이터를 저장될 수 있다. 예를 들어 수백만의 도큐먼트가 하나의 인덱스에 저장되고 1TB의 용량을 초과한다면 검색 요청은 매우 느리게 동작하게 될 것이다. 

이러한 문제를 해결하기 위해 엘라스틱 서치는 shard라고 불리는 다수의 조각으로 인덱스를 분리하는 기능을 제공한다. 인덱스를 생성하며 원하는 만큼 샤딩을 지정할 수 있다. 각 샤드는 모든 기능을 수행하는 독립된 인덱스이다.

샤딩은 두가지 이유에서 매우 중요하다.
  • 컨텐츠의 볼륨을 수평적으로 분할 및 확장할 수 있다.
  • 샤드에 분산되고 병렬의 작업을 수행할 수 있으므로, 성능이나 처리량을 증대시킬수 있다.
샤드에 어떻게 분산되는지에 대한 메커니즘과 또한 검색 요청에 도큐먼트를 조합하여 돌려주는 것에 대한 것은 전적으로 엘라스틱 서치에 의해 관리된다

네트워크나 클라우드 환경에서 언제든 고장이 발생할 수 있다. 이러한 이유에서 샤드 노드는 오프라인이 되거나 사라져 버릴 수 있다. 이러한 경우에 대비하여 엘라스틱 서치는 인덱스의 샤드의 복제 샤드라고 불리는 복제본을 만든다.

리플리케이션은 주요한 두가지 이유는 다음과 같다.
  • 샤드나 노드가 고장났을때 고가용성을 제공한다. 이러한 이유에서 복제 샤드는 같은 노드에 위치해서는 안된다.
  • 검색은 모든 복제본에서 수행되기 떄문에 볼륨이나 처리량을 스케일 아웃을 할 수 있다.
요약하자면 각 인덱스는 다수의 샤드로 분할 될 수 있다. 인덱스는 또한 여러벌의 복제본을 가질 수 있다. 샤드의 수와 복제본의 수는 인덱스를 생성하는 시점에 정의할 수 있다. 인덱스가 생성된 후에도 복제본의 수의 동적으로 변경할 수 있다. 하지만 샤드의 수는 after-the-fact 변경할 수 없다.

기본적으로 엘라스틱 서치에서 각 인덱스는 5개의 샤드와 하나의 복제본이 할당된다. 이 의미는 클러스터에 적어도 두개의 노드가 있어야 한다는 것을 의미한다. 그리고 5개의 샤드당 하나의 복제본이 지정되므로 하나이 인덱스당 총 10개의 샤드를 가지게 될 것이다.

Installation

엘라스틱 서치는 Java7을 요구한다. 더 정확하게 말하자면 Oracle JDK 버젼 1.8.0_25를 추천한다. 자바 설치는 플랫폼에 따라 다양한 방식이므로 여기서는 자세한 설명을 생략한다. 엘라스틱 서치를 설치하기 전에 수행중인 자바 버젼을 체크하자.

java -version
echo $JAVA_HOME

자바가 설치되어 있다면 엘라스틱 서치를 다운로드 받고 실행할 수 있다. 바이너리는 www.elastic.co/downloads에서 이용가능하다. 각 릴리이즈는 zip 또는 tar 압축이거나 DEB 또는 RPM 패키지이다. 가장 간단하게 tar 파일을 이용하자.

엘라스틱 서치 1.5.2를 다음과 같이 다운로드 하자. (윈도우 유저는 아래와 같이 하면 안된다.)

curl -L -O https://download.elastic.co/elasticsearch/elasticsearch/elasticsearch-1.5.2.tar.gz

그리고 다음과 같이 압축을 풀자.

tar -xvf elasticsearch-1.5.2.tar.gz

현재 디렉토리에 파일과 폴더들이 생성될 것이다. bin 디렉토리를 접근하자.

cd elasticsearch-1.5.2/bin

이제 우리는 싱글 클러스터와 우리의 노드를 시작할 준비가 되었다.

./elasticsearch

만약 모든 명령이 잘 수행되었다면 다음과 같은 메시지를 확인할 수 있을 것이다. 

./elasticsearch
[2014-03-13 13:42:17,218][INFO ][node           ] [New Goblin] version[1.5.2], pid[2085], build[5c03844/2014-02-25T15:52:53Z]
[2014-03-13 13:42:17,219][INFO ][node           ] [New Goblin] initializing ...
[2014-03-13 13:42:17,223][INFO ][plugins        ] [New Goblin] loaded [], sites []
[2014-03-13 13:42:19,831][INFO ][node           ] [New Goblin] initialized
[2014-03-13 13:42:19,832][INFO ][node           ] [New Goblin] starting ...
[2014-03-13 13:42:19,958][INFO ][transport      ] [New Goblin] bound_address {inet[/0:0:0:0:0:0:0:0:9300]}, publish_address {inet[/192.168.8.112:9300]}
[2014-03-13 13:42:23,030][INFO ][cluster.service] [New Goblin] new_master [New Goblin][rWMtGj3dQouz2r6ZFL9v4g][mwubuntu1][inet[/192.168.8.112:9300]], reason: zen-disco-join (elected_as_master)
[2014-03-13 13:42:23,100][INFO ][discovery      ] [New Goblin] elasticsearch/rWMtGj3dQouz2r6ZFL9v4g
[2014-03-13 13:42:23,125][INFO ][http           ] [New Goblin] bound_address {inet[/0:0:0:0:0:0:0:0:9200]}, publish_address {inet[/192.168.8.112:9200]}
[2014-03-13 13:42:23,629][INFO ][gateway        ] [New Goblin] recovered [1] indices into cluster_state
[2014-03-13 13:42:23,630][INFO ][node           ] [New Goblin] started

너무 자세히 살펴보는 것은 피하겠다. 위에서 설펴보면 우리는 노드명은 "New Goblin" 라는 이름으로 시작되었다는 것을 알 수 있고, 싱글 클러스터에서 자신의 마스터로 선출된 것을 알 수 있다. 아직의 마스터의 의미를 알 필요는 없다. 여기서 가장 중요한 것은 하나의 클러스터에 하나의 노드가 시작되었다는 것이다. 

앞에서 언급했다시피, 클러스터나 노드명은 오버라이드할 수 있다. 엘라스틱 서치를 싲가할때 다음과 같이 커맨드라인에 명시하면 된다.

./elasticsearch --cluster.name my_cluster_name --node.name my_node_name

또한 HTTP address (192.168.8.112) 와 port (9200)가 우리의 노드에 접근할 수 있는 주소이다. 기본으로 엘라스틱 서치를 REST API을 제공하기 위해 9200 포트를 사용한다. 이 포트는 필요에 의해 변경할 수 있다.


반응형

'Programming > Elasticsearch' 카테고리의 다른 글

Elasticsearch - Mapping  (0) 2015.05.28
Elasticsearch - API Conventions  (1) 2015.05.28
Elasticsearch - Exploring Your Data  (0) 2015.05.27
Elasticsearch - Modifying Your Data  (0) 2015.05.27
Elasticsearch - Exploring Cluster  (2) 2015.05.22

+ Recent posts