Cómo agregar un poderoso motor de búsqueda a tu backend Rails

Foto de Simon Abrams en Unsplash

En mi experiencia como desarrollador de Ruby on Rails, a menudo tuve que lidiar con la adición de funcionalidad de búsqueda a las aplicaciones web. De hecho, casi todas las aplicaciones en las que trabajé en algún momento necesitaban capacidades de motor de búsqueda, mientras que muchas de ellas tenían un motor de búsqueda como la funcionalidad principal más importante.

Muchas aplicaciones que usamos todos los días serían inútiles sin un buen motor de búsqueda en su núcleo. Por ejemplo, en Amazon, puede encontrar un producto en particular entre los más de 550 millones de productos disponibles en el sitio en cuestión de segundos, todo gracias a una búsqueda de texto completo combinada con filtros de categoría, facetas y un sistema de recomendación.

En Airbnb, puede buscar un apartamento combinando una búsqueda geoespacial con filtros sobre las características de la casa, como dimensión, precio, fechas disponibles, etc.

Y Spotify, Netflix, Ebay, Youtube ... todos ellos dependen en gran medida de un motor de búsqueda.

En este artículo, describiré cómo desarrollar un back-end API Ruby on Rails 5 con Elasticsearch. Según DB Engines Ranking, Elasticsearch es actualmente la plataforma de búsqueda de código abierto más popular.

Este artículo no entrará en los detalles de Elasticsearch y cómo se compara con sus competidores como Sphinx y Solr. En cambio, será una guía paso a paso sobre cómo implementar un backend API JSON con Ruby on Rails y Elasticsearch, utilizando un enfoque de desarrollo impulsado por pruebas.

Este artículo cubrirá:

  1. Configuración de Elasticsearch para entornos de prueba, desarrollo y producción
  2. Configuración del entorno de prueba de Ruby on Rails
  3. Indexación de modelos con Elasticsearch
  4. Buscar punto final de API

Como en mi artículo anterior, Cómo mejorar su rendimiento con arquitectura sin servidor, cubriré todo en un tutorial paso a paso. Luego, puede probarlo usted mismo y tener un ejemplo de trabajo simple sobre el cual construir algo más complejo.

La aplicación de ejemplo será un motor de búsqueda de películas. Tendrá un único punto final de API JSON que le permite realizar una búsqueda de texto completo en títulos y vistas generales de películas.

1. Configuración de Elasticsearch

Elasticsearch es un motor de búsqueda y análisis distribuido y RESTful capaz de resolver un número creciente de casos de uso. Como el corazón de Elastic Stack, almacena de forma centralizada sus datos para que pueda descubrir lo esperado y descubrir lo inesperado. - www.elastic.co/products/elasticsearch

Según el ranking de motores de búsqueda de DB-Engines, Elasticsearch es, con mucho, la plataforma de motores de búsqueda más popular en la actualidad (a partir de abril de 2018). Y lo ha sido desde finales de 2015, cuando Amazon anunció el lanzamiento de AWS Elasticsearch Service, una forma de iniciar un clúster Elasticsearch desde la consola de administración de AWS.

Tendencia de clasificación del motor de búsqueda de motores DB

Elasticsearch es de código abierto. Puede descargar su versión preferida de su sitio web y ejecutarla donde desee. Si bien sugiero usar el servicio AWS Elasticsearch para entornos de producción, prefiero que Elasticsearch se ejecute en mi máquina local para probar y desarrollar.

Comencemos descargando la (actualmente) versión más reciente de Elasticsearch (6.2.3) y descomprímala. Abre una terminal y ejecuta

$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.2.3.zip
$ descomprimir elasticsearch-6.2.3.zip

Alternativamente, puede descargar Elasticsearch desde su navegador aquí y descomprimirlo con su programa preferido.

2. Configuración del entorno de prueba

Vamos a construir una aplicación de back-end con Ruby on Rails 5 API. Tendrá un modelo que representa películas. Elasticsearch lo indexará, y se podrá buscar a través de un punto final API.

En primer lugar, creemos una nueva aplicación de rieles. En la misma carpeta que descargó Elasticsearch anteriormente, ejecute el comando para generar una nueva aplicación de rieles. Si es nuevo en Ruby on Rails, consulte esta guía de inicio para configurar su entorno primero.

