MongoDb recherche par communes

Map-Reduce & Optimisation

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je travaille actuellement sur plusieurs sites internet dont le besoin est de rechercher des communes de France à partir d’un nom ou d’un code postal.

Pour réaliser cette recherche, j’utilise Map-Reduce de MongoDb.

Voici la requête :

db.communes.mapReduce(
    /* Map */
    function () {
        var i, qsearch, entries = [this.nom,this.cp].join(' ').toLowerCase();
        /* Remplace les accents */
        entries = entries
            .replace(/[ç]/g,'c').replace(/[ñ]/g,'n')
            .replace(/[áàâäãå]/g,'a').replace(/[éèêë]/g,'e')
            .replace(/[íìîï]/g,'i').replace(/[óòôöõ]/g,'o')
            .replace(/[úùûü]/g,'u').replace(/[ýÿ]/g,'y');
        /* Effectue une recherche */
        qsearch = entries.match(new RegExp('($REQUEST)', 'gi'));
        if(qsearch != null) {
            emit({
                'count': qsearch.length * -1,
                'id': this.id,
            }, {
                'nom': this.nom,
                'cp': this.cp,
            });
        }
    },
    /* Reduce */
    function (key, values) { return; },
    /* Output */
    {
        'out': { inline: 1 }
    }
);

Une partie de la requête est automatiquement généré en PHP. Les champs en entrée (ceci explique le join à la ligne 4, [this.nom,this.cp]), les mots-clés $REQUEST (ex: montpel|34000) et les champs en sortie (ex: 'nom': this.nom, 'cp': this.cp).

Le problème c’est qu’il prend entre 2 et 3 secondes d’exécutions pour traiter 36 877 documents en entrées. Si je remplace le procédé de remplacement des accents plutôt par un champ dans chaque document, je peux gagner 500ms mais ce n’est pas encore satisfaisant.

D’après la doc de MongoDB, il faudrait créer des index. J’en ai donc créé un, comme suit db.communes.createIndex( { "nom": "text", "cp": "text" } ). Mais je ne sais pas du tout si c’est la bonne utilisation des index.

Qu’en pensez-vous ? Quelle solution devrais-je adopter dans ce cas de figure ? Peut-être que Map-Reduce n’est pas adapté, après tout je n’utilise même pas la partie reduce (c’était simplement pour trier les résultats par pertinence).

Merci d’avance.

+0 -0

Salut,

Pour le nom, tu veut faire quoi comme recherche exactement ? Tu veut faire une recherche floue (permettant, par exemple, de trouver “Toulouse” à partir de quelque chose comme “oulose”, avec d’éventuelles fautes de frappe ou des bouts de mots manquants) ? Ou tu veut juste trouver les mots exacts mais sans tenir compte de la casse, des accents et des tirets ?

+0 -0

Bonjour Motet-a,

Je souhaite effectivement avoir une recherche floue. Pas forcément avec fautes de frappe, simplement trouver par petit ensemble de mots ("oulose" dans ton exemple).

Ce n’est pas du Node.js. MongoDb est capable d’interpréter du Javascript (bien que dans mon exemple, ce soit la commande brut interprétable dans la console Mongo). Le module de PHP MongoDb Driver permet la liaison avec la base.

Ah, OK (je sais que MongoDB peut lancer du JS, mais comme je voyais que ton exemple n’était pas lancé depuis du PHP, je me suis posé des questions).

Si tu veut une recherche floue, je pense que tu as (principalement) le choix entre :

Édité : En fait, la recherche de texte de MongoDB ne supporterait pas trop la recherche floue et partielle. Donc si tu tiens à ça, il te faut un Elasticsearch ou autre.

+0 -0

Merci pour ta réponse Motet-a.

Je ne suis pas tenté par la solution d’ElasticSearch pour l’instant, la solution nécessite un serveur avec de bonnes performances (parce que Java :p ) mais celui de dév. n’est pas tout à fait adapté.

Après quelques essais, j’ai trouvé une solution acceptable - je passe de 3sec en local à 125 ms. Sur le serveur ça prend plutôt 750ms en moyenne. Il faut créer un WebService / API en Node.js qui va récupérer périodiquement les collections nécessitant une recherche. Ensuite un simple algo. de mapping permet de choisir les bons termes. Un tri des résultats avec l’algo. quicksort en JS permet de retourner les résultats en maximisant la pertinence.

Pour les prochains qui se poseront la question, voici le code source principal :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
var yengin = require('yengin'),
    express = require('express'),
    bodyParser = require('body-parser'),
    MongoClient = require('mongodb').MongoClient,
    dbMap = require('./core/dbmap'),
    settings = require('./core/settings');

/* Création du serveur */
var app = express();
app.use(bodyParser.urlencoded({ extended: false }));

