엔지니어링

Elasticsearch Aggregation 및 D3를 통한 데이터 시각화.

2015년 11월 2일 추가 : Elasticsearch 2.0+ 이상에서 더 이상 facet은 지원되지 않는다는 점을 명시하십시오.

Elasticsearch 에 친숙한 분들이라면 Elasticsearch가 Apache Lucene역 인덱스 를 핵심으로 하는 굉장히 확장성이 있고 전문 검색이 가능한 최신 검색 엔진이라는 점을 알고 계실 것입니다. Elasticsearch를 사용하여 데이터에 대한 질의를 하고 엄청나게 효율적이고 빠른 속도로 문서를 찾을 수 있어 실시간 분석 대시보드를 생성하기에 매우 적합합니다.

현재, Elasticsearch는 사용자가 데이터 집계를 연산할 수 있는 facet 검색 기능을 지원합니다. 예를 들어 트윗 데이터를 가지고 있는 사용자는 date histogram facet을 사용하여 연, 분기, 월, 일, 주, 시간 또는 분당 트윗 개수에 대한 버킷을 만들 수 있어 히스토그램 생성이 매우 간단해집니다.

facet 검색은 데이터 시각화에 대해 강력한 기능을 가지고 있습니다. Kibana 는 facet을 유용하게 사용하는 프런트 엔드 인터페이스의 좋은 예입니다. 그러나 facet에는 몇 가지 주요 제약이 있습니다. facet에는 어떤 문서가 어떤 버킷에 속하는지에 대한 정보가 없기 때문에 복합 질의 작업이 어려워집니다. 이것이 바로 Elasticsearch가 1.0 릴리스에 aggregation 프레임워크를 도입한 이유입니다. aggregation은 facet의 제한을 보완하고 개발자들이 훨씬 더 다양한 시각화 기능을 구현할 수 있는 가능성을 제시합니다.

Aggregation (=환상적입니다!)

aggregation은 "facet 의 재 탄생" 입니다. aggregation은 facet의 모든 기능을 내포하고 있을 뿐만 아니라 훨씬 강력한 기능들도 제공합니다. aggregation은 이 새로운 기능의 총칭이며, 실제로 다양한 유형의 aggregation(집계)을 작성할 수 있는 매우 강력한 프레임워크 입니다. aggregation은 다양한 유형이 있지만 크게 bucket과 metric이라는 2개의 범주로 나뉩니다. bucket aggregation은 다양한 종류의 bucket들을 생성하는데, 각 bucket은 그에 속하는 도큐먼트의 세트로 구성됩니다(예: terms, range, date range, histogram, date histogram, geo distance). metric aggregation은 하나의 도큐먼트 세트에 대해 지속적인 추적과 연산을 수행합니다(예: 최소, 최대, 합계, 평균, 통계, 확장 통계).

(D3 와) Aggregation 을 이용한 데이터 시각화

지금부터 데이터 시각화를 위한 aggregation의 강력한 기능들을 확인해 보십시오. 이제 Elasticsearch aggregation 프레임워크, Elasticsearch JavaScript 클라이언트D3 를 사용하여 도넛 차트와 덴드로그램(dendrogram)을 만들어보겠습니다.

Elasticsearch를 처음 사용하는 사람들도 아주 쉽게 시작할 수 있습니다. Elasticsearch 개요 페이지를 방문해서 Elasticsearch 버전 1.0을 다운로드, 설치 및 실행하는 방법을 알아보십시오.

요구 사항:

  1. elasticsearch.js
  2. d3.v3.js (또는 html의 스크립트 태그에 D3 소스 포함)
  3. require.js (require.js로 파일 저장)
  4. nfl_2013.json 및 nfl_mapping.json(다운로드)
  5. 텍스트 편집기
  6. 웹 서버

* index.html 및 메인 JavaScript 파일에 대한 링크는 여기에서 찾을 수 있습니다. 또는 GitHub 에서 프로젝트를 복제할 수도 있습니다.

Elasticsearch 인덱스로 데이터 업로드

