プログラム

nroongaを使ってデータストアして全文検索するまで

昨日の続き。

繰り返しになるが、解決したい課題と現状は、

  • 数ギガサイズのアクセスログがある
  • 上記に対して数万パターンの文字列それぞれが何回出現するかを数え上げたい

という状況。なので、このログファイルをngroongaを介してgroongaのデータベースを新規作成し、そこに格納してみる。

もちろん事前にGroongaのインストールが必要なのでインストールする。Macであればbrewでインストールすることができる。

$ brew install groonga

で、次に、

  • Groonga DB作成
  • DBへデータを格納するためのテーブル作成
    • 今回はシンプルに1テーブルにbodyという1カラムを作ってそこにアクセスログ1行分を格納する
  • 上記のテーブル中のカラムを全文検索するための語彙表(インデックス)用のテーブル作成
  • テキストファイルベースのログファイルを格納

という上記の一通りの作業は以下のコードで実現できる。

var nroonga = require('nroonga'),
byline = require('byline'),
fs = require('fs');
var db = new nroonga.Database('db/accesslog');
db.commandSync('table_create Logs TABLE_NO_KEY');
db.commandSync('column_create Logs body --type Text');
db.commandSync('table_create', {'name':'LogIndexes', 'flags':'TABLE_PAT_KEY',  'key_type':'ShortText', 'default_tokenizer':'TokenBigram', 'normalizer':'NormalizerAuto'});
db.commandSync('column_create', {'table':'LogIndexes', 'name':'body_index', 'flags':'COLUMN_INDEX|WITH_POSITION', 'type':'Logs',  'source':'body'});
var stream = fs.createReadStream('access.log', { encoding: 'utf8' });
stream = byline.createStream(stream);
stream.on('data', function(line) {
var log = JSON.stringify([
{'body':line}
]);
db.commandSync("load", {'table':'Logs', 'values': log});
});
stream.on('finish', function(){
console.log("Finish parse log file.");
db.close();
});

上記の格納が完了した後*1、実際に格納した情報にたいして全文検索するには以下の様に select のクエリを発行する。limit:0 を指定することでGroongaでは件数のみを取得できるので、例えば今回格納したログファイル中に「Windows」という文字列が何回出現したかということを得たい場合は以下の様な形になる。

var nroonga = require('nroonga');
var db = new nroonga.Database('db/accesslog');
var res = db.commandSync('select', {'table': 'Logs', 'match_columns':'body', 'query': 'Windows', 'limit':0});
console.log(res);
//=> [ [ [ 1869923 ], [ [Object], [Object] ] ] ] 

かなり気楽に全文検索できる状態を作れる。最初スクリプトgrepツールで解決しようと思ったが少し時間がかかりすぎなところがあったので対象を絞ることを考えていたりしたが自分が解決したかったことは解決できそう。

例えばサービスの方針等々を検討する際の数値集めが簡単に行える様にするためにログ基盤を整える等が最も正しい解決策だとは思うのだが、ひとまず何かしらログが残されていれば手元にこういった環境を作って調べてみるのも一つのやり方かなと思う。

参照

GitHub - nroonga/nroonga: A library for building Groonga powered nodes

Groonga - カラムストア機能付き全文検索エンジン

*1:ちなみにGroongaのDBとして作られたファイルのサイズは投入したアクセスログのサイズが1.8Gbyteに対して、4.3Gbyteになった。

google-search-rankというnpmを作った

以前紹介した google-search_rank | RubyGems.org | your community gem host のインスパイア。

再発明感あるけどGoogleのCustom search APIを操作するgoogle-searchというクライアントを使って任意の条件で検索をかけて検索順位を取っている。

該当APIで取得した検索結果の情報と検索順位がコールバックされる。

引数で検索ワード, 対象ドメイン, 最大検索結果何ページ目までを走査するかを指定する。

var searchrank = require('google-search-rank');
searchrank.key = '(API Key)';
searchrank.cx  = '(Custom search engine ID)';
searchrank.find("安住紳一郎の日曜天国", "hideack.hatenablog.com", 10, function(rank, result){
console.log("RANK => " + rank);
console.log(result);
});