/* Base de données (mise en cache) */
var dbCache = false;

/* Connexion à la base de données */
MongoClient.connect(settings.connectChain, function (err, db) {
    dbCache = require('./core/dbcache')(db);
    setInterval(function () { dbCache.action(); }, 5*60*1000);
    dbCache.add('communes');
    dbCache.action();
});

/*
*   JSON Answers
*/

var jsonObj = function () {
    var obj = {};
    obj._pack = {'success': false, 'error': false, 'data': [], 'timer': 0};
    obj._timer = (new Date()).getTime();
    obj.success = function (data) {
        obj._pack.success = true;
        obj._pack.error = false;
        obj._pack.data = data;
    };
    obj.end = function (res) {
        obj._pack.timer = (new Date()).getTime()-obj._timer;
        res.writeHead(200, {'Content-Type': 'application/json; charset=utf-8'});
        res.write(JSON.stringify(obj._pack));
        res.end();
    };
    return obj;
};

/*
*   Routes
*/

app.get('/', function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
    res.end();
});

app.get('/communes', function (req, res) {
    /* Données */
    var json = jsonObj(), q = req.query.q;

    /* Recherche */
    q = q.toLowerCase()
        .replace(/[ç]/g,'c').replace(/[ñ]/g,'n')
        .replace(/[áàâäãå]/g,'a').replace(/[éèêë]/g,'e')
        .replace(/[íìîï]/g,'i').replace(/[óòôöõ]/g,'o')
        .replace(/[úùûü]/g,'u').replace(/[ýÿ]/g,'y')
        .replace(/[^\w ]/g,'')
        .replace(/ +/g,'|');

    /* Communes */
    if(q.length) {
        dbCache.on('communes', function (docs) {

            /* Recherche dans la collection */
            var results = dbMap(docs, function () {
                var qsearch, entries = [this.nom,this.cp].join(' ').toLowerCase();
                /* Remplace les accents */
                entries = entries.replace(/[ç]/g,'c').replace(/[ñ]/g,'n')
                    .replace(/[áàâäãå]/g,'a').replace(/[éèêë]/g,'e')
                    .replace(/[íìîï]/g,'i').replace(/[óòôöõ]/g,'o')
                    .replace(/[úùûü]/g,'u').replace(/[ýÿ]/g,'y')
                    .replace(/[^\w ]/g,'');
                /* Effectue une recherche */
                qsearch = entries.match(new RegExp('('+q+')', 'gi'));
                if(qsearch != null) {
                    this.emit({
                        'id': this.id
                    }, {
                        'nom': this.nom,
                        'cp': this.cp,
                        'count': qsearch.length
                    });
                }
            });

            /* Dans l'ordre */
            results = yengin.quickSortArray(results, function (a,b) {
                var sa = a[1].count, sb = b[1].count;
                return (sa == sb ? 0 : (sa > sb ? -1 : 1));
            });

            results = results.slice(0, 10);

            json.success(results);
            json.end(res);
        });
    } else {
        json.end(res);
    }
});

/* Lancement du serveur */
app.listen(settings.port);
console.log('Server is started');

Et le système de mise en cache des collections :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var dbCache = (function (db) {
    var obj = {};

    /* Données */
    var callbackCache = {}, dataCache = {}, indexCache = [];

    obj.action = function () {
        var i, callback
        for(i=0; i < indexCache.length; i++) {
            (function (collection) {
                db.collection(collection).find({}).toArray(function (err, docs) {
                    /* Enregistrement */
                    dataCache[collection] = docs;
                    /* Appels les callbacks caches */
                    if(callbackCache[collection] != undefined) {
                        while((callback = callbackCache[collection].pop(), callback)) {
                            callback(docs);
                        }
                        delete callbackCache[collection];
                    }
                });
            })(indexCache[i]);
        }
    };

    obj.on = function (collection, callback) {
        if(dataCache[collection] != undefined) {
            if(callback) { callback(dataCache[collection]); }
        } else {
            if(callbackCache[collection] == undefined) {
                callbackCache[collection] = []; }
            callbackCache[collection].push(callback);
        }
    };

    obj.add = function (collection) {
        indexCache.push(collection);
    };

    return obj;
});

module.exports = dbCache;

(Edit: Et dbmap pour le mapping)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var dbMap = function (docs, mapFct) {
    var i, dataMap = [];

    /* Emission */
    var emit = function (index, values) { dataMap.push([index, values]); }

    /* Mapping */
    for(i=0; i < docs.length; i++) {
        docs[i].emit = emit;
        docs[i].mapFct = mapFct;
        docs[i].mapFct();
    }

    return dataMap;
};

module.exports = dbMap;

Comme on peut le constater, c’est une structure plutôt flexible - un cadre de travail pour des multiples applications. :)

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte