Cómo implementar la búsqueda del lado del cliente con Fuse.js

Cómo implementar una búsqueda en tiempo real del lado del cliente usando Fuse.js.

Para algunas aplicaciones, ejecutar un servidor de búsqueda completo y conectar un índice es una exageración. En otros, no es práctico debido a requisitos como la necesidad de estar solo fuera de línea. Mientras que una rica experiencia de búsqueda debería de forma predeterminada, un motor de búsqueda real se ejecuta en un servidor; en algunos casos, se prefiere implementar la búsqueda del lado del cliente.

Primeros pasos

Para comenzar, para este tutorial, usaremos CheatCode Next.js Boilerplate como punto de partida. Para clonarlo, ejecuta:

Terminal

git clone https://github.com/cheatcode/nextjs-boilerplate.git

A continuación, cd en el proyecto clonado e instale sus dependencias:

Terminal

cd nextjs-boilerplate && npm install

A continuación, instalemos el fuse.js dependencia a través de NPM:

Terminal

npm i fuse.js

Finalmente, ejecutemos el proyecto:

Terminal

npm run dev

Una vez que todo esté completo, estaremos listos para comenzar.

Configurando nuestros datos de prueba

Primero, para conectar nuestra búsqueda, necesitaremos algunos datos de prueba. Vamos a usar esta lista de países de Github. Debido a que nuestro objetivo es construir esto completamente del lado del cliente, crearemos un archivo JavaScript estático y colocaremos este contenido en él:

/lib/países.js

export default [
  { code: "AF", name: "Afghanistan" },
  [...]
  { code: "ZW", name: "Zimbabwe" },
];

A continuación, estamos listos para comenzar a desarrollar nuestra búsqueda. Para demostrar la configuración, agregaremos un /search página en el repetitivo:

/páginas/búsqueda/index.js

import React, { useState } from "react";

const Search = () => {
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);

  return (
    <div>
      // We'll build out our search and results UI here...
    </div>
  );
};

Search.propTypes = {};

export default Search;

Para comenzar, aquí hemos creado un componente React esqueleto utilizando el patrón de componente de función. En la parte superior, definimos nuestro componente de función con const Search . Justo dentro del cuerpo de la función, utilizamos el useState() gancho en React para crear dos valores de estado que necesitaremos:searchQuery y searchResults .

Algunas cosas a tener en cuenta cuando usamos el useState() anzuelo:

  • Cuando llamamos a useState() el valor que le pasamos representa el valor predeterminado (aquí, para searchQuery pasamos una cadena vacía y para searchResults pasamos una matriz vacía).
  • Una llamada al useState() devuelve una matriz que contiene dos valores:el valor actual y un setter para actualizar el valor (aquí, searchQuery es el nombre que usamos para el valor del estado y setSearchQuery nos permite actualizar ese valor).

A continuación, para crear nuestro componente base, return un <div></div> vacío etiqueta donde irá el núcleo de nuestra interfaz de usuario de búsqueda.

Inicializando nuestro índice

Ahora, busquemos nuestra lista de países y creemos nuestro índice de búsqueda usando Fuse:

/páginas/búsqueda/index.js

import React, { useState } from "react";
import Fuse from "fuse.js";
import countries from "../../lib/countries";

const Search = () => {
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);

  const searchIndex = new Fuse(countries, {
    includeScore: true,
    threshold: 0.4,
    keys: ["name"],
  });

  return (
    <div>
      // We'll build out our search and results UI here...
    </div>
  );
};

Search.propTypes = {};

export default Search;

Hemos añadido algunas cosas aquí. Primero, en la parte superior, importamos el countries.js archivo que creamos anteriormente. A continuación, creamos una nueva variable searchIndex que se establece en new Fuse() pasándole dos cosas:nuestra lista de countries (los datos que queremos agregar al índice) y un options objeto con tres configuraciones:

  1. includeScore le dice a Fuse que queremos que cada resultado de búsqueda reciba una puntuación de relevancia y queremos que esa puntuación se devuelva en los datos de los resultados de búsqueda.
  2. threshold es un número que dicta qué tan "confusa" debe ser nuestra búsqueda. Un threshold de 0 significa que la búsqueda tiene que coincidir exactamente mientras que un threshold de 1.0 significa cualquier cosa coincidirá. 0.4 es arbitrario aquí, así que siéntete libre de jugar con él.
  3. keys es una matriz de cadenas que describen las claves de objeto que queremos buscar. En este caso, solo queremos que nuestra búsqueda sea contra el name propiedad en cada uno de nuestros objetos del país.

Aunque puede no parecer mucho, este es el núcleo de trabajar con Fuse. Sencillo, ¿verdad? Con esto, ahora estamos listos para configurar una interfaz de usuario de búsqueda y ver algunos resultados en tiempo real.

Conexión de la IU de búsqueda

Primero, necesitamos agregar un <input /> donde un usuario puede escribir una consulta de búsqueda:

/páginas/búsqueda/index.js

import React, { useState } from "react";
import Fuse from "fuse.js";
import countries from "../../lib/countries";

const Search = () => {
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);

  const searchIndex = new Fuse(countries, {
    includeScore: true,
    threshold: 0.4,
    keys: ["name"],
  });

  const handleSearch = (searchQuery) => {
    setSearchQuery(searchQuery);
    const results = searchIndex.search(searchQuery);
    setSearchResults(results);
  };

  return (
    <div>
      <div className="mb-4">
        <input
          type="search"
          name="search"
          className="form-control"
          value={searchQuery}
          onChange={(event) => handleSearch(event.target.value)}
        />
      </div>
    </div>
  );
};

Search.propTypes = {};

export default Search;

Estamos agregando dos cosas importantes aquí:primero, abajo en el return (el marcado de nuestro componente), hemos agregado un <input /> etiqueta con un tipo de search (esto alterna las características especiales del navegador para una entrada de búsqueda como un botón borrar).

También le hemos dado un className de form-control para darle un estilo base a través de Bootstrap (incluido en el modelo que estamos usando). A continuación, configuramos el value de la entrada a nuestro searchQuery indique el valor y luego agregue un onChange controlador, pasando una función que llama a otra función que hemos definido arriba, handleSearch() , pasando el event.target.value que representa el valor actual ingresado en la entrada de búsqueda.

/páginas/búsqueda/index.js

const handleSearch = (searchQuery) => {    
  setSearchQuery(searchQuery);
  const results = searchIndex.search(searchQuery);
  setSearchResults(results);
};

Acercándonos a ese handleSearch() función, aquí es donde ocurre la magia. Primero, nos aseguramos de configurar nuestro searchQuery (event.target.value , pasado al handleSearch funcionar como searchQuery ) para que nuestra interfaz de usuario se actualice a medida que el usuario escribe. En segundo lugar, realizamos nuestra búsqueda real, usando el .search() devuelto como parte de la instancia del índice Fuse (lo que almacenamos en el searchIndex variables).

Finalmente, tomamos el results volvemos de Fuse y luego los ponemos en estado. Ahora, estamos listos para generar nuestros resultados y ver cómo funciona todo en tiempo real.

Conexión de la interfaz de usuario de resultados

Para terminar, a continuación, debemos mostrar los resultados de nuestra búsqueda. Recuerda que anteriormente, como parte del objeto de opciones que pasamos a Fuse, agregamos un includeScore configuración, establecer en true . Antes de representar nuestros resultados de búsqueda, queremos crear una versión ordenada de los resultados, basada en este score valor.

/páginas/búsqueda/index.js

import React, { useState } from "react";
import Fuse from "fuse.js";
import countries from "../../lib/countries";

const Search = () => {
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);
  const sortedSearchResults = searchResults.sort((resultA, resultB) => {
    return resultA.score - resultB.score;
  });

  const searchIndex = new Fuse(countries, {
    includeScore: true,
    threshold: 0.4,
    keys: ["name"],
  });

  const handleSearch = (searchQuery) => {
    setSearchQuery(searchQuery);
    const results = searchIndex.search(searchQuery);
    setSearchResults(results);
  };

  return (
    <div>
      <div className="mb-4">
        <input
          type="search"
          name="search"
          className="form-control"
          value={searchQuery}
          onChange={(event) => handleSearch(event.target.value)}
        />
      </div>
    </div>
  );
};

Search.propTypes = {};

export default Search;

Aquí, hemos agregado un sortedSearchResults variable justo debajo de nuestro useState() declaración para el searchResults variable. Se le asigna el resultado de llamar a searchResults.sort() (la matriz JavaScript nativa .sort() método). A él, le pasamos una función de comparación que toma dos argumentos:el elemento actual que estamos comparando resultA (el que se repite en la ordenación) y el elemento siguiente resultB .

Nuestra comparación es comprobar la diferencia entre cada puntuación. Automáticamente, el .sort() El método usará esto para devolvernos una copia ordenada de nuestra matriz de resultados de búsqueda, por score de cada resultado propiedad.

Ahora estamos listos para renderizar los resultados. Agreguemos un código repetitivo y luego analicemos:

/páginas/búsqueda/index.js

import React, { useState } from "react";
import Fuse from "fuse.js";
import countries from "../../lib/countries";

const Search = () => {
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);
  const sortedSearchResults = searchResults.sort((resultA, resultB) => {
    return resultA.score - resultB.score;
  });

  const searchIndex = new Fuse(countries, {
    includeScore: true,
    threshold: 0.4,
    keys: ["name"],
  });

  const handleSearch = (searchQuery) => {
    setSearchQuery(searchQuery);
    const results = searchIndex.search(searchQuery);
    setSearchResults(results);
  };

  return (
    <div>
      <div className="mb-4">
        <input
          type="search"
          name="search"
          className="form-control"
          value={searchQuery}
          onChange={(event) => handleSearch(event.target.value)}
        />
      </div>
      {sortedSearchResults.length > 0 && (
        <ul className="list-group">
          {sortedSearchResults.map(({ item }) => {
            return (
              <li className="list-group-item" key={item.name}>
                {item.name} ({item.code})
              </li>
            );
          })}
        </ul>
      )}
    </div>
  );
};

Search.propTypes = {};

export default Search;

Esto finaliza nuestra interfaz de usuario de búsqueda. Aquí, hemos tomado el sortedSearchResults creamos y primero comprobamos si tiene una longitud superior a 0 . Si lo hace , queremos representar nuestros resultados de búsqueda <ul></ul> . Si no, queremos que se oculte. Para esa lista, hemos usado Bootstrap list-group para dar a nuestros resultados de búsqueda algo de estilo junto con el list-group-item clase en cada uno de nuestros resultados de búsqueda individuales.

Para cada resultado de búsqueda, solo representamos el name y code (entre paréntesis) lado a lado.

¡Eso es todo! Ahora, si cargamos nuestra aplicación en el navegador y nos dirigimos a http://localhost:5000/search , deberíamos ver nuestra interfaz de usuario de búsqueda en funcionamiento.

Terminando

En este tutorial, aprendimos cómo crear una búsqueda en tiempo real del lado del cliente usando Fuse. Aprendimos cómo configurar un componente de búsqueda simple en React, crear un índice de búsqueda con Fuse (rellenándolo con datos en el proceso) y realizar una consulta de búsqueda en ese índice.