Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Ruby on RailsとApache Solrで構築するドキュメント全文検索システムの開発 | サイバーエージェント 公式エンジニアブログ
はじめまして、アプリケーションエンジニアをしています、たかぎわ (@shun_tak) と申します。
2013年度4月入社予定の内定者で、現在はアルバイトとして週に2日間ほど勤務しております。

今回のエントリーでは、私が業務で開発した社内システムについて紹介しようと思います。


社内システム Tech Search を開発・リリースしました。

Tech Searchのリリースにより、社内に蓄積された知識資産の活用が大幅に改善されました。

Tech Searchとは、

弊社の研究レポート制度を利用して執筆されたテックレポートの全ファイル・全文章を横断的に検索するための社内システムで、Ruby on RailsApache Solrを利用して実現しました。全文検索だけでなく絞り込み検索も実装し、UIにも手を抜かず、ユーザーに使いやすいシステムを目指して開発しました。

Tech Searchがリリースされる以前、

テックレポートはネットワーク上に雑然と保存されているだけで、添付資料などの関連ファイルも含め数千のファイル群としてファイルサーバーの奥深い階層に眠っていました。レポートは提出年月>部署>執筆者というディレクトリ構成で保存され、ファイル名は「研究課題レポート2009.doc」や「report200909.pdf」などと書かれ、探そうにもどこに何があるか不明、ファイルを開いてみるまで内容も分からないという状況でした。 知識資産を活用しようにも不可能でした。

Tech Searchのリリースにより、

ただ検索するだけでほしい情報にアクセスできるようになりました。提出時期での絞り込みはもちろん、受賞レポートに限定して一覧表示することもできます。docファイルはすべてPDFに変換済みなので、ブラウザによってはダウンロードせずにブラウザ上で閲覧することも可能です。8月いっぱいはユーザーフィードバックを聞きながら、改善や新機能追加をしていく予定です。

※受賞したレポートに関しては社内向けに製本されているので (PDF版もあり)、優秀な作品は製本されたものから探すことはできます。ただし紙媒体なので (PDF版も募集回ごとに分かれている)、ほしい情報へのアクセスは簡単ではありません。

Tech Searchのシステム構成

まず図をご覧ください。


また、仮想サーバーのスペックは以下の通りです。


CPU仮想2コア 2.2GHz
メモリ4GB
ディスク容量70GB

処理の流れとしては、ブラウザから送信されたリクエストをApacheが受け取り、検索クエリがあればアプリからSolrに問い合わせ全文検索を実行します。その結果を受けてMySQLにある各種メタデータを取得し、結果一覧を生成しブラウザへレスポンスします。結果一覧のリンクをクリックすると、ファイルシステムのPDFを返します。

ApacheとPassengerはとりあえずで選んだのですが、結果的にはよい選択だったと思います。Passengerは軽量なWebアプリケーションに向いている[*1]といわれ、今回開発したアプリケーションのRailsが担当する処理が軽く、社内のエンジニア向けのシステムということで同時接続数も少なかったため、結果的に軽快に動作するアプリケーションとなりました。Railsは関係ないですが、PDFも静的ページの画面遷移並みに高速に表示されます。

フレームワーク選定のお話

私自身の経験としては、今まで関わったプロジェクトではPHP (Codeigniter, Symfony1系) やJavaを使って開発をしてきましたので、こういったフレームワークを選ぶのが自然だったと思います。ではなぜRuby on Railsで開発することになったのかといいますと、案件の自由度の高さが大きな要因だったと思います。今回の案件では混沌としたファイル群を整理して、全文検索するシステムをつくってくれと言われただけで、フレームワークやミドルウェアの選択、設計・開発について特に指示はありませんでした。それじゃNodeかRailsだろ!ということになりました。全然理由になってないですね。

一応Javaも検討したのですが、個人的にとにかく早くプロトタイプを作って方向性の確認をしたいと考えていたので、あまり慣れていないJavaフレームワークで設定に苦しむのは避けたかったのと、少しだけ経験があったRailsへの理解をもっと深めたいということで結局Railsを採用することになりました。本当は、使ってみたいからという理由で選択するのはよくないです。

