「7つのデータベース 7つの世界」第8章の学習のために、Redisをインストールします。
radish(大根)を連想してしまうよ...
7つのデータベース 7つの世界
- PotgreSQL
- Riak
- HBase
- MongoDB
- CouchDB
- Neo4j
- Redis
書籍で使っているRedisのバージョンは、2.4
最新バージョンは、6.0.4。
インストール
Windows 10
公式のWindowsバイナリはないようです。
githubにmicrosoftarchivesの3.2.100が残っています。msiインストーラとzipファイルをダウンロードできます。
zipファイル
ここでは、zipファイルをダウンロードして、Windowsサービスではなくフォアグラウンドでredis-serverを起動してみます。
エクスプローラで、ダウンロードしたzipファイルを選択して、右クリックして、「すべて展開...」を選びます。Redis-x64-3.2.100フォルダに展開します。
コマンドプロンプトまたはPoweShellを起動して、展開したフォルダに移動します。
C:> cd Downloads\Redis-x64-3.2.100
Code language: Bash (bash)
バージョンを確認します。
C:> redis-cli --version
redis-cli 3.2.100
C:> redis-server --version
Redis server v=3.2.100 sha=00000000:0 malloc=jemalloc-3.6.0 bits=64 build=dd26f1f93c5130ee
Code language: Bash (bash)
redis-cliを起動すると、「not connected」プロンプトになってしまいました。quitで終了します。
C:> redis-cli
Could not connect to Redis at 127.0.0.1:6379: 対象のコンピューターによって拒否されたため、接続できませんでした。
Could not connect to Redis at 127.0.0.1:6379: 対象のコンピューターによって拒否されたため、接続できませんでした。
not connected> quit
Code language: Bash (bash)
redis-serverを起動します。confファイルは、ファイル名に「-service」がつかない「redis.windows.conf」を指定します。
redis-serverを停止するには、Ctrl+Cを押します。
C:> redis-server redis.windows.conf
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 3.2.100 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 22904
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
[22904] 29 May 09:09:34.136 # Server started, Redis version 3.2.100
[22904] 29 May 09:09:34.139 * DB loaded from disk: 0.000 seconds
[22904] 29 May 09:09:34.140 * The server is now ready to accept connections on port 6379
Code language: Bash (bash)
別のコマンドプロンプトを起動して、redis-cliを起動します。
C:> cd Downloads\Redis-x64-3.2.100
C:> redis-cli
127.0.0.1:6379>
Code language: Bash (bash)
msiインストーラ
msiインストーラで、Windowsサービスとしてインストールする手順は、次の記事を参考にしてください。
https://weblabo.oscasierra.net/redis-windows-install/
https://qiita.com/okoi/items/3bb5ae26ad559e4f39a0
Mac OS X
homebrewで6.0.4を、homebrew caskで4.0.2をインストールできます。
ここでは、homebrewの6.0.4をインストールしました。
$ brew install redis
(省略)
==> Caveats
To have launchd start redis now and restart at login:
brew services start redis
Or, if you don't want/need a background service you can just run:
redis-server /usr/local/etc/redis.conf
(省略)
Code language: Bash (bash)
$ redis-server --version
Redis server v=6.0.4 sha=00000000:0 malloc=libc bits=64 build=992d36c7134f1fc8
$ redis-cli --version
redis-cli 6.0.4
Code language: Bash (bash)
バージョンを確認します。
$ redis-server --version
Redis server v=6.0.4 sha=00000000:0 malloc=libc bits=64 build=992d36c7134f1fc8
$ redis-cli --version
redis-cli 6.0.4
Code language: Bash (bash)
起動方法は2種類あります。サービスとして起動する方法、フォアグラウンドで起動する方法です。
サービスとして起動する方法
$ brew services start redis
==> Successfully started `redis` (label: homebrew.mxcl.redis)
$ redis-cli
127.0.0.1:6379> quit
$ brew services stop redis
Stopping `redis`... (might take a while)
==> Successfully stopped `redis` (label: homebrew.mxcl.redis)
Code language: Bash (bash)
フォアグラウンドで起動する方法。別のターミナルを開いて、redis-cliを起動します。フォアグラウンドのredis-serverをストップするには、Ctrl+Cを押します。
$ redis-server /usr/local/etc/redis.conf
75019:C 29 May 2020 08:52:25.243 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
75019:C 29 May 2020 08:52:25.243 # Redis version=6.0.4, bits=64, commit=00000000, modified=0, pid=75019, just started
75019:C 29 May 2020 08:52:25.243 # Configuration loaded
75019:M 29 May 2020 08:52:25.245 * Increased maximum number of open files to 10032 (it was originally set to 256).
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.0.4 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 75019
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
75019:M 29 May 2020 08:52:25.246 # Server initialized
75019:M 29 May 2020 08:52:25.247 * Loading RDB produced by version 6.0.4
75019:M 29 May 2020 08:52:25.247 * RDB age 8 seconds
75019:M 29 May 2020 08:52:25.247 * RDB memory usage when created 0.95 Mb
75019:M 29 May 2020 08:52:25.247 * DB loaded from disk: 0.000 seconds
75019:M 29 May 2020 08:52:25.247 * Ready to accept connections
Code language: Bash (bash)
Ubuntu 18.04
aptでインストールできます。
$ sudo apt install redis
Code language: Bash (bash)
バージョンは、4.0.9 でした。
$ redis-server --version
Redis server v=4.0.9 sha=00000000:0 malloc=jemalloc-3.6.0 bits=64 build=9435c3c2879311f3
$ redis-cli --version
redis-cli 4.0.9
Code language: Bash (bash)
サービスとしてインストールされます。インストール後、すでにサービスがスタートしていました。
$ sudo service redis stop
$ sudo service redis start
Code language: Bash (bash)
試しに、サービスをストップして、redis-cliを起動してみました。redis-cliのプロンプトが「not connected>」と表示されました。
$ redis-cli
Could not connect to Redis at 127.0.0.1:6379: Connection refused
Could not connect to Redis at 127.0.0.1:6379: Connection refused
not connected> quit
Code language: Bash (bash)
サービスがスタートしていれば、redis-cliのプロンプトは「127.0.0.1:6379>」と表示されました。
$ redis-cli
127.0.0.1:6379>
Code language: Bash (bash)
書籍内コードほか
p261 データダンプ
http://download.freebase.com/datadumps/latest/browse/book/isbn.tsv は404エラー。
http://freebase-easy.cs.uni-freiburg.de/ から、freebase-easy-14-04-14.zip をダウンロードして、"isbn-13"でgrepして、列を入れ替えて、isbn.tsv を作りました。
isbn.tsv.gz 15MB、69万件
p262のisbn.rbでインポートしたところ、28.45秒。isbn_pipelined.rbでインポートしたところ、9.4秒。パイプラインで改善できるとの説明どおり、処理時間は約1/3になりました。
p270 データの投入
http://download.freebase.com/datadumps/latest/browse/music/group_membership.tsv は404エラー
http://freebase-easy.cs.uni-freiburg.de/ から、freebase-easy-14-04-14.zip をダウンロードして、もろもろ処理をして、group_membership.tsv を作りました。
group_membership.tsv 2MB、15万件
p271 フェーズ1: データ変換
最新のnodejsで動くように、pre_populate.jsの一部を書き直しました。
/***
* Excerpted from "Seven Databases in Seven Weeks",
* published by The Pragmatic Bookshelf.
* Copyrights apply to this code. It may not be used to create training material,
* courses, books, articles, and the like. Contact us if you are in doubt.
* We make no guarantees that this code is fit for any purpose.
* Visit http://www.pragmaticprogrammer.com/titles/rwdata for more book information.
***/
var
// The band data file name in tab-seperated form
tsvFileName = 'group_membership.tsv',
// track how many file lines we've processed
processedLines = 0,
// standard libraries
fs = require('fs'),
csv = require('csv'),
redis = require('redis'),
// database clients
redis_client = redis.createClient(6379);
/**
* A helper function that splits up the comma-seperated list of roles and
* converts it to an array. If no valid roles exist, return an empty array.
* @param string the CSV to split into a role array
*/
function buildRoles( string ) {
var roles = string.split(',');
if(roles.length === 1 && roles[0] === '') roles = [];
return roles;
};
/**
* Utility function that increments the total number
* of lines (artists) processed and outputs every 1000.
*/
function trackLineCount() {
if( ++processedLines % 1000 === 0 )
console.log('Lines Processed: ' + processedLines);
}
/**
* Does all heavy lifting. Loops through the CSV file
* and populate Redis with the given values.
*/
function processData(data) {
var artist = data[1];
var band = data[2];
var roles = buildRoles(data[3]);
if ( band === '' || artist === '') {
trackLineCount();
return true;
}
redis_client.sadd('band:' + band, artist);
roles.forEach(function(role) {
redis_client.sadd('artist:' + band + ':' + artist, role);
});
trackLineCount();
}
function populateRedis() {
const parser = csv.parse({
delimiter: "\t",
quote: false
});
parser.on('error', (err) => {
console.log(err.message);
});
parser.on('readable', () => {
var data;
while (data = parser.read()) {
processData(data);
}
});
parser.on('end', () => {
console.log('Total Lines Processed: ' + processedLines);
redis_client.quit();
});
fs.createReadStream(tsvFileName).pipe(parser);
};
populateRedis();
Code language: JavaScript (javascript)
populate_couch.js
/***
* Excerpted from "Seven Databases in Seven Weeks",
* published by The Pragmatic Bookshelf.
* Copyrights apply to this code. It may not be used to create training material,
* courses, books, articles, and the like. Contact us if you are in doubt.
* We make no guarantees that this code is fit for any purpose.
* Visit http://www.pragmaticprogrammer.com/titles/rwdata for more book information.
***/
var
// how many bands we expect to process
totalBands = null,
// and keep track of how many bands we have processed
processedBands = 0,
// The name of the couch database
couchDBpath = '/bands',
// standard libraries
http = require('http'),
redis = require('redis'),
// database clients
redisClient = redis.createClient(6379);
/**
* A helper function that builds a good CouchDB key
* @param string the unicode string being keyified
*/
function couchKeyify(string) {
// remove bad chars, and disallow starting with an underscore
return string.
replace(/[\t \?\#\\\-\+\.\,'"()*&!\/]+/g, '_').
replace(/^_+/, '');
};
/*
* Keep track of the number of bands processed, output every 1000 loaded,
* and close the Redis client when we've loaded them all.
*/
function trackLineCount( increment ) {
processedBands += increment;
// output once every 1000 lines
if (processedBands % 1000 === 0) {
console.log('Bands Loaded: ' + processedBands);
}
// close the Redis Client when complete
if (totalBands <= processedBands) {
console.log('Total Bands Loaded: ' + processedBands);
redisClient.quit();
}
};
/*
* Post one or more documents into CouchDB.
* @param url is where we POST to.
* @param docString a stringified JSON document.
* @param count the number of documents being inserted.
*/
function postDoc(path, docsString, count) {
console.log("postDoc " + path + " " + count);
const options = {
host: '127.0.0.1',
port: 5984,
method: 'POST',
path: path,
headers: {
'Content-Type' : 'application/json'
}
};
const req = http.request(options, (res) => {
res.on('data', (chunk) => {
trackLineCount(count);
});
res.on('error', (err) => {
console.log('response.error: ' + err.message);
});
});
req.on('error', (err) => {
console.log('request.error: ' + err.message);
});
req.write(docsString);
req.end();
};
/*
* Loop through all of the bands populated in Redis. We expect
* the format of each key to be 'band:Band Name' having the value
* as a set of artist names. The artists each have the list of roles
* they play in each band, keyed by 'artist:Band Name:Artist Name'.
* The band name, set of artists, and set of roles each artist plays
* populates the CouchDB documents. eg:
{
name:"Nirvana",
artists:[{
name: "Kurt Cobain",
roles:["Lead Vocals", "Guitar"]
},...]
}
*/
function populateBands() {
// First, create the couch database
const options = {
host: '127.0.0.1',
port: 5984,
method: 'PUT',
path: couchDBpath,
};
const req = http.request(options).end();
redisClient.keys('band:*', (error, bandKeys) => {
totalBands = bandKeys.length;
var
readBands = 0,
bandsBatch = [];
bandKeys.forEach((bandKey) => {
// substring of 'band:'.length gives us the band name
var bandName = bandKey.substring(5);
redisClient.smembers(bandKey, (error, artists) => {
// batch the Redis calls to get all artists' information at once
var roleBatch = [];
artists.forEach((artistName) => {
roleBatch.push([
'smembers',
'artist:' + bandName + ':' + artistName
]);
});
// batch up each band member to find the roles they play
redisClient
.multi(roleBatch)
.exec((err, roles) => {
var
i = 0,
artistDocs = [];
// build the artists sub-documents
artists.forEach( function(artistName) {
artistDocs.push({ name: artistName, role : roles[i++] });
});
// add this new band document to the batch to be executed later
bandsBatch.push({
_id: couchKeyify( bandName ),
name: bandName,
artists: artistDocs
});
// keep track of the total number of bands read
readBands += 1;
// upload batches of 50 values to couch, or the remaining values left
if (bandsBatch.length >= 50 || totalBands - readBands == 0) {
postDoc(
couchDBpath + '/_bulk_docs',
JSON.stringify({ docs : bandsBatch }),
bandsBatch.length
);
// empty out the batch array to be filled again
bandsBatch = [];
}
});
});
});
});
};
// expose couchKeyify function
exports.couchKeyify = couchKeyify;
// start populating bands if running as main script
if(!module.parent) {
populateBands();
}
Code language: JavaScript (javascript)
bands.js
/***
* Excerpted from "Seven Databases in Seven Weeks",
* published by The Pragmatic Bookshelf.
* Copyrights apply to this code. It may not be used to create training material,
* courses, books, articles, and the like. Contact us if you are in doubt.
* We make no guarantees that this code is fit for any purpose.
* Visit http://www.pragmaticprogrammer.com/titles/rwdata for more book information.
***/
var
port = 8080,
jsonHeader = {'Content-Type':'application/json'},
// standard libraries
http = require('http'),
redis = require('redis'),
bricks = require('bricks'),
mustache = require('mustache'),
fs = require('fs'),
// custom libraries
couchUtil = require('./populate_couch.js'),
neo4j = require('./neo4j_caching_client.js'),
// database clients
//couchClient = http.createClient(5984, 'localhost'),
neo4jClient = neo4j.createClient(),
redisClient = redis.createClient(6379);
var
gremlin = neo4jClient.runGremlin;
/**
* A convenience function for wrapping the
* reading of JSON reponse data chunks.
* @param response A Node HTTP response object.
* @param callback the function to populate and call on completion.
*/
function processBuffer( response, callback )
{
var buffer = '';
response.on('data', function(chunk) {
buffer += chunk;
});
response.on('end', function() {
if(buffer === '') buffer = 'null';
callback( JSON.parse(buffer) );
});
};
/*
* Post one or more documents into CouchDB.
* @param url is where we POST to.
* @param docString a stringified JSON document.
* @param count the number of documents being inserted.
*/
function getCouchDoc( path, httpResponse, callback )
{
var options = {
hsot: "127.0.0.1",
port: 5984,
method: 'GET',
path: path,
headers: {
jsonHeader
}
}
var request = http.request(options);
request.end();
request.on('response', function( response ) {
if( response.statusCode != 200 ) {
writeTemplate( httpResponse, '', { message: "Value not found" } );
} else {
processBuffer( response, function( couchObj ) {
callback( couchObj );
});
}
}).
on('error', function(e) {
console.log('postDoc Got error: ' + e.message);
});
};
/**
* Wraps a block of HTML with a standard template. HTML lives in template.html.
* @innerHtml populates the body of the template
*/
function htmlTemplate( innerHtml )
{
var file_data = fs.readFileSync( 'template.html', 'utf8' );
return file_data.replace("[[YIELD]]", innerHtml);
};
function writeTemplate( response, innerHtml, values )
{
response.write( mustache.render(htmlTemplate(innerHtml), values));
response.end();
}
// A Nodejs web app utility setup
appServer = new bricks.appserver();
// attach request plugin to easily extract params
appServer.addRoute("^/", appServer.plugins.request);
/*
* Just display a blank form if no band is given.
*/
appServer.addRoute("^/$", function(req, res) {
writeTemplate(res, '', { message: "Find a band" });
});
/*
* Accepts a band name and displays all artists in the band.
* Also displays a list of suggested bands where at least
* one artist has played at one time.
*/
appServer.addRoute("^/band$", function(req, res) {
var
bandName = req.param('name'),
bandNodePath = '/bands/' + couchUtil.couchKeyify( bandName ),
membersQuery = 'g.V[[name:"'+bandName+'"]]'
+ '.out("member").in("member").uniqueObject.name';
getCouchDoc( bandNodePath, res, function( couchObj ) {
gremlin( membersQuery, function(graphData) {
var artists = couchObj && couchObj['artists'];
var values = { band: bandName, artists: artists, bands: graphData };
var body = '<h2>{{band}} Band Members</h2>';
body += '<ul>{{#artists}}';
body += '<li><a href="/artist?name={{name}}">{{name}}</a></li>';
body += '{{/artists}}</ul>';
body += '<h3>You may also like</h3>';
body += '<ul>{{#bands}}';
body += '<li><a href="/band?name={{.}}">{{.}}</a></li>';
body += '{{/bands}}</ul>';
writeTemplate( res, body, values );
});
});
});
/*
* Accepts an artist name and displays band and role information
*/
appServer.addRoute("^/artist$", function(req, res) {
var
artistName = req.param('name'),
rolesQuery = 'g.V[[name:"'+artistName+'"]].out("plays").role.uniqueObject',
bandsQuery = 'g.V[[name:"'+artistName+'"]].in("member").name.uniqueObject';
gremlin( rolesQuery, function(roles) {
gremlin( bandsQuery, function(bands) {
var values = { artist: artistName, roles: roles, bands: bands };
var body = '<h3>{{artist}} Performs these Roles</h3>';
body += '<ul>{{#roles}}';
body += '<li>{{.}}</li>';
body += '{{/roles}}</ul>';
body += '<h3>Play in Bands</h3>';
body += '<ul>{{#bands}}';
body += '<li><a href="/band?name={{.}}">{{.}}</a></li>';
body += '{{/bands}}</ul>';
writeTemplate( res, body, values );
});
});
});
/*
* A band name search. Used for autocompletion.
*/
appServer.addRoute("^/search$", function(req, res) {
var query = req.param('term');
redisClient.keys("band-name:"+query+"*", function(error, keys) {
var bands = [];
keys.forEach(function(key){
bands.push(key.replace("band-name:", ''));
});
res.write( JSON.stringify(bands) );
res.end();
});
});
// catch all unknown routes with a 404
appServer.addRoute(".+", appServer.plugins.fourohfour);
appServer.addRoute(".+", appServer.plugins.loghandler, { section: "final" });
// start up the server
console.log("Starting Server on port " + port);
appServer.createServer().listen(port);
Code language: JavaScript (javascript)