Fusejs Highlighting

Grazie a Fuse.js è molto facile implementare sistemi di fuzzy searching (corrispondenza approssimativa delle stringhe) anche nei siti statici.

Nel mio caso, ho utilizzato questa libreria per implementare la ricerca degli articoli in base a titolo, estratto o tag di un blog costruito con Astro per il frontend e WordPress come headless CMS.

Il risultato lo si può testare direttamente dal progetto finito: Ricerca – La Casa di Paola,

Come base di partenza ho usato il codice della pagina di ricerca del tema sarissa, elencato nella pagina dedicata ai temi del sito ufficiale di Astro: manca però il feedback visivo all’utente dato dall’highlighting dei risultati che molti sistemi di ricerca offrono.

Fuse non dispone di questa funzionalità, però ci fornisce i dati utili ad evidenziare le corrispondenze (Match) abilitando l’opzione di configurazione includeMatches.

Soluzione

Il primo passo è costruire una funzione che prenda in input una stringa, un indice di inizio e uno di fine.

highlightText (text, start, end)

Lo scopo di questa funzione è quello di ritornare la stringa data in input evidenziando i caratteri compressi tra l’indice di inizio e di fine. Per farlo bisogna che la sottostringa data da tali caratteri sia incapsulata in un tag a cui si può aggiungere lo stile desiderato.

Nel mio progetto ho preso spunto dalla documentazione di Tailwind in cui viene mostrato un esempio di utilizzo dello pseudo-element before.

La funzione ottenuta è questa:

const highlightText = (text, start, end) => {
  return text.substr(0, start) +
      '<span class="before:block before:absolute before:inset-0 before:bg-yellow-200 text-primary relative inline-block">' + 
      text.substr(start, end - start + 1) +
      '</span>' +
      text.substr(end + 1)
};

Ora bisogna capire come quali indici utilizzare. Fuse, infatti, abilitando l’opzione includesMatches aggiunge all’oggetto dei risultati un elenco di indices per ciascuna key impostata nella ricerca. Serve quindi un’altra funzione che prenda l’array di indici per una certa key e ne ritorni l’elemento “migliore”.

Per filtrare i matches in base alla key basta fare matches.filter(matches => matches.key=="keyName").

const getBestMatch = (matches) => {
    if(matches.length){
      let indices = matches[0].indices;
      if(indices){
        const lengthsArray = indices.map(indices=>indices[1]-indices[0]);
        return matches[0].indices[lengthsArray.indexOf(Math.max(...lengthsArray))];
      }
    }
  }

Questa funzione sceglie tra le corrispondenze quella con più caratteri e ritorna solo quella.

Non resta che combinare le due funzioni ed utilizzarle ogni volta che bisogna evidenziare il testo di un elemento key. Ad esempio, assumendo che res contenga i risultati della funzione search di Fuse e “title” sia una key di ricerca, il testo evidenziato si ottiene con:

highlightMatch(res[i].item.title, getBestMatch(res[i].matches.filter(matches => matches.key=="title")))