ElasticSearch – In production

Voici un très bon article en Anglais, concernant tous les aspects de l’utilisation d’ElasticSearch en production :

https://www.found.no/foundation/elasticsearch-in-production/

Memory
Search engines are designed to deliver answers fast. Really fast. To do this, most of the data structures they use must reside in memory. To a large extent, they assume you provide them with enough memory to do so. This can lead to problems when that is not the case – not just with performance, but also with your cluster’s reliability.
Security
Elasticsearch does not consider authentication or authorization to be its job (which is perfectly fine!), so it has no features for it. Thus, there are several things developers must be aware of, to avoid disclosing data that should be private, being denied service due to prohibitively expensive queries, or letting users run arbitrary code with access to anything Elasticsearch has access to.
Networking
Elasticsearch works brilliantly on a single machine, and easily lets you scale out to multiple machines when your data size requires it. It is impressively easy to use for a distributed system, but distributed systems are complicated – and can fail in many ways.
Client-side considerations
Assuming you have a reliable cluster, there are still some things you need to get right in your clients/applications to be reliable and performant.

https://www.found.no/foundation/elasticsearch-in-production/

ElasticSearch – Alias et filtres en action

Article rédigé et publié par Yannick Pereira-Reis expert ElasticSearch, Symfony et AngularJS.

Avant de commencer, décrivons le besoin que nous avons en termes d’indexation et recherche de contenu pour notre exemple, nous détaillerons ensuite comment implémenter la meilleure solution avec ElasticSearch.

Notre application contient de nombreux utilisateurs dont les données (images, vidéos, textes, pdf, …) doivent être indéxées afin d’offrir une recherche multicritères performante sur ces documents. Chaque utilisateur ne pourra rechercher que parmi les documents lui appartenant.

The « Users » data flow

La solution qui vient à l’esprit dans un premier temps consiste en la création d’un index ElasticSearch par utilisateur. Cette méthode à différents avantages :

  • L’indexation et la recherche sont des mécanismes très simples et intuitifs car pour un utilisateur donné tous les documents sont stockés dans le même index.
  • ElasticSearch permet de contrôler le nombre de « shards » et de « replicas » par index, ce qui permet de définir des configurations différentes en fonctions du volume de données indexées pour chaque utilisateur.

Avec ce « pattern » les recherches sont très simples :

curl -XPUT localhost:9200/user_1/documents/_search -d '{...}'
curl -XPOST localhost:9200/user_1/documents -d '{...}'

Malheureusement cette solution a un inconvénient majeur : avec le succès de l’application et l’augmentation du nombre d’utilisateurs, il y a une augmentation du nombre d’index, du nombre de « shards » et de « replicas » qui ont besoin de beaucoup de ressources système, un shard étant une instance Lucene. Avec quelques milliers d’utilisateurs, on peut faire tomber un petit cluster ElasticSearch très facilement.

Il faut également noter que le nombre de documents indexés peut grandement varier d’un utilisateur à l’autre, ainsi certains « shards » n’indexent que quelques documents alors qu’ils pourraient en indexer des millions. Donc un gaspillage important de ressources système.

 L’utilisation des alias et des filtres

Puisqu’on ne souhaite pas que le nombre de « shards » explose, il nous faut diminuer le nombre d’index. On pourrait par exemple indexer tous les documents de tous les utilisateurs dans un seul index dédié aux utilisateurs, en stockant un identifiant dans le document pour être capable de filtrer par utilisateur.

Cette approche a elle aussi un inconvénient majeur : une recherche de documents filtrée sur un utilisateur conduirait à rechercher dans tous les shards de l’index, potentiellement répartis dans un grand nombre de nœuds du cluster. En effet, par défaut ElasticSearch se base sur l’id du document pour déterminer le routing des documents vers les « shards » de l’index.

Heureusement, Elastic Search permet de définir une clé différente pour le routing des documents vers les « shards ». Il faudrait que l’on puisse router tous les documents d’un utilisateur avec la même clé, pour que la recherche ne porte ensuite que sur les « shards » pertinents. On pourrait ainsi router tous les documents d’un utilisateur vers le même « shard » et fortement améliorer les performances des requêtes lors d’une recherche.

Ceci peut être fait en utilisant les alias et les filtres…

Définissons un index global pour les utilisateurs :

curl -XPUT localhost:9200/users -d '{
  "settings": {
    "index": {
      "number_of_shards": 500,
      "number_of_replicas": 1
    }
  }
}'

Puis définissions un alias pour chaque utilisateur avec une clé de routing et un filtre :

curl -XPOST localhost:9200/_aliases -d '{
  "actions": [{
    "add": {
      "index": "users",
      "alias": user_1,
      "filter": {"term": {"user_id": 1}},
      "routing": 1
    }
  }]
}'

Maintenant on agit sur les données de la même façon qu’avant mais en utilisant les alias.

Lorsque l’on indexe des documents en utilisant l’alias, la clé de routing est automatiquement ajoutée au document. De la même manière, pas besoin d’ajouter cette clé lors des recherches car elle est automatiquement ajoutée par l’alias. Désormais notre recherche ne ciblera que les « shards » pertinents et la recherche sera extrêmement performante.

AngularJs : Où sont mes models ?

Lorsqu’on commence à développer une nouvelle application avec le Framework Angular, il est très aisé d’obtenir un résultat rapide et fonctionnel. C’est l’un des atouts majeurs d’AngularJs vis-à-vis de ses concurrents.

