Ingeniería

Cómo crear rápidamente experiencias de búsqueda excelentes en React

15 de noviembre de 2019: Se actualizó el código de este blog para incluir las llaves faltantes. También se ajustó el flujo para evitar errores de tiempo de ejecución.

Crear experiencias de búsqueda es un trabajo duro. Puede parecer fácil a simple vista: crear una barra de búsqueda, colocar datos en una base de datos y hacer que los usuarios alimenten búsquedas a la base de datos. Pero se deben tener en cuenta muchas cosas para el modelado de datos, la lógica subyacente y, por supuesto, el diseño general y la experiencia del usuario.

Revisaremos cómo crear experiencias de búsqueda excelentes basadas en React con la biblioteca open source Search UI de Elastic. Nos tomará unos 30 minutos, luego estarás listo para incorporar búsquedas en cualquier aplicación que lo necesite.

Pero, primero, ¿por qué es tan complejo crear búsquedas?

Las búsquedas son complicadas

Hace unas pocas semanas, un muy buen artículo se hizo popular: Falsehoods Programmers Believe About Search (Falsedades que los programadores creen sobre la búsqueda). Contiene un listado exhaustivo de suposiciones falsas que los desarrolladores trasladan al desarrollo de la búsqueda.

Entre las muchas falsedades consideradas ciertas:

  • “Los clientes que saben lo que buscan buscarán de la manera que esperas”.
  • “Puedes escribir un parseador de búsqueda que siempre parsee la búsqueda correctamente”.
  • “Una vez configurada, la búsqueda funcionará del mismo modo la semana próxima”.
  • “Los sinónimos son fáciles”.
  • ...Y muchas otras joyas; deberías echarle un vistazo.

La conclusión es que la búsqueda presenta muchos desafíos, pero no son solamente técnicos. Tienes que pensar en cómo gestionar el estado, crear componentes para filtrado, facetado, clasificación, paginación, sinónimos, procesamiento de lenguaje y mucho, mucho más. Pero, en resumen:

Crear búsquedas excelentes requiere dos partes sofisticadas: (1) el motor de búsqueda, que proporciona las API para impulsar la búsqueda, y (2) la biblioteca de búsqueda, que presenta la experiencia de búsqueda.

En cuanto al motor de búsqueda, echaremos un vistazo a Elastic App Search.

En lo que respecta a la experiencia de búsqueda, presentaremos una biblioteca de búsqueda de SO: Search UI.

Una vez que terminemos, se verá así:

image2.png

Motor de búsqueda: Elastic App Search

App Search está disponible como servicio gestionado pago o como distribución autogestionada gratuita. En este tutorial usaremos el servicio gestionado, pero recuerda que tu equipo puede usar Search UI y App Search con una licencia básica sin costo si lo hospedas por tu cuenta.

El plan: indexa los documentos que representan los mejores videojuegos de todos los tiempos en un motor de búsqueda, luego diseña y optimiza una experiencia de búsqueda en la que puedas explorarlos.

Primero, suscríbete para una prueba gratuita de 14 días; no se requiere tarjeta de crédito.

Crea un motor. Puedes elegir entre 13 idiomas diferentes.

Nombrémoslo video-games y establezcamos el idioma a English.

image4.png

Descarga el conjunto de datos de los mejores videojuegos y luego usa el importador para cargarlo a App Search.

A continuación, haz clic en el motor y selecciona la pestaña Credentials (Credenciales).

Crea una Public Search Key (Clave pública de prueba) nueva con Limited Engine Access (Acceso limitado al motor) solo al motor video-games.

Recupera la Public Search Key nueva y tu Host Identifier (Identificador de host).

Aunque no haya parecido mucho, ahora tenemos un motor de búsqueda totalmente funcional listo para buscar en los datos de los videojuegos mediante una API de búsqueda refinada.

Hasta ahora, hicimos lo siguiente:

  • Creamos un motor de búsqueda.
  • Ingestamos documentos.
  • Creamos un esquema predeterminado.
  • Recuperamos una credencial definida y desechable que podemos exponer al navegador.

Eso es todo con App Search, por ahora.

Continuemos con la creación de la experiencia de búsqueda con Search UI.

Biblioteca de búsqueda: Search UI

Usaremos la utilidad de andamiaje create-react-app para crear una app React:

npm install -g create-react-app
create-react-app video-game-search --use-npm
cd video-game-search

En esta base, instalaremos Search UI y el conector App Search:

npm install --save @elastic/react-search-ui @elastic/search-ui-app-search-connector

E iniciaremos la app en modo de desarrollo:

npm start

Abre src/App.js en tu editor de texto favorito.