ミドルウェアはDBにMySQLを、検索エンジンにApache Solrを採用しました。MySQLという選択は自然だと思いますが、検索エンジンのSolrは薦められて使ってみました。初めての利用でしたが、それでも短期間で簡単な設定はできるようになりました。


Solrで手軽に構築

Railsの便利な機能については、rakeとかScafoldingとかmigrationとかActiveRecordとかAssetsPipelineとかキャッシュ機能とか、ググればいくらでも出てくると思うので、ここではSolrに絞って書いてみようと思います。Railsについての情報が必要な方は、今週発売のWEB+DB誌 Vol. 70で特集されているのでそちらも合わせてご参照ください。

Solrとは?

Apache Solr入門によると、

Apache Solr (アパッチ ソーラーと読みます) はYonik Seeley氏によりCNET社向けに開発された検索エンジンサーバーです。2006年1月にApacheコミュニティにソースコードが寄贈されオープンソース・ソフトウェアとなって以来、その高い機能性・拡張性・利便性および性能などが話題となり、瞬く間にユーザは世界中に広がりました。~略~Solrの特徴を挙げると、以下のようになるでしょう。
  1. 実績のある全文検索ライブラリ Apache Lucene を用いた完成された検索エンジンサーバである
  2. 膨大な検索結果の中からユーザが求めているであろうドキュメントに適切に誘導するさまざまなしくみを備える
  3. さまざまなキャッシュのしくみによりI/Oレスで高速な検索が行える
  4. インデックスのレプリケーション機構を備える
  5. 分散検索により膨大な量の検索対象ドキュメントを扱える
  6. Apache Licence 2.0 の OSS であり機能拡張のためのインタフェースを備える
以下略。
といったように、Solr単体で検索エンジンとして利用することができます。「完成された検索エンジンサーバである」という特徴により、システムにすぐに導入することができました。

Solrのバージョン

執筆時点での最新版は3.6.1で4系もbetaが出ていますが、システム構築時の3.5.0を前提にします。また、参考文献としてApache Solr入門というオレンジ色の本があります。Solr 1.4をもとに書かれているので今と仕様が異なる部分もあるのですが、基本は同じなので参考になることは間違いありません。最新情報についてはApache Solr入門の著者の一人である大谷 純氏 (@johtani) のブログも参考になります。こちらも要チェックです。

Cellでドキュメントを読み込み、インデックスを作成する

さてSolrの使い方ですが、Solrで検索をするには当然ですがデータが必要です。今回開発したTech Searchの検索に必要なデータはWordまたはPDFです。このままでは扱えないので、プレーンテキストに出力してやる必要があります。Solrへのインデックス登録はXMLで流し込めるので、出力したプレーンテキスト群を一つのXMLにまとめてやります。処理をまとめると以下の図のようになります。

インデックス作成フロー


Word or PDFからXMLへの変換には、LuceneのTikaをSolrに取り込んだCellという機能を使って行います。curlでコマンドを叩けるので、shellを書いて全Word&PDFについて一気に変換します。たまに完全に文字化けしてしまうファイルもありましたが、日本語を扱うエンジニアならそこは頑張りましょう。

続いて生成したXML群をインデックス登録用の一つのXMLにまとめる作業に入ります。プレーンテキストに変換するステップを挟む必要はないのですが、MySQLに格納するデータをスクレイピングしたりするついでに作りました。この辺はスクリプトを書いて処理しました。

あとはSolrのschemaに合わせてadd用のXMLを生成したら、post.jarを使ってインデックス登録するだけです。ここも自作スクリプトですね。

RailsからSolrにクエリを投げる

簡単に動きます。まずGemfileにsolr-rubyを追加します。
gem 'solr-ruby'