$ rails nuevas películas de búsqueda --api; búsqueda de películas en cd

Cuando se utiliza la opción "api", no se incluye todo el middleware utilizado principalmente para aplicaciones de navegador. Exactamente lo que queremos. Lea más sobre esto directamente en la guía de ruby ​​on rails.

Ahora agreguemos todas las gemas que necesitaremos. Abra su Gemfile y agregue el siguiente código:

# Gemfile
...
# Integración Elasticsearch
gema 'modelo de búsqueda elástica'
gema 'rieles elásticos de búsqueda'
grupo: desarrollo,: prueba hacer
  ...
  # Marco de prueba
  joya 'rspec'
  gema 'rspec-rails'
fin
grupo: hacer prueba
  ...
  # Base de datos limpia entre pruebas
  joya 'database_cleaner'
  # Iniciar y detener programáticamente ES para pruebas
  gema 'elasticsearch-extensiones'
fin
...

Estamos agregando dos Gemas Elasticsearch que proporcionarán todos los métodos necesarios para indexar nuestro modelo y ejecutar consultas de búsqueda en él. rspec, rspec-rails, database_cleaner y elasticsearch-extensions se utilizan para las pruebas.

Después de guardar su Gemfile, ejecute el paquete de instalación para instalar todas las gemas agregadas.

Ahora configuremos Rspec ejecutando el siguiente comando:

los rieles generan rspec: instalar

Este comando creará una carpeta de especificaciones y agregará spec_helper.rb y rails_helper.rb. Se pueden usar para personalizar rspec a las necesidades de su aplicación.

En este caso, agregaremos un bloque DatabaseCleaner a rails_helper.rb para que cada prueba se ejecute en una base de datos vacía. Además, modificaremos spec_helper.rb para iniciar un servidor de prueba Elasticsearch cada vez que se inicie el conjunto de pruebas, y volver a cerrarlo una vez que el conjunto de pruebas haya finalizado.

Esta solución se basa en el artículo de Rowan Oulton Testing Elasticsearch in Rails. ¡Muchos aplausos para él!

Comencemos con DatabaseCleaner. Dentro de spec / rails_helper.rb agregue el siguiente código:

# spec / rails_helper.rb
...
RSpec.configure do | config |
  ...
config.before (: suite) hacer
    DatabaseCleaner.strategy =: transacción
    DatabaseCleaner.clean_with (: truncamiento)
  fin
config.around (: each) do | ejemplo |
    DatabaseCleaner.cleaning do
      ejemplo.run
    fin
  fin
fin

A continuación, pensemos en la configuración del servidor de prueba Elasticsearch. Necesitamos agregar algunos archivos de configuración para que Rails sepa dónde encontrar nuestro ejecutable Elasticsearch. También le dirá en qué puerto queremos que se ejecute, en función del entorno actual. Para hacerlo, agregue un nuevo yaml de configuración a su carpeta de configuración:

# config / elasticsearch.yml
desarrollo: y por defecto
  es_bin: '../elasticsearch-6.2.3/bin/elasticsearch'
  host: 'http: // localhost: 9200'
  puerto: '9200'
prueba:
  es_bin: '../elasticsearch-6.2.3/bin/elasticsearch'
  host: 'http: // localhost: 9250'
  puerto: '9250'
puesta en escena:
  <<: * predeterminado
producción:
  es_bin: '../elasticsearch-6.2.3/bin/elasticsearch'
  host: 'http: // localhost: 9400'
  puerto: '9400'

Si no creó la aplicación rails en la misma carpeta donde descargó Elasticsearch, o si está usando una versión diferente de Elasticsearch, deberá ajustar la ruta es_bin aquí.

Ahora agregue un nuevo archivo a su carpeta de inicializadores que leerá de la configuración que acabamos de agregar:

# config / initializers / elasticsearch.rb
if File.exists? ("config / elasticsearch.yml")
   config = YAML.load_file ("config / elasticsearch.yml") [Rails.env] .symbolize_keys
   Elasticsearch :: Model.client = Elasticsearch :: Client.new (config)
fin

Y finalmente, cambiemos spec_helper.rb para incluir la configuración de prueba Elasticsearch. Esto significa iniciar y detener un servidor de prueba Elasticsearch y crear / eliminar índices Elasticsearch para nuestro modelo Rails.