Même si AngularJs semble plus proche du pattern MVVM (MVW selon Igor Mina pour ModelViewWhatever) que MVC, on fait vite l’analogie avec les frameworks MVC backends classiques que la plupart des développeurs ont l’habitude d’utiliser (Symfony, Spring, …).

The $scope object could be considered the ViewModel that is being decorated by a function that we call a Controller.

MVC. Modèle, Vue, Contrôleur… Mais au fait, où est le modèle sur Angular (angular models) ? Lorsque j’ai découvert AngularJs, ayant eu auparavant l’occasion de développer quelques grosses applications avec EmberJs, je m’attendais à hériter d’un type NgModel quelconque. Mais la philosophie Angular est radicalement différente de celle d’Ember (http://emberjs.com/guides/models) à ce sujet :

Unlike other frameworks, there is no need to inherit from proprietary types in order to wrap the model in accessors methods. Angular models are plain old JavaScript objects. This makes your code easy to test, maintain, reuse, and again free from boilerplate.

Donc en gros, un modèle, c’est un objet javascript tout simple. Voici donc un article de blog :

{
  title: "Article 1",
  description: "Gouzigouza",
  content: "The content"
}

Cela tombe bien car cela ressemble étrangement à ce que je pourrais récupérer directement depuis mon backend via une API REST en faisant quelque chose de ce genre via $http ou grâce à $resource :

function MonController($http) {

  $http.get('url/vers/mon/api/articles/1').then(function(articleData) {
    // Injection dans le scope pour afficher mon article dans une vue
    $scope.article = articleData.data;

  }).then(null, function(rejection) {
    // manage error
  });
}

Notez que la logique de récupération d’un article ici implémentée dans un controller aurait tout intérêt à être elle-même extraite dans un service dédié :

angular.module('monModule').factory('ArticlesService', function($http) {

  return {
    getArticle: function(id) {

      return $http.get('url/vers/mon/api/articles/' + id).then(function(articleData) {
        return articleData.data;
      });
    }
  }
}

Un problème simple

Tout cela est très bien mais hormis dans un contexte de consultation simple de la donnée, il est peu probable qu’il n’y ait pas un peu de traitement à faire sur cet objet pour l’utiliser. Par exemple, si je veux gérer une description courte à afficher dans ma vue.

Logique dans le controller ?

Une première approche un peu naïve serait de faire quelque chose du genre :

function MonController($http) {
  // ...
  $scope.shortDescription = function(description) {
    return description.substring(0, 100);
  };
}

Pas terrible de laisser cette logique dans notre controller. Face à ce problème, différents points de vues s’affrontent et il est parfois difficile de s’y retrouver.

La logique au sein du modèle

Une première solution serait peut-être d’implémenter notre logique justement au niveau de notre modèle en faisant quelque chose du style :

function Article (data) {

  this.title = data.title;
  this.description = data.description;
  // ...
}

Article.getShortDescription = function() {
  return this.description.substring(0, 100);
}

Ou plus simplement avec un angular.extend par exemple :

function Article (data) {

  angular.extend(this, {
    title: "",
    description: "",

    getShortDescription: function() {
      return this.description.substring(0, 100);
    }
  }, data);
}

Pour rendre ce bout de code exploitable et injectable proprement, il suffit d’utiliser une nouvelle Factory :

angular.module('monModule').factory('Article', function() {

  var Article = function (data) {
    angular.extend(this, {
      title: "",
      description: "",

      getShortDescription: function() {
        return this.description.substring(0, 100);
      }
    }, data);
  }

  return Article;
}

Il nous suffit alors de modifier notre service de récupération d’un article pour faire quelque chose de ce type :

angular.module('monModule').factory('ArticlesService', function($http, Article) {

  return {
    getArticle: function(id) {

      return $http.get('url/vers/mon/api/articles/' + id).then(function(articleData) {
        return new Article(articleData.data);
      });
    }
  }
}

Et depuis un controller par exemple :

angular.module('monModule').controller('ArticleController', function(ArticlesService) {

  ArticlesService.getArticle(100).then(function(article) {
    $scope.shortDescription = article.getShortDescription();
  }).then(null, function(article) {
    // handle error
  };
}

Une autre approche : les filtres Angular

Certains militent contre le besoin de créer des services spécifiques pour représenter un modèle. L’argument qui prévaut est le suivant :

Your model lives on the server

Grosso modo, aucune logique business ne doit être implémentée au sein d’un projet Angular et la seule logique qui appartient au front, est la logique de présentation.

L’auteur de cet article préfère traiter notre problème via l’utilisation d’un filtre Angular.

A filter formats the value of an expression for display to the user. They can be used in view templates, controllers or services and it is easy to define your own filter.

Pour répondre différemment à notre problème on peut alors implémenter un filtre de la manière suivante :

angular.module('monModule', []).filter('summerizeArticle', function() {

  return function(article) {
    return article.description.length > 100 ? article.description.substring(0, 100) + "..." : article.description;
  };
});

Et si la problématique est suffisamment générique pour être valable hors du contexte d’un article, on peut alors modifier notre filtre de la sorte :

angular.module('monModule', []).filter('summerize', function() {

  return function(input) {
    return input.length > 100 ? input.substring(0, 100) + "..." : input;
  };
});

Quelques ressources intéressantes pour aller plus loin :

Awesome PHP

Une ressource inestimable découverte ce WE pour tous les développeurs PHP : Awesome PHP

A curated list of amazingly awesome PHP libraries, resources and shiny things.

Vous y trouverez une liste régulièrement mise à jour de librairies de qualité pour répondre à différentes problématiques. La liste est classé par catégorie : AwesomePHP