Le moteur de recherche de votre site Internet ne vous satisfait pas ? Vous souhaitez intégrer un moteur de recherche performant sans devoir modifier le code source de celui-ci ?

Vous pouvez utiliser pour cela ElasticSearch pour indexer vos contenus rédactionnels et permettre leur recherche via l'API REST native d'ElasticSearch qui offre de nombreuses options en plus d'être performante.

C'est la solution que j'ai opté pour ce blog, l'ensemble du contenu éditorial est administré au travers de la solution de blog Ghost qui ne dispose pas nativement d'un moteur de recherche de contenu. L'ensemble des fonctionnalités de recherche disponibles sur ce site sont désormais déléguées à ElasticSearch.

Cette option n'est pas sans contrainte, elle nécessite d'avoir des ressources disponibles sur votre serveur pour héberger le processus ElasticSearch et Logstash, idéalement 2Go de RAM. Si votre site Internet possède peu de contenus, FuseJS peut être un très bon compromis, mais imposera aux visiteurs de votre site de télécharger tout le contenu éditorial de votre site sur leur navigateur.

Voici l'architecture mise en oeuvre :
architecture-blog-elasticsearch-logstash

Pour mettre en place une telle architecture, vous trouverez ci-dessous les différentes étapes à suivre.

Le prérequis de cet article est de bénéficier d'un site hébergé avec Nginx et disposant de contenus dans une base MySQL, le tout sur un serveur Linux Ubuntu. Dans l'exemple ci-dessous, j'utilise Ghost comme solution de CMS.

Remarque importante : Certaines solutions CMS possèdent des connecteurs natifs à ElasticSearch pour indexer et rechercher des contenus automatiquement, c'est le cas par exemple de Drupal. Sur ce type d'intégration, Logstash n'est probablement pas nécessaire.

Ajoutez le repository ElasticSearch

La première étape consiste à ajouter le repository ElasticSearch à la liste des repository de votre distribution Ubuntu.

$ sudo apt-get install apt-transport-https
$ wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
$ echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-7.x.list"

Installez ElasticSearch

Une fois que le repository d'ElasticSearch a été ajouté à votre distribution, il est temps de faire une mise à jour de votre cache et d'installer le package ElasticSearch.

$ sudo apt-get update
$ sudo apt-get install elasticsearch

Installez Logstash

Une fois ElasticSearch installé, vous pouvez installer Logstash qui se trouve sur le même repository que ElasticSearch.

$ sudo apt-get install logstash

Ajoutez ElasticSearch et Logstash au démarrage de votre serveur

Assurez-vous que ElasticSearch et Logstash démarreront à chaque redémarrage de votre serveur pour éviter tout arrêt de service.

$ sudo /bin/systemctl daemon-reload
$ sudo /bin/systemctl enable elasticsearch.service
$ sudo /bin/systemctl enable logstash.service

Indexez vos contenus éditoriaux

Il est temps maintenant d'indexer vos contenus éditoriaux grâce au connecteur MySQL de Logstash.
Celui-ci aura en charge de vérifier la présence de nouveaux contenus dans votre base de données et de l'indexer directement dans ElasticSearch.

Pour cela, créez un fichier de configuration Logstash à l'emplacement suivant

$ sudo vim /etc/logstash/conf.d/mon-site.conf

Ce fichier de configuration permettra d'indiquer à Logstash où se trouvent les articles à indexer.

Indiquez dans la partie Input :

  • statement : Le contenu à indexer qui se traduit par une requête SQL qui retournera toutes les données qui seront stockées dans le document d'ElasticSearch.
  • tracking_column : Le champ que doit utiliser Logstash pour identifier les nouveaux contenus.
  • schedule : La fréquence d'indexation des contenus, ici toutes les nuits à 1h du matin.
  • jdbc_driver_library : Le driver Java MySQL que vous pouvez télécharger ici et que vous pourrez positionner par exemple dans le répertoire suivant : "/usr/share/logstash/vendor/mysql/"

Dans la partie Output :

  • index : Le nom de la collection à créer.
  • document_type : le type de document à indexer.
  • document_id : L'identifiant unique de votre article à indexer.
input {
        jdbc {
                jdbc_connection_string => "jdbc:mysql://localhost:3306/ghost"
                jdbc_user => "ghost"
                jdbc_password => "xxxxxx"
                jdbc_driver_library => "/usr/share/logstash/vendor/mysql/mysql-connector-java-5.1.42.jar"
                jdbc_driver_class => "com.mysql.jdbc.Driver"
                statement => "SELECT id,title,plaintext,slug,featured,published_at,updated_at FROM posts WHERE status='published' and updated_at > :sql_last_value"
                use_column_value => true
                tracking_column => "updated_at"
                tracking_column_type => "timestamp"
                schedule => "0 1 * * *"
        }
}
output {
        stdout { codec => json_lines }
        elasticsearch {
                hosts => ["http://localhost:9200"]
                index => "mon_site"
                document_type => "article"
                document_id => "%{id}"
        }
}

Démarrez les services ElasticSearch et Logstash

Démarrez le service d'ElasticSearch dans un premier temps, puis celui de Logstash.