이제 index를 만들고 몇 가지 데이터를 추가해야 합니다.

미식 축구 시즌이 이제 막 끝났으니, NFL 데이터를 탐색하는 것으로 시작해 보겠습니다. 이 튜토리얼에서는 터치다운고려하면 됩니다. 위의 링크를 사용하여 데이터 세트(nfl_2013.json)와 매핑(nfl_mapping.json)을 다운로드하십시오.

로컬 호스트에서 Elasticsearch를 시작합니다. 이제 index와 몇 가지 데이터를 추가합니다. 터미널에서 다음과 같이 실행합니다.

curl -XPOST localhost:9200/nfl?pretty
curl -XPUT localhost:9200/nfl/2013/_mapping?pretty -d @nfl_mapping.json
curl -XPOST localhost:9200/nfl/2013/_bulk?pretty --data-binary @nfl_2013.json

NFL 데이터 파일은 18개의 필드를 갖는 경기별 데이터로 구성됩니다.

{
    def: "HOU", // defensive team
    defscore: "13", // score for defensive team
    description: "(11:34) S.Ridley right guard for 8 yards TOUCHDOWN.", // play description
    down: "2", // down for current play (up to 4)
    gameid: "20130113_HOU@NE", // game id with date and teams
    min: "26", // minutes remaining in game
    nextscore: "7", // ???
    off: "NE", // offensive team
    offscore: "17", // score for offensive team
    qtr: "3", // quarter
    scorechange: "0", // amount offensive score changed
    scorediff: "4", // difference in score
    season: "2012", // nfl season (year)
    sec: "34", // seconds remaining within minute of play
    series1stdn: "1", // ???
    teamwin: "1", // either 1 or 0, demarks the team winning
    togo: "6", // number of yards to go to reach a first down
    ydline: "8" // yard line ball is on to start play
}

HTML 및 JavaScript 파일 설정

프로젝트를 위한 디렉토리를 만드는 것부터 시작해 보겠습니다. 프로젝트명은 nfl이라고 하겠습니다. 이제 index.html과 스크립트 하위 디렉토리를 추가합니다. 다운로드한 elasticsearch.js, d3.js, require.js 파일을 스크립트 디렉토리로 옮깁니다. 스크립트 디렉토리에 main.js 파일도 추가 할 것입니다.

index.html에 변경 내용을 추가합니다.

<!DOCTYPE html>
<html>
    <head>
        <title>Elastic Aggregations</title>
        <script src="scripts/require.js"></script>
        <script>require(["scripts/main"], function () {})</script>
        <style></style>
    </head>
    <body>
        <div id="donut-chart"></div>
    </body>
</html>

이제 main.js에 JavaScript를 추가합니다.

define(['scripts/d3.v3', 'scripts/elasticsearch'], function (d3, elasticsearch) {
    "use strict";
    var client = new elasticsearch.Client();
});

Terms Aggregation 을 이용한 도넛 차트 구성

각 쿼터에 득점된 터치다운 횟수를 보여주는 도넛형 차트를 만드는 것으로 시작해 보겠습니다. 여기에는 Terms Aggregation 이 사용될 것입니다. 이 작업은 terms facet으로도 수행할 수 있지만, aggregation으로 facet과 동일한 결과를 얻을 수 있다는 점을 보여주기 위해 terms aggregation을 사용하였습니다.

일반적으로 NFL 경기는 4쿼터로만 진행되므로 4개의 조각이 있는 도넛형 차트를 만들어야 합니다. 일반적으로 NFL 경기는 4쿼터로만 진행되므로 4개의 조각이 있는 도넛형 차트를 만들어야 합니다. 또한 터치다운에 대해서도 필터링해야 합니다.

그러면 Elasticsearch JavaScript 클라이언트를 사용하여 쿼리를 만들고 main.js에 추가해 보겠습니다.

