Tecnologías NoSQL -- Tutorial en JISBD 2017

Toda la información de este tutorial está disponible en https://github.com/dsevilla/jisbd17-nosql. Diego Sevilla Ruiz, dsevilla@um.es.


In [ ]:
%load extra/utils/functions.py

In [ ]:
ds(1,2)

In [ ]:
ds(3)

In [ ]:
yoda(u"Una guerra SQL vs. NoSQL no debes empezar")

In [ ]:
ds(4)

In [ ]:
%%bash
sudo docker pull mongo
pip install --upgrade pymongo

In [ ]:
!sudo docker run --rm -d --name mongo -p 27017:27017 mongo

In [ ]:
import pymongo
from pymongo import MongoClient
client = MongoClient("localhost", 27017)
client

Creamos una base de datos presentations:


In [ ]:
db = client.presentations

Y la colección jisbd17:


In [ ]:
jisbd17 = db.jisbd17
jisbd17

In [ ]:
jisbd17.insert_one({'_id' : 'jisbd17-000',
                   'title': 'blah',
                   'text' : '',
                   'image': None,
                   'references' : 
                       [{'type' : 'web',
                         'ref' : 'http://nosql-database.org'},
                        {'type' : 'book',
                         'ref' : 'Sadalage, Fowler. NoSQL Distilled'}
                       ],
                   'xref' : ['jisbd17-010', 'jisbd17-002'],
                   'notes': 'blah blah'
                  })

In [ ]:
client.database_names()

In [ ]:
DictTable(jisbd17.find_one())

Voy a añadir todas las imágenes de la presentación a la base de datos

Primero se buscan todos los ficheros, y después se utiliza la función update_one() para añadir o actualizar los valores de la base de datos (ya habíamos metido información parcial para jisbd17-000).


In [ ]:
import os
import os.path
import glob
files = glob.glob(os.path.join('slides','slides-dir','*.png'))

Añadiendo las imágenes a la base de datos...


In [ ]:
from bson.binary import Binary

for file in files:
    img = load_img(file)
    img_to_thumbnail(img)
    slidename = os.path.basename(os.path.splitext(file)[0])
    jisbd17.update_one({'_id': slidename},
                       {'$set' : {'image': Binary(img_to_bytebuffer(img))}},
                      True)

In [ ]:
for slide in jisbd17.find():
    print(slide['_id'], slide.get('title'))

In [ ]:
slide0 = jisbd17.find_one({'_id': 'jisbd17-000'})

In [ ]:
img_from_bytebuffer(slide0['image'])

Añado la presentación de JISBD 2017 a la colección presentations.


In [ ]:
presentations = db.presentations
slides = [r['_id'] for r in jisbd17.find({'_id' : {'$regex' : '^jisbd17-'}},projection={'_id' : True}).sort('_id', 1)]
presentations.insert_one({'name' : 'Tecnologías NoSQL. JISBD 2017',
                          'slides' : slides
                         })

In [ ]:
presentations.find_one()

In [ ]:
yoda(u'Modelado de datos tú no hacer...')

In [ ]:
inciso_slide = 9
ds(inciso_slide,3)

In [ ]:
jisbd17.find_one({'_id': 'jisbd17-000'})

Introducción a NoSQL


In [ ]:
ds(13,7)

In [ ]:
ds(20,5)

¡Escalabilidad!


In [ ]:
ds(25,6)

Schemaless


In [ ]:
ds(31,6)

In [ ]:
ds(37)

Modelado de datos en NoSQL


In [ ]:
ds(38,11)

Eficiencia raw


In [ ]:
ds(49,6)

Tipos de Sistemas NoSQL

MongoDB (documentos)

Base de datos documental que usaremos como ejemplo. Una de las más extendidas:

  • Modelo de documentos JSON (BSON, en binario, usado para eficiencia)
  • Map-Reduce para transformaciones de la base de datos y consultas
  • Lenguaje propio de manipulación de la base de datos llamado "de agregación" (aggregate)
  • Soporta sharding (distribución de partes de la BD en distintos nodos)
  • Soporta replicación (copias sincronizadas master-slave en distintos nodos)
  • No soporta ACID
  • La transacción se realiza a nivel de DOCUMENTO

Usaremos pymongo desde Python. Para instalarlo:

sudo pip install --upgrade pymongo

Texto y título de las diapositivas