# spec / spec_helper.rb
requiere 'elasticsearch / extensiones / prueba / cluster'
requiere 'yaml'
RSpec.configure do | config |
  ...
  # Inicie un clúster en memoria para Elasticsearch según sea necesario
  es_config = YAML.load_file ("config / elasticsearch.yml") ["prueba"]
  ES_BIN = es_config ["es_bin"]
  ES_PORT = es_config ["puerto"]
config.before: all, elasticsearch: true do
    Elasticsearch :: Extensiones :: Prueba :: Cluster.start (comando: ES_BIN, puerto: ES_PORT.to_i, nodos: 1, tiempo de espera: 120) a menos que Elasticsearch :: Extensiones :: Prueba :: Cluster.running? (Comando: ES_BIN, en: ES_PORT.to_i)
  fin
# Detener el clúster elasticsearch después de la ejecución de prueba
  config.after: suite do
    Elasticsearch :: Extensiones :: Prueba :: Cluster.stop (comando: ES_BIN, puerto: ES_PORT.to_i, nodos: 1) si Elasticsearch :: Extensiones :: Prueba :: Cluster.running? (Comando: ES_BIN, en: ES_PORT. to_i)
  fin
# Crear índices para todos los modelos de búsqueda elásticos
  config.before: each, elasticsearch: true do
    ActiveRecord :: Base.descendants.each do | model |
      if model.respond_to? (: __ elasticsearch__)
        empezar
          modelo .__ elasticsearch __. create_index!
          modelo .__ elasticsearch __. refresh_index!
        rescue => Elasticsearch :: Transporte :: Transporte :: Errores :: NotFound
          # Esto elimina los errores de "Índice no existe" que se escriben en la consola
        rescate => e
          STDERR.puts "Hubo un error al crear el índice de búsqueda elástica para # {model.name}: # {e.inspect}"
        fin
      fin
    fin
  fin
# Eliminar índices para todos los modelos de búsqueda elásticos para garantizar un estado limpio entre pruebas
  config.after: each, elasticsearch: true do
    ActiveRecord :: Base.descendants.each do | model |
      if model.respond_to? (: __ elasticsearch__)
        empezar
          modelo .__ elasticsearch __. delete_index!
        rescue => Elasticsearch :: Transporte :: Transporte :: Errores :: NotFound
          # Esto elimina los errores de "Índice no existe" que se escriben en la consola
        rescate => e
          STDERR.puts "Hubo un error al eliminar el índice de búsqueda elástica para # {model.name}: # {e.inspect}"
        fin
      fin
    fin
  fin
fin

Hemos definido cuatro bloques:

  1. un bloque before (: all) que inicia un servidor de prueba Elasticsearch, a menos que ya se esté ejecutando
  2. un bloque after (: suite) que detiene el servidor de prueba Elasticsearch, si se está ejecutando
  3. un bloque before (: each) que crea un nuevo índice Elasticsearch para cada modelo configurado con Elasticsearch
  4. un bloque after (: each) que elimina todos los índices de Elasticsearch

Agregar elasticsearch: true asegura que solo las pruebas etiquetadas con elasticsearch ejecutarán estos bloques.

Creo que esta configuración funciona muy bien cuando ejecuta todas sus pruebas una vez, por ejemplo, antes de una implementación. Por otro lado, si está utilizando un enfoque de desarrollo basado en pruebas y ejecuta sus pruebas con mucha frecuencia, entonces probablemente tendrá que modificar esta configuración ligeramente. No desea iniciar y detener su servidor de prueba Elasticsearch en cada ejecución de prueba.

En este caso, puede comentar el bloque after (: suite) donde se detiene el servidor de prueba. Puede apagarlo manualmente o usar un script cuando ya no lo necesite.

requiere 'elasticsearch / extensiones / prueba / cluster'
es_config = YAML.load_file ("config / elasticsearch.yml") ["prueba"]
ES_BIN = es_config ["es_bin"]
ES_PORT = es_config ["puerto"]
Elasticsearch :: Extensiones :: Prueba :: Cluster.stop (comando: ES_BIN, puerto: ES_PORT.to_i, nodos: 1)

3. Indexación de modelos con Elasticsearch