define(['scripts/d3.v3', 'scripts/elasticsearch'], function (d3, elasticsearch) {
    "use strict";
    var client = new elasticsearch.Client();
    client.search({
        index: 'nfl',
        size: 5,
        body: {
            // Begin query.
            query: {
                // Boolean query for matching and excluding items.
                bool: {
                    must: { match: { "description": "TOUCHDOWN" }},
                    must_not: { match: { "qtr": 5 }}
                }
            },
            // Aggregate on the results
            aggs: {
                touchdowns: {
                    terms: {
                        field: "qtr",
                        // order by quarter, ascending
                        order: { "_term" : "asc" }
                    }
                }
            }
            // End query.
        }
    }).then(function (resp) {
        console.log(resp);
        // D3 code goes here.
    });
});

웹 서버를 시작하고 나면, 브라우저를 열고 개발자 도구 아래에서 콘솔을 탐색하여 결과를 확인할 수 있습니다. 데이터를 확인해 보세요!

이제 도넛형 차트를 만들어 보겠습니다. 도넛형 차트 D3 코드를 main.js에 추가합니다.

...
    }).then(function (resp) {
        console.log(resp);
        // D3 code goes here.
        var touchdowns = resp.aggregations.touchdowns.buckets;
        // d3 donut chart
        var width = 600,
            height = 300,
            radius = Math.min(width, height) / 2;
        var color = ['#ff7f0e', '#d62728', '#2ca02c', '#1f77b4'];
        var arc = d3.svg.arc()
            .outerRadius(radius - 60)
            .innerRadius(120);
        var pie = d3.layout.pie()
            .sort(null)
            .value(function (d) { return d.doc_count; });
        var svg = d3.select("#donut-chart").append("svg")
            .attr("width", width)
            .attr("height", height)
            .append("g")
            .attr("transform", "translate(" + width/1.4 + "," + height/2 + ")");
        var g = svg.selectAll(".arc")
            .data(pie(touchdowns))
            .enter()
            .append("g")
            .attr("class", "arc");
        g.append("path")
            .attr("d", arc)
            .style("fill", function (d, i) { return color[i]; });
        g.append("text")
            .attr("transform", function (d) { return "translate(" + arc.centroid(d) + ")"; })
            .attr("dy", ".35em")
            .style("text-anchor", "middle")
            .style("fill", "white")
            .text(function (d) { return d.data.key; });
    });
});

index.html에 몇 가지 스타일을 추가합니다.

<style>
    body {
        font: 14px sans-serif;
    }
    .arc path {
        stroke: #fff; 
        stroke-width: 3px;
    }
</style>

브라우저에서 페이지를 새로 고치면 근사한 도넛형 차트가 표시됩니다.

물론 terms facet을 사용해서도 동일한 도넛형 차트를 만들 수 있습니다. 이번에는 좀 더 복잡한 작업을 시도해 보겠습니다.

Nested Terms Aggregation을 통한 덴드로그램(Dendrogram)

이제 2013년 시즌(12주 동안 진행) 동안 각 팀별로 터치다운을 득점한 선수의 이름과 이 선수가 각 쿼터에서 득점한 총 터치다운 횟수를 알고 싶다고 가정해 보겠습니다. 예를 들어 Denver Broncos(NFL 팀)의 Peyton Manning(선수)은 다음과 같이 36회의 터치다운을 기록했습니다.

  1. 1쿼터 터치다운 9회
  2. 2쿼터 터치다운 6회
  3. 3쿼터 터치다운 12회
  4. 4쿼터 터치다운 9회

이 쿼리를 작성해 봅시다. 보다 정확한 수치를 얻기 위해 인컴플리트 패스, 인터셉션, 펌블, 오버턴드 플레이는 필터링을 통해 걸러내야 합니다.