$ sudo /bin/systemctl restart elasticsearch.service
$ sudo /bin/systemctl restart logstash.service

Testez que le service ElasticSearch est fonctionnel

Le service ElasticSearch ouvre par défaut un serveur HTTP en localhost sur le port 9200. Vérifiez que le service est fonctionnel en réalisant un GET HTTP sur la racine du serveur.

$ curl -X GET "localhost:9200"

Configurer NGinx comme reverse proxy d'ElasticSearch

Afin de rendre le service ElasticSearch accessible depuis votre site Internet, vous allez devoir configurer un "reverse proxy" pour restreindre l'accès aux API ElasticSearch depuis Internet.

Dans la configuration de mon site j'ai créé une "Location" Nginx dédiée au service de recherche qui adresse les API d'ElasticSearch à chaque requête HTTP sur le chemin "/search".

Pour des raisons de sécurité, j'ai volontairement restreint le chemin du reverse proxy à l'API de recherche de mon index sur ElasticSearch : http://localhost:9200/mon_site/article/_search

# Search API delivered by ElasticSearch
location ^~ /search {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_http_version 1.1;
    proxy_set_header Connection "Keep-Alive";
    proxy_set_header Proxy-Connection "Keep-Alive";
    proxy_pass  http://localhost:9200/mon_site/article/_search;
    proxy_read_timeout 600;
}

Après un redémarrage de Nginx, vous devriez avoir accès à l'API de recherche d'ElasticSearch depuis l'adresse suivante : https://mon-site.com/search

Requêter les API ElasticSearch depuis les pages de votre site

L'étape suivante consiste à positionner un formulaire de recherche sur votre site, d'appeler l'API de recherche et d'afficher les résultats dans une liste dynamique.

Pour cela j'utilise la fonction suivante inspirée du thème de Nurui que j'ai mis en oeuvre sur ce blog.

function blogSearch() {

    // Construction de la requête de recherche où a est le formulaire de recherche
    var t = '/search?q='+a.value+'&size=20&default_operator=AND',
    s = new XMLHttpRequest;
    s.open("GET", t, !0);

    // Dès que l'API a retourné un résultat au navigateur
    s.onload = function() {
        var f, t;

        // Si ElasticSearch répond correctement
        if (s.status >= 200 && s.status < 400){

            f = JSON.parse(s.responseText);
            i.innerHTML = f.hits.total.value;

            // Pour chaque résultat remonté par ElasticSearch 
            f.hits.hits.map(function(e) {
                    var t = new Date(e._source.published_at).toLocaleDateString(document.documentElement.lang, {
                            year: "numeric",
                            month: "long",
                            day: "numeric"
                        }),
                    n = document.createElement("h4");
                    n.textContent = e._source.title, n.innerHTML += '<span class="search-date">' + t + ' - Match ' + Math.round(e._score * 10) + '%</span>', e._source.featured && (n.innerHTML += '<span class="search-featured">' + searchFeaturedIcon + "</span>");

                    var s = document.createElement("a");
                    s.setAttribute("href", "/"+e._source.slug), s.appendChild(n), o.appendChild(s)
                })
            }
        }
    }
}

Cet exemple de code est à adapter en fonction de vos besoins. Dans le cadre de ce site Internet, voici comment ce code est intégré:
https://www.geeek.org/?q=domotique

Comme vous le remarquerez dans la requête HTTP réalisée à ElasticSearch, seulement 20 résultats sont demandés et l'opérateur 'AND' est utilisé comme opérateur par défaut.

Configurer le SearchAction pour les moteurs de recherche

Pour permettre aux moteurs de recherche de connaitre la présence de ce moteur de recherche et surtout permettre de le proposer aux visiteurs de votre site. Vous pouvez le déclarer au travers d'un snippet JSON LD SearchAction.

<!-- Search JSON-LD -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "url": "https://www.mon-site.com/",
  "potentialAction": {
    "@type": "SearchAction",
    "target": "https://www.mon-site.com/?q={search_term_string}",
    "query-input": "required name=search_term_string"
  }
}
</script>

Si votre site possède un traffic important, un moteur de recherche que vous avez mis en place sera proposé directement à vos visiteurs depuis les pages de résultats de recherche.

Pour que cela fonctionne, il faudra vous assurer qu'une recherche est exécutée sur votre site dès lors que le paramètre "q" est passé en paramètre de votre page.

Vous pouvez pour cela utiliser le code suivant :

// On récupère les paramètres en GET de la page.
const urlParams = new URLSearchParams(window.location.search);

// On extrait le paramètre "q"
const searchQuery = urlParams.get('q');

// Si une valeur est résente
if (searchQuery !== null){

    // On affiche le formulaire de recherche
    t.classList.add("search-opened");
    
    // On positionne la valeur du paramètre 'q' dans l'input du formulaire
    a.value = searchQuery;
    
    // On met le focus sur l'input 
    a.focus();
    
    // On lance la recherche 
    blogSearch();
}

Vous voilà avec un moteur de recherche performant !

N'hésitez surtout pas à réagir à cet article en laissant un commentaire. J'espère que cet article vous a été utile et vous simplifiera l'intégration d'ElasticSearch à votre site Internet.