Un moteur de recherche sur un site statique grâce à Algolia
SSG ·Quand je travaillais à enrichir la documentation de Cecil, je me suis dit qu’il serait pertinent d’offrir un moteur de recherche full text aux utilisateurs.
La documentation de Cecil est composée de moins de 10 pages : une par thématique (configuration, gestion des contenus, création des templates, etc.) et chacune d’elle contient de nombreuses sections, accessibles par des ancres.
Aussi, il est important que les résultats retournés par un moteur de recherche soient granulaires, c’est à dire qu’ils ciblent ces sections au sein d’une page.
Quelle solution technique ?
Google CSE
Dans un premier temps j’ai expérimenté le moteur de recherche personnalisé de Google (CSE) qui permet de présenter les résultats indexés par Google pour un site donné (comme avec le préfixe site:
).
Si les résultats sont pertinents pour un site contenant de nombreuses pages, il ne semble pas possible de personnaliser les résultats en fonction de sections au sein d’une même page, ce qui ne correspondant pas à mon besoin.
Algolia
Aussi, après plusieurs comparatifs, j’ai finalement retenu la solution Algolia pour les raisons suivantes :
- Pertinence des résultats
- Tarifs abordables (dont un plan gratuit)
- Documentation riche
- Nombreuses bibliothèques de code open source
Étapes clefs
Je souhaitais que le champ de recherche soit disponible sur chacune des pages et qu’il montre immédiatement un extrait des résultats lors de la saisie d’un ou plusieurs mots clefs, et laissant le choix à l’utilisateur de sélectionner la section à consulter : j’ai donc opté pour l’approche Autocomplete (cf. la capture d’écran en début de billet).
Créer un index
Algolia s’appuie sur un index, c’est à dire une collection d’enregistrements dans laquelle la recherche va être effectuée et dont le résultat permet d’afficher un certain nombre d’informations et de pointer vers la page Web correspondante.
Cet index est une structure JSON relativement libre, composée de couples clef-valeur, permettant d’avoir de la matière dans laquelle chercher et également ajuster les critères de recherche.
Transmettre l’index
Algolia propose plusieurs méthodes afin de transmettre ou de mettre à jour l’index :
- à la main, via le dashboard, en uploadant le fichier JSON
- en ligne de command, via Algolia CLI
- programmatiquement, en PHP, en JavaScript, etc.
Paramétrer l’index
Le paramétrage de l’index, c’est à dire déterminer les attributs dans lesquels rechercher, le classement et l’ordonnancement, etc. est relativement simple à réaliser depuis le dashboard.
Je dis relativement car il peut être nécessaire d’effectuer quelques tests avant de maitriser les règles de priorisation des résultats.
Mise en œuvre
Dans le cas de la documentation de Cecil, il faut donc :
- créer un fichier d’index au format JSON
- le transmettre à application Algolia
- afficher un champs de recherche avec auto-complétion
Création de l’index
Avec Cecil il est plutôt aisé de créer un fichier JSON puisque, par définition, c’est son job de générer des fichiers statiques 😊
Ainsi, l’objectif est de :
- collecter le contenu des pages de la documentation (fichiers au format Markdown dans
pages/documentation
), converti en HTML - découper ce contenu de manière cohérente (l'objectif n’est pas de pointer sur la page, mais bien sur une section de la page), via un template Twig sur mesure
- générer un fichier
algolia.json
grâce aux formats de sortie
Résultat cible
Le fichier d’index va ressembler à ça :
[
{
"objectID": "documentation/quick-start#download-cecil",
"page": "Quick Start",
"title": "Download Cecil",
"description": "Download cecil.phar from your terminal:",
"content": "...",
"date": "2020-12-19T00:00:00+00:00",
"href": "documentation/quick-start/#download-cecil"
},
...
]
Création du template
Comme indiqué précédemment, dans le contexte de Cecil, pour créer ce fichier il est nécessaire de créer un template Twig qui va collecter les donner et les rendre au format JSON.
S’agissant de rechercher dans la documentation, j’aurais pu créer ce template dans le dossier « documentation » (layouts/documentation/list.algolia.twig
).
Mais comme je souhaite potentiellement étendre la recherche à plusieurs types de contenus (tels que les « news ») je préfère créer un template applicable à l’ensemble des contenus du site, donc une liste par défaut : layouts/_default/list.algolia.twig
.
Ainsi, au sein du template, il suffit de boucler sur les contenus de la section « documentation », à l’aide du tag for
:
{% for p in site.pages|filter(p => p.section == 'documentation')|sort_by_weight %}
...
{% endfor %}
Ensuite, toute l’astuce consiste à « jouer » sur les header HTML, en l’occurence le « H3 » afin de découper le contenu d’une page en sous sections :
{% set sections = p.content|preg_split('/<h3[^>]*>/') %}
Le filtre Twig
preg_split
à été créé pour l’occasion afin de permettre le découpage d’une chaine de caractère en un tableau, selon une expression régulière.
De là, il suffit ensuite d’extraire les contenus cibles de chaque section, via de la manipulation de chaines de caractères, pour alimenter un « dataset » :
{
"objectID": "Un ID unique",
"page": "Le nom de la page de documentation",
"title": "Le titre de section",
"description": "Le premier paragraphe de la section (utilisé pour illustrer l’aperçu des résiltats)",
"content": "Le contenu de la section, dans laquelle la recherche est effectuée",
"date": "La date de la page, utilisée pour pondérer les résultats",
"href": "Le lien vers la page de la documentation, combinée à une ancre afin d’emmener l’internaute à la bonne section",
}
Voir le template complet sur GitHub.
Associer ce template à un format de sortie
En l’état, Cecil ne sait pas qu’il faut utiliser ce template et surtout à quel type de contenu il doit être associé. C’est embêtant 😅
Pour régler ce soucis il suffit de compléter la configuration de la manière suivante :
output:
formats:
- name: algolia
mediatype: 'application/json'
filename: 'algolia'
extension: 'json'
pagetypeformats:
homepage: ['html', 'atom', 'algolia']
Maintenant Cecil sait que :
- Les pages dont la variable
format
a pour valeur le nom du format « algolia » peuvent utiliser un template de la forme<layout>.algolia.twig
- Enregistrer le fichier généré sous
filename.extension
, soit « algolia.json » - La page de type
homepage
(listant toutes les pages du site) doit être générée dans le format « algolia » (en plus de « html » et « atom »)
Et voilà, l’index est maintenant généré et disponible à la racine du site généré : https://cecil.app/algolia.json.
Transmission de l’index
Comme indiqué précédemment, Algolia offre plusieurs méthodes d’envoi de l’index.
Dans un premier temps j’ai déposé manuellement le fichier généré localement directement depuis le dashboard : c’est un moyen simple et rapide de faire des tests d’intégrité de l’index.
J’ai ensuite cherché à automatisé cette procédure, et j’ai donc opté pour le client d’API JavaScript.
Cecil.app étant hébergé par Netlify, j’ai utilisé un plugin pour me simplifier la mise en oeuvre : Algolia Index Refresh Build Plugin :
[[context.production.plugins]]
package = "netlify-plugin-refresh-algolia"
[context.production.plugins.inputs]
appId = "APP_ID"
indexName = "INDEX"
filePath = "_site/algolia.json"
Formulaire de recherche
La mise en œuvre est relativement simple :
- intégrer un champ de saisi (élément
input
) - lui associer la bibliothèque Autocomplete.js
Champ de saisie :
<input type="text" id="search-input" placeholder="{% trans %}Search the Docs [Alt+S]{% endtrans %}" accesskey="s" />
Autocomplete.js :
// client Algolia
var client = algoliasearch('APP_ID', 'API_KEY');
// index dans lequel rechercher
var index = client.initIndex('INDEX');
// fonction de recherche
function newHitsSource(index, params) {
return function doSearch(query, cb) {
index
.search(query, params)
.then(function(res) {
cb(res.hits, res);
})
.catch(function(err) {
console.error(err);
cb([]);
});
};
}
// association de la lib au champ "search-input"
autocomplete('#search-input', { hint: false }, [
{
source: newHitsSource(index, {
// paramètres de mise en valeur des résultats suggérés
hitsPerPage: 4,
attributesToHighlight: ['description', 'page', 'title'],
highlightPreTag: '<strong>',
highlightPostTag: '</strong>',
attributesToSnippet: ['description:25'],
snippetEllipsisText: '…'
}),
// clef d'affichage principale (ici le titre de la section)
displayKey: 'title'
}
]).on('autocomplete:selected', function(event, suggestion, dataset, context) {
// au clic sur une suggestion on envoie l'internaute vers la page#ancre correspondante
window.location.href = '{{ url() }}' + suggestion.href;
});
Voir le template complet sur GitHub.
Et voilà ! 🎉
Notes :
- Il s’agit ici de la version 0 de Autocomplete.js qui reste fonctionnelle mais commence à vieillir
- La personnalisation de l’apparence des suggestions est un peu pénible car il faut arriver à « retrouver » les classes CSS générées à la volée via JavaScript, ce qui n’est pas toujours évident…
Conclusion
Je me suis bien amusé à créer ce moteur de recherche, et je suis plutôt satisfait de la fonctionnalité, qui est fonctionnelle et surtout très utile.
Pour tester, ça se passe par ici : https://cecil.app/documentation/