define(['scripts/d3.v3', 'scripts/elasticsearch'], function (d3, elasticsearch) {
    "use strict";
    var client = new elasticsearch.Client();
    client.search({
        index: 'nfl',
        size: 5,
        body: {
            query: {
                bool: {
                    must: { match: { "description": "TOUCHDOWN"}},
                    must_not: [
                        { match: { "description": "intercepted"}},
                        { match: { "description": "incomplete"}},
                        { match: { "description": "FUMBLES"}},
                        { match: { "description": "NULLIFIED"}}
                    ]
                }
            },
            aggs: {
                teams: {
                    terms: {
                        field: "off",
                        exclude: "", // exclude empty strings.
                        size: 5 // limit to top 5 teams (out of 32).
                    },
                    aggs: {
                        players: {
                            terms: {
                                field: "description",
                                include: "([a-z]?[.][a-z]+)", // regex to pull out player names.
                                size: 20 // limit to top 20 players per team. 
                            },
                            aggs: {
                                qtrs: {
                                    terms: {
                                        field: "qtr"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }).then(function (resp) {
        console.log(resp);
        // D3 code goes here.
    });
});

콘솔에서 결과를 확인할 수 있습니다.

* 팀 노드는 리프 노드의 합계보다 적은 터치다운 횟수를 갖는다는 점에 유의하십시오. 그 이유는 패스로 인한 터치다운은 선수가 패스를 받을 때와 쿼터백 (예: Peyton Manning)이 패스를 할 때가 각각 1회로 계산되어 2배로 계산되기 때문입니다.

덴드로그램을 만들어 보겠습니다. index.html을 수정할 것입니다.

...
<body>
    <div id="donut-chart"></div>
    <div id="dendrogram"></div>
</body>
...

D3 코드를 main.js에 추가합니다.

...
}).then(function (resp) {
    console.log(resp);
    // D3 code goes here.
    var root = createChildNodes(resp);
    // d3 dendrogram
    var width = 600,
        height = 2000;
    var color = ['#ff7f0e', '#d62728', '#2ca02c', '#1f77b4'];
    var cluster = d3.layout.cluster()
        .size([height, width - 200]);
    var diagonal = d3.svg.diagonal()
        .projection(function(d) { return [d.y, d.x]; });
    var svg = d3.select("#dendrogram").append("svg")
        .attr("width", width)
        .attr("height", height)
        .append("g")
        .attr("transform", "translate(120,0)");
    var nodes = cluster.nodes(root),
        links = cluster.links(nodes);
    var link = svg.selectAll(".link")
        .data(links)
        .enter().append("path")
        .attr("class", "link")
        .attr("d", diagonal);
    var node = svg.selectAll(".node")
        .data(nodes)
        .enter().append("g")
        .attr("class", "node")
        .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
    node.append("circle")
        .attr("r", 4.5)
        .style("fill", function (d) {
            return d.children ? "#ffffff" : color[d.key - 1];
        })
        .style("stroke", function (d) {
            return d.children ? "#4682B4" : color[d.key - 1];
        });
    node.append("text")
        .attr("dx", function(d) { return d.children ? -8 : 8; })
        .attr("dy", 3)
        .style("text-anchor", function(d) { return d.children ? "end" : "start"; })
        .text(function(d) { return d.children? d.key : d.key + ": " + d.doc_count; });
    d3.select(self.frameElement).style("height", height + "px");
    function createChildNodes(dataObj) {
        var root = {};
        root.key = "NFL";
        root.children = dataObj.aggregations.teams.buckets;
        root.children.forEach(function (d) { d.children = d.players.buckets; });
        root.children.forEach(function (d) { 
            d.children.forEach(function (d) { 
                d.children = d.qtrs.buckets; 
            });
        });
        return root;
    }
});
...

그리고 마지막으로 스타일을 추가합니다.

<style>
    ...
    .node circle {
        fill: #fff;
        stroke: steelblue;
        stroke-width: 1.5px;
    }
    .node {
        font: 10px sans-serif;
    }
    .link {
        fill: none;
        stroke: #ccc;
        stroke-width: 1.5px;
    }
</style>

브라우저를 새로고침 하면 멋진 덴드로그램이 표시됩니다.

dendrogram

이와 같이 aggregation은 시각화 자료를 생성하는 강력한 도구입니다. 물론 여기에서 만든 것보다 훨씬 풍부하고 역동적인 시각화 자료를 생성할 수 있지만 위의 예시가 좋은 기초가 되길 바랍니다. 즐거운 검색 되세요!