この記事は Redmine Advent Calendar jp: 2011 の16日目です。
前日は changeworldさんの「Redmine の plugin に手を出すならまずこれから」でした。
明日の担当は。。どなたか!!
急遽参戦を決めたので内容は薄っぺらいですが、良かったらお付き合い下さい。
コードが書きたかったので、プラグインを作成してみました。
Redmineに全文検索エンジンを呼び出すプラグインを組み込んで、リポジトリを検索してみようと思います。
全文検索のオープンソースプロダクトで、Javaで作成されたFessという物があります。
http://fess.sourceforge.jp/ja/
全文検索Apache Solrベースに、Webやファイル(xlsやpdf)、はたまたDB!!をクロールできる素晴らしいプロダクトです。
なにより日本人の方(id:shinsuke_sugaya)がメインコミッターをされていますので、ドキュメントやメーリングリストでのサポートもしっかりしています。
Fessを導入するとWebDavで公開したリポジトリをクロールして、全文検索することができるようになります。
これの何が嬉しいかというと、あらかじめクロールしているのでgrepより早い!
目的のリソースにすぐ辿りつけるので、リポジトリとの相性は抜群です。
Fessの構築
それではFessを取り敢えずクライアントに構築してみます。まずはFessをダウンロードします。
http://sourceforge.jp/projects/fess/releases/解凍します。
Unix環境の場合は*.shにchmodで実行権限つけて下さい。実行します。
bin/startup.sh(Unix系)bin/startup.bat(Win)
もう動いてます。
http://localhost:8080/fess/にアクセスするとTop画面が上がります。
簡単!!
リポジトリのクロール
次はインストールしたFessにリポジトリを食わせてみせます。管理画面にアクセス
http://localhost:8080/fess/adminウェブクロール画面を開く
クロール → ウェブ → 新規作成リポジトリのURLを入力
ここを参考にします。
※スレッド数と間隔は公開サーバに迷惑をかけない範囲で設定しないとサーバ管理者に怒られるかもです。
クロール開始
システム → システム設定 → クローラプロセス → 開始
しばらくするとクロールが完了します。
Top画面に戻り、キーワードを入力して検索すると結果が出てきます。

