Solr を導入
Solr を導入しました。インストール先のサーバに合わせたディレクトリ構成です。
- Ubuntu 10.04.3 LTS
- aptパッケージを使わず、自前で Solr を落としています。多少古くてもいいなら solr-tomcat や solr-jetty などのパッケージの利用を検討すべきです。
- http://wiki.apache.org/solr/SolrInstall や http://wiki.apache.org/solr/SolrJetty を一読することをおすすめします。
このようなディレクトリ構成でインストールしていきます。
# ライブラリ本体(solr.war、jetty-*.jar、その他もろもろの jar 置き場所) /usr/local/lib/solr-4.6.0 # シンボリックリンク /usr/local/lib/solr -> /usr/local/lib/solr-4.6.0 # Solr Home ディレクトリ /var/solr # ログ(Solr Home からのデフォルト設定) /var/solr/logs # 各Solr コアのディレクトリ(Solr Home からのデフォルト設定) /var/solr/solr/CORE_NAME # コアごとのインデックスデータ(Solr Home からのデフォルト設定) /var/solr/solr/YOUR_CORE_DIRECTORY/data
最新の Solr を落とします。24 November 2013 に 4.6.0 がリリースされたようです。
$ su - $ cd /usr/local/lib $ wget http://ftp.kddilabs.jp/infosystems/apache/lucene/solr/4.6.0/solr-4.6.0.tgz -O - | tar zxvf - $ ln -s solr-4.6.0 solr
Solr の読み書き・実行は安全のため、root でなくに常に特定のユーザーにやらせます。ここでは www-data というユーザーを使います。
$ chown -R www-data:www-data solr solr-4.6.0
続いて、コアごとの設定ファイルを増やしていくためのディレクトリを用意します。
$ cd /var $ mkdir solr
Solr のサンプルの中から、最もベーシックな設定例をコピーして利用します。
$ cp -R /usr/local/lib/solr-4.6.0/example/solr /var/solr/ $ chown -R www-data:www-data /var/solr $ cd solr
コピーした設定では、collection1 というコアが 1個だけ宣言されています。まずはこれを起動してみます。
/var/solr/solr/collection1/conf/solrconfig.xml を編集し、ライブラリのパスを書き換えます。
75,76c75,76 < <lib dir="../../../contrib/extraction/lib" regex=".*\.jar" /> < <lib dir="../../../dist/" regex="solr-cell-\d.*\.jar" /> --- > <lib dir="${solr.solr.lib}/contrib/extraction/lib" regex=".*\.jar" /> > <lib dir="${solr.solr.lib}/dist/" regex="solr-cell-\d.*\.jar" /> 78,79c78,79 < <lib dir="../../../contrib/clustering/lib/" regex=".*\.jar" /> < <lib dir="../../../dist/" regex="solr-clustering-\d.*\.jar" /> --- > <lib dir="${solr.solr.lib}/contrib/clustering/lib/" regex=".*\.jar" /> > <lib dir="${solr.solr.lib}/dist/" regex="solr-clustering-\d.*\.jar" /> 81,82c81,82 < <lib dir="../../../contrib/langid/lib/" regex=".*\.jar" /> < <lib dir="../../../dist/" regex="solr-langid-\d.*\.jar" /> --- > <lib dir="${solr.solr.lib}/contrib/langid/lib/" regex=".*\.jar" /> > <lib dir="${solr.solr.lib}/dist/" regex="solr-langid-\d.*\.jar" /> 84,85c84,85 < <lib dir="../../../contrib/velocity/lib" regex=".*\.jar" /> < <lib dir="../../../dist/" regex="solr-velocity-\d.*\.jar" /> --- > <lib dir="${solr.solr.lib}/contrib/velocity/lib" regex=".*\.jar" /> > <lib dir="${solr.solr.lib}/dist/" regex="solr-velocity-\d.*\.jar" />
${solr.solr.lib} という変数は私が適当につけた変数で、あとで Jetty を起動するときに引数を与えることで値が入ります。
Jetty は tomcat のようなサーブレットコンテナの1つで、他のプロジェクトに埋め込みやすいのが特徴のようです。今回は Jetty も含め "Solr サーバ" として扱いたいと思います。
Solr をサービスとして起動するための service スクリプトを書きます。以下のスクリプトは http://stackoverflow.com/a/8189312/804314 を元に作りました。あくまで「Solr 付属の Jetty で Solr を起動する」という書き方になっています。適宜書き換えてご利用ください。
それでは起動します。
$ sudo service solr start
Webブラウザで http://localhost:8983/solr にアクセスし、管理画面を確認してください。
左下のプルダウンから、宣言されているコアを選択することができます。
次に、コアを増やしてみます。既存のコアをフルコピーし、data フォルダを削除してください。
$ cd /var/solr/solr $ cp -R collection1 yasai $ rm -rf yasai/data # 既存のデータが大きいと大変かも・・ $ vi core.properties # コアの名前を変更します。
コアの名前を書き換えます。
1c1 < name=collection1 --- > name=yasai
Solr を再起動します。
$ sudo service solr restart
新しいコア yasai が確認できます。
以上で、Solr でコアを増やしていく準備ができました。あとは使うアプリに従って、コアごとに主に schema.xml(スキーマ定義) 、 data-config.xml(データインポート時のカラム対応やSQLなど) をいじるだけです。
ところで、各コアのディレクトリ内に共通化できそうなファイルがたくさんあります。これを外に出す方法がわかりませんでした。もし知っている方がいたら教えてください。
sed でマッチした行だけ標準出力する
マッチした行だけを出力するためには -n オプションと p コマンドの両方が必要。
$ sed -n -e '/nobody/p' /etc/passwd nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
そもそも sed は、取り込んだ全てのデータを標準出力するデフォルト挙動がある。それを -n オプションによって抑制することができる。これとは別に、p コマンドはマッチした行を出力する。
参考: SoftwareDesign 2013年9月号
arch, gnome, terminal, vim, tmux, clipboard setup
arch 入れた。gnome 入れた。ターミナルで vim 使う。* にヤンクする。クリップボードにコピーされない。悲しい。
$ vim --version ... -clipboard -xterm_clipboard ...
コピーされそうな気配が無い。オプションを足して、コンパイルしなおす。
クリップボードオプション有りで Vim をコンパイルする
$ sudo abs extra/vim $ cd /var/abs/extra/vim # 私は /var/abs 配下で一般ユーザーに編集権限を持たせてます
abs 最高。tarball 触りたくない。
.. vim-build .. ./configure らへんにコンパイルオプションを追加する。
$ vi PKGBUILD ... --with-x \ # これを追記しました --with-gnome \ # これを追記しました
makepkg でコンパイル。-s は依存解決を含める。-i はそのままインストールの「イ」。
$ makepkg -s -i
古いバイナリをきれいさっぱり置き換えてくれる感じがすごくいい・・
$ vim --version ... +clipboard +xterm_clipboard
ヨッシャー。
ここまでで、vim の * レジスタが X のクリップボードと仲良くなってるはず。(vim の clipboard オプションに unnamed が必要)
tmux のコピーモードを Vi にし、コピーしたものをクリップボードにつっこむ
つづいて tmux で vim みたいなコピーができるようにもする。tmux は最近使い始めたターミナル系便利ツール。べんり。
要るもの。
$ sudo pacman -S tmux xclip
tmux が 1.8 以上であることを確認。でも arch を使っているとパッケージが古いと感じることが無い。debian とは違う。
$ tmux -V tmux 1.8
.tmux.conf を編集。
$ vi ~/.tmux.conf # emacs じゃなくて vi モードを使います set-window-option -g mode-keys vi # コピー開始を Vim のヴィジュアルモード開始みたいにします bind-key -t vi-copy 'v' begin-selection # コピー終了を Vim のヴィジュアルモードヤンクみたいにします ついでに内容を X のクリップボードに突っ込みます bind-key -t vi-copy 'y' copy-pipe "xclip -i -selection clipboard" # スペースキーのコピー開始を unbind します unbind -t vi-copy Space # お好みで。 # unbind -t vi-copy Enter
tmux で config をリロードする(PREFIX + :source-file ~/.tmux.conf)。 するとコピーモード開始後(PREFIX + [ )、画面をVim のように動き回れるではないか。/ で検索もできるし、v で選択開始、y でコピーもできるではないか。ヨッシャーもうこれ。
iTerm, Vim, Solarized, command-t
MacVim.app と Terminal.app を交互に切り替えて使っていたけど、iTerm を使うと全画面でターミナル、Vim を表示できるらしい。
- iTerm2 をインストール
- さて Vim を起動
- :colorscheme solarized
- 色がなんか変。iterm 側に色のプリセットを入れる必要があるらしい。
- iterm2-colors-solarized
- $ https://github.com/altercation/solarized/tree/master/iterm2-colors-solarized の2つ
- iTerm Preference > Profiles > Colors > Load Presets.. > Import で落とした2つのファイルをインポート
- どちらかのプリセットを選択する。色はOK。
- さて Vim
- $ which vim
Command-d で縦分割、Command-] でペイン移動できるようになった。"mouseless copy" とか便利そう。
参考:
マージャンのやつ JavaScript
あなたのスキルで飯は食えるか? 史上最大のコーディングスキル判定
http://www.itmedia.co.jp/enterprise/articles/1004/03/news002_2.html
※トータルで1週間以上かかっているorz
var assert = require('assert'); var test = function(results, answers) { if (results == null && answers == null) return; assert.equal(results.length, answers.length); answers.forEach(function(answer) { assert(~results.indexOf(answer)); }); }; var machi = (function() { var format = function(answer) { var s = ''; answer.grouped.sort().forEach(function(g) { s += '(' + g + ')'; }); return s + (answer.rest ? '[' + answer.rest + ']' : ''); }; var collectFlattens = function(input, enough) { var regexp = new RegExp('(.)\\1{' + (enough - 1) +'}', 'g'); return input.match(regexp); }; test(collectFlattens('112224588899', 2), ['11', '22', '88', '99']); test(collectFlattens('12345', 2), null); test(collectFlattens('1112224588899', 3), ['111', '222', '888']); test(collectFlattens('124589', 3), null); var collectAscendings = function(input, enough) { var results = []; var collected; var curr; var foundIndex; var isEnough = function() { return collected && collected.length >= enough; }; while (input.length >= 2) { collected = ''; curr = 0; foundIndex = -1; while (~(foundIndex = input.indexOf(+input[curr] + 1, curr + 1))) { if (!collected) collected = input[curr]; collected += input[foundIndex]; if (isEnough()) break; curr = foundIndex; } if (isEnough()) results.push(collected); input = input.slice(1); } return results.length >= 1 ? results : null; }; test(collectAscendings('1122335556799', 3), ['123', '123', '567', '567', '567']); test(collectAscendings('112255578', 3), null); var getRest = function(collected, original) { collected.split('').forEach(function(letter) { original = original.replace(letter, ''); }); return original; }; assert.equal(getRest('123', '1122335556799'), '1235556799'); assert.equal(getRest('567', '11225556799'), '11225599'); assert.equal(getRest('789', '1122555789'), '1122555'); var hasHead = function(grouped) { return grouped.some(function(group) { return group.length == 2; }); }; assert.equal(hasHead(['234', '555', '888']), false); assert.equal(hasHead(['234', '555', '88']), true); return function(original) { var answers = []; (function collect(grouped, input) { var finalizer = function(candidates) { if (!candidates) return; candidates.forEach(function(newGroup) { var newGrouped = grouped.slice(0); newGrouped.push(newGroup); var rest = getRest(newGroup, input); if (rest && ( collectFlattens(rest, 3) || collectAscendings(rest, 3))) { collect(newGrouped.slice(0), rest); } else if (rest.length <= 3) { var heads = collectFlattens(rest, 2); if (rest.length == 1 || (heads && heads.length >= 1) || collectAscendings(rest, 2)) { var a = format({ grouped: newGrouped, rest: rest }); if (!~answers.indexOf(a)) answers.push(a); } } }); }; if (!hasHead(grouped)) { finalizer(collectFlattens(input, 2)); } finalizer(collectFlattens(input, 3)); finalizer(collectAscendings(input, 3)); })([], original); return answers; }; })(); test(machi('1112224588899'), [ '(111)(222)(888)(99)[45]' ]); test(machi('1122335556799'), [ '(123)(123)(55)(567)[99]', '(123)(123)(555)(99)[67]', '(123)(123)(567)(99)[55]' ]); test(machi('1112223335559'), [ '(123)(123)(123)(555)[9]', '(111)(222)(333)(555)[9]' ]); test(machi('1223344888999'), [ '(123)(44)(888)(999)[23]', '(123)(234)(888)(999)[4]', '(234)(234)(888)(999)[1]' ]); test(machi('1112345678999'), [ '(11)(123)(456)(999)[78]', '(11)(123)(678)(999)[45]', '(11)(345)(678)(999)[12]', '(11)(123)(456)(789)[99]', '(111)(234)(567)(99)[89]', '(111)(234)(789)(99)[56]', '(111)(456)(789)(99)[23]', '(123)(456)(789)(99)[11]', '(111)(234)(567)(999)[8]', '(111)(234)(678)(999)[5]', '(111)(345)(678)(999)[2]' ]);
Ti ヘッダ 取りにくい
検索クエリ「Ti ヘッダ 取りにくい」の方にのみお送りしております。
私の場合 Titanium でHTTP 通信したくなった結果こういうことになりました。
気づいたことなど
- iPhone だと、getAllResponseHeaders できないらしい。(動きとドキュメント見た限り)だから、ヘッダは個別に取得する必要がある。
- iPhone シミュレータで、 req.getStatus() するとどうしてもエラーになる。req.status すると素直に取れた。ドキュメントの嘘つき・・。
- Titanium Studio についてる Ajax Monitor は、ブラウザの通信しか教えてくれない。Ti.Network.createHTTPClient でやるなら、WireShark とか使う必要がありあそう。
- いろいろとたまに動かなくなって、JavaScript がミスってるのか、不可解なTitanium のエラーなのかが分からなくなってきてしんどい。慣れてきたけど。
- 非同期管理モジュール q を使ってみた。node 系のモジュールがそのまま使えるからありがたい。
- 追記)cookie は勝手に管理してくれているようだ。
- 追記)Titanium Studio を開くたびに「アップデートがあるよ」と言われる。
// 使用例 var http = require('http'); // リクエストを送る http.post('http://localhost:3000/post', { title: 'タイトル', body: '本文' }) .then(function(res) { console.log(res.getStatus()); // 200 とか console.log(res.getJson()); // responseText の JSON.parse 結果 console.log(res.getHeader('Content-Type')); // 'application/json;charset=UTF8' }) .fail(function(reason) { console.log(reason); // 失敗した理由 });
http.js の中身
var Q = require('../libs/q'); var Response = require('./response'); var _ = require('../libs/underscore'); var URL = require('../libs/url'); var querystring = require('../libs/querystring'); /** * @param {string} url . * @param {Object} params . */ var buildUrlForGet = function(url, params) { url = URL.parse(url); url.query = _.extend(url.query, querystring.parse(params)); return url.toString(); }; /** * @param {string} method . * @param {string} url . * @param {Object} params . * @return {Object} Promise to response. */ var request = function(method, url, params) { var defer = Q.defer(), usePost = method === 'POST', req = Ti.Network.createHTTPClient(); if (!usePost && params) { url = buildUrlForGet(url, params); } if (usePost) { req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); } req.setAutoRedirect(false); req.onload = function() { defer.resolve(new Response(req)); }; req.onerror = function(reason) { defer.reject({ reason: reason }); }; req.open(method, url); req.send(usePost && params); return defer.promise; }; /** * @param {string} url . * @param {Object} query . * @return {Object} Promise to response. */ var get = function(url, query) { return request('GET', url, query); }; /** * @param {string} url . * @param {Object} body . * @return {Object} Promise to response. */ var post = function(url, body) { return request('POST', url, body); }; module.exports.get = get; module.exports.post = post;
更にクッキーを保存・送信する仕組みも必要だなー。大変だ。
追記)クッキーは自動的に保存しているようだった。2度目に作った HTTPClient には、1度目の responseHeader の Set-Cookie がちゃんと載っている。
WebDriverJs 〜果たされた約束〜
相変わらずSEO無視気味のうざいブログタイトルシリーズです。
WebDriverJs というのがあって、SeleniumのJavaScriptドライバです。npm で簡単に入れられます。
$ npm install selenium-webdriver
https://code.google.com/p/selenium/wiki/WebDriverJs
WebDriverJsは私の中で話題のPromiseを全面的にサポートしています。
それで、SeleniumはPromiseと非常に相性がいいと思うんです。
Seleniumはブラウザ自体の操作や状態を知るのは得意ですが、スクリプトで変化したページの状態を知るのは苦手な部分があります。例えばこれは得意です:ページ遷移後、ページが表示されたタイミングに、ページタイトルを確認してテストする。でも、私たちが実際にテストしたいのは、もっとこういうものです:ページ内のボタンをクリックしたら、XHR通信が発生し、サーバの健康状態にもよるが、2秒後ぐらいにレスポンスが返り、ページ内の複数のDOMが書き換えられるので、その一連の動作をテストしたい。
XHR後でかつレンダリングが終わったタイミングというのは、確かに知りにくいです。XHR通信の到着直後にテストをしてもいけない。レンダリングが終わった直後にテストをしてもいけない。他でもなく、テストしたい場所がテストできる状態になってからテストしたいのです。これは、ブラウザAPIでは検出しにくい情報です。もっと緩やかな(人間の視点に近い)条件でステップを進行できないものかな? と私はかれこれ中学生くらいの時からずっと考えていました。嘘です。
それで、方法がありました。Promiseを用いたSeleniumテストです。PromiseといえばJavaScriptです。JavaScriptは私が唯一書ける言語です。ラッキーです。
Promiseは、非同期テストに最適です。Promiseは処理の塊をステップとして扱い、それらを勝手に順序よく実行してくれます。ステップを外に出し、複数のテストでステップを共有することもできます。ステップをネストさせれば(たぶんWebDriverJsでもできると思うんだが)、よくある一連の操作を1つにまとめて使うこともできます。テストがスケールするかどうかは、それが増えて来た時に必ず重要になるでしょう。Promiseなら、理解し易い形で処理を増やして行くことができるでしょう。
例えば、こういう変なページがあったとしましょう。今回これをテストします。
タイトルは無視してください。作り直すのが面倒です。この画面をしばらく眺め、pushボタンをクリックしたくなったら(貴方はきっとなる)クリックします。すると2秒後に・・・
successと表示されます。わざと2秒もかかるようにしてみました。サーバのコードはこちらです。
// Node server app.get('/timeout', function(req, res) { setTimeout(function() { res.end(JSON.stringify({ success: 1 })); }, 2000); });
1. クリックする、2. success と表示される。それでは、この動作を実際にテストをしてみます。mochaしか使えませんのでmochaを使います。CoffeeScriptが書けますのでCoffeeScriptで書きます。
# CoffeeScript assert = require("assert") webdriver = require("selenium-webdriver") buildDriver = -> d = new webdriver.Builder().usingServer("http://localhost:4444/wd/hub").withCapabilities(browserName: "firefox").build() d.manage().window().setSize 350, 200 d describe "strange page", -> it "should display 'success' on click", (done) -> # DOMセレクタたち。 BUTTON = webdriver.By.id("b") BOX = webdriver.By.id("box") # :3000にアクセスする。 d = buildDriver() d.get "http://localhost:3000/" # クリックして、"success" と表示されるまで最大4秒まで待つ。 d.findElement(BUTTON).click() d.wait (-> # この条件式が何度も実行される d.findElement(BOX).getText().then (text) -> text is "success" ), 4000, "OMG" # 一応テストしてみる。 d.findElement(BOX).getText() .then((text) -> assert.equal text, "success") # 100%通るはず。 # どこかでエラーが起きたら、ここに来る。 .then(null, (reason) -> d.quit().then -> throw new Error(reason) ) # 問題なければテストを終了。 .then -> d.quit().then -> done()
WebDriverJs は.click()や.findElement() などの関数をコールすると、Promiseオブジェクトを返します。でもthenしていないのは、コールするだけで、Promiseがスケジュールに自動的に追加されて行くからです。つまり、d.then(function(){ return d.findElement(BOX).getText() }).then(...) とも書けますが、代わりに上記のようにも淡々とスケジュールを登録していくことができます。これは npm q には無い考え方でした。
重要なのは driver.wait(condition, ms, message) です。第1引数に、Booleanを返すFunctionを渡しましょう。すると、何度も何度もこの関数が呼ばれ、trueになるまで、ステップを待機する事ができます。つまり、任意の条件を作り出し、ページがその状態になるまでステップを止めることができます。今回の2秒かかるレスポンスのテストでは、condition関数が13回呼ばれていました。
会社の人が言っていましたが、この「何度も叩く」仕組みはテストフレームワークのJasmineの非同期テストと同じ仕組みのようです。合理的です。mochaには恐らく無く、mochaでは非同期後にユーザーが done() を実行することでテストが完了する仕組みを採用しています。それで十分だし、Promiseのresolveの考え方が好きなので私は気に入っています。
$ mocha ----------------------------- ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅✈ ----------------------------- 1 test complete (6 seconds) $
ビィィュュュォォァァァ・・・・。着陸成功です。ブラウザを立ち上げたりするのに時間がかかりましたが、Promiseのおかげで最短に近いスピードだと思います。
あとは特に書くこともないですが、mochaのデフォルトタイムアウトは2000ミリ秒なので mocha.opts に --timeout 10000 などと書いておきましょう。