Catégorie : Programmation     Tags :      0 Commentaire

    Retrouvez cet article dans : Linux Magazine 105

     

    Lors du hors-série n°33 consacré à Ruby (pardon, j’ai dû hiberner depuis...), nous avons vu les fondamentaux du langage et je vous ai montré comment développer des applications Web avec différents frameworks. Dans cet article, je vous propose de voir un cas pratique d’utilisation. Nous allons créer notre propre moteur de recherche. Ne rêvez pas, nous n’allons pas essayer de rivaliser avec Google ou Yahoo! L’idée est simplement de vous présenter différentes bibliothèques Ruby et de voir qu’avec très peu de code nous pouvons facilement ébaucher un petit moteur simple, mais efficace.

     

    1. Moteur de recherche

    Nous avons besoin de trois éléments pour créer un moteur de recherche. Le plus évident est bien entendu l’interface de recherche elle-même. Sans entrer dans les considérations techniques pour le moment, nous savons déjà qu’il s’offre à nous un grand nombre de solutions pour ce travail. Certains prôneront Rails, d’autre Camping, Nitro ou eRuby... Pour ma part, je vous proposerai une solution encore plus simple consistant à utiliser Mongrel sans autre framework.

    Avant la recherche, il nous faut un crawler qui va se charger, à partir d’une source donnée, de suivre les liens hypertextes de façon récursive. Ruby a tout ce qu’il faut dans sa bibliothèque standard pour nous aider à développer ce crawler.

    Une fois les données récupérées, il faudra les indexer. Ceci se fait en extrayant les mots de chaque page et en les stockant de façon à pouvoir les utiliser comme des entrées d’index pointant vers les différentes pages. Sans en dire plus pour le moment, sachez que nous n’utiliserons pas de base de données au profit d’une solution basée sur Ferret.

    Décomposons le travail en trois parties. Tout d’abord, nous fabriquerons le crawler. Ensuite, nous implémenterons l’indexeur. Puis, nous mettrons en place une interface Web afin de permettre à nos utilisateurs de faire des recherches.

    2. Le crawler

    Un crawler ou " robot " est par définition quelque chose d’assez simple. Et si vous avez déjà utilisé wget ou curl, vous avez déjà utilisé un crawler. Dans le principe, vous lui passez une adresse de page web (une URL) et il va se charger de récupérer le contenu de cette page, d’y rechercher les liens hypertextes, puis de traiter récursivement chacun de ces liens. Bien entendu, le contenu de chaque page sera envoyé à l’indexeur qui se charge de traiter et stocker les données.

    Pour créer notre crawler, nous allons utiliser Net::http [1]. Cette bibliothèque permet de récupérer des documents via le protocole HTTP 1.1. Il est donc possible de construire des requêtes de type GET ou POST. Il est également possible de gérer le passage par proxy, les redirections, l’authentification,...

    Nous n’allons pas aller aussi loin. En effet, nous avons seulement besoin de traiter des requêtes simples de type GET. Pour cela, nous pouvons utiliser la méthode Net::HTTP.get_response :

    require ‘net/http’
    response = Net::HTTP.get_response(‘rubyfrance.org’, ‘/’)

    Net::HTTP faisant partie de la bibliothèque standard du langage, il faut la charger via un require avant de pouvoir l’utiliser (ligne 1).

    À la ligne 2, nous récupérons le contenu de la page ‘/’ sur le serveur d’adresse ‘rubyfrance.org’. Ce résultat est une instance de Net::HTTPResponse, que nous plaçons dans la variable response.

    Pour rechercher et traiter les liens hypertextes, nous avons besoin du contenu de la réponse. Il suffit pour cela d’utiliser la méthode Net::HTTPResponse#body :

    	pageContent = response.body( )

    Si vous affichez le contenu de pageContent, vous verrez que nous avons bien le code HTML de la page demandée.

    Avant de parser le résultat, attardons-nous deux secondes sur un point de détail. Vous l’avez vu, get_response prend en argument l’adresse du serveur et le chemin de la ressource à récupérer. Donc, si nous voulons récupérer le contenu à l’adresse http://monserveur.com/chemin/page.html, il faudra découper cette URL de façon à avoir d’un côté monserveur.com et de l’autre /chemin/page.html. Nous pouvons faire cela à coup d’expressions rationnelles, mais ce n’est pas toujours très évident. Nous allons donc préférer, là encore, utiliser une bibliothèque : URI [2]. Ce module permet de traiter des URL. Nous n’entrerons pas dans les détails, nous avons seulement besoin de découper une URL et nous utiliserons donc la méthode URI.parse renvoyant un objet de type URI::http :

    require ‘uri’
    uri = URI.parse( "http://monserveur.com/chemin/page.html" )

    Nous pouvons alors récupérer le nom du serveur, le chemin d’accès à la ressource, le port et le contenu de la requête s’il y en a une :

    	puts uri.host
    	# => monserveur.com
    puts uri.path
    	# => /chemin/page.html
    puts uri.post
    	# => 80
    puts uri.scheme
    	# => http

    Comble du bonheur, Net::HTTP.get_response peut prendre en argument un objet de type URI::HTTP. Nous pouvons donc modifier notre code de la façon suivante :

    require ‘net/http’
    require ‘uri’
    response = Net::HTTP.get_response( URI.parse( ‘http://rubyfrance.org/’ ) )
    pageContent = response.body( )

    Passons maintenant à la récupération des liens dans le corps de la réponse. Certes, nous pouvons là aussi envisager d’utiliser des expressions rationnelles. Heureusement, il y a plus simple. Si nous partons du principe que le HTML est " parsable " comme XML, nous pourrions explorer de ce côté-là. Il y a encore plus simple : Why The Lucky Stiff (encore lui) nous propose un parseur HTML de toute beauté, au doux nom de Hpricot [3], disponible sous forme de gem.

    Pour obtenir la liste des liens hypertextes de la page, nous allons passer à Hpricot le contenu de la page, sous forme de texte, puis, via la méthode Hpricot::Doc#/, nous récupérons la liste des liens :

    require ‘rubygems’
    require ‘hpricot’
    pageDocument = Hpricot( pageContent )
    links = pageDocument / “a”
    	# => #<Hpricot::Elements[...]>

    links est un objet de type Hpricot::Elements que nous pouvons voir comme un tableau d’éléments. Donc, nous parcourons l’ensemble des liens via un each :

    	links.each do |element|
    	# faire quelque chose avec element
    end

    Chaque element est lui-même un objet de type Hpricot::Elem que nous pouvons voir comme un hachage dans notre cas. Si nous voulons récupérer la valeur de l’attribut href pour l’élément courant, nous utiliserons donc la notation element[‘href’].

    Voilà, nous avons un code pour le crawler :

    require ‘net/http’
    require ‘uri’
    def crawler( uri )
      response = Net::HTTP.get_response( URI.parse( uri ) )
      pageContent = response.body( )
      pageDocument = Hpricot( pageContent )
      links = pageDocument / “a”
      links.each do |element|
        link = element[‘href’]
        crawler( link )
      end
    end

    Surtout, ne vous amusez pas à essayer ce bout de code. En effet, vous risqueriez de parcourir tout internet sans jamais vous arrêter ! De plus, avec ce simple code, vous récupérerez tout, même les éléments " non HTML ".

    Il faut donc ajouter trois choses à notre crawler. En premier, il faut éliminer tout ce qui n’est pas de type HTML ou texte. Ensuite, il serait bon de borner l’étendue du parcours des liens. Enfin, il faudrait éviter de passer deux fois par la même page. Voyons cela dans l’ordre.

    Si nous voulons éviter de traiter autre chose que du HTML ou du texte, il suffit simplement d’ignorer les ressources dont le content-type ne serait pas "text/html" ou "text/plain". Pour cela, nous récupérons l’information via la méthode Net::HTTPHeader#content_type :

    case response.content_type
      when "text/html"
      	# Traiter le contenu comme du HTML
      when "text/plain"
       # traiter le contenu comme du texte
      else
       # ignorer
    end

    Nous distinguons ce qui est de type HTML de ce qui est de type texte. En effet, il est inutile de rechercher des liens hypertextes dans un document texte.

    Pour borner la recherche et éviter d’indexer tout internet, je vous propose que notre crawler n’agisse que pour un domaine donné. Pour cela, on ne traitera que les liens dont le hostname correspond à celui de la page courante :

    (pageDocument / "a").each do |element|
      link = element[‘href’]
      if /#{url.host}/.match( link )
        # traiter le lien
      else
        # ignorer le lien
      end
    end

    Enfin, pour éviter d’indexer deux fois la même page, je vous propose de calculer et stocker, pour chaque page, une chaîne MD5 calculée selon son contenu, et de vérifier, pour chaque nouvelle page, que la chaîne MD5 correspondante n’a pas déjà été calculée :

    require ‘digest/md5’
    ...
    @pagesMD5 = Array::new( )
    ...
    pageDigest = Digest::MD5.hexdigest( pageContent )
    if @pagesMD5.include?( pageDigest )
      # la page a déjà été indexée, l’ignorer
    else
      @pageMD5 << pageDigest
      # indexer la page
    end

    Voilà, nous pouvons maintenant mettre en place la nouvelle version de notre crawler.

    Et tant qu’à faire, allons-y pour de l’objet :

    require ‘digest/md5’
    require ‘net/http’
    require ‘uri’
    require ‘rubygems’
    require ‘hpricot’
    class WebIndexer
      def initialize
        @pagesMD5 = Array::new( )
      end
      def indexer( uri )
        puts “Indexation de #{uri}”
      end
      def crawler( uri )
        # Récupération de la ressource
        url = URI.parse( uri )
        response = Net::HTTP.get_response( url )
        # Récupération du contenu de la page
        pageContent = response.body
        # Calcul du MD5 de la page
        pageDigest = Digest::MD5.hexdigest( pageContent )
        if @pagesMD5.include?( pageDigest )
          return
        end
        @pagesMD5 << pageDigest
        # Indexation
        case response.content_type
          when "text/html"
            # Indexation
            indexer( uri )
            # Parcour des liens
            (pageDocument/"a").each do |element|
              href = element[‘href’]
              if /#{url.host}/.match( href )
                href += "/" if URI.parse( href ).path == ""
                crawler( href )
              else
                puts "\t[INFO] - Ignore #{href} : Host not match!"
              end
            end
          when "text/plain"
            # Indexation
            indexer( uri )
          else
            puts "\t[IGNORE] - #{uri} : Not Text or HTML!"
        end
      end
    end
    site = WebIndexer.new( )
    site.crawler( ARGV[0] )

    Nous pouvons faire un test :

    $ ruby WebIndex.rb http://greg.rubyfr.net
    Indexation de http://greg.rubyfr.net
    Indexation de http://greg.rubyfr.net/pub
    	[INFO] - Ignore http://forum.rubyfr.net : Host not match!
    	[INFO] - Ignore http://planet.rubyfr.net : Host not match!
    	[INFO] - Ignore http://www.rubyfrance.org : Host not match!
    	[INFO] - Ignore http://www.railsfrance.org/ : Host not match!
    Indexation de http://greg.rubyfr.net/pub/?page_id=22
    	[INFO] - Ignore http://forum.rubyfr.net : Host not match!
    	[INFO] - Ignore http://planet.rubyfr.net : Host not match!
    	[INFO] - Ignore http://www.rubyfrance.org : Host not match!
    	[INFO] - Ignore http://www.railsfrance.org/ : Host not match!
    
    	Indexation de http://greg.rubyfr.net/pub/?page_id=14
    	[INFO] - Ignore http://forum.rubyfr.net : Host not match!
    	[INFO] - Ignore http://planet.rubyfr.net : Host not match!
    	[INFO] - Ignore http://www.rubyfrance.org : Host not match!
    	[INFO] - Ignore http://www.railsfrance.org/ : Host not match!
    Indexation de http://greg.rubyfr.net/pub/?page_id=15
    ...

    Ça fonctionne...

    3. L’indexeur

    Contrairement à ce que l’on pourrait croire, l’indexation sera beaucoup plus facile à mettre en œuvre. En effet, grâce à la bibliothèque Ferret [4], vous allez voir que ce travail est un jeu d’enfant.

    Ferret est un portage en Ruby du projet Lucen [5] de Dave Balmain. Il s’agit d’un moteur de recherche permettant d’indexer et de rechercher du texte. Pour installer Ferret, un petit gem install ferret fera l’affaire.

    Avant de continuer notre développement, passons en revue les principales fonctionnalités mises à notre disposition par Ferret. Si, par la suite, vous voulez en savoir plus, il existe un excellent focus [6] chez O’Reilly également disponible en français [7].

    L’indexation se fait en utilisant la classe Ferret::Index::Index. Il suffit pour cela de créer une instance de cette classe. Lors de cette instanciation, nous avons deux possibilités : soit créer un index en mémoire, soit permettre de stocker les données dans des fichiers. Voici les deux formes d’appel correspondantes :

    	require ‘ferret’
    index_en_memoire = Ferret::Index::Index.new( )
    index_persistant = Ferret::Index::Index.new( :path => "/path/to/index" )

    L’option :path permet de préciser le répertoire dans lequel devront être stockés les différents fichiers permettant la persistance de l’index. Nous pouvons maintenant ajouter des documents :

    	index << "Contenu du document à indexer..."
    index << "Contenu d’un autre document à indexer..."

    Nous ajoutons à l’index directement le contenu du document. Le plus souvent, nous préférerons ajouter des champs tels que le titre, l’auteur,... Bref tout ce qui pourrait être utile par la suite pour identifier un contenu :

    	index << { :titre => "Mon super livre", :auteur => "Moah", :contenu => "bla bla bla..." }
    index << { :titre => "La cuisine pour les nuls", :auteur => "Bryan Miller", :contenu => "En premier, aller dans la cuisine..." }

    Nous venons d’ajouter dans notre index deux entrées, l’une dont le titre est " Mon super livre ", l’auteur est " Moah " et le contenu est " bla bla bla... " et l’autre dont le titre est " La cuisine pour les nuls ", l’auteur est " Bryan Miller " et le contenu est " En premier, aller dans la cuisine... ". Si nous voulions comparer avec une base de données (chose non comparable, mais bon), c’est comme si nous avions eu une table avec trois colonnes : titre, auteur et contenu... Je pense que vous avez compris le reste, je n’en dirai pas plus, car cela reviendrait à comparer des sushis avec des quenelles !

    Il ne reste plus qu’à faire des recherches. Pour cela, nous utilisons la méthode Ferret::Index::Index#search_each prenant en paramètre la requête de recherche et un bloc auquel est passé, pour chaque élément trouvé, son identifiant et son score (compris entre 0.0 et 1.0) :

    	index.search_each(‘content:"cuisine"’) do |id, score|
      puts “Document ID ##{id} trouvé avec le score #{score}"
    end

    Si maintenant, pour un id de document donné, vous voulez récupérer le titre ou l’auteur par exemple, rien de plus simple :

    	titre = index[id][:titre]
    auteur = index[id][:auteur]

    Bien entendu, nous pouvons faire des recherches bien plus complexes que cela. Tout d’abord, en passant des options à la méthode search_each. Mais aussi en se faisant la main avec le Ferret Query Language. Ceci dépasse largement le cadre de cette présentation. Je vous propose donc d’aller consulter la documentation [8] pour plus d’information.

    Connaissant maintenant les bases de Ferret, vous n’aurez aucun mal à créer la partie indexée. Je vous propose de stocker, pour chaque page, les informations suivantes :

    • Le contenu de la page bien entendu, dans :content ;
    • Le titre de la page, que nous récupérerons grâce à Hpricot, et que nous mettrons dans :title ;
    • L’URL, dans :url ;
    • La valeur du MD5, ce qui permettra d’éviter, quand nous relancerons l’indexation sur un site, d’indexer à nouveau des pages déjà présentes dans l’index. Nous mettrons cette information dans :digest.

    Voici alors la nouvelle version de notre crawler/indexeur :

    require ‘digest/md5’
    require ‘net/http’
    require ‘uri’
    require ‘rubygems’
    require ‘hpricot’
    require ‘ferret’
    include Ferret
    class WebIndexer
      def initialize
        @index = Index::Index.new(:path => ‘./index’)
      end
      def indexer( uri, title, content, digest )
        print "[INFO] - Index #{uri} / #{title} : "
        @index << {:title => title, :content => content, :url => uri, :digest => digest}
        puts "ok!"
      end
      def crawler( uri )
        # Récupération de la ressource
    
    	    url = URI.parse( uri )
        response = Net::HTTP.get_response( url )
        # Récupération du contenu de la page
        pageContent = response.body
    
        # Calcul du MD5 de la page
        pageDigest = Digest::MD5.hexdigest( pageContent )
        @index.search_each(‘digest:"’ + pageDigest + ‘"’) do |id, score|
          if @index[id][:digest] == pageDigest
            puts "\t[INFO] - Ignore #{uri} : Already parsed as #{@index[id][:url]}!"
            return
          end
        end
    
        # Indexation
        case response.content_type
          when "text/html"
            pageDocument = Hpricot( pageContent )
            pageTitle = (pageDocument/”title”)[0]
            pageTitle = ((pageTitle.nil?)?””:pageTitle.inner_html) # “
    
            indexer( uri, pageTitle, pageContent, pageDigest )
    
            # Parcour des liens
            (pageDocument/”a”).each do |element|
              href = element[‘href’]
              if /#{url.host}/.match( href )
                href += "/" if URI.parse( href ).path == ""
                crawler( href )
              else
                puts "\t[INFO] - Ignore #{href} : Host not match!"
              end
            end
          when "text/plain"
            pageDocument = pageContent
            pageTitle = uri
    
            indexer( uri, pageTitle, pageContent, pageDigest )
          else
            puts "\t[INFO] - Ignore #{uri} : Not Text or HTML!"
        end
      end
    end
    
    site = WebIndexer.new( )
    site.crawler( ARGV[0] )

    4. L’interface de recherche

    Nous avons maintenant un index, nous pouvons l’utiliser pour créer une interface de recherche. Comme je vous l’ai proposé, nous allons créer cette interface Web en utilisant Mongrel [9].

    Mongrel n’est pas qu’un serveur HTTP, c’est également un ensemble de bibliothèques. Dans le principe, un serveur HTTP doit permettre de servir des pages statiques et dynamiques. Si vous avez déjà développé avec PHP par exemple, vous avez certainement mêlé ces deux types de pages. Une page statique est délivrée au client " telle quelle ". Une page dynamique a un contenu qui change en fonction d’un contexte. Partons du principe que vous connaissez déjà cela.

    Dans le cas qui nous intéresse, nous avons besoin de servir des pages dynamiques. En effet, l’utilisateur de notre moteur de recherche va saisir un terme et, en fonction de ce terme, nous lui renverrons une liste de pointeurs correspondant à des entrées de notre index matchant avec le terme demandé. Bien entendu, la première page, celle contenant le formulaire de recherche, peut être statique.

    Pour mettre en place un serveur avec la bibliothèque Mongrel, nous avons besoin de créer une nouvelle instance de type HttpServer en précisant sur quelle IP peut être attaqué le serveur, ainsi que le port qu’il doit utiliser :

    #!/usr/bin/env ruby
    require ‚rubygems‘
    require ‚mongrel‘
    s = Mongrel::HttpServer.new( „0.0.0.0“, „3000“ )
    
    Il ne reste plus qu’à démarrer le serveur :
    	s.run.join

    Si vous exécutez ce script et que vous vous connectez sur l’URL http://localhost:3000 depuis votre navigateur, vous aurez le droit à un magnifique message NOT FOUND. Rien de plus normal, car finalement nous venons de faire un serveur qui ne sert rien. En effet, si nous voulons être capables de délivrer des données au client, il faut préciser au serveur quoi faire en fonction de la requête. Pour cela, nous devons enregistrer auprès du serveur des handlers indiquant ce qui doit être envoyé au client en fonction de la requête qu’il passe. Mongrel met à notre disposition le handler DirHandler permettant de servir des pages statiques. Pour enregistrer ce handler, nous utilisons la méthode Mongrel::HttpServer#register :

    #!/usr/bin/env ruby
    require ‚rubygems‘
    require ‚mongrel‘
    h = Mongrel::HttpServer.new(„0.0.0.0“, „3000“)
    h.register(„/“, Mongrel::DirHandler.new(„./static“))
    h.run.join

    La ligne

    	h.register("/", Mongrel::DirHandler.new("./static"))

    est celle par laquelle nous " enregistrons " le handler. Le premier paramètre ("/") permet de préciser la route (le chemin d’accès dans l’URL), le second indique le handler utilisé. Dans notre cas, comme nous l’avons prévu, nous utilisons le handler Mongrel::DirHandler en indiquant qu’il doit renvoyer les fichiers placés dans le répertoire static placé au pied de l’application. Donc, quand nous ferons un accès via l’URL http://localhost:3000/hello.html, Mongrel nous renverra le fichier ./static/hello.html.

    Pour vous en convaincre, créez donc un répertoire static au pied de ce script et ajoutez dedans une page index.html contenant du HTML valide. Relancez le script et faites pointer votre navigateur sur l’URL http://localhost:3000/. Si tout va bien, vous voyez le contenu de votre page index.html. Vous pouvez vous amuser à ajouter des pages dans le répertoire static et faire des liens entre ces pages. Nous avons maintenant un serveur Web minimaliste.

    Je pense que vous l’aurez compris, nous pourrons délivrer des pages dynamiques en créant notre propre handler. Pour cela, nous devons créer une classe héritant de Mongrel::HttpHandler, puis nous l’enregistrons auprès du serveur :

    #!/usr/bin/env ruby
    require ‚rubygems‘
    require ‚mongrel‘
    class TestHandler < Mongrel::HttpHandler
      def process( request, response )
        response.start(200) do |head, body|
          head[„Content-Type“] = „text/plain“
          body.write( „Hello World!“ )
        end
      end
    end
    h = Mongrel::HttpServer.new( „0.0.0.0“, „3000“ )
    h.register( „/“, Mongrel::DirHandler.new(„./static/“) )
    h.register( „/test“, TestHandler.new( ) )
    h.run.join

    L’enregistrement du handler est conforme à ce à quoi l’on pouvait s’attendre. Attardons-nous plutôt sur l’écriture de la classe TestHandler. Comme je vous l’avais dit, cette classe hérite de Mongrel::HttpHandler. Nous y définissons une méthode process prenant en argument deux paramètres : request et response. Nous reviendrons un peu plus tard sur le premier paramètre. Le second paramètre de process est un objet de type Mongrel::HttpResponse utilisé pour formater la réponse à envoyer au client. Pour cela, nous utilisons la méthode start à laquelle nous passons un statut, correspondant à un code HTTP – 200, soit OK, dans notre cas –, et un bloc dans lequel nous précisons le contenu de l’en-tête et du corps de la réponse. Dans cet exemple, nous passons dans l’en-tête une information indiquant que nous renvoyons du texte, puis nous écrivons un simple message de bienvenue dans le corps via la méthode Mongrel::HttpResponse#write.

    Il ne nous manque plus qu’une chose, et vous aurez tout en main pour terminer notre mini-moteur de recherche : savoir récupérer des paramètres passés depuis un formulaire. Et c’est là que nous parlons du paramètre request injustement laissé de côté au paragraphe précédent. Il s’agit d’un objet de type Mongrel::HttpRequest via lequel nous pouvons récupérer les paramètres et le corps de la requête passée, respectivement grâce à ses attributs params et body. params est un hachage dans lequel vous trouverez les variables classiques d’une requête HTTP dont, ce qui va nous intéresser ici, la QUERY_STRING. Pour nous aider encore plus, il existe une méthode de classe Mongrel::HttpRequest::query_parse permettant de parser le contenu de la QUERY_STRING. Ainsi, imaginons que nous fassions une requête sur l’URL http://monserveur/search?p1=v1&p2=v2, dans le handler matchant avec /search, nous pourrions récupérer la valeur du paramètre p1 et p2 de la façon suivante :

    d = Mongrel::HttpRequest.query_parse( request.params[‘QUERY_STRING’] )
    v1 = d[‘p1’]
    v2 = d[‘p2’]

    Ceci étant dit, nous pouvons maintenant développer notre moteur de recherche. Pour cela, nous allons utiliser deux handlers, l’un permettant de servir des données statiques, que nous utiliserons pour renvoyer la page d’index, mais également les CSS et images de notre site. Le second handler servira à afficher le résultat de la recherche. Nous avons donc le squelette suivant :

    WebSearch.rb
    #!/usr/bin/env ruby
    require ‚rubygems‘
    require ‚mongrel‘
    class ResultHandler < Mongrel::HttpHandler
      def process( request, response )
        response.start(200) do |head, out|
          head[„Content-Type“] = „text/html“
          # ... Affichage de la liste de résultats
        end
      end
    end
    h = Mongrel::HttpServer.new( "0.0.0.0", "3000" )
    h.register( "/", Mongrel::DirHandler.new("./static/") )
    h.register( "/r", ResultHandler.new )
    h.run.join

    Le handler pour l’affichage des résultats est défini via la classe ResultHandler et il renvoie une réponse pour les requêtes faites via la route /r.

    Nous pouvons maintenant mettre en place la page d’index de notre moteur de recherche. Pour cela, il suffit de créer un fichier index.html que nous plaçons dans le répertoire static, lui-même situé au pied de notre script WebSearch.rb :

    	<html>
    <head>
      <meta http-equiv=content-type content="text/html; charset=UTF-8">
      <title>Mooteur</title>
      <link rel="stylesheet" href="/style.css" type="text/css" media="screen" />
    </head>
    <body>
      <div id="center">
        <img src=”/mooteur.gif” alt=”Mooteur...”>
        <form action=”/r”>
    
    	      <input type=”text” name=”s” size=”55” /><br />
          <input type=”submit” value=”Recherche Mooteur”/>
        </form>
        ©2007 Mooteur
      </div>
    </body>
    </html>

    Comme vous pouvez le voir, cette page utilise une CSS (style.css) et une image (mooteur.gif). Ces fichiers étant respectivement accessibles via les chemins /style.css et /mooteur.gif, il faut également les placer dans le répertoire static. À cette étape, vous devez avoir l’arborescence suivante :

    +- WebSearch.rb
    |
    ±- static
       |
       +- index.html
       |
       +- style.css
       |
       +- mooteur.gif

    Il ne reste plus qu’à compléter le handler de recherche.

    Dans la page index.html, nous avons créé un formulaire avec un champ texte s. C’est dans ce champ que nos utilisateurs saisiront leur terme de recherche. Donc, nous devons commencer par récupérer le terme saisi :

    search_term = begin
      Mongrel::HttpRequest.query_parse( request.params[‘QUERY_STRING’] )[‘s’]
    rescue
      nil
    end

    Nous utilisons ici une méthode qui nous permet de mettre à nil la valeur de search_term si quelque chose se passe mal. Cela peut être la cas si, par exemple, un utilisateur accède directement à l’URL http://localhost:3000/r. En effet, dans ce cas-là, il n’y a pas de QUERY_STRING et le parsing échoue forcement. Grâce au rescue, nous " trappons " cette erreur éventuelle et nous évitons un plantage de notre application.

    Nous pouvons maintenant aller rechercher dans l’index créé précédemment les résultats correspondants à la recherche :

    index = Index::Index.new(:path => ‘./index’)
    index.search_each(‘content:"’ + search_term + ‘"’) do |id, score|
      out.write( "<p>" )
      out.write( "<a href=’#{index[id][:url]}’>#{index[id][:title]}</a> <small>- [score of #{score}]</small><br />" )
      out.write( "<small><a href=’#{index[id][:url]}’>#{index[id][:url]}</a></small>")
      out.write( "</p>\n" )
    end

    Nous commençons par ouvrir l’index, puis, pour chaque entrée dont le contenu (content:) matche avec le terme de recherche (search_term), nous affichons un lien sur le titre de l’entrée de l’index (index[id][:title]) pointant vers l’URL de cette même entrée (index[id][:url]). À côté, nous affichons entre crochets le score pour l’entrée courante. Pour faire un peu plus " Google ", nous affichons également, en plus petit et en dessous, le même lien que précédemment, mais sur l’URL elle-même.

    Ainsi, chaque résultat aura la présentation suivante :

     

    /img-articles/lm/105/cc-art-moteur/t1.jpg

    Pour être propre, nous proposons, en début de page, un formulaire permettant de refaire une recherche sans retourner à la page d’index. Voici alors ce que donnerait le code complet de notre handler :

    class ResultHandler < Mongrel::HttpHandler
      def process( request, response )
        response.start(200) do |head, out|
          head["Content-Type"] = "text/html"
    
          search_term = begin
            Mongrel::HttpRequest.query_parse( request.params[‘QUERY_STRING’] )[‘s’]
          rescue
            nil
          end
          out.write( "<html>
    <head>
      <meta http-equiv=content-type content=’text/html; charset=UTF-8’>
      <title>Mooteur</title>
      <link rel=’stylesheet’ href=’/style.css’ type=’text/css’ media=’screen’/>
    </head>
    <body>
      <div id=’left’>
      <table><tr>
        <td><a href=’/’><img src=’/mooteur.gif’ alt=’Mooteur...’ width=’200’ border=’0’></a></td>
        <td><form action=’/r’>
          <input type=’text’ name=’s’ size=’41’ value=’#{search_term}’ />
          <input type=’submit’ value=’Recherche Mooteur’/>
        </form></td>
      </tr></table>
      </div>")
    
          if search_term
            out.write( "<div id=’results’>
    <div id=’head’>
      Résultats pour <b>#{search_term}</b>
    </div>
    
    <div id=’list’>")
    
            index = Index::Index.new(:path => ‘./index’)
            index.search_each(‘content:"’ + search_term + ‘"’) do |id, score|
              out.write( "<p>" )
              out.write( "<a href=’#{index[id][:url]}’>#{index[id][:title]}</a> <small>- [score of #{score}]</small><br />" )
              out.write( "<small><a href=’#{index[id][:url]}’>#{index[id][:url]}</a></small>")
              out.write( "</p>\n" )
            end
    
            out.write( "</div>
      </div>
    </body>
    </html>" )
          end
        end
      end
    end

    Voilà, vous avez maintenant un petit moteur de recherche 100% fonctionnel.

     

    /img-articles/lm/105/cc-art-moteur/t1.jpg

    Figure 1 : Inclusions multiples

    
    

    Conclusion

    Cet article n’a fait qu’effleurer les possibilités offertes par Ferret et Mongrel. Si vous savez en plus qu’il existe un plugin Ferret pour Rails, je suis sûr que certains d’entre vous ont les yeux qui brillent...

    Liens

    • [1] http://www.ruby-doc.org/stdlib/libdoc/net/http/rdoc/index.html
    • [2] http://www.ruby-doc.org/stdlib/libdoc/uri/rdoc/
    • [3] http://code.whytheluckystiff.net/hpricot/
    • [4] http://ferret.davebalmain.com/trac/
    • [5] http://lucene.apache.org/
    • [6] http://www.oreilly.com/catalog/9780596527853/index.html
    • [7] http://www.oreilly.fr/catalogue/2354020694
    • [8] http://ferret.davebalmain.com/api/classes/Ferret/QueryParser.html
    • [9] http://mongrel.rubyforge.org/

    Retrouvez cet article dans : Linux Magazine 105

    Posté par (La rédaction) | Signature : Grégoire Lejeune | Article paru dans Creative Commons License

    Laissez une réponse

    Vous devez avoir ouvert une session pour écrire un commentaire.