portaldacalheta.pt
  • Principal
  • Design De Marque
  • Personnes Et Équipes Produit
  • Innovation
  • Kpi Et Analyses
La Technologie

Elasticsearch pour Ruby on Rails: un tutoriel sur le Chewy Gem



Elasticsearch fournit une interface HTTP puissante et RESTful pour l'indexation et l'interrogation des données, construite au-dessus du Apache Lucene bibliothèque. Dès la sortie de la boîte, il fournit une recherche évolutive, efficace et robuste, avec prise en charge UTF-8. C'est un outil puissant pour indexer et interroger d'énormes quantités de données structurées et, ici à ApeeScape , il alimente notre recherche de plate-forme et sera bientôt également utilisé pour la saisie semi-automatique. Nous sommes de grands fans.

Chewy étend le client Elasticsearch-Ruby, le rendant plus puissant et offrant une intégration plus étroite avec Rails.

Puisque notre plateforme est construite en utilisant Rubis sur rails , notre intégration d'Elasticsearch tire parti de la elasticsearch-rubis projet (un cadre d'intégration Ruby pour Elasticsearch qui fournit un client pour se connecter à un cluster Elasticsearch, une API Ruby pour l'API REST d'Elasticsearch et diverses extensions et utilitaires). Sur cette base, nous avons développé et publié notre propre amélioration (et simplification) de l'architecture de recherche d'applications Elasticsearch, présentée sous la forme d'un joyau Ruby que nous avons nommé Moelleux (avec un exemple d'application disponible Ici ).



Chewy étend le client Elasticsearch-Ruby, le rendant plus puissant et offrant une intégration plus étroite avec Rails. Dans ce guide Elasticsearch, j'explique (à travers des exemples d'utilisation) comment nous y sommes parvenus, y compris les obstacles techniques apparus lors de la mise en œuvre.



La relation entre Elasticsearch et Ruby on Rails est décrite dans ce guide visuel.



Quelques remarques rapides avant de passer au guide:

  • Tous les deux Moelleux et un Application de démonstration Chewy sont disponibles sur GitHub.
  • Pour ceux qui souhaitent en savoir plus sur Elasticsearch 'sous le capot', j'ai inclus une brève description en tant que appendice à ce poste.

Pourquoi Chewy?

Malgré l'évolutivité et l'efficacité d'Elasticsearch, son intégration à Rails ne s'est pas avérée aussi simple que prévu. Chez ApeeScape, nous avons dû augmenter considérablement le client Elasticsearch-Ruby de base pour le rendre plus performant et prendre en charge des opérations supplémentaires.



Malgré l'évolutivité et l'efficacité d'Elasticsearch, son intégration à Rails ne s'est pas avérée aussi simple que prévu.

Et ainsi, le joyau Chewy est né.

Quelques caractéristiques particulièrement remarquables de Chewy incluent:



  1. Chaque indice est observable par tous les modèles associés.

    La plupart des modèles indexés sont liés les uns aux autres. Et parfois, il est nécessaire de dénormaliser ces données associées et de les lier au même objet (par exemple, si vous souhaitez indexer un tableau de balises avec leur article associé). Chewy vous permet de spécifier un index modifiable pour chaque modèle, de sorte que les articles correspondants seront réindexés chaque fois qu'une balise pertinente est mise à jour.



  2. Les classes d'index sont indépendantes des modèles ORM / ODM.

    Avec cette amélioration, la mise en œuvre de la saisie semi-automatique inter-modèles, par exemple, est beaucoup plus facile. Vous pouvez simplement définir un index et travailler avec lui de manière orientée objet. Contrairement à d'autres clients, la gemme Chewy supprime le besoin d'implémenter manuellement les classes d'index, les rappels d'importation de données et d'autres composants.



  3. L'importation groupée est partout .

    Chewy utilise l'API Elasticsearch en masse pour une réindexation complète et des mises à jour d'index. Il utilise également le concept de mises à jour atomiques, collectant les objets modifiés dans un bloc atomique et les mettant à jour tous en même temps.



  4. Chewy fournit un DSL de requête de style AR.

    En étant chaînable, fusionnable et paresseux, cette amélioration permet aux requêtes d'être produites de manière plus efficace.

OK, voyons comment tout cela se joue dans la gemme…

comment trouver un développeur de logiciel

Le guide de base d'Elasticsearch

Elasticsearch a plusieurs concepts liés aux documents. Le premier est celui d'un index (l'analogue de a database dans SGBDR ), qui consiste en un ensemble de documents, qui peut être de plusieurs types (où a type est une sorte de table SGBDR).