プラグイン作成
Fessは検索結果をXMLやJSONで取得できるRESTAPIを提供しています。本題のRedmineに全文検索システムを呼び出すプラグインを作成したいと思います。
作者の菅谷さんが書かれた記事を参考に、用意されたAPIに問い合わせて結果を描画します。
http://codezine.jp/article/detail/5667/?p=2
http://codezine.jp/article/detail/5667?p=3
と書きたい所ですが、時間がなくてただ移植しただけです。
RedmineなのにRESTではないとか、ソース開いて戻ると画面はリセットされてるとかのつっこみは泣きます。
かといって戻るためにPushStateでURL汚すとさらに怒られそう。。。
まずは雛形を生成します。
jruby -S script/generate redmine_plugin fessinit.rbを設定します。
require 'redmine'
Redmine::Plugin.register :redmine_fess do
name 'Redmine Fess plugin'
author 'Author name'
description 'This is a plugin for Redmine'
version '0.0.1'
url 'http://example.com/path/to/plugin'
author_url 'http://example.com/about'
project_module :fess do
permission :show_fess, :fess => [:index]
end
menu :project_menu, :fess, { :controller => :fess, :action => :index }, :caption => :label_fess, :param => :project_id
end
コントローラ(fess_controller.rb)を変更します。
今回は取り敢えずindexを見せるだけなので中身はありません。
# -*- coding: utf-8 -*-S
class FessController < ApplicationController
unloadable
menu_item :standard
before_filter :find_project, :authorize
def index
end
private
def find_project
begin
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
end
ここを参考にview(index.html.erb)を変更します。
divはバッティングしそうだったので元記事から変えました。
<h2><%=l(:label_fess)%></h2>
<div id="redmineFess">
<form id="searchForm">
<input id="searchQuery" type="text" name="query" size="30"/>
<input id="searchButton" type="submit" value="Search"/>
<input id="searchStart" type="hidden" name="start" value="0"/>
<input id="searchNum" type="hidden" name="num" value="20"/>
</form>
</div>
<div id="fesssubheader"></div>
<div id="fessresult"></div>
<%
baseurl = url_for(:controller => 'redmine_fess', :action => 'index', :id => @project) + '/../../..'
-%>
<% content_for :header_tags do %>
<%= stylesheet_link_tag(baseurl + "/plugin_assets/redmine_fess/stylesheets/fess.css") %>
<%= javascript_include_tag("http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js") %>
<%= javascript_include_tag(baseurl + "/plugin_assets/redmine_fess/javascripts/fess.js") %>
<% end %>
ここを参考にassets/javascripts/fess.jsを作成します。
Redmine標準のprototype.jsとバッティングしないようにしています。項番4で書き換えたIDの変更も行っています。
jQuery.noConflict();
jQuery(document).ready(function($){
$(function(){
// (1) Fess の URL
var baseUrl = "http://localhost:8080/fess/json?callback=?&query=";
// (2) 検索ボタンのjQueryオブジェクト
var $searchButton = $('#searchButton');
// (3) 検索処理関数
var doSearch = function(event){
// (4) 表示開始位置、表示件数の取得
var start = parseInt($('#searchStart').val()),
num = parseInt($('#searchNum').val());
// 表示開始位置のチェック
if(start < 0) {
start = 0;
}
// 表示件数のチェック
if(num < 1 || num > 100) {
num = 20;
}
// (5) 表示ページ情報の取得
switch(event.data.navi) {
case -1:
// 前のページの場合
start -= num;
break;
case 1:
// 次のページの場合
start += num;
break;
default:
case 0:
start = 0;
break;
}
// 検索フィールドの値をトリムして格納
var searchQuery = $.trim($('#searchQuery').val());
// (6) 検索フォームが空文字チェック
if(searchQuery.length != 0) {
var urlBuf = [];
// (7) 検索ボタンを無効にする
$searchButton.attr('disabled', true);
// (8) URL の構築
urlBuf.push(baseUrl, encodeURIComponent(searchQuery),
'&start=', start, '&num=', num);
// (9) 検索リクエスト送信
$.ajax({
url: urlBuf.join(""),
dataType: 'jsonp',
success: function(data) {
// 検索結果処理
var dataResponse = data.response;
// (10) ステータスチェック
if(dataResponse.status != 0) {
alert("検索中に問題が発生しました。管理者にご相談ください。");
return;
}
var $subheader = $('#fesssubheader'),
$result = $('#fessresult'),
recordCount = dataResponse.recordCount,
offset = 0,
buf = [];
if(recordCount == 0) { // (11) 検索結果がない場合
// サブヘッダー領域に出力
$subheader[0].innerHTML = "";
// 結果領域に出力
buf.push("<b>", dataResponse.query, "</b>に一致する情報は見つかりませんでした。");
$result[0].innerHTML = buf.join("");
} else { // (12) 検索にヒットした場合
var pageNumber = dataResponse.pageNumber,
pageSize = dataResponse.pageSize,
pageCount = dataResponse.pageCount,
startRange = (pageNumber - 1) * pageSize + 1,
endRange = pageNumber * pageSize,
i = 0,
max;
offset = startRange - 1;
// (13) サブヘッダーに出力
buf.push("<b>", dataResponse.query, "</b> の検索結果 ",
recordCount, " 件中 ", startRange, " - ",
endRange, " 件目 (", dataResponse.execTime,
" 秒)");
$subheader[0].innerHTML = buf.join("");
// 検索結果領域のクリア
$result.empty();
// (14) 検索結果の出力
var $resultBody = $("<ol/>");
var results = dataResponse.result;
for(i = 0, max = results.length; i < max; i++) {
buf = [];
buf.push('<li><h3 class="title">', '<a href="',
results[i].urlLink, '">', results[i].contentTitle,
'</a></h3><div class="body">', results[i].contentDescription,
'<br/><cite>', results[i].site, '</cite></div></li>');
$(buf.join("")).appendTo($resultBody);
}
$resultBody.appendTo($result);
// (15) ページ番号情報の出力
buf = [];
buf.push('<div id="pageInfo">', pageNumber, 'ページ目<br/>');
if(pageNumber > 1) {
// 前のページへのリンク
buf.push('<a id="prevPageLink" href="#"><<前ページへ</a> ');
}
if(pageNumber < pageCount) {
// 次のページへのリンク
buf.push('<a id="nextPageLink" href="#">次ページへ>></a>');
}
buf.push('</div>');
$(buf.join("")).appendTo($result);
}
// (16) ページ情報の更新
$('#searchStart').val(offset);
$('#searchNum').val(num);
// (17) ページ表示を上部に移動
$(document).scrollTop(0);
},
complete: function() {
// (18) 検索ボタンを有効にする
$searchButton.attr('disabled', false);
}
});
}
// (19) サブミットしないので false を返す
return false;
};
// (20) 検索入力欄でEnterキーが押されたときの処理
$('#searchForm').submit({navi:0}, doSearch);
// (21) 前ページリンクが押されたときの処理
$('#fessresult').delegate("#prevPageLink", "click", {navi:-1}, doSearch)
// (22) 次ページリンクが押されたときの処理
.delegate("#nextPageLink", "click", {navi:1}, doSearch);
});
});
ロケールの設定
取り敢えず(汗)ja.yml
ja:
label_fess: "リポジトリ検索"
button_search: "検索"
permission_show_fess: リポジトリ検索
en.yml
label_fess: "RepoSearch"
button_search: "Search"
permission_show_fess: "Use Repo Search"
動かしてみます
プロジェクトの設定でモジュールにチェックを入れてアクセス!!タブが出てる♪

後はタブを開いて

検索!

できました。
これだけでは面白くないので、Redmineのリポジトリブラウザで開いてシンタックスハイライトしてもらいましょう。
Fessの機能を使用すれば簡単に設定できます。
Fessの検索結果を消去
管理画面 → システム設定 → Solrアクション → セッションを選択 → 削除
URLマッピングを変える
管理画面 → クロール → パスマッピング → 新規作成クロールしたURLをRedmineのリポジトリブラウザに置き換え
EX):http://localhost/svn/redmine/redmine-1.2.1/ → http://localhost:8080/redmine-1.2.1/projects/test/repository/entry/

再度クロール
Redmineにもどり検索結果をクリックすると、見慣れたリポジトリブラウザが開きます。

そういえばRedmine@福岡全然活動してない。。