Comenzaremos con un código modelo y luego lo desarmaremos.

Ten en cuenta los comentarios.

// Paso n.º 1: Importar expresiones
import React from "react";
import AppSearchAPIConnector from "@elastic/search-ui-app-search-connector";
import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";
import { Layout } from "@elastic/react-search-ui-views";
import "@elastic/react-search-ui-views/lib/styles/styles.css";
// Paso n.º 2: El conector
const connector = new AppSearchAPIConnector({
  searchKey: "[YOUR_SEARCH_KEY]",
  engineName: "video-games",
  hostIdentifier: "[YOUR_HOST_IDENTIFIER]"
});
// Paso n.º 3: Opciones de configuración
const configurationOptions = {
  apiConnector: connector
  // Completemos esto juntos.
};
// Paso n.º 4: SearchProvider: los toques finales
export default function App() {
  return (
    <SearchProvider config={configurationOptions}>
      <div className="App">
        <Layout
        // Completemos esto juntos.
        />
      </div>
    </SearchProvider>
  );
}

Paso 1: Importar expresiones

Tendremos que importar nuestras dependencias de Search UI y React.

Los componentes fundamentales, el conector y los componentes de visualización se encuentran en tres paquetes diferentes:

  • @elastic/search-ui-app-search-connector
  • @elastic/react-search-ui
  • @elastic/react-search-ui-views

Conoceremos más sobre cada uno a medida que avancemos.

import React from "react";
import AppSearchAPIConnector from "@elastic/search-ui-app-search-connector";
import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";
import { Layout } from "@elastic/react-search-ui-views";

También importaremos una hoja de estilo predeterminada para este proyecto, la cual nos brindará una apariencia agradable sin necesidad de escribir una línea de nuestra propia CSS:

import "@elastic/react-search-ui-views/lib/styles/styles.css";

Paso 2: El conector

Tenemos nuestra clave de búsqueda pública e identificador de host de App Search.

Es hora de ponerlos a trabajar.

El objeto conector en Search UI usa las credenciales para acceder a App Search e impulsar la búsqueda:

const connector = new AppSearchAPIConnector({
  searchKey: "[YOUR_SEARCH_KEY]",
  engineName: "video-games",
  hostIdentifier: "[YOUR_HOST_IDENTIFIER]"
});

Search UI funciona con cualquier API de búsqueda. Pero los conectores hacen que una API de búsqueda simplemente funcione, sin más configuración.

Paso 3: configurationOptions

Antes de adentrarnos en configurationOptions, reflexionemos un momento.

Importamos un conjunto de datos a nuestro motor de búsqueda. Pero, ¿qué tipo de datos son?

Mientras más sepamos sobre los datos, mejor comprenderemos cómo presentarlos a quienes realizan la búsqueda. Y eso nos indicará cómo configurar la experiencia de búsqueda.

Miremos un objeto, el mejor de todos los que se incluyen en este conjunto de datos:

{ 
  "id":"final-fantasy-vii-ps-1997",
  "name":"Final Fantasy VII",
  "year":1997,
  "platform":"PS",
  "genre":"Role-Playing",
  "publisher":"Sony Computer Entertainment",
  "global_sales":9.72,
  "critic_score":92,
  "user_score":9,
  "developer":"SquareSoft",
  "image_url":"https://r.hswstatic.com/w_907/gif/finalfantasyvii-MAIN.jpg"
}

Podemos ver que tiene varios campos de texto como name, year, platform, etc. y algunos campos de números como critic_score, global_sales y user_score.

Si hacemos tres preguntas clave, sabremos lo suficiente para crear una buena experiencia de búsqueda:

  • ¿Cómo buscará la mayoría de las personas? Por el nombre del videojuego.
  • ¿Qué deseará ver la mayoría de las personas en un resultado? El nombre del videojuego, el género, el publicador, las puntuaciones y la plataforma.
  • ¿Cómo filtrará, clasificará y facetará la mayoría de las personas? Por puntuación, género, publicador y plataforma.

Luego podemos trasladar esas respuestas a configurationOptions:

const configurationOptions = {
  apiConnector: connector,
  searchQuery: {
    search_fields: {
      // 1. Busca por nombre de videojuego.
      name: {}
    },
    // 2. Resultados: nombre, género, publicador, puntuaciones y plataforma.
    result_fields: {
      name: {
        // Un fragmento (snippet) significa que los términos de búsqueda que coincidan estarán entre etiquetas <em>.
        snippet: {
          size: 75, // Limita el fragmento a 75 caracteres.
          fallback: true // Retrocede (fallback) a un resultado “sin procesar”.
        }
      },
      genre: {
        snippet: {
          size: 50,
          fallback: true
        }
      },
      publisher: {
        snippet: {
          size: 50,
          fallback: true
        }
      },
      critic_score: {
        // Las puntuaciones son numéricas, por lo que no usaremos un fragmento.
        raw: {}
      },
      user_score: {
        raw: {}
      },
      platform: {
        snippet: {
          size: 50,
          fallback: true
        }
      },
      image_url: {
        raw: {}
      }
    },
    // 3. Faceta por puntuaciones, género, publicador y plataforma, lo usaremos para crear filtros más adelante.
    facets: {
      user_score: {
        type: "range",
        ranges: [
          { from: 0, to: 5, name: "Not good" },
          { from: 5, to: 7, name: "Not bad" },
          { from: 7, to: 9, name: "Pretty good" },
          { from: 9, to: 10, name: "Must play!" }
        ]
      },
      critic_score: {
        type: "range",
        ranges: [
          { from: 0, to: 50, name: "Not good" },
          { from: 50, to: 70, name: "Not bad" },
          { from: 70, to: 90, name: "Pretty good" },
          { from: 90, to: 100, name: "Must play!" }
        ]
      },
      genre: { type: "value", size: 100 },
      publisher: { type: "value", size: 100 },
      platform: { type: "value", size: 100 }
    }
  }
};

Conectamos Search UI a nuestro motor de búsqueda y ahora tenemos opciones que rigen cómo buscaremos en los datos, cómo mostraremos los resultados y cómo luego exploraremos dichos resultados. Pero necesitamos algo que una todo con los componentes frontend dinámicos de Search UI.

Paso 4: SearchProvider

Este es el objeto que gobierna a todos. SearchProvider es donde se anidan todos los demás componentes.

Search UI proporciona un componente Layout, que se usa para presentar un diseño de búsqueda típico. Hay opciones de configuración detallada, pero no las analizaremos en este tutorial.

Haremos dos cosas:

  1. Especificar configurationOptions para SearchProvider
  2. Colocar algunos elementos estructurales esenciales en Layout y agregar dos componentes básicos: SearchBox y Results
export default function App() {
  return (
    <SearchProvider config={configurationOptions}>
      <div className="App">
        <Layout
          header={<SearchBox />}
          // titleField es el campo más prominente en un resultado: el encabezado del resultado.
          bodyContent={<Results titleField="name" urlField="image_url" />}
        />
      </div>
    </SearchProvider>
  );
}

En este momento, tenemos las configuraciones básicas en el front-end. Hay algunos detalles más para definir en el back-end antes de poder ejecutarlo. También deberíamos trabajar sobre el modelo de relevancia para refinar la búsqueda según las necesidades específicas de este proyecto.

Pasemos a App Search...

De regreso al laboratorio

App Search cuenta con características de motor de búsqueda poderosas y refinadas. Hace que el ajuste que solía ser sofisticado sea mucho más agradable. Podemos realizar ajustes de relevancia afinada y cambios de esquema sin problemas con tan solo unos clics. 

Primero ajustaremos el esquema para verlo en acción.

Inicia sesión en App Search, ingresa al motor video-games y luego haz clic en Schema (Esquema) en la sección Manage (Gestionar).

Aparece el esquema. Cada uno de los 22 campos se considera text (texto) de forma predeterminada.

En el objeto configurationOptions, definimos dos facetas de rango para facilitar la búsqueda en los números: user_score y critic_score. Para que una faceta de rango funcione como se espera, el tipo de campo debe ser un número.

Haz clic en el menú desplegable junto a cada campo y cambia a number, luego haz clic en Update Types (Actualizar tipos):

image1.png

El motor vuelve a indexarse sobre la marcha. Y luego, cuando agreguemos los componentes de facetado a nuestro diseño, los filtros de rango funcionarán como esperamos. Ahora, pasemos a lo verdaderamente interesante.

Esta sección es sumamente relevante

Hay tres características de relevancia claves: Synonyms (Sinónimos), Curations (Curaciones) y Relevance Tuning (Afinación de la relevancia).

Selecciona cada característica en la sección Search Settings (Configuración de búsqueda) en la barra lateral:

image8.png

Sinónimos

Algunas personas conducen coches, otras automóviles, otras pueden conducir un cacharro. Internet es global, y las personas alrededor del mundo usan diferentes palabras para describir las cosas. Los sinónimos te ayudan a crear conjuntos de términos que se consideran una misma cosa.

En el caso de un motor de búsqueda de videojuegos, sabemos que las personas querrán encontrar Final Fantasy. Pero quizás escriban FF en su lugar.

