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())
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'})
In [ ]:
ds(13,7)
In [ ]:
ds(20,5)
In [ ]:
ds(25,6)
In [ ]:
ds(31,6)
In [ ]:
ds(37)
In [ ]:
ds(38,11)
In [ ]:
ds(49,6)
Base de datos documental que usaremos como ejemplo. Una de las más extendidas:
Usaremos pymongo
desde Python. Para instalarlo:
sudo pip install --upgrade pymongo
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
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()
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
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()
Framework de agregación: https://docs.mongodb.com/manual/reference/operator/aggregation/. Y aquí una presentación interesante sobre el tema: https://www.mongodb.com/presentations/aggregation-framework-0?jmp=docs&_ga=1.223708571.1466850754.1477658152
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)
In [ ]:
list(jisbd17.aggregate( [
{'$lookup' : {
"from": "jisbd17",
"localField": "xref",
"foreignField": "_id",
"as": "xrefTitles"
}},
{'$project' : {
'_id' : True,
'xref' : True,
'xrefTitles.title' : True
}}
]))
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()
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:
ROWCOL
hace muy eficiente buscar por una columna en particularIDEA: 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).
In [ ]:
h_jisbd17.row('jisbd17-001')
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')"))
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))
In [ ]:
ds(154,7)
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
In [ ]:
ds(164,6)