Ahora comenzamos a implementar nuestro Modelo de película con capacidades de búsqueda. Utilizamos un enfoque de desarrollo impulsado por pruebas. Esto significa que primero escribimos las pruebas, las vemos fallar y luego escribimos el código para que pasen.

Primero necesitamos agregar el modelo de película que tiene cuatro atributos: un título (Cadena), una descripción general (Texto), una imagen_url (Cadena) y un valor de voto promedio (Flotante).

$ rails g model Título de la película: resumen de cadena: texto image_url: string vote_average: float
$ rails db: migrar

Ahora es el momento de agregar Elasticsearch a nuestro modelo. Escribamos una prueba que verifique que nuestro modelo esté indexado.

# spec / models / movie_spec.rb
requiere 'rails_helper'
RSpec.describe Movie, elasticsearch: true,: type =>: model do
  'debe ser indexado' hacer
     esperar (Película .__ elasticsearch __. index_exists?). a be_truthy
  fin
fin

Esta prueba verificará si se creó un índice de búsqueda elástica para la película. Recuerde que antes de comenzar las pruebas, creamos automáticamente un índice de búsqueda elástica para todos los modelos que responden al método __elasticsearch__. Eso significa para todos los modelos que incluyen los módulos de búsqueda elástica.

Ejecute la prueba para ver que falla.

paquete exec rspec spec / models / movie_spec.rb

La primera vez que ejecute esta prueba, debería ver que el servidor de prueba Elasticsearch se está iniciando. La prueba falla porque no agregamos ningún módulo Elasticsearch a nuestro modelo de película. Vamos a arreglar eso ahora. Abra el modelo y agregue el siguiente Elasticsearch para incluir:

# app / models / movie.rb
clase Movie 

Esto agregará algunos métodos Elasticsearch a nuestro modelo de película, como el método __elasticsearch__ faltante (que generó el error en la ejecución de la prueba anterior) y el método de búsqueda que usaremos más adelante.

Ejecute la prueba nuevamente y vea cómo pasa.

paquete exec rspec spec / models / movie_spec.rb

Excelente. Tenemos un modelo de película indexada.

Por defecto, Elasticsearch :: Model configurará un índice con todos los atributos del modelo, inferiendo automáticamente sus tipos. Por lo general, esto no es lo que queremos. Ahora vamos a personalizar el índice del modelo para que tenga el siguiente comportamiento:

  1. Solo se debe indexar el título y la descripción general
  2. Se debe utilizar la función Stemming (lo que significa que la búsqueda de "actores" también debe devolver películas que contengan el texto "actor" y viceversa)

También queremos que nuestro índice se actualice cada vez que se agrega, actualiza o elimina una película.

Vamos a traducir esto en pruebas agregando el siguiente código a movie_spec.rb

# spec / models / movie_spec.rb
RSpec.describe Movie, elasticsearch: true,: type =>: model do
  ...
describe '#search' do
    antes (: cada) hacer
      Movie.create (
        título: "vacaciones romanas",
        resumen: "Una película de comedia romántica estadounidense de 1953 ...",
        image_url: "wikimedia.com/Roman_holiday.jpg",
        vote_average: 4.0
      )
      Película .__ elasticsearch __. Refresh_index!
    fin
    "debe indexar el título" hacer
      esperar (Movie.search ("Vacaciones"). records.length) .to eq (1)
    fin
    "debe indexar resumen" hacer
      esperar (Movie.search ("comedia"). records.length) .to eq (1)
    fin
    "no debe indexar image_path" hacer
      esperar (Movie.search ("Roman_holiday.jpg"). records.length) .to eq (0)
    fin
    "no debe indexar vote_average" hacer
      esperar (Movie.search ("4.0"). records.length) .to eq (0)
    fin
  fin
fin

Creamos una película antes de cada prueba, porque configuramos DatabaseCleaner para que cada prueba esté aislada. Película .__ elasticsearch __. Refresh_index! es necesario para asegurarse de que el nuevo registro de la película esté disponible de inmediato para la búsqueda.

Como antes, ejecute la prueba y vea que falla.

Parece que nuestra película no está siendo indexada. Esto se debe a que aún no le dijimos a nuestro modelo qué hacer cuando cambian los datos de la película. Afortunadamente, esto se puede solucionar agregando otro módulo a nuestro modelo de película:

clase Movie 