Chaque document a un ensemble de fields. Chaque champ est analysé indépendamment et ses options d'analyse sont stockées dans le répertoire mapping pour son type. Chewy utilise cette structure «telle quelle» dans son modèle d'objet:

class EntertainmentIndex { author.name } field :author_id, type: 'integer' field :description field :tags, index: 'not_analyzed', value: ->{ tags.map(&:name) } end {movie: Video.movies, cartoon: Video.cartoons}.each do |type_name, scope| define_type scope.includes(:director, :tags), name: type_name do field :title, analyzer: 'title' field :year, type: 'integer' field :author, value: ->{ director.name } field :author_id, type: 'integer', value: ->{ director_id } field :description field :tags, index: 'not_analyzed', value: ->{ tags.map(&:name) } end end end

Ci-dessus, nous avons défini un index Elasticsearch appelé entertainment avec trois types: book, movie et cartoon. Pour chaque type, nous avons défini des mappages de champs et un hachage des paramètres pour tout l'index.

Donc, nous avons défini le EntertainmentIndex et nous voulons exécuter certaines requêtes. Dans un premier temps, nous devons créer l'index et importer nos données:

EntertainmentIndex.create! EntertainmentIndex.import # EntertainmentIndex.reset! (which includes deletion, # creation, and import) could be used instead

Le .import La méthode est consciente des données importées car nous avons passé des étendues lorsque nous avons défini nos types; ainsi, il importera tous les livres, films et dessins animés stockés dans le stockage persistant.

Cela fait, nous pouvons effectuer quelques requêtes:

EntertainmentIndex.query(match: {author: 'Tarantino'}).filter{ year > 1990 } EntertainmentIndex.query(match: {title: 'Shawshank'}).types(:movie) EntertainmentIndex.query(match: {author: 'Tarantino'}).only(:id).limit(10).load # the last one loads ActiveRecord objects for documents found

Maintenant, notre index est presque prêt à être utilisé dans notre implémentation de recherche.

Intégration des rails

Pour l'intégration avec Rails, la première chose dont nous avons besoin est de pouvoir réagir aux changements d'objet du SGBDR. Chewy prend en charge ce comportement via des callbacks définis dans le update_index méthode de classe. update_index prend deux arguments:

  1. Un identifiant de type fourni dans le 'index_name#type_name' format
  2. Un nom de méthode ou un bloc à exécuter, qui représente une référence arrière à l'objet ou à la collection d'objets mis à jour

Nous devons définir ces rappels pour chaque modèle dépendant:

class Book

Étant donné que les balises sont également indexées, nous devons ensuite effectuer un patch de certains modèles externes afin qu'ils réagissent aux changements:

ActsAsTaggableOn::Tag.class_eval do has_many :books, through: :taggings, source: :taggable, source_type: 'Book' has_many :videos, through: :taggings, source: :taggable, source_type: 'Video' # Updating all tag-related objects update_index 'entertainment#book', :books update_index('entertainment#movie') { videos.movies } update_index('entertainment#cartoon') { videos.cartoons } end ActsAsTaggableOn::Tagging.class_eval do # Same goes for the intermediate model update_index('entertainment#book') { taggable if taggable_type == 'Book' } update_index('entertainment#movie') { taggable if taggable_type == 'Video' && taggable.movie? } update_index('entertainment#cartoon') { taggable if taggable_type == 'Video' && taggable.cartoon? } end

À ce stade, chaque objet enregistrer ou détruire mettra à jour le type d'index Elasticsearch correspondant.

Atomicité

Nous avons encore un problème persistant. Si nous faisons quelque chose comme books.map(&:save) pour enregistrer plusieurs livres, nous demanderons une mise à jour de entertainment indice chaque fois qu'un livre individuel est enregistré . Ainsi, si nous sauvegardons cinq livres, nous mettrons à jour l’index Chewy cinq fois. Ce comportement est acceptable pour REPL , mais certainement pas acceptable pour les actions du contrôleur dans lesquelles les performances sont essentielles.

Nous abordons ce problème avec le Chewy.atomic bloquer:

class ApplicationController

En bref, Chewy.atomic regroupe ces mises à jour comme suit:

  1. Désactive le after_save rappeler.
  2. Collecte les ID des livres enregistrés.
  3. À la fin de Chewy.atomic block, utilise les ID collectés pour effectuer une seule demande de mise à jour d'index Elasticsearch.

Recherche