Como ya tenemos populada la colección jisbd17, podemos actualizar los documentos para añadir el título y el texto de cada diapositiva. Lo extraeremos del fichero slides.tex.


In [ ]:
import re

def read_slides():
    in_slide = False
    slidetitle = ''
    slidetext = ''
    slidenum = 0
    with open('slides/slides.tex', 'r') as f:
        for line in f:
            # Remove comments
            line = line.split('%')[0]
            
            if not in_slide:
                if '\\begin{frame}' in line:
                    in_slide = True
            elif '\\frametitle' in line:
                q = re.search('\\\\frametitle{([^}]+)',line)
                slidetitle = q.group(1)
                continue
            elif '\\framebreak' in line or re.match('\\\\only<[^1]',line) or '\\end{frame}' in line:
                
                # Añadir la diapositiva a la lista
                slideid = 'jisbd17-{:03d}'.format(slidenum)
                print(slideid)
                jisbd17.update_one({'_id': slideid},
                       {'$set' : {'title': slidetitle,
                                  'text' : slidetext
                                 }},
                      True)

                # Next
                slidetext = ''
                slidenum += 1
                if '\\end{frame}' in line:
                    in_slide = False
                    slidetitle = ''
            else:
                slidetext += line
                
# Llamar a la función
read_slides()

Para usar el shell de mongo en Javascript:

docker exec -it mongo mongo

Consultas sencillas

Distribución del tamaño del texto de las transparencias.


In [ ]:
slides = jisbd17.find(filter={},projection={'text': True})
df = pd.DataFrame([len(s.get('text','')) for s in slides])
df.plot()

La función find() tiene un gran número de posibilidades para especificar la búsqueda. Se pueden utilizar cualificadores complejos como:

  • $and
  • $or
  • $not

Estos calificadores unen "objetos", no valores. Por otro lado, hay otros calificadores que se refieren a valores:

  • $lt (menor)
  • $lte (menor o igual)
  • $gt (mayor)
  • $gte (mayor o igual)
  • $regex (expresión regular)

In [ ]:
jisbd17.find_one({'text': {'$regex' : '[Mm]ongo'}})['_id']

También permite mostrar el plan de ejecución:


In [ ]:
jisbd17.find({'title' : 'jisbd17-001'}).explain()

Se puede crear un índice si la búsqueda por ese campo va a ser crítica. Se pueden crear más índices, de tipos ASCENDING, DESCENDING, HASHED, y otros geoespaciales. https://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.create_index


In [ ]:
jisbd17.create_index([('title', pymongo.HASHED)])

In [ ]:
jisbd17.find({'title' : 'jisbd17-001'}).explain()

Map-Reduce


In [ ]:
ds(59,9)

Mongodb incluye dos APIs para procesar y buscar documentos: el API de Map-Reduce y el API de agregación. Veremos primero el de Map-Reduce. Manual: https://docs.mongodb.com/manual/aggregation/#map-reduce

Histograma de tamaño del texto de las diapositivas

Con Map-Reduce se muestra el tamaño del texto de cada diapositiva, y el número de diapositiva que tienen ese tamaño de texto.


In [ ]:
from bson.code import Code
map = Code(
    '''function () {
           if ('text' in this)
               emit(this.text.length, 1)
           else
               emit(0,1)
       }''')
reduce = Code(
    '''function (key, values) {
            return Array.sum(values);
        }''')
results = jisbd17.map_reduce(map, reduce, "myresults")
results = list(results.find())
results

Como un plot:


In [ ]:
df = pd.DataFrame(data = [int(r['value']) for r in results], 
                  index = [int(r['_id']) for r in results], 
                  columns=['posts per length'])
df.plot(kind='bar',figsize=(30,10))

O un histograma:


In [ ]:
df.hist()

In [ ]:
list(jisbd17.aggregate( [ {'$project' : { 'Id' : 1 }}, {'$limit': 20} ]))

In [ ]:
nposts_by_length = jisbd17.aggregate( [
        #{'$match': { 'text' : {'$regex': 'HBase'}}},
        {'$project': {
            'text' : {'$ifNull' : ['$text', '']}
        }},
        {'$project' : {
             'id' : {'$strLenBytes': '$text'},
             'value' : {'$literal' : 1}
        }
        },
        {'$group' : {
            '_id' : '$id',
            'count' : {'$sum' : '$value'}
        }
        },
        {'$sort' : { '_id' : 1}}
        ])