Con Elasticsearch :: Model :: Callbacks, cada vez que se agrega, modifica o elimina una película, su documento en Elasticsearch también se actualiza.

Veamos cómo cambia la salida de prueba.

Okay. Ahora el problema es que nuestro método de búsqueda también devuelve consultas que coinciden con los atributos vote_average e image_url. Para solucionarlo, debemos configurar la asignación de índice de Elasticsearch. Por lo tanto, debemos decirle a Elasticsearch específicamente qué atributos del modelo indexar.

# app / models / movie.rb
clase Movie 
# Índice ElasticSearch
  índice de configuración: {number_of_shards: 1} do
    asignaciones dinámicas: 'falso' hacer
      índices: título
      índices: resumen
    fin
  fin
fin

Ejecute la prueba nuevamente y vea cómo pasa.

Bueno. Ahora agreguemos un stemmer para que no haya diferencia entre "actor" y "actores". Como siempre, primero escribiremos la prueba y veremos que falla.

describe '#search' do
    antes (: cada) hacer
      Movie.create (
        título: "vacaciones romanas",
        resumen: "Una película de comedia romántica estadounidense de 1953 ...",
        image_url: "wikimedia.com/Roman_holiday.jpg",
        vote_average: 4.0
      )
      Película .__ elasticsearch __. Refresh_index!
    fin
...
"debería aplicarse derivando al título" hacer
      esperar (Movie.search ("Vacaciones"). records.length) .to eq (1)
    fin
"debería aplicarse derivando a la visión general" hacer
      esperar (Movie.search ("película"). records.length) .to eq (1)
    fin
fin

Tenga en cuenta que estamos probando en ambos sentidos: los días festivos deberían regresar también los días festivos, y la película también debería devolver las películas.

Para que estas pruebas pasen nuevamente, necesitamos modificar la asignación de índice. Lo haremos esta vez agregando un analizador de inglés a ambos campos:

clase Movie 
# Índice ElasticSearch
  índice de configuración: {number_of_shards: 1} do
    asignaciones dinámicas: 'falso' hacer
      índices: título, analizador: 'inglés'
      índices: resumen, analizador: 'inglés'
    fin
  fin
fin

Ejecute sus pruebas nuevamente para verlas pasar.

Elasticsearch es una plataforma de búsqueda muy poderosa, y podríamos agregar muchas funcionalidades a nuestro método de búsqueda. Pero esto no está dentro del alcance de este artículo. Así que nos detendremos aquí y pasaremos a construir la parte del controlador de la API JSON a través de la cual se accede al método de búsqueda.

4. Punto final de la API de búsqueda

La API de búsqueda que estamos creando debería permitir a los usuarios realizar una búsqueda de texto completo en la Tabla de películas. Nuestra API tiene un único punto final definido de la siguiente manera:

Url:
 GET / api / v1 / movies
Parámetros:
 * q = [cadena] requerido
Ejemplo de URL:
 GET / api / v1 / movies? Q = Roma
Respuesta de ejemplo:
[{"_index": "movies", "_ type": "movie", "_ id": "95088", "_ score": 11.549209, "_ source": {"id": 95088, "title": "Roma" , "overview": "Un retrato impresionista de Roma, prácticamente sin trama, llamativo a través de los ojos de uno de sus ciudadanos más famosos", "image_url": "https://image.tmdb.org/t/p/w300/ rqK75R3tTz2iWU0AQ6tLz3KMOU1.jpg "," vote_average ": 6.6," created_at ":" 2018-04-14T10: 30: 49.110Z "," updated_at ":" 2018-04-14T10: 30: 49.110Z "}}, ...}, ... ]

Aquí estamos definiendo nuestro punto final de acuerdo con algunas mejores prácticas Diseño RESTful API:

  1. La URL debe codificar el objeto o recurso, mientras que la acción a realizar debe estar codificada por el método HTTP. En este caso, el recurso son las películas (colección) y estamos utilizando el método HTTP GET (porque estamos solicitando datos del recurso sin producir ningún efecto secundario). Utilizamos parámetros de URL para definir mejor cómo se deben obtener estos datos. En este ejemplo, q = [cadena], que especifica una consulta de búsqueda. Puede leer más sobre cómo diseñar API RESTful en el artículo de Mahesh Haldar Directrices de diseño de API RESTful: las mejores prácticas.
  2. También agregamos versiones a nuestra API agregando v1 a nuestra URL de punto final. La versión de su API es muy importante, ya que le permite introducir nuevas características que no son compatibles con versiones anteriores sin romper todos los clientes que se desarrollaron para versiones anteriores de su API.

Okay. Comencemos a implementar.

Como siempre, comenzamos con las pruebas fallidas. Dentro de la carpeta de especificaciones, crearemos la estructura de carpetas que refleja nuestra estructura de URL de punto final de API. Esto significa controladores → api → v1 → movies_spec.rb

Puede hacerlo manualmente o desde su terminal ejecutando:

mkdir -p spec / controllers / api / v1 &&
Especificaciones táctiles / controladores / api / v1 / movies_spec.rb

Las pruebas que vamos a escribir aquí son pruebas de controlador. No necesitan verificar la lógica de búsqueda definida en el modelo. En su lugar, probaremos tres cosas:

  1. Una solicitud GET a / api / v1 / movies? Q = [string] llamará a Movie.search con [string] como parámetro
  2. La salida de Movie.search se devuelve en formato JSON.
  3. Se devuelve un estado de éxito
Una prueba de controlador debe probar el comportamiento del controlador. Una prueba de controlador no debe fallar debido a problemas en el modelo.
(Prescripción 20 - Recetas de prueba de Rails 4. Noel Rappin)

Transformemos esto en código. Dentro de spec / controllers / api / v1 / movies_spec.rb agregue el siguiente código:

# spec / controllers / api / v1 / movies_spec.rb
requiere 'rails_helper'
RSpec.describe Api :: V1 :: MoviesController, escriba:: request do
  # Buscar película con texto título de película
  describe "GET / api / v1 / movies? q =" do
    let (: title) {"movie-title"}
    let (: url) {"/ api / v1 / movies? q = # {title}"}
"llama a Movie.search con los parámetros correctos" hacer
      esperar (Película) .para recibir (: buscar) .con (título)
      obtener url
    fin
"devuelve la salida de Movie.search" do
      permitir (Película). recibir (: buscar). y_retornar ({})
      obtener url
      esperar (respuesta.cuerpo) .to eq ({}. to_json)
    fin
'devuelve un estado de éxito' hacer
      permitir (Película). para recibir (: buscar) .con (título)
      obtener url
      esperar (respuesta) .para tener éxito
    fin
  fin
fin

La prueba fallará inmediatamente porque Api :: V1 :: MoviesController no está definido, así que hagamos eso primero. Cree la estructura de carpetas como antes y agregue el controlador de películas.

mkdir -p app / controllers / api / v1 &&
toque la aplicación / controllers / api / v1 / movies_controller.rb

Ahora agregue el siguiente código a app / controllers / api / v1 / movies_controller.rb:

# app / controllers / api / v1 / movies_controller.rb
módulo Api
  módulo V1
    clase MoviesController 

Es hora de ejecutar nuestra prueba y ver que falla.

Todas las pruebas fallan porque aún necesitamos agregar una ruta para el punto final. Dentro de config / routes.rb agregue el siguiente código:

# config / routes.rb
Rails.application.routes.draw do
  espacio de nombres: api do
    espacio de nombres: v1 do
      recursos: películas, solo: [: índice]
    fin
  fin
fin

Vuelva a ejecutar sus pruebas y vea qué sucede.

El primer error nos dice que necesitamos agregar una llamada a Movie.search dentro de nuestro controlador. El segundo se queja de la respuesta. Agreguemos el código que falta al controlador de películas:

# app / controllers / api / v1 / movies_controller.rb
módulo Api
  módulo V1
    clase MoviesController 

Ejecute la prueba y vea si hemos terminado.

Sip. Eso es todo. Hemos completado una aplicación de fondo realmente básica que permite a los usuarios buscar un modelo a través de API.

Puede encontrar el código completo en mi repositorio de GitHub aquí. Puede llenar su tabla de películas con algunos datos ejecutando rails db: seed para que pueda ver la aplicación en acción. Esto importará alrededor de 45k películas de un conjunto de datos descargado de Kaggle. Eche un vistazo al archivo Léame para obtener más detalles.

Si le gustó este artículo, recomiéndelo presionando el icono de aplauso que encontrará en la parte inferior de esta página para que más personas puedan verlo en Medium.