lunes, 29 de noviembre de 2010

MongoDB: Python y MapReduce

El presente post está inspirado en el artículo "MapReduce with MongoDB and Python", de Marcel Caraciolo, y en el cual veremos cómo implementar la función Map-Reduce mediante Python y MongoDB, donde podremos apreciar su potencial en grandes conjuntos de datos en computación distribuída.

Como ejemplo sencillo e ilustrativo, contaremos la frecuencia de palabras a través de varios documentos. En primer lugar, veremos cómo funciona map-reduce,utilizando una lista de sentencias simples. Para ello, tenemos el siguiente diagrama, en donde las palabras son divididas y agrupadas por la función map, y después reducidas independientemente (agregación) por la función reduce. Nuestra consulta puede estar distribuída en cuatro ordenadores, por lo que su procesamiento es mucho más rápido en tiempo de ejecución. El ejemplo, además, muestra un árbol balanceado, pero podría estar no balanceado o incluso tener alguna redundancia.
Antes de comenzar a desarrollar las funciones map y reduce debes saber que:
1) El motor MapReduce puede invocar funciones reduce de forma iterativa, por lo que éstas deben ser idempotentes:
   for all k,vals : reduce( k, [reduce(k,vals)] ) == reduce(k,vals)
1) Actualmente, el retorno de valores de una función reduce no puede ser un array (ha de ser un objeto o un número)
3) Si necesitas realizar una operación una única vez, utiliza la funión finalize.

Para implementar el código debes utilizar el framework Pymongo, el cual tiene soporte para Map/Reduce. Como ejemplo, el discurso de Obama en 2009 tiene muchas palabras repetidas, como puede apreciarse en la siguiente nube de etiquetas, cuyo tamaño de fuente indica la frecuencia de cada palabra:
MongoDB permite a los clientes enviar las implementaciones JavaScript de map y reduce, las cuales se evaluarán y ejecutarán en el servidor. La función map sería la siguiente (wordMap.js):

function wordMap() {
  // Buscar palabras en el texto del documento
  var palabras = this.text.match(/\w+g);

  if (palabras == null) {
     return;
  }

  for (var i = 0; i < palabras.length; i++) {
    // Emitir cada palabra, con contador de uno
    emit(words[i], {count: 1});
  }
}

MongoDB llamará a la función map por cada documento en la colección que está consultando, y apuntará al documento donde tendrá acceso una clave como text mediante this.text. Esta función no retorna una lista, si no que llama a una función emit que espera ser definida. Los parámetros de esta función (clave, valor), serán agrupados con otros resultados intermedios de otras evaluaciones map que tengan la misma clave (clave, [valor1, valor2]) y pasada a la función reduce (wordReduce.js
function wordReduce(key, values) {

  var total = 0;
  for (var i = 0; i < values.length; i++) {
    total += values[i].count;
  }

  return {count: total};
}

La función reduce ha de reducir una lista de un tipo dado a un valor simple del mismo tipo; debe transsitivo para que no tenga problemas sobre cómo están agrupados los elementos mapeados.

A continuación, el código del cliente Pymongo que pasa las funciones map/reduce al servidor: