Rails 4.2 and Elastic Search – Querying and Filtering Across Multiple Nested Models

At work recently I have been working on a new filtering and search page for ourselves. The requirements were searching and filtering said search results. Things get complicated and performance tends to suffer when dealing with complicated and far reaching relationships. You end joining every table in the database and even then you are only filtering and not actually searching. Sometimes denormalized data just makes sense. It’s still a WIP but here it as anyway, hope this snippet helps you get rolling with your project.

def self.search(query="", options={})

   params = {
      query: {
        filtered: {
          query: {
            multi_match: {
              query: query,
              fields: ['title^10', 'overview']
            },
            match_all: {}
          },
          filter: {
            bool: {
              must: [
                {
                  nested: {
                    path: 'regions',
                    filter: {
                      bool: {
                        must: [
                          {
                            terms: { 'regions.id' => options[:regions] }
                          }
                        ]
                      }
                    }
                  }
                },
                {
                  nested: {
                    path: 'genres',
                    filter: {
                      bool: {
                        must: [
                          {
                            terms: { 'genres.id' => options[:genres] }
                          }
                        ]
                      }
                    }
                  }
                }
              ]
            }
          }
        }
      },
      sort: [
        {
          options[:col].try(:downcase) => {
            order: options[:direction].try(:downcase) #, ignore_unmapped: true
          }
        }
      ]
    }

    params[:query][:filtered][:query].delete(:match_all) if query.present?
    params[:query][:filtered][:query].delete(:multi_match) if query.blank?
    params.delete(:sort) if options[:col].blank? || options[:direction].blank?

    __elasticsearch__.search(params).page(options[:page]).per(options[:limit])
  end

 

Angular JS persisting data across controller instances

Where do we want to store our data in Angular JS and how does it flow through your web/mobile app. This is especially important if you are using Angular JS in the mobile context. Recently I’ve been working on some Ionic Framework apps. Here is my example.

The user leaves the page then navigates back. We are duplicating API calls and losing the users context within the data. Behind the scenes we did this:

  1. Initialize controller A
  2. Load data set A into controller A via API call
  3. Load page B
  4. Initialize controller A
  5. Load data set A into controller A via API call

Alternatively we could have done this:

  1. Initialize controller A
  2. Load data set A into service A via API call
  3. Pass data from service A to Controller A via a promise
  4. Load page B
  5. Initialize controller A
  6. Load cached data set A from service A

An example of a controller and service pairing might look like this (NOTE: This is specific to my Ionic Cordova mobile app, but you can get the idea. Also note that the more function returns a promise):

.controller('HotCtrl', function($scope, UtilService, HotService) {
 UtilService.trackGAView('Hot');
 $scope.util = UtilService;
 $scope.movies = HotService.all();

 $scope.doRefresh = function() {
   HotService.clear();
   $scope.movies = [];
   setTimeout(function() {
     $scope.$broadcast('scroll.refreshComplete');
     $scope.moreMovies();
   }, 700);
 };

 $scope.moreMovies = function() {
   $scope.util.loading(true);
   HotService.more().$promise.then(function(){
     $scope.movies = HotService.all();
     setTimeout(function() {
       $scope.$broadcast('scroll.infiniteScrollComplete');
     }, 200);
     $scope.util.loading(false);
     $scope.hasMore = HotService.hasMore();
   });
 };
})

.factory('HotService', ['$resource', 'MovieService', 'UtilService', function($resource, MovieService, UtilService) {
 var page = 1;
 var movies = [];
 var regions = UtilService.getRegions();
 var hasMore = true;
 
 return hotObj = {
   all: function() {
     if(regions !== UtilService.getRegions()) {
       regions = UtilService.getRegions();
       hotObj.clear();
     }
     return movies;
   },
   more: function() {
     options = {page: page, n: 'hot', per_page: 500};
     return MovieService.query(UtilService.getParams(options), null, function(response, headers){
       if(response.length < 1) {
         hasMore = false; 
       } else {
       hasMore = true;
     }
     page++;
     angular.forEach(response, function(value, key) {
       movies.push(value);
     });
     },
       function(response) {
     });
   },
   get: function(index) {
     return movies[index];
   },
   hasMore: function(){
     return hasMore;
   },
   clear: function() {
     page = 1;
     movies = [];
     hasMore = true;
   }
 }
}])

Angular JS reusable controller functions

So this problem initially started when I found myself repeating similar functions across controllers. I also wanted to be able to access these functions from the view, which is why they are attached to the $scope. Notice both controllers contain the same function. I suppose we could attach this function to the $rootScope but that sounds messier and harder to test. Also referencing that from the view would not be as clean.

<a href="" ng-click="linkTo('https://www.digitalocean.com/?refcode=0fb20044d22d')" />

app.controller('MoviesCtrl',['$scope', function($scope){
  $scope.linkTo = function(link) {
    window.open(link, '_system');
  };
});

app.controller('TvShowsCtrl',['$scope', function($scope){
  $scope.linkTo = function(link) {
    window.open(link, '_system');
  };
});

My preferred solution is to this is

<a href="" ng-click="util.linkTo('https://www.digitalocean.com/?refcode=0fb20044d22d')" />

app.factory('UtilService', function() {
  return {
    linkTo: function(link) {
      window.open(link, '_system');
    }
  }
});

app.controller('MoviesCtrl',['$scope', 'UtilService', function($scope, UtilService){
  $scope.util = UtilService;
});

I like to use a utility service like this dry out my controllers. This was especially useful in my most recent Ionic Cordova app.