Récemment, je cherchais comment faire de l’indexation de flux RSS pour pouvoir par la suite monter un véritable et pertinent moteur de recherche. Alors que certains préconiseront l’implémentation Zend de Lucene, je voulais tester quelque chose d’autre que je pourrai réutiliser sans trop de mal avec d’autres applications développées dans d’autre langages. C’est là qu’on a porté à mon attention ElasticSearch. ElasticSearch est une solution opensource, basée sur Lucene proposant un moteur d’indexation et de recherche distribué RESTful.

Il est temps lire rapidement la doc et de tester son efficacité. Je suis pressé mais je reste néanmoins un peu sérieux: Je ne vais pas utiliser la bibliothèque PHP, je vais faire la mienne. Elle sera bien plus simple mais permettra de bien voir ce que l’on fait.

Télécharger et lancer ElasticSearch: C’est tout simple:

$ wget http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.11.0.zip
$ unzip elasticsearch-0.11.0.zip
$ cd elasticsearch-0.11.0
elasticsearch-0.11.0$ bin/elasticsearch -f
[...]

On a donc besoin d’une interface permettant de créer l’index, de rajouter des documents dans cet index et de faire des recherches dedans. C’est ce que je fais dans la classe suivante:

<?php

class Elastic
{
    // On est en REST. Donc appels HTTP avec Curl. Rien de spécial, on agit
    // directement en local sur le port 9200 (par défaut).
    public function request($method = 'GET', $where, $params = NULL)
    {
        $ch = curl_init();

        $url = 'http://localhost:9200/' . $where;

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_HEADER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURL_HTTP_VERSION_1_1, TRUE);

        if($method == 'POST')
        {
            curl_setopt($ch, CURLOPT_POST, TRUE);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
        }
        else if($method != 'GET')
        {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
            if(NULL !== $params)
            {
                curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
            }
        }

        $response = curl_exec($ch);

        curl_close($ch);

        return $response;
    }

    // Creation d'un index
    // cf: http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/create_index/
    public function createIndex($name)
    {
        $response = $this->request('PUT', $name . '/');
        $code = json_decode( $response , TRUE );

        if(isset($code['ok']) && $code['ok'] == TRUE)
            return TRUE;
        else
            return $code['error'];

        return FALSE;
    }

    // Ajout d'un document d'un type donné dans un index
    // cf: http://www.elasticsearch.com/docs/elasticsearch/rest_api/index/
    public function index($indexName, $type, $id, $document)
    {
        if(is_array($document) || is_object($document))
        {
            $doc = json_encode($document);
        }
        else
        {
            $doc = $document;
        }
        $response = $this->request('PUT', $indexName . '/' . $type . '/' . $id, $doc);
        return $response;
    }

    // Recherche toute simple
    // Il y a 15000 moyens de faire des recherches avec ElasticSearch, moi je ne fais
    // que l'une des plus simples.
    // cf: http://www.elasticsearch.com/docs/elasticsearch/rest_api/search/
    public function search($indexName, $type, $query)
    {
        $q = json_encode( array('query' => array('query_string' => array('query' => $query))) );
        $response = $this->request('POST', $indexName . '/' . $type . '/_search' , $q);
        return $response;
    }

};

Il n’y a plus qu’à tester ça sur le sample du firehose Twitter. J’ai mis à jour ma lib twitter pour l’occasion. On va récupérer chaque tweet et l’indexer dans l’ElasticSearch.

<?php

require 'Twitter.class.php';
require 'Elastic.class.php';

$cs_key = '...';
$cs_secret = '...';

$oauth_token = '...';
$oauth_token_secret = '...';

$e = new Elastic();

// Creation d'un index. Si il existe deja, cela sera ignore
$e->createIndex('twitter');

$t = new Twitter($cs_key, $cs_secret, $oauth_token, $oauth_token_secret);

$fd = $t->getSampleFirehose();

while($data = fgets($fd))
{
    $data = trim($data);
    $datas = json_decode($data, TRUE);
    if(FALSE == is_array($datas))
        break;

    if( FALSE == array_key_exists('id', $datas) )
    {
        continue;
    }
    $id = $datas['id'];
    $user_name = $datas['user']['screen_name'];
    $text = $datas['text'];

    $e->index('twitter', 'tweet', $id, array('user' => $user_name, 'text' => $text));
}

$t->closeSampleFirehose($fd);

Voilà, on indexe. Maintenant, on recherche. Un petit script d’une demi douzaine de ligne suffit:

<?php

require 'Elastic.class.php';

if($argc != 2)
{
    echo "Missing arg.";
    break;
}

$e = new Elastic();

$r = $e->search('twitter', 'tweet', $argv[1]);

$results = json_decode($r, TRUE);

echo "Results: " . $results['hits']['total'] . " hit(s)\n";

foreach($results['hits']['hits'] as $hit)
{
    echo "Tweet " . $hit['_id'] . "; " . $hit['_source']['user'] . " said: " . $hit['_source']['text'] . "\n";
}

On teste les résultats:

$ php search.php usa
Results: 0 hit(s)

$ php search.php wow
Results: 3 hit(s)
Tweet 27119101300; KStewyLove said: Wow, my ftfth chapter is so... terrible. :/
Tweet 27119097100; crowchild1997 said: wow rob is slaying in this song! via @youtube
Tweet 27120707400; KrockGFW said: Good Tuesday Morn! Welcome back to reality folks!! Hope your turkey was as moist as mine...wait wow thats a gross word "moist"...ewwww

$ php search.php 'fin*'
Results: 2 hit(s)
Tweet 27119099500; MissIcele said: @justinbieber RT if your excited to finally come to Vancouver Canada! i'm praying that you won't cancel again!follow me please! [:
Tweet 27119181800; WillieJoe said: RT @GAA_BEO: Coldrick backs umpire moves: All-Ireland final referee David Coldrick last night gave a guarde... Via  ...

$ php search.php 'rt AND retweet'
Results: 1 hit(s)
Tweet 27119183300; TayKiddCuhD said: RT @K1SS3S4LIFE: RETWEET IF YOUR #TEAMILOVE

Leave a reply

required

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>