20/07/2021

Créer un moteur de recherche avec Symfony

Introduction

Pour mettre en place un moteur de recherche sur votre application vous pouvez utiliser des solutions comme ElasticSearch qui est une base de données non relationnel pour les recherches. Cependant cette solution est plus couteuse en ressources et en temps de développement. Pour réaliser un moteur de recherche simple et rapide vous pouvez suivre ce tutoriel.

Ce moteur de recherche se base sur une requete SQL de la mort. Je l’ai écris de sorte à ce qu’il soit facile à implémenter dans d’autres projets. L’implémentation avec Symfony se situe à la fin de l’article.

Cette solution restera tout de même moins performante qu’un moteur de recherche plus classique.

Code source

La structure de la requete SQL

La requete SQL se divise en plusieurs sous requetes relisé par des UNION.

Skelete de la requete

SELECT *, SUM(coeff) AS pertinence
FROM (
	-- ARTICLE
	--  ARTICLE BY name (exact) 100
	SOUS-REQUETE 1
	UNION
	-- APPLICATION 50
	SOUS-REQUETE 2
	UNION

	-- COMMENT
	--  COMMENT BY name (exact) 100
	SOUS-REQUETE 3
	UNION
	-- COMMENT 50
	SOUS-REQUETE 4
	UNION

) as A

group by id, type
ORDER BY pertinence DESC, name ASC
LIMIT $max

Toutes les sous requetes définie un critère de sélection. On attribue à chaque critère un score (ex: la recherche est égale au nom de l’article +100). Ensuite on calcule la pertinence avec la somme des scores que la recherche aura validée. On trie tout par pertinence et voilà la requête SQL est ok.

Exemple de sous requête : score de 100 pour la recherche qui est égale au nom de l’article

SELECT id, name, description, 100 as coeff, 'article' as type
	FROM article 
	WHERE UPPER(name) LIKE '$value'

Requete globale

SELECT *, SUM(coeff) AS pertinence
FROM (
	-- ARTICLE
	--  ARTICLE BY name (exact) 100
	SELECT id, name, description, 100 as coeff, 'article' as type
	FROM article 
	WHERE UPPER(name) LIKE '$value'
	UNION
	-- APPLICATION 50
	SELECT id, name, description, 50 as coeff, 'article' as type
	FROM article 
	WHERE UPPER(name) LIKE '$value' OR description LIKE '%$value%'
	UNION

	-- COMMENT
	--  COMMENT BY name (exact) 100
	SELECT id, name, content as description, 100 as coeff, 'comment' as type
	FROM comment
	WHERE UPPER(name) LIKE '$value'
	UNION
	-- COMMENT 50
	SELECT id, name, content as description, 50 as coeff, 'comment' as type
	FROM comment
	WHERE UPPER(name) LIKE '$value' OR content LIKE '%$value%'
	UNION

) as A

group by id, type
ORDER BY pertinence DESC, name ASC
LIMIT $max

ATTENTION Plus la base de données sera grande plus le temps de recherche sera plus long. Et plus il y a de sous requetes, plus le moteur mettra du temps à chercher.

Implementation avec Symfony

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

class SearchController extends AbstractController
{
    /**
     * @Route("/search", name="search")
     */
    public function index(Request $request)
    {
    	$search = $request->query->get('q');
    	$result = $this->search($search);
    	return new JsonResponse($result, 200);
    }

    private function search($value, $max = 100) 
    {
		$connection = $this->getDoctrine()->getManager()->getConnection();
		$query = $connection->prepare("
			SELECT *, SUM(coeff) AS pertinence
			FROM (
				-- ARTICLE
				--  ARTICLE BY name (exact) 100
				SELECT id, name, description, 100 as coeff, 'article' as type
				FROM article 
				WHERE UPPER(name) LIKE '$value'
				UNION
				-- APPLICATION 50
				SELECT id, name, description, 50 as coeff, 'article' as type
				FROM article 
				WHERE UPPER(name) LIKE '$value' OR description LIKE '%$value%'
				UNION

				-- ARTICLE
				--  ARTICLE BY name (exact) 100
				SELECT id, name, content as description, 100 as coeff, 'comment' as type
				FROM comment
				WHERE UPPER(name) LIKE '$value'
				UNION
				-- APPLICATION 50
				SELECT id, name, content as description, 50 as coeff, 'comment' as type
				FROM comment
				WHERE UPPER(name) LIKE '$value' OR content LIKE '%$value%'
				UNION

			) as A

			group by id, type
			ORDER BY pertinence DESC, name ASC
			LIMIT $max
		");
		// $query->execute(['val' => strtoupper($value)]);
		$query->execute();
		$result = $query->fetchAll();

		// POST TRAITEMENT
		// On ajoute les liens
		foreach ($result as $key => $value) 
		{
			switch ($value['type']) {
				case 'article':
					$result[$key]['link'] = $_ENV['ARTICLE_URL'] . $result[$key]['id'];
					break;
				case 'comment':
					$result[$key]['link'] = $_ENV['COMMENT_URL'] . $result[$key]['id'];
					break;
				default:
					$result[$key]['link'] = null;
					break;
			}
		}
		return $result;
    }
}

J’ai rajouté un post traitement (qui est biensur optionnel) pour ajouter d’autres infos (ici les liens des article et commentaires) aux résultats.

La variable $_ENV['COMMENT_URL'] se situe dans le .env