Haz clic en Synonyms (Sinónimos), seleccionaCreate a Synonym Set (Crear un conjunto de sinónimos) e introduce los términos:

image6.png

Haz clic en Save (Guardar). Puedes agregar la cantidad de conjuntos de sinónimos que desees.

Las búsquedas de FF ahora tendrán el mismo peso que las búsquedas de Final Fantasy.

Curaciones

Las curaciones son una de las características favoritas. ¿Y si alguien busca Final Fantasy o FF? Hay muchos juegos en la serie, ¿cuál obtendrán?

De manera predeterminada, los cinco resultados principales se ven así:

1. Final Fantasy VIII

2. Final Fantasy X

3. Final Fantasy Tactics

4. Final Fantasy IX

5. Final Fantasy XIII

Eso no parece estar bien... Final Fantasy VII fue el mejor Final Fantasy de todos. Y Final Fantasy XIII ni siquiera fue bueno. 😜

¿Podemos hacer que alguien que busque Final Fantasy vea Final Fantasy VII como el primer resultado? ¿Y podemos eliminar Final Fantasy XIII de nuestros resultados de búsqueda?

¡Claro!

Haz clic en Curations (Curaciones) e introduce una búsqueda: Final Fantasy.

A continuación, arrastra el documento Final Fantasy VII hacia arriba hasta la sección Promoted Documents (Documentos ascendidos) tomando la barra de control que se encuentra en el extremo izquierdo de la tabla. Y luego haz clic en el botón Hide Result (Ocultar resultado) en el documento Final Fantasy XIII (el ojo con una línea que lo cruza):

image7.png

Cualquiera que busque Final Fantasy o FF ahora verá primero Final Fantasy VII.

Y no verán Final Fantasy XIII en ningún lado. ¡Ja!

Podemos ascender y ocultar muchos documentos. Incluso podemos clasificar los documentos ascendidos para mantener un control total sobre lo que aparece al principio de cada búsqueda.

Afinación de la relevancia

Haz clic en Relevance Tuning (Afinación de la relevancia) en la barra lateral.

Buscamos en un campo de texto: el campo name. Pero, ¿qué sucede si tenemos varios campos de texto en los que las personas buscarán, como un campo name y un campo description? El conjunto de datos de videojuegos que usamos no contiene un campo de descripción, por lo que falsificaremos algunos documentos para razonarlo.

Imaginemos que nuestros documentos son similares a lo siguiente:

{ 
  "name":"Magical Quest",
  "description": "A dangerous journey through caves and such." 
},
{ 
  "name":"Dangerous Quest",
  "description": "A magical journey filled with magical magic. Highly magic." 
}

Si alguien quisiera encontrar el juego Magical Quest, introducirían eso como búsqueda. Pero el primer resultado sería Dangerous Quest:

image3.png

¿Por qué? Porque la palabra “magical” aparece tres veces en la descripción de Dangerous Quest, y el motor de búsqueda no sabrá que un campo es más importante que otro. Entonces, ubicará Dangerous Quest en una posición superior. La afinación de la relevancia existe precisamente por esta cuestión.

Podemos seleccionar un campo y, entre otras cosas, aumentar el peso de su relevancia:

image5.gif

Podemos observar que cuando aumentamos el peso, el elemento de la derecha (Magical Quest) sube de posición porque el campo name adquiere más importancia. Todo lo que tenemos que hacer es arrastrar el control deslizante a un valor mayor y hacer clic en Save (Guardar).

Ahora usamos App Search para lo siguiente:

  • Ajustar el esquema y cambiar user_score y critic_score a campos number (número)
  • Refinar el modelo de relevancia

Y eso es todo con respecto a las características de “dashboard” sofisticadas; cada una tiene un punto final de API equivalente que puedes usar para que todo funcione de forma programática si las GUI no son lo tuyo.

Ahora, cerremos el tema de la UI.

Toques finales

En este momento, tu UI debería ser funcional. Intenta con algunas consultas y explora un poco. Lo primero que observamos es que nos faltan herramientas para explorar los resultados, como filtrado, facetado, clasificación, etc., pero la búsqueda funciona. Tendremos que elaborar la UI.

En el archivo src/App.js inicial, importamos tres componentes básicos:

import { SearchProvider, Results, SearchBox } from "@elastic/react-search-ui";

Agreguemos algunos más, dado lo que definimos para nuestras opciones de configuración.

Importar los componentes siguientes habilitará las capacidades faltantes en la UI:

  • PagingInfo: muestra información sobre la página actual.
  • ResultsPerPage: configura cuántos resultados aparecen en cada página.
  • Paging: navega por diferentes páginas.
  • Facet: filtra y explora los datos de formas exclusivas para el tipo de datos.
  • Sorting: reorienta los resultados de un campo dado.