list(nposts_by_length)

Simulación de JOIN: $lookup

El framework de agregación introdujo también una construcción equivalente a JOIN de SQL. Por ejemplo, se puede mostrar los títulos de las transparencias referenciadas además de los identificadores:


In [ ]:
list(jisbd17.aggregate( [
        {'$lookup' : {
            "from": "jisbd17",
            "localField": "xref",
            "foreignField": "_id",
            "as": "xrefTitles"
        }},
        {'$project' : {
            '_id' : True,
            'xref' : True,
            'xrefTitles.title' : True
        }}
        ]))

HBase (wide-column)

Usaré la imagen docker de HBase a partir de aquí: https://github.com/krejcmat/hadoop-hbase-docker, ligeramente modificada. Para iniciar los contenedores (un master y dos "slave"):

git clone https://github.com/dsevilla/hadoop-hbase-docker.git
cd hadoop-hbase-docker
./start-container.sh latest 2

# Un conenedor máster, 2 slave, simulan un clúster distribuido de tres nodos
# Los contenedores arrancan, el shell entra en el master:

./configure-slaves.sh
./start-hadoop.sh
hbase-daemon.sh start thrift  # Servidor para conexión externo
./start-hbase.sh

Ahora ya podemos conectar a la base de datos. Dentro del contenedor, ejecutando hbase shell nos vuelve a mostrar el shell. En él, podemos ejecutar consultas, creación de tablas, etc.:

status
# Crear tabla
# Put
# Consultas sencillas

In [ ]:
%%bash
cd /tmp && git clone https://github.com/dsevilla/hadoop-hbase-docker.git

In [ ]:
ds(84,11)

In [ ]:
ds(97,2)

También se puede conectar de forma remota. Usaremos, desde Python, el paquete happybase:


In [ ]:
!pip install --upgrade happybase

In [ ]:
import happybase
happybase.__version__

In [ ]:
host = '127.0.0.1'
hbasecon = happybase.Connection(host)
hbasecon.tables()

In [ ]:
ds(103,3)

In [ ]:
try:
    hbasecon.create_table(
        "jisbd17",
        {
            'slide': dict(bloom_filter_type='ROW',max_versions=1),
            'image' : dict(compression='GZ',max_versions=1),
            'text' : dict(compression='GZ',max_versions=1),
            'xref' : dict(bloom_filter_type='ROWCOL',max_versions=1)
        })
except:
    print ("Database slides already exists.")
    pass

In [ ]:
hbasecon.tables()

Copiar la tabla jisbd17 de mongo

Se hará respetando las familias de columnas creadas. En particular, se dejará por ahora el campo xref, del que se verá después una optimización.


In [ ]:
h_jisbd17 = hbasecon.table('jisbd17')

In [ ]:
with h_jisbd17.batch(batch_size=100) as b:
    for doc in jisbd17.find():
        b.put(doc['_id'], {
            'slide:title' : doc.get('title',''),
            'slide:notes' : doc.get('notes',''),
            'text:' : doc.get('text', ''),
            'image:' : str(doc.get('image',''))
        })

Para el caso de xref usaremos una optimización posible en HBase:

  • Las filas pueden crecer tanto como se quiera también en columnas
  • El filtro Bloom ROWCOL hace muy eficiente buscar por una columna en particular

IDEA: Usar los elementos del array como nombres de las columnas. Convierte automáticamente a esa columna en un índice inverso:


In [ ]:
with h_jisbd17.batch(batch_size=100) as b:
    for doc in jisbd17.find():
        if 'xref' in doc:
            for ref in doc['xref']:
                b.put(doc['_id'], {
                    'xref:'+ref : ''
                })

In [ ]:
list(h_jisbd17.scan(columns=['xref']))

Y finalmente el índice inverso. Es muy eficiente ya que para esa familia de columnas xref se ha usado el filtro Bloom ROWCOL.


In [ ]:
list(h_jisbd17.scan(columns=['xref:jisbd17-002']))

Finalmente, en HBase, un scan es una pérdida de tiempo. Se debería precomputar la referencia inversa e incluirla en cada slide. La búsqueda así es O(1).

Obtención de una fila con happybase


In [ ]:
h_jisbd17.row('jisbd17-001')

Ejemplos de filtros con happybase


In [ ]:
ds(114)

In [ ]:
list(h_jisbd17.scan(filter="KeyOnlyFilter()"))

