Para implementar funcionalidades de búsqueda flexible en el lado del cliente, una excelente opción es la librería Fuse.js. Esta herramienta destaca por su capaciadd de realizar búsquedas difusas (fuzzy search), ofrecer una puntuación de relevancia en los resultados y permitir la ponderación de campos, lo que la hace muy adaptable a diversas necesidades de filtrado de datos.
Uso Básico
Comenzar con Fuse.js es sencillo, tanto para arrays de cadenas de texto como para estructuras de datos más complejas.
Búsqueda en Arrays Simples
En el siguiente ejemplo, inicializamos Fuse.js con una lista de nombres de productos y buscamos términos que pueden no ser una coincidencia exacta.
<script src="ruta/a/fuse.min.js"></script>
<script>
// Ejemplo de búsqueda en un array de cadenas
{
const listaNombres = ["Laptop Ultrabook X1", "Mouse Ergonómico Inalámbrico", "Teclado Mecánico RGB", "Monitor Curvo 27 pulgadas"];
const opcionesBusqueda = {
includeScore: true, // Incluir la puntuación de coincidencia en los resultados
};
const buscador = new Fuse(listaNombres, opcionesBusqueda);
const terminos = ["lapto", "monitr"]; // Términos con errores tipográficos
for (let termino of terminos) {
console.log(`Buscando "${termino}":`, buscador.search(termino));
}
// Salida esperada para "lapto": [{item: "Laptop Ultrabook X1", refIndex: 0, score: ...}]
// Salida esperada para "monitr": [{item: "Monitor Curvo 27 pulgadas", refIndex: 3, score: ...}]
}
</script>
Búsqueda en Arrays de Objetos con Claves Específicas
Fuse.js permite especificar qué campos de un objeto deben considerarse para la búsqueda mediante la propiedad keys.
<script>
// Ejemplo de búsqueda en un array de objetos especificando claves
{
const catalogoProductos = [
{
nombre: "Auriculares Bluetooth Premium",
categoria: "Audio",
descripcion: "Sonido de alta fidelidad, cancelación de ruido."
},
{
nombre: "Smartwatch Deportivo Pro",
categoria: "Wearables",
descripcion: "Monitor de frecuencia cardíaca, GPS integrado."
},
{
nombre: "Cámara Web Full HD",
categoria: "Accesorios PC",
descripcion: "Ideal para videoconferencias y streaming."
},
];
const configuracionBusqueda = {
includeScore: true,
keys: ["nombre", "categoria"], // Buscar en los campos 'nombre' y 'categoria'
};
const motorBusqueda = new Fuse(catalogoProductos, configuracionBusqueda);
const consultas = ["auricula", "wearabl"];
for (let consulta of consultas) {
console.log(`Consulta "${consulta}":`, motorBusqueda.search(consulta));
}
}
</script>
Búsqueda en Claves Anidadas
Para objetos con estructuras más profundas, Fuse.js permite definir rutas a claves anidadas o utilizar funciones personalizadas para extraer los valores.
<script>
// Búsqueda en claves anidadas
{
const inventario = [
{
id: 101,
articulo: "Tablet Pro X",
detalles: {
fabricante: {
nombre: "TechCorp",
pais: "Estados Unidos",
},
especificaciones: ["128GB", "8GB RAM"],
},
},
{
id: 102,
articulo: "Smartphone Ultra Z",
detalles: {
fabricante: {
nombre: "Global Devices",
pais: "China",
},
especificaciones: ["256GB", "12GB RAM"],
},
},
];
const optAnidada = {
includeScore: true,
keys: ["detalles.fabricante.pais"], // Ruta de la clave anidada
};
const buscadorAnidado = new Fuse(inventario, optAnidada);
const resultadoAnidado = buscadorAnidado.search("unidos");
console.log("Búsqueda anidada 'unidos':", resultadoAnidado);
// Definición de ruta anidada como un array
const optArrayPath = {
includeScore: true,
keys: [["detalles", "fabricante", "pais"]],
};
const buscadorArrayPath = new Fuse(inventario, optArrayPath);
const resultadoArrayPath = buscadorArrayPath.search("china");
console.log("Búsqueda anidada 'china' (ruta array):", resultadoArrayPath);
// Uso de funciones `getFn` para claves personalizadas
const optCustomGetter = {
includeScore: true,
keys: [
{ name: "nombreArticulo", getFn: (prod) => prod.articulo },
{ name: "paisOrigen", getFn: (prod) => prod.detalles.fabricante.pais },
],
};
const buscadorCustom = new Fuse(inventario, optCustomGetter);
// Para buscar, la clave en el objeto de consulta debe coincidir con 'name'
const resultadoCustom = buscadorCustom.search({ paisOrigen: "estados" });
console.log("Búsqueda con getter personalizado 'estados':", resultadoCustom);
}
</script>
Búsqueda con Ponderación
Es posible asignar pesos a diferantes claves, dando más importancia a ciertos campos sobre otros al calcular la puntuación de relevancia.
<script>
// Búsqueda con ponderación de campos
{
const articulosBlog = [
{
titulo: "Introducción a JavaScript Moderno",
autor: "Ana López",
etiquetas: ["programacion", "frontend", "js"],
},
{
titulo: "Guía Completa de CSS Flexbox",
autor: "Carlos García",
etiquetas: ["diseño web", "css", "maquetacion"],
},
{
titulo: "Patrones de Diseño en Python",
autor: "Ana López",
etiquetas: ["backend", "python", "arquitectura"],
},
];
const opcionesPeso = {
includeScore: true,
keys: [
{
name: "titulo",
weight: 0.7, // Más importancia al título
},
{
name: "etiquetas",
weight: 0.3, // Menos importancia a las etiquetas
},
],
};
const buscadorPeso = new Fuse(articulosBlog, opcionesPeso);
const busquedaConPeso = buscadorPeso.search("ana");
console.log("Búsqueda ponderada 'ana':", busquedaConPeso);
// Otro ejemplo donde se define el peso de forma explícita o se usa el valor por defecto (1)
const opcionesPeso2 = {
keys: [
"autor", // Peso por defecto: 1
{
name: "titulo",
weight: 2, // Doble importancia para el título
},
],
};
const buscadorPeso2 = new Fuse(articulosBlog, opcionesPeso2);
const busquedaConPeso2 = buscadorPeso2.search("guia");
console.log("Búsqueda ponderada 'guia' (sin score):", busquedaConPeso2);
}
</script>
Búsqueda Extendida
Activar el modo de búsqueda extendida (useExtendedSearch: true) permite utilizar operadores lógicos como AND, OR y NOT, así como expresiones regulares para consultas más complejas.
<script>
// Búsqueda extendida con operadores
{
const documentos = [
{ nombre: "Introducción a Desarrollo Web" },
{ nombre: "Tutorial de Python para Principiantes" },
{ nombre: "Desarrollo Web Avanzado con React" },
{ nombre: "Diseño de APIs con Python" },
];
const opcionesExtendida = {
includeScore: true,
useExtendedSearch: true, // Habilitar el modo de búsqueda extendida
keys: ["nombre"],
};
const buscadorExtendida = new Fuse(documentos, opcionesExtendida);
// Buscar elementos que contengan "Desarrollo" Y "React", O que terminen con "Python"
const resultadoExtendida = buscadorExtendida.search("'Desarrollo & React | Python$'");
console.log("Búsqueda extendida 'Desarrollo & React | Python$':", resultadoExtendida);
}
</script>
Ejemplo Práctico con PHP y Alpine.js
Fuse.js se integra fácilmente con frameworks frontend reactivos como Alpine.js y datos provenientes de un backend (en este caso, PHP). A continuación, se muestra un ejemplo donde PHP recupera registros de una base de datos, los codifica en JSON, y Alpine.js utiliza Fuse.js para ofrecer una experiencia de búsqueda dinámica.
<?php
// Configuración de la conexión a la base de datos (se recomienda usar variables de entorno)
$dsn = "mysql:host=127.0.0.1;port=3306;dbname=test;charset=utf8";
$user = "root";
$password = "";
$pdo = null;
try {
$pdo = new PDO($dsn, $user, $password, [PDO::ATTR_PERSISTENT => true]);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $error) {
error_log("Error de conexión a la base de datos: " . $error->getMessage());
die('Error al conectar con la base de datos.');
}
// Consulta para obtener datos de ejemplo
$sql = "SELECT id, titulo, autor, contenido FROM tabla_documentos_ejemplo"; // Tabla y columnas de ejemplo
$stmt = $pdo->prepare($sql);
$stmt->execute();
$documentosAlmacenados = [];
while ($fila = $stmt->fetch()) {
// Combinar campos relevantes en una sola cadena para una búsqueda simple con Fuse.js
$documentosAlmacenados[] = str_replace(["\n", "\r"], '', $fila['titulo'] . ' - ' . $fila['autor'] . ' - ' . $fila['contenido']);
}
?>
<!-- Asegúrate de que Alpine.js y Fuse.js estén cargados -->
<!-- <script type="text/javascript" src="../plugins/alpine.min.js"></script> -->
<!-- <script src="../plugins/fuse/fuse.min.js"></script> -->
<script>
// Cargar datos de PHP en una variable JavaScript
let coleccionDocumentos = JSON.parse('<?= json_encode($documentosAlmacenados) ?>');
const opcionesMotorBusqueda = {
includeScore: true
};
// Inicializar Fuse.js con la colección de documentos
const instanciaFuse = new Fuse(coleccionDocumentos, opcionesMotorBusqueda);
</script>
<script>
// Componente Alpine.js para la funcionalidad de búsqueda
function controladorBusqueda() {
return {
terminoBusqueda: "", // El texto introducido por el usuario
conteoItems: 0, // Número de resultados encontrados
// Propiedad computada que filtra los elementos cuando `terminoBusqueda` cambia
get itemsFiltrados() {
if (this.terminoBusqueda.trim() === "") {
this.conteoItems = coleccionDocumentos.length;
return coleccionDocumentos; // Mostrar todos los elementos si no hay término de búsqueda
}
const resultadosCrudos = instanciaFuse.search(this.terminoBusqueda);
resultadosCrudos.sort((a, b) => a.score - b.score); // Ordenar por puntuación (menor es mejor)
this.conteoItems = resultadosCrudos.length;
return resultadosCrudos.map((resItem) => resItem.item); // Mapear a los elementos originales
},
};
}
</script>
<div x-data="controladorBusqueda()">
<input x-model="terminoBusqueda" placeholder="Buscar..." />
<p>Elementos encontrados: <span x-text="conteoItems"></span></p>
<ul>
<template x-for="(elemento, indice) in itemsFiltrados" :key="indice">
<li x-text="elemento"></li>
</template>
</ul>
</div>
Los resultados de la búsqueda se muestran prácticamente al instante, demostrando la eficiencia de Fuse.js para el filtrado de grandes volúmenes de datos directamante en el cliente.