続いてcontrollerを作成します。サンプルは以下です。
class SearchController < ApplicationController

  def initialize

    @solr = Solr::Connection.new('http://localhost:8983/solr', :autocommit => :on)

  end


  def index

    if params[:q] then

      data = submit(params)
      # dataを@変数に格納したり、描画に必要な処理をする

      render 'result'

    else

      render 'index'

    end

  end


  private


  def submit( p = {} )

    query = p[:q]

    fq = p[:fq]

    field = (p[:fl] ? p[:fl] : 'id,score')

    start = (p[:start] ? p[:start] : '0')

    rows = (p[:rows] ? p[:rows] : '10' )

    highlight = true

    hl_fl = "text"

    hl_pre = ""

    hl_post = "
"

    sort = (query == '*:*' ? 'date desc' : 'score desc')


    select = Solr::Request::Select.new(nil, { 'q' => query, 'fq' => fq, 'fl' => field, 'start' => start, 'rows' => rows, 'hl' => highlight, 'hl.fl' => hl_fl, 'hl.simple.pre' => hl_pre, 'hl.simple.post' => hl_post, 'sort' => sort } )

    return @solr.send(select).data

  end
end


受け取ったパラメータをsubmitに渡し、Solr::Request::Select.newでsolrのselectクエリを生成します。それをinitializeで生成したSolrコネクションによって送信します。
結果のdataはRubyのHashで返ってくるので、あとはご自由に使ってください。

社内非標準技術 Ruby on Rails で開発してみてわかったこと

以上、こんな感じでドキュメント全文検索ができるようになりました。

今回社内非標準のフレームワークを使って開発してみて、コードレビューできる人がいないという事実の重さを改めて実感しました。自ら進んで学ばなければ成長はあり得ませんし、コードに対する責任は自分がすべて負わなければなりません。


開発を終えて

今回このシステムの開発を任せてもらい、開発日数50日で、自ら開発スケジュール・工程を決め、システム設計からWeb Appフレームワークの選定、サーバーの構築・設定、DBの設計・構築、実装・コーディング、テスト、UI/UXの設計・実装、運用、ログの解析、改善・機能追加まで一つのシステムの中で一貫して体験できたのが大きな収穫でした。検索エンジンSolrも勉強して導入してみて、技術的にも大きな学びを得ることができました。

今回のシステム開発は自分一人だけで行いましたが、やはり組織に入るからにはチームでの開発をするのが楽しみです。
技術者として内定が決まっている同期とアプリ企画バトルというコンテストも行われるのですが、私の所属するチームでは企画に留まらずモックまでつくるつもりでいるので、まずはそこでチーム開発の腕試しです。がんばります!

それから、今後は自分の専門性をどこに定めるかについてさらに掘り下げて考えていきたいです。
PMやプロデューサとしてのサービス企画・開発にも興味がありますが、まずは技術者としての地盤を固めていこうと思います。その上で技術者としての専門性を身につけながら、社内の企画コンテストなどを通してチャンスを掴んでいきます。

相談に乗ってくださる方、募集中です!

参考資料

*1 WEB+DB PRESS Vol. 70
*2 Apache Solr 入門 - オープンソース全文検索エンジン

* * Again: this will **not** prevent inline script, e.g.: * . * * This workaround is possible because Safari supports the non-standard 'beforeload' event. * This allows us to trap the module and nomodule load. * * Note also that `nomodule` is supported in later versions of Safari - it's just 10.1 that * omits this attribute. * --> */ (function () { const check = document.createElement('script'); if (!('noModule' in check) && 'onbeforeload' in check) { let support = false; document.addEventListener( 'beforeload', (e) => { if (e.target === check) { support = true; } else if (!e.target.hasAttribute('nomodule') || !support) { return; } e.preventDefault(); }, true, ); check.type = 'module'; const blob = URL.createObjectURL( new window.Blob([], { type: 'text/javascript' }), ); check.src = blob; check.onload = () => { URL.revokeObjectURL(blob); }; document.head.appendChild(check); check.remove(); } })();