とすると

RANK => 70
{ kind: 'customsearch#result',
title: '「安住紳一郎の日曜天国」というラジオ番組を毎週聞いている話 ...',
htmlTitle: '「<b>安住紳一郎の日曜天国</b>」というラジオ番組を毎週聞いている話 ...',
link: 'http://hideack.hatenablog.com/entry/2016/04/20/225328',
displayLink: 'hideack.hatenablog.com',
snippet: '2016年4月20日 ... 安住紳一郎の日曜天国 (通称 "にちてん")という番組があって自分は大好きなので毎週\n日曜日の朝10時から聞いているのですが、twitterで まもなくTBSラジオで安住紳一郎の\n日曜天国のお時間。今日はインスタントラーメン研究家 大山即席斎 ...',
htmlSnippet: '2016年4月20日 <b>...</b> <b>安住紳一郎の日曜天国</b> (通称 &quot;にちてん&quot;)という番組があって自分は大好きなので毎週<br>\n日曜日の朝10時から聞いているのですが、twitter��� まもなくTBSラジオで<b>安住紳一郎の</b><br>\n<b>日曜天国</b>のお時間。今日はインスタントラーメン研究家 大山即席斎&nbsp;...',
cacheId: 'Nckg46Amc5IJ',
formattedUrl: 'hideack.hatenablog.com/entry/2016/04/20/225328',
htmlFormattedUrl: 'hideack.hatenablog.com/entry/2016/04/20/225328',
(snip)

といった具合。あぁ、前回調べた時より大分順位下がったな。などといったことが知ることができて便利。

www.npmjs.com

FIFOの作り方を考える

FIFO(First In, First Out) を利用したときにソフトウェアだとArrayにメソッドとして実装されていたり、またKVSに実装されていたりして、あまり中身意識をすることが無いかもしれないけど、定期的に振り返って実装を考えると頭の体操になって楽しい。といってもそんなに難しいものじゃない。

雑に書いてしまえば以下の様な形になる。*1

FIFOの深さを256(8bit)、Push/Popされる値の大きさを32bitで考えることにしてみる。型をきっちり指定したいのでGoで。

package main
import "fmt"
var pushAddr, popAddr uint8
var stack[256] int
func push(value int) {
stack[pushAddr] = value
pushAddr++
}
func pop() (popValue int){
if popAddr >= pushAddr {
panic("NO stack values.")
}
popValue = stack[popAddr]
popAddr++
return
}
func main() {
push(1)
push(2)
push(3)
fmt.Println(pop())
fmt.Println(pop())
}

上を実行してみると

$ go run fifo.go
1
2

最初にPushしたものが最初にPopされている。非常にシンプル。FIFOのどこのアドレスまでPushされているか管理する変数と、逆にどこまでPopされているかを管理する変数があればよいだけである。

こうすることでアドレスの加算だけで表現できる。上の例だと pushAddr は符号なし8bitの変数なので255までアドレスの値が到達すると更にpushされたときは0に戻るのでその点も気にすることがない。いくらか雑に書いているのでFIFOがあふれる処理とかを書いていないので厳密ではないがおおよそこういった中身になっているということを知って使うだけでも少し考え方が変わったりするかもしれない。

最近できるだけ原理的なところも知ろうとする様にしている。とはいっても、端折ってしまうことも正直多いのだけど概略だけでも抑えておく意味合いでも。

*1:一つの実装の考え方なので他の実装方法もあると思う

npmのtunnel-sshを利用してSSHポートフォワーディングする

SSHでポートフォワードするのをNode.jsだけで行うことを試してみたのでその際のメモ。

下の例では、local(127.0.0.1)の13306番にアクセスするとSSH接続先のホストから接続できる mysql***.heteml.jp の3306番にフォワードしている。

npmで公開されている tunnel-ssh を利用するとシンプルにできる。

$ npm install tunnel-ssh --save

SSH接続するための設定とポートフォワードする先の設定、ローカルで接続待ちをするポート番号を指定する。

// tunnel.js
var tunnel = require('tunnel-ssh');
var config = {
username: 'ssh-hideack',
port: 2222,
host: 'ssh***.heteml.jp',
privateKey:require('fs').readFileSync('/Users/hideack/.ssh/id_rsa'),
passphrase: '******',
dstPort: 3306,
dstHost: 'mysql***.heteml.jp',
localHost: 'localhost',
localPort: 13306
};
var server = tunnel(config, function (error, result) {
if (error) {
console.log(error);
process.exit();
}
console.log('connected');
// これ以降にMySQLクライアント等でlocalhostの13306番に接続する処理を書けばよい
});

というメモエントリでした。

greenkeeper.io を利用してプロジェクトが依存するnpmを最新の状態で保つ

プロジェクトで依存しているnpmを最新に保つのにgreenkeeperというサービスを見つけたので試してみました。

まずはcliツールをインストール。このサービスのカッコイイのがOAuthの部分を除いてコマンドラインで完了するというところ。

$ npm install -g greenkeeper

まずはgreenkeeperにログインします。コマンドラインでログインを指定。

$ greenkeeper login

そうするとGithubOAuth認証用にブラウザが立ち上がって認証を求められます。

f:id:hideack:20151002210341p:plain

その手続が済むとgreenkeeperを利用可能な状態になっているので、greenkeeperでpackage.jsonで管理されているnpmパッケージを最新に保ちたいプロジェクトのルートでgreenkeeperを有効にします。

☁  remp-api [master] greenkeeper enable
🌴  info enable The GitHub slug is: remp-team/remp-api

そうすると該当リポジトリ内にある package.json の監視が始まり、最新のnpmパッケージがあれば自動的にプルリクエストが作成されます。

f:id:hideack:20151002210518p:plain

github側で連携していれば当たり前ですが自動的にSlackに流れてきて, CI走って便利感ある。

f:id:hideack:20151002210650p:plain

例によってプライベートリポジトリだと課金が必要なのですがパブリックなリポジトリであれば無料で利用できる様なのでぜひお試しください。


greenkeeper

http://greenkeeper.io/

過去作成したloopbackアプリのloopback-component-explorerへの対応

過去に作成したloopbackアプリの場合、slcコマンドでscafoldすると loopback-explorer と呼ばれるloopbackで作成したAPICRUDするUIが含まれた形でアプリが作成されますが、今回この loopback-explorer に大きな変更が加えられて単純に npm update するとアプリの起動が行えなくなるのでその際の対応方法のメモです。

loopback(2.22.0)以降だとslcコマンドでscafoldされた際に新しいAPI explorerに対応しているので問題はないので、既にアプリを作っている人向けの内容です。

package.jsonの修正

これまで利用されていた loopback-explorer を削除。新しく同じ役割をするコンポーネントとして追加された loopback-component-explorer を追加します。

$ npm install loopback-component-explorer --save
$ npm uninstall loopback-explorer --save

修正後のpackage.jsonは以下の様な形になります。

  "dependencies": {
(snip)
"loopback-component-passport": "^1.5.0",
+   "loopback-component-explorer": "^2.1.0",
"loopback-connector-mongodb": "^1.13.0",
(snip)
},
- "optionalDependencies": {
-   "loopback-explorer": "^2.0.1"
- },

component-config.json を追加

loopback-component-explorer をlooopbackアプリにマウントさせてやるために server/component-config.json を新規に追加します。

explorer呼び出すパスをJSON内で記述します。

{
"loopback-component-explorer": {
"mountPath": "/explorer"
}
}

以上の操作で http://localhost:5000/explorer/ へアクセスすることで従来通りのAPI Explorerを利用することができます。

参照

StrongLoop | What’s New in the LoopBack Node.js Framework – July & August 2015


全く蛇足ですが、StrongloopのロゴにIBMのクレジットが入っていた。

slack-winstonでログをSlack通知する

Node.js でloopbackを使ってAPIを書くことをここ最近ずっとしていて、winstonでアプリケーションのログを取る様にする際にした際、ついでに一定レベル以上のログ(エラーログ)の場合は内容をSlackにも通知できないかなと調べると由ななwinstonのプラグインを見つけたので使ってみた。

github.com

お約束。npmコマンドでインストール。

$ npm install slack-winston --save

以下の様な形で利用できます。

var winston = require('winston');
var slackWinston = require('slack-winston').Slack;
var options = {
domain: 'remp',
token: '******',
channel: '#remp',
username: 'REMP API winston',
message: '[{{level}}] {{message}}',
level: 'warn'
}
winston.add(slackWinston, options)

なお、token の部分には、Slackの"Incoming WebHooks"の設定で得られるWebhook URLの末尾の文字列を渡します。

あとは、winstonでログ出力をする要領で記述を行うと

winston.warn("Winston test");

winstonのログは出力されつつ...

f:id:hideack:20150816114012p:plain

Slackの指定したチャンネルにも通知されます。

f:id:hideack:20150816114005p:plain

先の例に書いた通り、メッセージ内容や通知レベルも個別で指定できるので便利。

blanketでmochaで書いたテストのカバレッジを測定する

mochaで書いたテストのカバレッジ(網羅率)を取るにはどうしたらよいのかと思っていたところ、blanketというのを見つけたので試してみました。

早速、blanketとmochaでテストを走らせた際にカバレッジを表示させられる様にするためのレポーターを追加します。

$ npm install --save-dev blanket
$ npm install --save-dev mocha-spec-cov-alt

blanketの設定をpackage.jsonに追記します。data-cover-neverにはカバレッジ測定の対象としないディレクトリを明記します。ここではテストが入っているディレクトリとnpmのパッケージがインストールされているディレクトリを除外しています。

  "config": {
"blanket": {
"pattern": [
""
],
"data-cover-never": [
"node_modules",
"test"
]
}
}

更に npm test を実行する際の設定を以下の様に修正します。mochaを呼び出す際に以下の2つのオプションを追加します。

  • レポーターにmocha-spec-cov-altを指定 -R mocha-spec-cov-alt
  • 実行時にblanketを読み込む様に指定 --require blanket

修正例としては以下の様な形。

  "scripts": {
"test": "NODE_ENV=\"testing\" mocha -R mocha-spec-cov-alt --recursive --require blanket "
},

ここまででテストのカバレッジがmochaの実行結果として表示される様になります。*1

npm test を実行すると

f:id:hideack:20150801114446p:plain

といった形でテストの末尾にカバレッジが表示される様になります。

参考

blanketjs.org

github.com

*1:コメントに従ってscripts修正しました

Google docsのスプレッドシート上に記載されたURLのいいね数を取得する

Google docsネタ第2弾 エントリ。

仕事でGoogle docsにメモられたURLにつけられたfacebookのいいね数をとりたいなと思ったので、以下の様なスクリプトgoogle docsで作成して解決。

スプレッドシートの「ツールスクリプトエディタ」で空のプロジェクトを使って以下の様なスクリプトを書きます。

function fb(uri) {
var cache = CacheService.getPublicCache();
var cacheKey = "fb:like:" + uri;
var likeNum;
likeNum = cache.get(cacheKey);
if (likeNum == null) {
var apiUrl = "http://api.facebook.com/method/fql.query?format=json&query=select+total_count+from+link_stat+where+url%3D%22";
apiUrl += uri;
apiUrl += "%22";
var responseJson = UrlFetchApp.fetch(apiUrl);
var items = Utilities.jsonParse(responseJson.getContentText());
likeNum = items[0].total_count;
if (isFinite(likeNum)) {
cache.put(cacheKey, likeNum, 60 * 60 * 8);
}
}
return likeNum;
}

味噌は使っているのがGoogle docsということで、同じ社内のネットワークから複数の人が同時にスプレッドシートを開いて閲覧することは当然あるわけでそうするとそのたびにfacebookAPIを叩くことになってAPIのレスポンスを得られなくなるので CacheService を使ってキャッシュさせているところですね。

とセルに書いてあげることで、いいね数が返ってくる様になります。

とても便利なのでぜひお試しを。

loopback-testingを利用してloopbackで作成したAPIのテストを書く

loopbackでAPIのテストを書く際にloopback-testingを利用してAPIのテストを書いてみたのでその際のメモ。

まずは必要なnpmパッケージをloopbackアプリケーションに導入します。loopbackアプリのルートで以下のnpmコマンドを実行。

www.npmjs.com

$ npm install loopback-testing --save-dev
$ npm install chai --save-dev

これで準備完了。加えて npm test でテストを実行できる様に package.json 内のscriptsの定義を以下の様に書き換えます。*1

"scripts": {
"test": "NODE_ENV=\"testing\" ./node_modules/loopback-testing/node_modules/.bin/mocha -R spec --recursive"
}

上記の意図は npm test が実行された際に testing 環境としてnode_modules配下のmochaを利用してテストを実行します。

それにともなってtesting環境でloopbackアプリが実行された際のデータソース(DB接続先)を定義しておきます。Circle CI等のCI環境でもすぐに実行できる様に、memoryを指定します。

~/server/datasources.testing.js というファイルを用意して以下の様にデータの保存先をmemoryに指定します。

module.exports = {
db: {
connector: "memory",
defaultForType: "db"
}
};

これでテストが実行できる様になりました。expressの流儀と同様にプロジェクト直下の test ディレクトリに現在開発しているloopbackアプリケーション(API)のテストを書いてみます。

例えばユーザ登録(メールアドレスとPWをPOSTするとユーザ登録できる)のAPIのテストであれば、

var lt = require('loopback-testing');
var chai = require('chai');
var assert = chai.assert;
var app = require('../../server/server.js');
describe('/api/users', function() {
var newUser = {email: "tester1@remp.jp", password:"tester1remp"};
describe('ユーザ登録', function() {
lt.describe.whenCalledRemotely('POST', '/api/users', {}, function() {
it('ユーザ登録の際にメールアドレス、パスワードのパラメータは必須', function() {
var codes = this.res.body.error.details.codes;
assert.include(codes.password, 'presence');
assert.include(codes.email, 'presence');
assert.include(codes.email, 'format.null');
});
});
lt.describe.whenCalledRemotely('POST', '/api/users', newUser, function() {
it('メールアドレス, パスワードを与えるとユーザ作成できる', function() {
assert.equal(this.res.statusCode, 200);
});
});
});
});

といった具合に記述することができます。

ほぼコードを読んだままなのですが、 lt.describe.whenCalledRemotely の記述で指定のエントリポイント下へ指定のメソッド及びパラメータでアクセスがあった場合のテストを書くことができます。

また、アクセスするユーザにロールや状態をもたせた状態のテストも同様に記述ができ、例えばログインを行ったユーザによるAPIへのアクセスを想定したテストは

  var user = {
id: 2,
email: "tester2@remp.jp",
username: "tester2",
password: "tester2remp"
};
// (snip)
describe('ユーザによるプレイリスト操作', function() {
lt.describe.whenCalledRemotely('GET', '/api/users/2/playlists', function() {
it('未ログインユーザはユーザのプレイリストは取得できない', function() {
assert.equal(this.res.statusCode, 401);
});
});
lt.describe.whenCalledByUser(user, 'GET', '/api/users/2/playlists', function() {
it('ログインしたユーザ自身のプレイリストは取得できる', function() {
assert.equal(this.res.statusCode, 200);
});
});
});

の様に lt.describe.whenCalledByUser という記述で実現することができます。他にも各種ロールを割り当てた上での記述が行える様になっています。

こういった形でテストを用意した上で npm test で実行してみると...

といった具合にmochaによるテストが実行できています。

loopbackで作成したAPIであれば、loopback-testingを用いることでアクセスするユーザの各種ロールを想定したテストが記述できて便利ですというお話でした。

まとめ

  • loopbackアプリケーションでのテストを行うために loopback-testing を導入しました
  • npm test でテストを実行できる様にpackage.jsonの一部を修正しました
  • whenCalledRemotely, whenCalledByUser 等 loopback-testingを利用することで用意に各種ロールを想定したテストが書けました

あわせて読みたい

*1:loopbackのscafold(slcコマンド)で作成された場合は仮のpretestの定義が書いてあります