mongooseの使い方まとめ

随時更新予定

目次


クエリ


動的query

  • ToDo

whereexec

var query;
if(screen_name) {
  query = query.where({ screen_name: screen_name });
} else if(user_id) {
  query = query.where({ user_id: user_id });
}
query.exec(function(err, data) {
  console.log(data);
});

要素数のカウント

query.countで要素数をカウントできる.limitとskipは無視される.

query.find(function(err, count) {
  console.log(count);
});

_id


uniqueで配列じゃなければなんでも使える

var User = new Schema({ _id: Number, screen_name: String });

埋め込みオブジェクトも使える

var Good = new Schema({ _id: { from: Number, to: Number }});

_idを明示的に定義しなかったら,ObjectIdという12byteの型で定義される. ObjectIdには4byteのタイムスタンプが含まれるのでcreated_atを定義する必要は無い.

var Id = new Schema({ text: String });
mongoose.model('Id', Id);
Id = mongoose.model('Id');
var id = new Id({ text: 'hoge' });
console.log(id._id);
console.log(id._id.getTimestamp());

結果

5073865056f6147bb1000001
Tue Oct 09 2012 11:05:04 GMT+0900 (JST)

node.js+MongoDBでtwitter apiを使うときに注意すること

こちらになんの変哲もないjsonがあります.

{"id":255113756093329409,"id_str":"255113756093329409"}

これをJSON.parseすると

{ id: 255113756093329400,  id_str: '255113756093329409' }

お分かりいただけるだろうか?不思議な力によりidの下1桁が9から0になってしまった. このidは先程自分が腹減ったとTwitterでつぶやいたもののstatus idなので最近のつぶやきを扱うには何らかの対策が必要である.

対策1: id_strを使う

Twitter APIは64bit整数が扱えない残念な言語の為にidとつくフィールド全てにid_strというidを文字列にしただけのフィールドを持っている. なのでよっぽどのことがない限りこちらを使えばよい.

対策2: MongoDBのNumberLongを使う

MongoDBではNumberLong('255113756093329409')と書けば64bit整数が扱える. sortしたい,コレクションが大量にあるのでidに20Byteも使うのは勿体無い等の場合こちらを使えばいいだろう.

みんなのアイドルmongooseでNumberLongを使いたい時は,mongoose-longというモジュールがある.

const mongoose = require('mongoose');
require('mongoose-long')(mongoose);
const Schema = mongoose.Schema;
var Status = new Schema({ id: Schema.Types.Long });
mongoose.model('Status', Status);
Status = mongoose.model('Status');
var status = new Status({ id: '255113756093329409' }); // Stringで渡す
console.log(status.id);
console.log(status.id.toString(10));

結果

{ _bsontype: 'Long', low_: 155332609, high_: 59398300 }
255113756093329409

しかしこのmongoose-longは不完全で検索,更新ができなかったりするので,mongooseのlib/schema/number.jsあたりとmongoose-longのlib/index.jsを参考にcastForQuery関数を追加したりする必要がある.

Expressの5行でできるCSRF対策

特殊なウィルス怖いですね.今後CSRF対策してないフォームを公開したらウイルス作成罪で逮捕されるかもしれません. NodeのWEBフレームワークExpress(が使ってるミドルウェアフレームワークのConnect)では数行追加するだけで,セッション毎の固定トークン方式でCSRF対策ができるので紹介する.

ソース デモ

基本的にはapp.use(express.session());app.use(app.router);の間に2つミドルウェアを追加し,ビューのformに隠し項目でトークンを追加するだけである.

// app.js
...
  app.use(express.session());
  app.use(express.csrf()); // 追加
  app.use(function(req, res, next) { // 追加
    res.locals._csrf = req.session._csrf;
    next();
  });
  app.use(app.router);
...

app.use(express.csrf());の部分はconnectのmiddleware/csrf.js参照. req.session._csrfにトークンが追加され,POSTメソッド等でtokenが一致してるかチェックされるようになる. res.locals._csrf = req.session._csrf;で_csrfをビューから参照できるようにしている.

後はビューのフォームにトークンを追加する.

// index.jade
...
  form(method='post')
    legend= 'CSRF対策済みフォーム'
    input(type='text', name='text')
    input(type='hidden', name='_csrf', value=_csrf) // 追加
    input(type='submit')
...

Redisで全文検索

台風一過で天気が良いのでnodeとRedisのSETを転置インデックスに使った全文検索を作った.
wikipediaのタイトル一覧(約100万件)をtri-gramで分解しRedisに突っ込んだ結果,約800万件の転置インデックスを挿入するのにおよそ3分,サイズは700MB程度だった.
同じようなことをmongodbでもできるが挿入速度ではRedisが有利だろう.

転置インデックス作成

const redis = require('redis');
const byline = require('byline');
const fs = require('fs');
const client = redis.createClient();

const ngram = function(str, n) {
  n = n ? n : 3;
  var grams = [];
  for(var i = 0; i <= str.length - n; i++) {
    grams.push(str.substr(i, n).toLowerCase());
  }
  return grams;
};

if(process.argv.length < 3) {
  process.stderr.write('argv error\n');
  process.exit(1);
}
var rs = fs.createReadStream(process.argv[2], { encoding: 'utf8' });
var ls = byline.createLineStream(rs)
var titles = [];
var iis = [];
var index = 0;
var ic = 0;
ls.on('data', function (data, line) {
  titles.push('key-' + index);
  titles.push(data);
  var grams = ngram(data); // Tri-gram
  for(var i = 0; i < grams.length; i++) {
     iis.push(['sadd', grams[i], index]);
  }
  index++;

  if(titles.length > 100000) {
    ls.pause();
    client.mset(titles, function(err, res) {
      ls.resume();
    });
    titles = [];
  }

  if(iis.length > 500000) {
    ic += iis.length;
    ls.pause();
    client.multi(iis).exec(function(err, res) {
      ls.resume();
    });
    iis = [];
  }
});

ls.on('end', function () {
  client.mset(titles, function(err, res) {
    client.multi(iis).exec(function(err, res) {
      ic += iis.length;
      console.log(ic);
      client.end();
    });
  });
});

検索

const redis = require('redis');
const client = redis.createClient();
const byline = require('byline');
const fs = require('fs');
const ngram = function(str, n) {
  n = n ? n : 3;
  var grams = [];
  for(var i = 0; i <= str.length - n; i++) {
    grams.push(str.substr(i, n).toLowerCase());
  }
  return grams;
};

const search = function(str, n) {
  var grams = ngram(str, n);
  var t = new Date;
  client.sinter(grams, function(err, res) {
    res = res.map(function(e) { return 'key-' + e; });
    client.mget(res, function(err, res) {
      console.log(res);
      console.log('----------------------------------------');
      console.log('' + res.length + 'results(' + (new Date - t) + 'ms)');
      client.end();
    });
  });
};

if(process.argv.length < 3) {
  process.stderr.write('argv error\n');
  process.exit(1);
}

search(process.argv[2]);