In [ ]:
list(h_jisbd17.scan(filter="PrefixFilter('jisbd17-0')",limit=5))

In [ ]:
list(h_jisbd17.scan(filter="ColumnPrefixFilter('t')"))

In [ ]:
list(h_jisbd17.scan(filter="RowFilter(<,'binary:jisbd17-1')",limit=5))

In [ ]:
list(h_jisbd17.scan(filter="SingleColumnValueFilter('slide', 'title', =,'binary:HBase')"))

Neo4j (Grafos)

Se puede utilizar el propio interfaz de Neo4j también en la dirección http://127.0.0.1:7474.


In [ ]:
%%bash
sudo docker pull neo4j
sudo docker run -d --rm --name neo4j -p 7474:7474 -p 7687:7687 --env NEO4J_AUTH=none neo4j

Vamos a cargar la extensión ipython-cypher para poder lanzar consultas Cypher directamente a través de la hoja. He iniciado la imagen de Neo4j sin autenticación, para pruebas locales.

Utilizaremos una extensión de Jupyter Notebook que se llama ipython-cypher. Está instalada en la máquina virtual. Si no, se podría instalar con:

pip install ipython-cypher

Después, todas las celdas que comiencen por %%cypher y todas las instrucciones Python que comiencen por %cypher se enviarán a Neo4j para su interpretación. También usaremos la librería py2neo para crear el grafo:

pip install py2neo

In [ ]:
%%bash
pip install ipython-cypher
pip install py2neo

In [ ]:
ds(135,3)

In [ ]:
ds(139,2)

In [ ]:
ds(148)

In [ ]:
ds(150,4)

In [ ]:
from py2neo import Graph

graph = Graph('http://localhost:7474/db/data/')

In [ ]:
graph.delete_all()

Importamos todas las diapositivas de MongoDB:


In [ ]:
from py2neo import Node

for doc in jisbd17.find():
    node = Node("Slide",
                name = doc.get('_id'),
                title = doc.get('title',''),
                notes = doc.get('notes',''),
                text = doc.get('text', ''))
    graph.create(node)

In [ ]:
graph.find_one('Slide', property_key='name', property_value='jisbd17-001')

In [ ]:
from py2neo import NodeSelection

NodeSelection(graph,conditions=["_.name='jisbd17-001'"]).first()['title']

Crearemos la relación :NEXT para indicar la siguiente diapositiva. Ahora se hará con py2neo y después con ipython-cypher.


In [ ]:
from py2neo import Relationship

for i in range(jisbd17.count() - 1):
    slide_pre = NodeSelection(graph,conditions=[
        '_.name = \'jisbd17-{:03d}\''.format(i)]).first()
    slide_next = NodeSelection(graph).where(
        '_.name = \'jisbd17-{:03d}\''.format(i+1)).first()
    
    graph.create(Relationship(slide_pre, "NEXT", slide_next))

El lenguaje Cypher


In [ ]:
ds(154,7)

Ipython-cypher


In [ ]:
%load_ext cypher

In [ ]:
%config CypherMagic.auto_html=False
%config CypherMagic.auto_pandas=True

In [ ]:
%%cypher
match (n) return n;

In [ ]:
%%cypher
match (n) return n.name;

Vamos a añadir las relaciones xref que haya en las diapositivas. Por ahora sólo había unas puestas a mano. Para las diapositivas que no tengan referencias, añado una al azar.


In [ ]:
import random

nslides = jisbd17.count()
for doc in jisbd17.find():
    for ref in doc.get('xref',['jisbd17-{:03d}'.format(random.randint(1,nslides))]):
        slide_from = doc['_id']
        slide_to = ref
        %cypher MATCH (f:Slide {name: {slide_from}}), (t:Slide {name: {slide_to}}) MERGE (f)-[:REF]->(t)

In [ ]:
%config CypherMagic.auto_networkx=False
%config CypherMagic.auto_pandas=False

In [ ]:
%%cypher
MATCH p=shortestPath(
  (s:Slide {name:"jisbd17-004"})-[*]->(r:Slide {name:"jisbd17-025"})
)
RETURN p

In [ ]:
# Tópicos de slides con expresiones regulares

In [ ]:
import cypher
cypher.run("MATCH (n) RETURN n")

In [ ]:
!sudo docker stop neo4j

In [ ]:
!sudo docker stop mongo

Nuestro trabajo de investigación


In [ ]:
ds(164,6)