Nous sommes maintenant prêts à mettre en œuvre une interface de recherche. Puisque notre interface utilisateur est un formulaire, la meilleure façon de la construire est, bien sûr, avec FormBuilder et ActiveModel . (Chez ApeeScape, nous utilisons ActiveData pour implémenter les interfaces ActiveModel, mais n'hésitez pas à utiliser votre gemme préférée.)

class EntertainmentSearch include ActiveData::Model attribute :query, type: String attribute :author_id, type: Integer attribute :min_year, type: Integer attribute :max_year, type: Integer attribute :tags, mode: :arrayed, type: String, normalize: ->(value) { value.reject(&:blank?) } # This accessor is for the form. It will have a single text field # for comma-separated tag inputs. def tag_list= value self.tags = value.split(',').map(&:strip) end def tag_list self.tags.join(', ') end end

Didacticiel sur les requêtes et les filtres

Maintenant que nous avons un objet de type ActiveModel qui peut accepter et transposer des attributs, implémentons la recherche:

class EntertainmentSearch ... def index EntertainmentIndex end def search # We can merge multiple scopes [query_string, author_id_filter, year_filter, tags_filter].compact.reduce(:merge) end # Using query_string advanced query for the main query input def query_string index.query(query_string: {fields: [:title, :author, :description], query: query, default_operator: 'and'}) if query? end # Simple term filter for author id. `:author_id` is already # typecasted to integer and ignored if empty. def author_id_filter index.filter(term: {author_id: author_id}) if author_id? end # For filtering on years, we will use range filter. # Returns nil if both min_year and max_year are not passed to the model. def year_filter body = {}.tap do |body| body.merge!(gte: min_year) if min_year? body.merge!(lte: max_year) if max_year? end index.filter(range: {year: body}) if body.present? end # Same goes for `author_id_filter`, but `terms` filter used. # Returns nil if no tags passed in. def tags_filter index.filter(terms: {tags: tags}) if tags? end end

Contrôleurs et vues

À ce stade, notre modèle peut effectuer des requêtes de recherche avec des attributs passés. L'utilisation ressemblera à quelque chose comme:

EntertainmentSearch.new(query: 'Tarantino', min_year: 1990).search

Notez que dans le contrôleur, nous voulons charger des objets ActiveRecord exacts au lieu de Moelleux wrappers de documents:

class EntertainmentController

Maintenant, il est temps d’en rédiger HAML à entertainment/index.html.haml:

= form_for @search, as: :search, url: entertainment_index_path, method: :get do |f| = f.text_field :query = f.select :author_id, Dude.all.map d, include_blank: true = f.text_field :min_year = f.text_field :max_year = f.text_field :tag_list = f.submit - if @entertainments.any? %dl - @entertainments.each do |entertainment| %dt %h1= entertainment.title %strong= entertainment.class %dd %p= entertainment.year %p= entertainment.description %p= entertainment.tag_list = paginate @entertainments - else Nothing to see here

Tri

En prime, nous ajouterons également le tri à notre fonctionnalité de recherche.

Supposons que nous ayons besoin de trier sur les champs de titre et d'année, ainsi que par pertinence. Malheureusement, le titre One Flew Over the Cuckoo's Nest sera divisé en termes individuels, donc le tri selon ces termes disparates sera trop aléatoire; à la place, nous aimerions trier par le titre entier.

La solution est d'utiliser un champ de titre spécial et d'appliquer son propre analyseur:

class EntertainmentIndex

En outre, nous allons ajouter ces nouveaux attributs et l'étape de traitement du tri à notre modèle de recherche:

class EntertainmentSearch # we are going to use `title.sorted` field for sort SORT = {title: {'title.sorted' => :asc}, year: {year: :desc}, relevance: :_score} ... attribute :sort, type: String, enum: %w(title year relevance), default_blank: 'relevance' ... def search # we have added `sorting` scope to merge list [query_string, author_id_filter, year_filter, tags_filter, sorting].compact.reduce(:merge) end def sorting # We have one of the 3 possible values in `sort` attribute # and `SORT` mapping returns actual sorting expression index.order(SORT[sort.to_sym]) end end

Enfin, nous allons modifier notre formulaire en ajoutant la boîte de sélection des options de tri:

= form_for @search, as: :search, url: entertainment_index_path, method: :get do |f| ... / `EntertainmentSearch.sort_values` will just return / enum option content from the sort attribute definition. = f.select :sort, EntertainmentSearch.sort_values ...

La gestion des erreurs

Si vos utilisateurs effectuent des requêtes incorrectes telles que ( ou AND, le client Elasticsearch générera une erreur. Pour gérer cela, apportons quelques modifications à notre contrôleur:

class EntertainmentController e @entertainments = [] @error = e.message.match(/QueryParsingException[([^;]+)]/).try(:[], 1) end end

De plus, nous devons rendre l'erreur dans la vue:

... - if @entertainments.any? ... - else - if @error = @error - else Nothing to see here

Test des requêtes Elasticsearch

La configuration de base du test est la suivante:

  1. Démarrez le serveur Elasticsearch.
  2. Nettoyez et créez nos index.
  3. Importez nos données.
  4. Exécutez notre requête.
  5. Croisez le résultat avec nos attentes.

Pour l'étape 1, il est pratique d'utiliser le cluster de test défini dans le elasticsearch-extensions gemme. Ajoutez simplement la ligne suivante à votre projet Rakefile installation post-gemme:

require 'elasticsearch/extensions/test/cluster/tasks'

Ensuite, vous obtiendrez ce qui suit Râteau Tâches:

$ rake -T elasticsearch rake elasticsearch:start # Start Elasticsearch cluster for tests rake elasticsearch:stop # Stop Elasticsearch cluster for tests

Elasticsearch et Rspec

Tout d'abord, nous devons nous assurer que notre index est mis à jour pour être synchronisé avec nos modifications de données. Heureusement, le joyau Chewy est livré avec le update_index rspec allumettes:

describe EntertainmentIndex do # No need to cleanup Elasticsearch as requests are # stubbed in case of `update_index` matcher usage. describe 'Tag' do # We create several books with the same tag let(:books) { create_list :book, 2, tag_list: 'tag1' } specify do # We expect that after modifying the tag name... expect do ActsAsTaggableOn::Tag.where(name: 'tag1').update_attributes(name: 'tag2') # ... the corresponding type will be updated with previously-created books. end.to update_index('entertainment#book').and_reindex(books, with: {tags: ['tag2']}) end end end

Ensuite, nous devons tester que les requêtes de recherche réelles sont correctement effectuées et qu'elles renvoient les résultats attendus:

describe EntertainmentSearch do # Just defining helpers for simplifying testing def search attributes = {} EntertainmentSearch.new(attributes).search end # Import helper as well def import *args # We are using `import!` here to be sure all the objects are imported # correctly before examples run. EntertainmentIndex.import! *args end # Deletes and recreates index before every example before { EntertainmentIndex.purge! } describe '#min_year, #max_year' do let(:book) { create(:book, year: 1925) } let(:movie) { create(:movie, year: 1970) } let(:cartoon) { create(:cartoon, year: 1995) } before { import book: book, movie: movie, cartoon: cartoon } # NOTE: The sample code below provides a clear usage example but is not # optimized code. Something along the following lines would perform better: # `specify { search(min_year: 1970).map(&:id).map(&:to_i) # .should =~ [movie, cartoon].map(&:id) }` specify { search(min_year: 1970).load.should =~ [movie, cartoon] } specify { search(max_year: 1980).load.should =~ [book, movie] } specify { search(min_year: 1970, max_year: 1980).load.should == [movie] } specify { search(min_year: 1980, max_year: 1970).should == [] } end end

Dépannage du cluster de test

Enfin, voici un guide de dépannage de votre cluster de test:

  • Pour commencer, utilisez un cluster à un nœud en mémoire. Ce sera beaucoup plus rapide pour les spécifications. Dans notre cas: TEST_CLUSTER_NODES=1 rake elasticsearch:start

  • Il existe des problèmes avec le elasticsearch-extensions tester la mise en œuvre du cluster elle-même liée à la vérification de l'état du cluster à un nœud (elle est jaune dans certains cas et ne sera jamais verte, de sorte que la vérification de démarrage du cluster de statut vert échoue à chaque fois). Le problème a été résolu dans un fork, mais j'espère qu'il sera bientôt résolu dans le repo principal.

    conduite pour uber vs lyft 2016
  • Pour chaque ensemble de données, regroupez votre demande dans les spécifications (c'est-à-dire importez vos données une fois, puis effectuez plusieurs demandes). Elasticsearch se réchauffe pendant longtemps et utilise beaucoup de mémoire lors de l'importation des données, alors n'en faites pas trop, surtout si vous avez un tas de spécifications.

  • Assurez-vous que votre machine dispose de suffisamment de mémoire ou Elasticsearch gèlera (nous avons requis environ 5 Go pour chaque machine virtuelle de test et environ 1 Go pour Elasticsearch lui-même).

Emballer

Elasticsearch se décrit comme «un moteur de recherche et d'analyse open source, distribué et en temps réel flexible et puissant». C'est la référence en matière de technologies de recherche.

Avec Chewy, notre développeurs de rails ont regroupé ces avantages sous la forme d'un joyau Ruby open source simple, facile à utiliser, de qualité production, qui offre une intégration étroite avec Rails. Elasticsearch et Rails - quelle combinaison géniale!

Elasticsearch et Rails - quelle combinaison géniale! Tweet


Annexe: Internes Elasticsearch

Voici un très brève introduction à Elasticsearch «sous le capot»…

Elasticsearch repose sur Lucène , qui lui-même utilise indices inversés comme structure de données principale. Par exemple, si nous avons les chaînes «les chiens sautent haut», «sautent par-dessus la clôture» et «la clôture était trop haute», nous obtenons la structure suivante:

'the' [0, 0], [1, 2], [2, 0] 'dogs' [0, 1] 'jump' [0, 2], [1, 0] 'high' [0, 3], [2, 4] 'over' [1, 1] 'fence' [1, 3], [2, 1] 'was' [2, 2] 'too' [2, 3]

Ainsi, chaque terme contient à la fois des références et des positions dans le texte. De plus, nous choisissons de modifier nos conditions (par exemple, en supprimant des mots vides comme «le») et d'appliquer hachage phonétique à chaque terme (pouvez-vous deviner l'algorithme ?):

'DAG' [0, 1] 'JANP' [0, 2], [1, 0] 'HAG' [0, 3], [2, 4] 'OVAR' [1, 1] 'FANC' [1, 3], [2, 1] 'W' [2, 2] 'T' [2, 3]

Si nous recherchons ensuite «le chien saute», il est analysé de la même manière que le texte source, devenant «DAG JANP» après le hachage («chien» a le même hachage que «chiens», comme c'est le cas avec «sauts» et 'sauter').

Nous ajoutons également une certaine logique entre les mots individuels de la chaîne (en fonction des paramètres de configuration), en choisissant entre («DAG» ET «JANP») ou («DAG» OU «JANP»). Le premier renvoie l'intersection de [0] & [0, 1] (c'est-à-dire le document 0) et ce dernier, [0] | [0, 1] (c'est-à-dire les documents 0 et 1). Les positions dans le texte peuvent être utilisées pour évaluer les résultats et les requêtes dépendant de la position.

Vice-président des communications

Autre

Vice-président des communications
Considérations pour lever votre propre fonds de capital-investissement

Considérations pour lever votre propre fonds de capital-investissement

Processus Financiers

Articles Populaires
Ingénieur senior full-stack, équipe post-embauche des talents
Ingénieur senior full-stack, équipe post-embauche des talents
En souvenir de Matthew Osborne
En souvenir de Matthew Osborne
Comment créer un pipeline de déploiement initial efficace
Comment créer un pipeline de déploiement initial efficace
L'impact du Brexit sur le secteur des services financiers
L'impact du Brexit sur le secteur des services financiers
Comment préparer un modèle de tableau des flux de trésorerie qui s'équilibre réellement
Comment préparer un modèle de tableau des flux de trésorerie qui s'équilibre réellement
 
Conquérir la recherche de chaînes avec l'algorithme Aho-Corasick
Conquérir la recherche de chaînes avec l'algorithme Aho-Corasick
Estimation des coûts logiciels dans la gestion de projet agile
Estimation des coûts logiciels dans la gestion de projet agile
5 qualités indispensables des meilleurs chefs de projet
5 qualités indispensables des meilleurs chefs de projet
Comment recréer gratuitement les ressources d'un terminal Bloomberg
Comment recréer gratuitement les ressources d'un terminal Bloomberg
Noyaux d'arbres: quantification de la similitude entre les données structurées en arborescence
Noyaux d'arbres: quantification de la similitude entre les données structurées en arborescence
Articles Populaires
  • combien de drones ont été vendus en 2016
  • que fait un chercheur ux
  • que pouvez-vous faire avec c
  • meilleures pratiques de mise en page de conception de site Web
  • combien vaut l'industrie de la beauté aux états-unis
  • le processus d'autorisation du budget d'investissement de l'entreprise comprend combien d'étapes ?
Catégories
  • Design De Marque
  • Personnes Et Équipes Produit
  • Innovation
  • Kpi Et Analyses
  • © 2022 | Tous Les Droits Sont Réservés

    portaldacalheta.pt