import {
  PagingInfo,
  ResultsPerPage,
  Paging,
  Facet,
  SearchProvider,
  Results,
  SearchBox,
  Sorting
} from "@elastic/react-search-ui";

Una vez importados, los componentes pueden colocarse en el diseño (layout).

El componente Layout divide la página en secciones, y los componentes pueden colocarse en estas secciones mediante propiedades.

Contiene secciones para lo siguiente:

  • header: cuadro/barra de búsqueda
  • bodyContent: contenedor de resultados
  • sideContent: barra lateral, que contiene las opciones de facetas y clasificación
  • bodyHeader: “envoltura” de los resultados con información completa del contexto, como página actual y cantidad de resultados por página
  • bodyFooter: opciones de paginación para navegar rápidamente entre páginas

Los componentes representan datos. Los datos se toman conforme a la configuración de búsqueda que proporcionamos en configurationOptions. Ahora, colocaremos cada componente en la sección de diseño adecuada.

Por ejemplo, describimos cinco dimensiones de facetado en configurationOptions, por lo que crearemos cinco componentes Facet. Cada componente Facet usará una propiedad “field” (campo) como clave hasta nuestros datos.

Los colocaremos en la sección sideContent, junto al componente Sorting, luego colocaremos los componentes Paging, PagingInfo y ResultsPerPage en las secciones que mejor se adecuen:

<Layout
  header={<SearchBox />}
  bodyContent={<Results titleField="name" urlField="image_url" />}
  sideContent={
    <div>
      <Sorting
        label={"Sort by"}
        sortOptions={[
          {
            name: "Relevance",
            value: "",
            direction: ""
          },
          {
            name: "Name",
            value: "name",
            direction: "asc"
          }
        ]}
      />
      <Facet field="user_score" label="User Score" />
      <Facet field="critic_score" label="Critic Score" />
      <Facet field="genre" label="Genre" />
      <Facet field="publisher" label="Publisher" isFilterable={true} />
      <Facet field="platform" label="Platform" />
    </div>
  }
  bodyHeader={
    <>
      <PagingInfo />
      <ResultsPerPage />
    </>
  }
  bodyFooter={<Paging />}
/>

Ahora echemos un vistazo a la experiencia de búsqueda en el entorno de desarrollo local.

Y lo que es mucho mejor, tenemos una variedad completa de opciones para explorar los resultados de búsqueda.

Agregamos algunos adicionales, como varias opciones de clasificación, y habilitamos el filtrado de la faceta del publicador agregando un solo indicador. Haz una búsqueda en blanco y explora las opciones.

Por último, veamos una última característica de la experiencia de búsqueda. Es conocida...

Autocompletar.

Tú me autocompletas

Quienes realizan búsquedas adoran la característica autocompletar porque brinda retroalimentación instantánea. Las sugerencias son de dos tipos: resultado y búsqueda. Según sea el caso, quien realiza la búsqueda recibirá resultados relevantes o búsquedas potenciales que pueden llevar a los resultados.

Nos enfocaremos en autocompletar como una forma de sugerencia de búsqueda.

Esto requiere dos cambios rápidos.

Primero, debemos agregar autocompletar al objeto configurationOptions:

const configurationOptions = {
  autocompleteQuery: {
    suggestions: {
      types: {
        documents: {
          // En qué campos buscar sugerencias
          fields: ["name"]
        }
      },
      // Cuántas sugerencias aparecen
      size: 5
    }
  },
  ...
};

Luego, debemos habilitar autocompletar como una función de SearchBox:

...
        <Layout
          ...
          header={<SearchBox autocompleteSuggestions={true} />}
/>
...

Yep — that’s it.

Intenta hacer una búsqueda. A medida que escribas, aparecerán sugerencias de búsqueda de autocompletar.

Resumen

Ahora tenemos una experiencia de búsqueda funcional y atractiva. Y evitamos una gran cantidad de errores que generalmente surgen cuando las personas intentan implementar la búsqueda. Un buen trabajo en 30 minutos, ¿verdad?

Search UI es un marco de trabajo de React moderno y flexible para el desarrollo rápido de experiencias de búsqueda. Elastic App Search es un motor de búsqueda robusto desarrollado sobre Elasticsearch. Es un servicio gestionado pago, o puedes ejecutarlo de forma gratuita por tu cuenta con una licencia básica amplia.

Nos encantaría ver lo que creas con Search UI. Visita Gitter y considera hacer tu aporte al proyecto.