Getting Started with elasticsearch and AngularJS: Part1 - Searching
Feb 28, 2013  (egaumer)

The ability to deliver sophisticated client-side JavaScript applications is an important aspect of data discovery and visualization. It’s no secret that elasticsearch is phenomenal at extracting meaning from enormous data sets in near real-time. Exposing that power to end users requires equally impressive applications.

Elasticsearch has made search more approachable by exposing Web friendly APIs (REST + JSON) that reduce the impedance mismatch associated with relational models, at no sacrifice to query capability. On the other side of the equation, AngularJS simplifies the effort required to build highly interactive, data-driven Web applications.

The goal here is to write a series of articles that help folks gain some insight into how these technologies fit together. I’ll start off with some basics behind AngularJS and build on that functionality in later articles. Once a solid foundation has been established, I’ll show how to leverage D3 to create interactive visualizations of responses from elasticsearch.

Getting Started

Loading Data

Throughout the series, I’ll be using the StackOverflow data that Matt used in this post, which also describes how to aquire and load the data. I recommend just grabbing a subset of maybe 100K records to get started. You’re free to use your own data but you’ll need to translate some of the examples.

Application Module

Angular provides its own module system for loading and bootstrapping applications. We need to define a root level module which we’ll later provide to angular using the ng-app directive. I’ll call this module demo.

// app.js
angular.module('demo', []);

This module currently has no dependencies but we’ll be adding some shortly.

Creating a Search Controller

The next thing we’ll need is the ability to execute searches against elasticsearch. We’re going to use elastic.js which provides an AngularJS client in the form of what Angular refers to as a service. The service is in a module called elasticjs.service which we’ll import into our application module. While we’re at it, lets import a (yet to be defined) controllers module.

// app.js
angular.module('demo', [
    'controllers', 
    'elasticjs.service'
]);

Go ahead and create the controllers module and add a new controller called SearchCtrl.

// controllers.js
angular.module('controllers', [])
    .controller('SearchCtrl', function($scope) {});

I’m using angular’s fluent interface to chain methods together which is a common pattern throughout many JavaScript APIs. This pattern is used heavily in elastic.js and D3 as well.

The controller() method creates a new controller from the given name and function. You’ll notice the function takes what appears to be an argument ($scope). As it turns out, this is not really an argument, it’s a dependency that angular will inject at runtime using dependency injection. This is another interesting feature of angular.

Throughout angular, $scope is essentially the context that binds the DOM to the controller. Anything we place into the $scope will automatically be bound and accessible to the DOM, and vice versa.

The elastic.js module (elasticsjs.service) has already been imported so now we can inject its ejsResource service into the controller. Once the service has been injected, we can use it to bind to an instance of elasticsearch.

// controllers.js
angular.module('controllers', [])
    .controller('SearchCtrl', function($scope, ejsResource) {

        var ejs = ejsResource('http://localhost:9200');

    });

From this point forward, elastic.js will handle all the marshalling of data to/from elasticsearch for us.

Executing Searches

We have a controller that’s able to communicate with elasticsearch so the next step is to write some code that takes the user’s input, sends it to elasticsearch, and handles the response. Thanks to elastic.js, this requires just a few additional lines of JavaScript code.

// controllers.js
angular.module('controllers', [])
    .controller('SearchCtrl', function($scope, ejsResource) {

        var ejs = ejsResource('http://localhost:9200');

        var oQuery = ejs.QueryStringQuery().defaultField('Title');

        var client = ejs.Request()
            .indices('stackoverflow')
            .types('question');

        $scope.search = function() {
            $scope.results = client
                .query(oQuery.query($scope.queryTerm || '*'))
                .doSearch();
        };

    });

Most of the code I’ve added is just elastic.js query builder syntax which is documented here. At this point, the controller is done. In fact, we’ve written all the JavaScript we need.

Yes, it really was that easy.

Displaying Results

AngularJS uses DOM based templating, meaning that data is rendered directly to the DOM. All we have to do is provide instructions (what Angular calls directives) and the browser, with help from Angular, will render the node according the these instructions.

Example Directive

<!-- create an <li> node for each item in items and populate it with the title property -->
<li ng-repeat="item in items">
    {{ item.title }}
</li>

This is one example of an angular directive (ng-repeat) but it outlines the general pattern we’ll use to bind data to the DOM. An interesting (and differentiating) feature of angular is the ability to create custom directives, allowing you to extend the browser’s capabilities. We’ll revisit this topic in a later post where we’ll create our own custom directive.

Below is a very basic index.html page that contains an input box and a search button. With just a few angular directives, we can bind the DOM to our controller logic to create an application that allows users to search through StackOverflow questions.

Result titles are displayed below the search input using a simple list format (for the sake of clarity).

<!DOCTYPE html>
<html ng-app="demo">
    <head>
        <link rel="stylesheet" href="/common/css/bootstrap.min.css">

        <!-- project dependency libs -->
        <script src="/common/lib/angular.min.js"></script>
        <script src="/common/lib/elastic.min.js"></script>
        <script src="/common/lib/elastic-angular-client.min.js"></script>
          
        <!-- project specific files -->
        <script src="js/app.js"></script>
        <script src="js/controllers.js"></script>
    </head>

    <body ng-controller="SearchCtrl">
        <div class="container-fluid">
            <div class="row-fluid">
                <span class="span3">
                    <input class="input-block-level" ng-model="queryTerm" type="text">
                </span>&nbsp;
                <button ng-click="search()" class="btn" type="button">Search</button>
            </div>

            <div class="row-fluid">
                <div class="span4">
                    <li ng-repeat="doc in results.hits.hits">
                        {{ doc._source.Title }}
                    </li>
                </div>
            </div>
        </div>
    </body>
</html>

Here’s a basic breakdown of the directives I used.

ng-app Tells angular how to load the application. Points to the root level module.
ng-controller Tells angular which controller scope to bind to. Binds this node and all child nodes.
ng-model Binds the input’s text to the $scope.queryTerm variable of the controller.
ng-click Binds the button’s click event to the controllers search function.
ng-repeat Stamps out an <li> node for each item in the array (in this case the elasticsearch hits array).

A complete list of angular directives can be found in the API documentation.

Wraping Up

It should be pretty obvious that this didn’t require a deep knowledge of Web development. There were no (exposed) Ajax calls, no jQuery code, and no direct DOM manipulation. Angular does a really nice job of reducing all that noise and complexity to the point where you almost forget you’re working in the browser. The code feels more like something you’d write in a server-side framework.

I’ll follow up with more articles that continue to extend on what I’ve done here. In the next article, we’ll add the ability to filter the results by Tag name using facets.


comments powered by Disqus