Redis, NoSQL y esas hierbas (II)
En esta entrega voy a explicar el modelo de datos que he empleado en la nueva bitácora, una vez expuestas mis conclusiones tras usar Redis como almacenamiento NoSQL.
Cuando me puse a ver la lista de comandos de Redis pensé: sí, no,... o igual sí :). Estamos bien educados en el modelo relacional, rápidamente vemos tablas, así que pensar cómo implementar la aplicación sin tablas ni relaciones me resultó un poco complicado al principio.
Lo que me pareció un primer buen paso fue acotar el problema: ¿qué es una bitácora? Veamos de nuevo la definición que propuse en la anterior entrega:
Una bitácora es una página web que consiste en una serie de artículos ordenados de forma cronológica inversa, de manera que el más reciente se muestra primero, seguido de los más antiguos.
Es decir, necesitamos almacenar una serie de documentos que contienen varios campos (titular, cuerpo, fecha, etc), y tenemos que poder acceder a ellos en orden cronológico inverso.
Adicionalmente, en mi caso, quiero poder recuperar a las anotaciones que están clasificadas en una determinada categoría. No he contemplado más posibilidades (como anotaciones por año o mes, por ejemplo), porque tampoco las utilizaba en el gestor de contenidos antiguo.
En mi concepto de bitácora también tenemos comentarios, que están ordenados según publicación, y están asociados a una anotación.
Por último quiero mostrar en la columna lateral los últimos comentarios, y por simplificar no voy a entrar en el tema de ma moderación de comentarios ;).
De está descripción del modelo de datos, más o menos ordenada :P, veo que necesito tres tipos distintos de entidades en el almacenamiento:
- Documento: con una estructura compleja, de tipo
post
ocomment
. - Índice: lo usaremos para asociar documentos bajo un criterio concreto (anotaciones en la bitácora, comentarios de una anotación, anotaciones en una categoría, etc), y estarán ordenados (la mayor parte del tiempo :D).
- Contador: como un documento puede estar en varios índices, tendré que guardar una referencia, que debe ser única por tipo de documento. Estos contadores serán los que nos den el ID del siguiente elemento.
Con este pequeño análisis ya tuve suficiente para detectar qué tipos de datos en Redis
me permitían implementar el almacenamiento adecuadamente:
Tipo en Redis | Operaciones (in/out/del) | |
Documento | String | set / get / delete |
Índice | List | rpush / lrange / lrem |
Contador | String | incr / - / - |
Para los siguientes ejemplos vamos a suponer que trabajamos con Python
y redis-py
:
import redis import simplejson as json rd = redis.Redis(host='localhost', port=6379, db=0)
Contadores
Voy a empezar por el final, porque es mejor tener clara la función de los contadores antes de ponernos a manejar documentos.
Un contador simplemente almacena un número, que vamos incrementando de forma atómica (una operación que no puede ser interrumpida), de manera que siempre obtengamos un identificador diferente, y que además tenga la propiedad de ser ordenable :).
En mi caso he definido dos contadores: post:next
y comment:next
.
Inicialmente valen cero, y cuando necesitamos un ID
nuevo es tan sencillo como:
id = rd.incr('post:next')
El comando INCR
incrementa en uno el valor de la clave que indicamos y nos devuelve su nuevo valor. Es un proceso que no puede ser interrumpido (o funciona, o no funciona), y nos garantiza un identificador único y ordenable.
Documentos
El dato elegido en Redis
es simple: una cadena de texto, así que para poder manejar la estructura compleja de uno de nuestros documentos, vamos a utilizar JSON
.
Siguiendo el ejemplo de la nueva anotación:
post = dict( title = u'Anotación de ejemplo', body = u'Este sería el cuerpo de la anotación. etc!', tags = [ u'ejemplos', u'pruebas' ], date = '2010-06-09 21:00', ) # en ID teníamos ya nuestro nuevo identificador rd.set('post:' + str(id) + ':store', json.JSONEncoder().encode(post))
He decidido seguir el siguiente esquema para los nombres de los documentos: post:ID:store
y commment:ID:store
, para post
y comment
respectivamente.
En realidad a Redis
le da igual, solo espera que la clave a la que asociar la cadena de texto (nuestro JSON
, recordemos), sea una cadena de texto sin espacios ni saltos de linea, pero es buena idea ceñirse a una convención interna.
Recuperar un documento es sencillo, una vez sabemos su ID
:
# recuperamos el post con ID = 1 post = rd.get('post:1:store') post = json.JSONDecoder().decode(post)
Para eliminar un documento, usaremos el comando DEL
indicando la clave a borrar (ojo, delete
en redis-py
).
Índices
Para implementar los índices en Redis
he elegido las listas, que son nada más que una serie de elementos enlazados en los que se respeta el orden (A → B → C).
Solo tenemos que decidir cuántos índices necesitamos, y esto es: uno por cada criterio para acceder a los documentos (y siempre siguiendo el orden de la lista, ascendente o descendente).
Por ejemplo, yo he implementado:
post::index
: lista de todas las anotaciones.comment::index
: lista de todos los comentarios, independientemente de la anotación a la que estén asociados.comment:POSTID:index
: los comentarios asociados a una anotación concreta.tag:TAG:index
: las anotaciones asociadas a una categoría concreta.
Siguiendo con el ejemplo de la anotación:
# indexamos el post donde toca rd.rpush('post::index', id) # y en cada índice de cada tag for t in post['tags']: # la clave no puede tener espacios, pero los tags sí t = t.replace(' ', '.') rd.rpush('tag:' + t + ':index', id)
De forma similar indexaríamos los comentarios asociados a la anotación, usando el índice adecuado (en el ejemplo: comment:1:index
para el post
con ID
igual a 1
).
Para recoger un elemento o varios de una lista, empleamos lrange
, que maneja rangos de listas. Por ejemplo, para recoger las 10
anotaciones que mostramos en portada:
# cogemos los 10 últimos elementos, esto es: del -10 al -1 index = rd.lrange('post::index', -10, -1) for p in index: post = rd.get('post:' + str(p) + ':store') post = json.JSONDecoder().decode(post) # hacemos algo con los posts :) print post['date'] + ': ' + post['title']
Para eliminar un elemento de un índice podemos usar LREM
(que en redis-py
intercambia el orden de los parámetros, cuidado) y, si es necesario, con SORT
podemos reordenar una lista mediante diferentes criterios.
En mi caso el uso de SORT
no es importante, porque siempre añado elementos al final de un índice, así que mis listas están (casi) siempre ordenadas (una excepción son los índices de las categorías, si añadimos una a una anotación vieja, en ese caso llamo a SORT
al final del proceso).
Conclusiones
Esta es la solución que he implementado, y me está dando buenos resultados, pero desde luego no es la única posible.
Podría haber usado más intensivamente Redis
en lugar de almacenar los documentos con JSON
(por ejemplo, haber usado post:ID:title
, post:ID:body
, post:ID:date
, etc), y no sé si hubiera sido mejor o peor. Mi propuesta hace menos llamadas a Redis
a cambio de llamadas a simplejson
.
Aunque la flexibilidad del modelo de datos es eso: poder diseñar como queramos el almacenamiento de nuestra aplicación, eligiendo lo que funciona mejor para solucionar nuestro problema.
La verdad es que he disfrutado mucho con esta parte del desarrollo, porque es completamente distinto a lo que estaba acostumbrado (SQL
, ORM
, y el modelo relacional), aunque en muchas etapas del proceso he pensado: uhm, esto con un ORM
sería más rápido de programar :D.
Siguiente entrega
Solo me queda hablar del mantenimiendo: revisar la integridad del almacenamiento, hacer copias de seguridad, y una de las cosas más interesantes de una solución NoSQL
... ¡replicación!
Hay 5 comentarios
Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 30 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.
por MarcosBL, en 2010-06-10 10:28:11 ∞