読者です 読者をやめる 読者になる 読者になる

CreateField Blog

オープンソースを使って個人でWebサービスを開発・運営していたブログ

JavaScriptでクライアントサイドだけで日本語PDF出力する

JavaScript

クライアントサイドでPDF出力できればサーバ負荷軽減できていいなぁとか考えることがあると思います。

そんなときは、 bpampuch/pdfmake · GitHub に日本語フォントを導入することにより 日本語でクライアントサイドだけでPDF出力することができます。

NotoSansを使おうと思ったのですが、otf形式でttfに変換してもうまく動きませんでしたので、以下の源真ゴシックのttfファイルを利用させてもらいました。

jikasei.me

ただ日本語のフォントは非常にサイズが大きく1つの太さの種類で5Mバイト以上あります。

これでは、クライアントサイドにやらして負荷を下げられればいいなという目論見よりも通信負荷の方が問題になってしまいます。

そこで、あまり使われない漢字等を省いてサブセット化して容量を減らします。 こちらを利用させてもらいました。

サブセットフォントメーカー

とりあえずNormalだけですが、2.2Mほどになりました。 もう少し減らせるかもしれません。

pdfmakeでこのフォントを使ってjsに変換します。

git clone https://github.com/bpampuch/pdfmake
cd pdfmake
npm install grunt grunt-text-replace grunt-browserify grunt-contrib-uglify grunt-dump-dir grunt-contrib-concat
mkdir example/font/bk
mv example/font/* example/font/bk
mv *ttf example/font
grunt dump_dir

Running "dump_dir:fonts" (dump_dir) task
File "build/vfs_fonts.js" created.

こちらのフォークではサブセットした源真ゴシックでjsを生成済みです。

naoa/pdfmake · GitHub

pdfmakeと生成されたbuild/vfs_fonts.jsを読みこませれば日本語でpdf出力することができるようになります。

  pdfMake.fonts = {
    GenShin: {
      normal: 'GenShinGothic-Normal-Sub.ttf',
      bold: 'GenShinGothic-Normal-Sub.ttf',
      italics: 'GenShinGothic-Normal-Sub.ttf',
      bolditalics: 'GenShinGothic-Normal-Sub.ttf'
    }
  }
  defaultStyle = 'GenShin'

  docDefinition = {
    content: '日本語テスト',
    defaultStyle: {
      font: defaultStyle
    }
  }
  pdfMake.createPdf(docDefinition).open()
  pdfMake.createPdf(docDefinition).download()

f:id:naoa_y:20151216052121p:plain

ちょっと前に調べたときには日本語できなくて残念だなぁと思ったことがあって、今回また調べたら実現できたので情報を残しておきます。

Groongaからword2vecを使って類似文書を取得してみる

word2vec Groonga

Groonga Advent Calendar 2015の13日目の記事です。

全文検索エンジンGroongaからword2vecを簡単に使えるプラグイン - CreateField Blog こちらで作ったプラグインのsentence_vectorsオプションを試してみました。

naoa/groonga-word2vec · GitHub

  • 学習ファイル生成
dump_to_train_file docs companies_[company:],categories_[category:],title/,body/@$ \
  --filter 'year >= 2010' --sentence_vectors 1

130万文書ぐらい

  • 学習
word2vec_train --min_count 1 --window 20 --cbow 0 --threads 12 --iter 10 --sentence_vectors 1
  • 元の文書
> select docs --filter "_id == 10876407" --limit 1 --offset 0
--n_sort 1 --output_columns app_id,title,company,body --output_pretty yes
[
  [0,1449906004.79365,0.00154924392700195],
  [
    [
      [1],
      [
        ["app_id","ShortText"],
        ["title","LongText"],
        ["company","company"],
        ["body","LongText"]
      ],
      [
        "JP20100000014",
        "画像形成装置、印刷データ生成装置、プログラム、及び印刷データ生成方法",
        [
          "コニカミノルタビジネステクノロジーズ株式会社"
        ],
        "複数の演算部を用いてPDLデータから印刷データを生成する際の処理速度を向上させる。
【解決手段】本発明に係る画像形成装置は、PDLデータを受信する受信部と、複数の演算部を有し、
前記複数の演算部のそれぞれがページ単位で処理を行うタスク又はバンド単位で処理を行うタスクを
実行することにより、前記PDLデータに基づいて印刷データを生成する画像処理部と、前記PDL
データを解析して印刷ページ数を取得し、当該取得された印刷ページ数に応じて、前記ページ単位で
処理を行うタスクと前記バンド単位で処理を行うタスクのそれぞれに割り当てる前記演算部の数を
動的に設定する制御部と、前記生成された印刷データに基づき印刷媒体に画像を形成する印刷部と、
を備える。【選択図】図5A"
      ]
    ]
  ]
]
  • 類似文書
> word2vec_distance "doc_id:10876407" --sentence_vectors 1
 --limit 10 --offset 0  --n_sort 10 
 --table ftext --column app_id,title,company,body --output_pretty yes
[
  [0,1449906234.40363,1.26499772071838],
  [
    [10],
    [
      ["app_id","ShortText"],
      ["title","LongText"],
      ["company","company"],
      ["body","LongText"]
    ],
    [
      "JP20100000012",
      "画像形成装置、印刷データ生成装置、プログラム、及び印刷データ生成方法",
      [
        "コニカミノルタビジネステクノロジーズ株式会社"
      ],
      "(57)【要約】【課題】複数の演算部を用いてPDLデータから印刷データを生成する場合に、
印刷対象となる個々のPDLデータに応じて最適な演算部の割り当てを行うことを可能とする。
【解決手段】本発明に係る画像形成装置は、PDLデータを受信する受信部と、複数の演算部を有し、
前記複数の演算部のそれぞれが前記PDLデータの言語解析処理又は前記言語解析処理後のラスタライズ
処理を実行することにより、前記PDLデータに基づいて印刷データを生成する画像処理部と、前記
PDLデータに基づいて前記ラスタライズ処理の重み値を算出し、当該算出された重み値に応じて、
前記言語解析処理と前記ラスタライズ処理のそれぞれに割り当てる前記演算部の数を動的に設定する
制御部と、前記生成された印刷データに基づき印刷媒体に画像を形成する印刷部と、を備える。
【選択図】図5A"
    ],
    [
      "JP20110129405",
      "画像形成装置、画像形成方法、及びコンピュータプログラム",
      [
        "キヤノン株式会社"
      ],
      "【要約】【課題】 1ページを複数のブロックに分割するための処理の負荷を分散させる。
【解決手段】 PDLデータから中間データを生成する処理と、中間データを画像データに変換する
処理とを、別々のハードウェア(CPU106、描画処理H/W109)で実現する。CPU106は、
複数のバンドに分割された中間データを生成する。描画処理H/W109は、1つのバンドを複数の
ブロックに分割し、各ブロックの画像データを生成する。【選択図】 図1"
    ],
    [
      "JP20100228837",
      "画像形成装置、画像処理方法、プログラム",
      [
        "株式会社リコー"
      ],
      "【要約】【課題】描画部の監視を必要としないで描画部への描画範囲の割り当てを動的に行う
ことができる画像形成装置及び画像処理方法を提供すること。【解決手段】外部から取得した印刷データ
の画像を転写媒体に印刷する画像形成装置150であって、印刷データを解析し、描画対象とされる
オブジェクトの描画命令をページ毎に記述した描画命令データを生成する描画命令生成手段12と、
描画命令データを記憶する描画命令データ記憶手段15と、描画命令データを読み出して、予め分割
されている描画範囲に対応づけられた描画命令を実行して描画処理する複数の描画処理手段302と、
第一の描画範囲の描画処理が終了した第一の描画処理手段から描画終了の通知を受けた第二の描画
処理手段が第二の描画範囲を分割して、第一の描画処理手段に第二の描画範囲の一部の描画を要求する
描画範囲制御手段303を有する。【選択図】図1"
    ],
    [
      "JP20120013853",
      "画像処理装置,画像形成装置及び画像処理方法",
      [
        "セイコーエプソン株式会社"
      ],
      "【要約】【課題】複数種類のフォーマットの画像データを所定形式の変換データに変換
する画像処理装置において、処理を効率的に行う。【解決手段】画像データの印刷要求を入力し、
入力された印刷要求で要求された印刷対象の画像データのフォーマットを判定し、判定された
フォーマットに応じて、変換処理に用いられるリソースを確保する処理でありフォーマット毎に
異なる所定のリソース確保処理としてのフォーマットA用リソース確保処理又はフォーマットB用
リソース確保処理を実行する(ステップS100〜S240)。続いて、判定されたフォーマット
に応じて、確保されたリソースを用いて印刷対象の画像データから変換データを作成する変換処理
を実行する(ステップS250)。そして、判定されたフォーマットに応じたリソースが既に確保
されているときには(ステップS130,S190でYES)、変換処理前のリソース確保処理を
省略する。【選択図】図2"
    ],
    [
      "JP20130191649",
      "画像形成装置、画像形成装置における画像処理方法及びプログラム",
      [
        "株式会社リコー"
      ],
      "【要約】【課題】  プリンタ描画の初期化処理を高速に行うことで、画像形成効率を向上
させる。【解決手段】画像形成装置であって、フレームメモリ211aに記憶したビットマップ
イメージを、当該ビットマップイメージを生成した印刷データに基づき消去する、それぞれクリア
処理部206の第1の消去手段及び前記フレームメモリ211aに記憶したビットマップイメージ
を全面消去する第2の消去手段と、第1または第2の消去手段によりフレームメモリ211aに
記憶されたビットマップイメージの消去に掛かる時間と、次に記憶するビットマップイメージの
生成に掛かる時間を比較する比較手段204aと、前記比較手段204aの比較結果に基づき、
当該生成されたビットマップイメージの消去手段として、第1または第2の消去手段のいずれかを
選択する選択手段204bを有する。【選択図】図2"
    ],
    [
      "JP20130192449",
      "印刷システム、印刷方法および印刷プログラム",
      [
        "コニカミノルタ株式会社"
      ],
      "【要約】【課題】印刷の全体処理時間を短縮できる印刷装置を提供する。【解決手段】
複数の描画用オブジェクトの画像データおよび印刷設定を含む印刷ジョブから、印刷ジョブに
属する各ページにおける前記描画用オブジェクト毎の配置位置情報および描画用オブジェクト
毎に必要な画像生成機能を解析して、解析結果情報を生成する解析部と、異なる画像生成機能
を有し、描画用オブジェクトの画像データから印刷用画像データを生成可能な複数の画像生成部と、
解析結果情報に基づいて、描画用オブジェクトの画像データおよび配置位置情報を、それぞれ
必要な画像生成機能を有する画像生成部に振り分ける振分部と、複数の画像生成部において
生成された複数の印刷用画像データおよび対応する配置位置情報を収集して、解析結果情報に
基づいて、ページ単位に印刷用画像データを合成して、ページ画像を生成する合成部と、ページ
画像を印刷する印刷部と、を有する。【選択図】図13"
    ],
    [
      "JP20120024523",
      "画像形成装置、その制御方法、及びプログラム",
      [
        "キヤノン株式会社"
      ],
      "【要約】【課題】複数ページの画像出力を行う際に、当該複数ページ間の出力形態に
応じた関連性をプレビュー表示する画像形成装置、その制御方法、及びプログラムを提供する。
【解決手段】本画像形成装置は、印刷データの設定情報から、ページ間における出力形態に
応じた所定の関連性を解析し、所定の関連性が特定されたページについては、当該所定の関連性
を有する複数のページをグループとしたプレビュー画像を生成し、所定の関連性が特定され
なかったページについては、当該ページのみのプレビュー画像を生成し、プレビュー画像を
ページ順に表示部に表示する。【選択図】図3"
    ],
    [
      "JP20110015400",
      "画像形成装置、画像形成装置の制御方法、プログラム",
      [
        "キヤノン株式会社"
      ],
      "【要約】【課題】 複数のレコードを含むVDPジョブにおいて有効な試し印刷が可能
な画像形成装置を提供する。【解決手段】 VDPジョブを処理するプリンタ103であって、
試し印刷ジョブであると判定されたVDPジョブに含まれる複数のレコードのうち一部の
レコードを試し印刷をプリンタ部407に行わせ、一部のレコードの印刷が行われた後に、
印刷されていないレコードの印刷指示を受け付け、受け付けた印刷指示に応じて、試し印刷
により印刷されていないレコードの印刷をプリンタ部407に行わせる制御部403と有する。
【選択図】 図5"
    ],
    [
      "JP20140008836",
      "印刷制御装置の制御方法、印刷制御装置の制御プログラム、および印刷制御装置の制御プログラムを記録したコンピューター読み取り可能な記録媒体",
      [
        "コニカミノルタ株式会社"
      ],
      "【要約】【課題】クライアントPCのユーザーが、編集対象のジョブを簡単に見つける
ことを可能にする印刷制御装置の制御方法を提供する。【解決手段】本発明の印刷制御装置の
制御方法は、印刷制御装置の記憶部に記憶されている一のジョブについて、ラスタライズ処理
を実行してジョブ識別用の画像データを生成するステップS107と、ジョブ識別用の画像
データが生成された後、記憶部に記憶されているジョブの中に、ジョブ識別用の画像データが
未生成のジョブがあるか否かを判断するステップS108と、ジョブ識別用の画像データが
未生成のジョブがあると判断される場合、ステップS107およびステップS108を繰り返す
一方で、ジョブ識別用の画像データが未生成のジョブがないと判断される場合、ジョブ識別用の
画像データが生成済みのジョブについて、ラスタライズ処理を実行してジョブ編集用の画像
データを生成するステップS111を有する。【選択図】図8"
    ],
    [
      "JP20110137220",
      "画像形成装置、画像形成装置の制御方法、及びプログラム",
      [
        "キヤノン株式会社"
      ],
      "【要約】【課題】PDLデータの複数ページを並列で処理する画像形成装置における
PDLデータの処理状況を適切にユーザに提示すること。【解決手段】PDLデータの複数
ページを並列で処理する画像形成装置101は、出力モードとして、印刷データの処理と
印刷出力を並行して実行する第1出力モード(RIP while Print)、又は、全ページについて
印刷データの処理が完了してから印刷出力を実行する第2出力モード(RIP then Print)の
設定入力をユーザから受け付けて、不揮発性メモリに保持しておく。そして、画像形成装置
101は、出力モードが第1出力モードの場合、PDLデータの処理が完了したページの中で、
連続出力可能ページ数として1ページ目から連続したページ番号の最大値を特定できる情報を
ユーザに提示する。また、出力モードが第2出力モードの場合、展開済ページ数としてPDL
データの処理が完了しているページの数を特定できる情報をユーザに提示する。【選択図】図1"
    ]
  ]
]

このプラグインでは元のテーブルとカラムを指定することにより、テーブルの内容を出力することができます。

  • 元の文書2
> select ftext --filter "_id == 10876408" --limit 1 --offset 0
--output_columns app_id,title,company,abstract --output_pretty yes
[
  [0,1449913923.39666,0.000921726226806641],
  [
    [
      [1],
      [
        ["app_id","ShortText"],
        ["title","LongText"],
        ["company","company"],
        ["abstract","LongText"]
      ],
      [
        "JP20100000015",
        "心室の拡張機能を改善するためのインビボ装置",
        [
          "コルアシスト カルジオヴァスキュラー リミテッド"
        ],
        "(57)【要約】【課題】生体の臓器または組織に内科的または外科的装置を
接続するために適した接続要素を提供すること。【解決手段】薄い織物のパッチの
形態のガードルを含み、そのガードルの側方の端部から延びた複数のタブが反対側
のものと対をなすように配置され、各タブはその反対側のものと結合することができ、
それによりループを形成し、該ループ内に、該臓器または組織と接続されることとな
る装置の一部分を挿入する接続要素。【選択図】図11"
      ]
    ]
  ]
]

*類似文書例2

> word2vec_distance "doc_id:10876408" --sentence_vectors 1 --limit 5 --offset 0
 --n_sort 5 --prefix_filter "doc_id:"
 --table ftext --column app_id,title,company,abstract --output_pretty yes
[
  [0,1449919740.36363,1.23700165748596],
  [
    [5],
    [
      ["app_id","ShortText"],
      ["title","LongText"],
      ["company","company"],
      ["abstract","LongText"]
    ],
    [
      "JP20150021968",
      "心臓病態を治療するための補助及びリコイル機能を備える二相性及び動的調整可能サポートデバイス及び方法",
      [
        "ザ  テキサス  エー  アンド  エム  ユニヴァーシティー  システム",
        "コーイノーヴァ  インコーポレイテッド"
      ],
      "【要約】【課題】心臓の成長及びリモデリングのための、回復及び自然に
潜在的リハビリとなる機械的環境を最適化するように設計された機械的配向デバイス
及び療法を提供する。【解決手段】鬱血性心不全及び関連する心臓病態に罹患している
患者に移植されるように適合した直接心臓接触デバイスであって、心室補助、心室
サポート及び拡張期リコイルを提供する、又は心室サポート及び拡張期リコイル
のみを提供する手段を有する心臓デバイスである。【選択図】図1"
    ],
    [
      "JP20130542174",
      "装着用ガイドルーメン",
      [
        "アビオメド  インコーポレイテッド",
        "タオ  ゼンホン",
        "ボーガン  ステファン",
        "モンゴー  マリー‐イブ"
      ],
      "【要約】心臓内ポンプデバイスの中を第一の開口部から第二の開口部まで
延在しているガイドワイヤ通路を有する該ポンプデバイスと;該ポンプデバイス
の外部に位置する第一の末端を起点に、ポンプデバイスの第一の開口部を通って
該ポンプデバイスの中に入り、ガイドワイヤ通路に沿って延び、第二の開口部を
通って該ポンプデバイスから出て、該ポンプデバイスの外部に位置する第二の
末端まで延在しているルーメンとを含む装置を開示する。ルーメンは、該ルーメン
の中をガイドワイヤが第一の末端から第二の末端まで通るときに該ガイドワイヤが
前記通路に沿って配置されるように、ガイドワイヤを収容すべく構成されている。"
    ],
    [
      "JP20120511994",
      "マルチルーメンカニューレ",
      [
        "ソラテック コーポレーション"
      ],
      "【要約】本出願は、血流を血液ポンプレシピエントに提供するための
方法及び材料に関する。例えば、哺乳動物の循環系に接続し、血液ポンプ(12)
(例えば、補助装置)と共に使用することができる、カニューレ(11)を提供
する。【選択図】図3"
    ],
    [
      "JP20140525200",
      "カウンターパルセーション及び血流導管の結合のための装置、方法、およびシステム",
      [
        "アビオメド  インコーポレイテッド",
        "クン  ボブ",
        "グラッツ  エリック",
        "ザイス  トーステン",
        "シュパニアー  ゲルト",
        "スペンス  ポール",
        "ダウリング  ロブ",
        "ヘイスティー  ケイトリン"
      ],
      "【要約】内腔の第一の部分を画定する第一の導管部分と、内腔の第二の
部分を画定する第二の導管部分とを含む、血流導管を開示する。第一または
第二の導管部分のうち少なくとも一方が先端部分を含み得、第一または第二の
導管部分のうちもう一方が、膨らんだ領域を含み得る。"
    ],
    [
      "JP20110123146",
      "カニューレおよび補助循環装置",
      [
        "国立大学法人 東京大学",
        "ニプロ株式会社"
      ],
      "【要約】【課題】血液の補助循環における循環効率の向上を図ることが
可能なカニューレおよび補助循環装置を提供する。【解決手段】先端31および
基端32,33を有する管状に構成され、先端31側には第1開口部11および
第2開口部21が設けられ、補助循環を行なうために先端31が心臓60に直接
穿刺された状態においては、第1開口部11は大動脈66内に位置し、第2開口部
21は心室64内に位置するカニューレ100は、第1開口部11を含み、
第1開口部31から基端32に向かって延在する第1内腔10と、第1開口部11
よりも基端33側の部分に形成された第2開口部21を含み、第2開口部21から
基端33に向かって第1内腔10と並んで延在する第2内腔20と、を備える。
【選択図】図6"
    ]
  ]
]

Patricia Trieを使ってdoc_id:のベクトルのみを距離演算していますが、130万件ほどの文書に対して1.3secぐらいかかっています。 もし使うなら、もうちょっと高速化したいところだなぁ。

結果はそこそこ分類されていますが、この結果だともうちょっと頑張らないとって感じかなぁ。

全文検索エンジンGroongaからword2vecを簡単に使えるプラグイン

word2vec 自然言語処理 Groonga

はじめに

Groonga Advent Calendar 2015の11日目の記事です。

GroongaはC/C++で書かれた高速な国産の全文検索エンジンです。

word2vecは、Googleが研究評価用に作った単語の特徴をベクトルで表現しニューラルネットモデルで教師なし学習をさせるツールです。

単語を文脈も考慮させたベクトルで表現しニューラルネットで学習することで、単語の意味的な足し引きができているんじゃないか( kingからman引いてwoman足したらqueenがでてきましたよとか。)ということで少し前に結構流行ったようです。

また、word2vecの作者が簡易的にsentenceのベクトル表現を使えるようにしたword2vecのコードもあるようです。

普段、Groongaを使ったそこそこの規模のデータベースをもっているので、Groongaに格納されたデータからword2vec/sentence2vecで簡単に学習させることができ、また、Groongaから単語ベクトルの演算ができるプラグインを作成しました*1

naoa/groonga-word2vec · GitHub

dump_to_train_fileコマンド Groongaのカラムから学習ファイル生成

Groongaのカラムからword2vecで学習できるように整形してファイルに出力します。

形態素解析やノーマライズ、記号削除などの前処理がオプション指定で行えるようになっています。

sentence_vectorsオプションを使うとdoc_id:とGroongaのテーブルの_idが紐付けられて出力されます。これにより後のベクトル演算時に元のテーブルと関連付ける事が可能です。

また、column名の後にカッコでカテゴリなど特定のカラムに格納されたタグ等に任意のラベルを付与できます。 たとえば、category:カテゴリAやcompany:会社名Aなどの形で学習させることで類似するカテゴリや会社のみを抽出することができます。

実行例

dump_to_train_file docs companies_[company:],categories_[category:],title/,body/@$ \
  --filter 'year >= 2010' --sentence_vectors 1

会社名とカテゴリは、スペースを_でつなげてフレーズ化してラベルをつけ、TitleとBodyは形態素解析、記号削除などをおこなっています。また--filterでテーブルの検索結果のみを出力することができます。

word2vec_trainコマンド word2vecコマンドを実行して学習

これはオプションを少しいじって実行パスにあるword2vecコマンドを実行するだけです。 このプラグインをインストールすると上記のsentence vector追記版のword2vecが自動的にbindirにインストールされます*2。 学習はGroongaを介して実行しなくても構いません。

word2vec_train --window 20 --cbow 0 --threads 12 --iter 10 --sentence_vectors 1

word2vec_distanceコマンド ベクトル距離を演算

word2vecで学習させたバイナリファイルを読み込み、単語ベクトルを演算します。初回のみバイナリファイル読み込んでをメモリに展開するため、少し時間がかかります。Groongaを常駐でサーバ実行している場合は一度読みこめば2度目からは素早く計算することができます。同時に複数のモデルファイルを読み込むことができます(20個まで)。

word2vecに付属しているdistanceコマンドをベースに以下を改良しています。

  • スペースと+/- で単語ベクトルの演算可
  • offset,limit,thresholdなどページ制御のためのオプション
  • sentence_vectorの場合、元のテーブルをひも付けてカラム出力、ソート
  • 高速化のため語彙表を配列ではなくPatricia Trieで保持

元々のdistance.cは語彙表から一致する単語を探すのに線形探索をしていますが、Patricia Trieを使うことによりマッチ時間の短縮が見込めます。

doc_id:や、category:など特定のラベル付けしたもののみを取得するような場合、Patricia Trieで非常に高速な前方一致検索が可能です。

以下の例では、2sec近くかかっていたのが0.01sec以下で類似カテゴリが取得可能となっています。

  • 改良前(全ワード+正規表現"company:.*"で絞込)
> word2vec_distance "company:apple" --is_phrase 1 --limit 5 --offset 0 \
  --n_sort 5 --white_term_filter "company:.*" --output_pretty yes
[
  [
    0,
    1449782162.00164,
    2.34969544410706
  ],
  [
    [
      5
    ],
    [
      [
        "_key",
        "ShortText"
      ],
      [
        "_value",
        "Float"
      ]
    ],
    [
      "company:google technology holdings",
      0.644274830818176
    ],
    [
      "company:research in motion",
      0.641874372959137
    ],
    [
      "company:htc",
      0.63908725976944
    ],
    [
      "company:lenovo (singapore) pte",
      0.63323575258255
    ],
    [
      "company:blackberry",
      0.622487127780914
    ]
  ]
]
  • 改良後(パトリシアトライで"company:"に前方一致)
> word2vec_distance "company:apple" --is_phrase 1 --limit 5 --offset 0 \
  --n_sort 5 --prefix_filter "company:" --output_pretty yes
[
  [
    0,
    1449781678.28364,
    0.00766372680664062
  ],
  [
    [
      5
    ],
    [
      [
        "_key",
        "ShortText"
      ],
      [
        "_value",
        "Float"
      ]
    ],
    [
      "company:google technology holdings",
      0.644274830818176
    ],
    [
      "company:research in motion",
      0.641874372959137
    ],
    [
      "company:htc",
      0.63908725976944
    ],
    [
      "company:lenovo (singapore) pte",
      0.63323575258255
    ],
    [
      "company:blackberry",
      0.622487127780914
    ]
  ]
]

このようにGroongaは全文検索だけでなくPatricia Trie、Double Array、HashのCライブラリとしても有用に使うことができます。今回のケースでは更新をしないので、Marisa Trieが使えたらより省メモリで構築できてよいかもしれません。

word2vec実行例

会社名のラベルをつけて類似会社名のみを取得してみます。

> word2vec_distance "company:トヨタ自動車株式会社" --limit 10 --offset 0 \
 --n_sort 10 --prefix_filter "company:" --output_pretty yes
[
  [
    0,
    1449815073.42663,
    0.138718605041504
  ],
  [
    [
      5
    ],
    [
      [
        "_key",
        "ShortText"
      ],
      [
        "_value",
        "Float"
      ]
    ],
    [
      "company:日産自動車株式会社",
      0.911168932914734
    ],
    [
      "company:三菱自動車工業株式会社",
      0.891977429389954
    ],
    [
      "company:富士重工業株式会社",
      0.870425820350647
    ],
    [
      "company:ダイハツ工業株式会社",
      0.858096361160278
    ],
    [
      "company:本田技研工業株式会社",
      0.839616179466248
    ]
  ]
]

類似しているぽい会社名が取得できました。

ベクトルの加減算も自由に行えます。

> word2vec_distance "company:トヨタ自動車株式会社 - company:日産自動車株式会社 + company:グーグル_インコーポレイテッド" \
--limit 5 --n_sort 5 --output_pretty yes
[
  [
    0,
    1449815137.37063,
    0.501566171646118
  ],
  [
    [
      5
    ],
    [
      [
        "_key",
        "ShortText"
      ],
      [
        "_value",
        "Float"
      ]
    ],
    [
      "company:グーグル__インコーポレイテッド",
      0.87317681312561
    ],
    [
      "company:グーグル・インコーポレーテッド",
      0.868086099624634
    ],
    [
      "company:ヤフー!_インコーポレイテッド",
      0.836208581924438
    ],
    [
      "company:アリババ・グループ・ホールディング・リミテッド",
      0.827649772167206
    ],
    [
      "company:マイクロソフト_コーポレーション",
      0.825306117534637
    ]
  ]
]

あ、表記ゆれが結構あるな。。

会社名の総数は単語の語彙数よりは数が少ないのでそこそこ高速に検索できています。最初にカテゴリーかなにかで分類すれば、結構高速に得られますね。

sentence2vec実行例

dump_to_train_file Entries title,tag,tags --sentence_vectors 1
word2vec_train --min_count 1 --cbow 1 --sentence_vectors 1
word2vec_distance "doc_id:2" --sentence_vectors 1 --table Entries --column _id,title,tag
[
  [
    0,
    0.0,
    0.0
  ],
  [
    [
      2
    ],
    [
      [
        "_id",
        "UInt32"
      ],
      [
        "title",
        "ShortText"
      ],
      [
        "tag",
        "Tags"
      ]
    ],
    [
      3,
      "Database",
      "Server"
    ],
    [
      1,
      "FulltextSearch",
      "Library"
    ]
  ]
]

doc_idを指定することにより(たぶん)類似する文書の取得でき、そのカラムを直接取得することができます。

実際の実行例のせようと思ってたのですが、min_countオプションを指定するのを忘れてdoc_idが除去されてしまいました。min_conutのデフォルトは5で出現数が5に満たない語彙は捨てられます。sentence vectorの場合は、--min_count 1にして除外されないようにしないといけませんね。実験結果はそのうち載せようと思います。

試してみました。

Groongaからword2vecを使って類似文書を取得してみる - CreateField Blog

おわりに

Groongaからword2vecを簡単に使うためのプラグインを紹介しました。 GroongaはMySQLでテーブル構築やデータ管理ができるMroongaや、PosrgreSQLからGroongaのインデックスを使うことができるPGroongaが開発されています。これらを使えば簡単にGroongaのテーブルを作ることができ、このプラグインを使えばword2vecを簡単に試すことができます。私はMroongaからこのプラグインを利用しています。GroongaやMySQLにデータがあって、わざわざ整形したりするのが面倒という場合はこのプラグインを使ってみてはいかがでしょうか。

word2vecと似たようなものでGloVeというのもあるみたいです。

この発表では編集距離ベースに誤記の表記揺れを抽出した例を紹介しましたが、word2vecをうまく使えば、略語や言い換えなど意味的な表記揺れもある程度取得可能かもしれませんね*3

*1:普通の人はgensimなどでPythonから使いたいでしょうが、なぜかGroonga first脳なので

*2:インストールしないことも可能

*3:ノイズも多いでしょうが

Groongaでのタグ検索と表記揺れとの戦い at Groonga Meatup 2015

Groonga Meatup 2015 - Groonga | Doorkeeper で発表してきました。

www.slideshare.net

英語のタグ検索での表記揺れをTrieで前方一致検索絞込、編集距離(Damerau–Levenshtein distance)、キーボード距離、DFを元に誤記を抽出して対応した話です。

naoa/groonga-term-similar · GitHub

naoa/groonga-tag-synonym · GitHub

ElasticsearchでもDamerauとprefixであいまい検索やっているみたい。 あいまい検索つくってみようかな〜

How to Use Fuzzy Searches in Elasticsearch | Elastic

MySQLでカラムごとに圧縮する方法

MySQL

MySQLでデータサイズが非常に大きいような場合、データを圧縮して格納したくなることがあります。

InnoDBではROW_FORMAT=compressedとすることで、テーブルを圧縮することができます。 MyISAMではmyisampackコマンドを利用することにより、テーブル全体を圧縮することができます。ただし、MyISAMでは読み取り専用となります。

通常、主キーやタイトル、メタデータなどのサイズは小さく、bodyなどのサイズが大きいことが多いと思います。そのため、テーブル全体ではなく、特定のカラムのみを圧縮するだけで事足りることが大半だと思います。

MySQLではCOMPRESS関数とUNCOMPRESS関数があります。

MySQL :: MySQL 5.6 Reference Manual :: 12.13 Encryption and Compression Functions

そこで、これとBLOB型のカラムを利用することによりカラム単位で圧縮することができます。

COMPRESS関数ではZLIB圧縮されるため、30%〜50%ぐらいになります。ただし、その分、伸長にかかるCPU負荷が増えるはずです*1

CREATE TABLE `comp` (
  `body` longblob NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT comp VALUES(COMPRESS("hoge hoge hoge hoge hoge hoge"));

SELECT UNCOMPRESS(body) FROM comp;
+-------------------------------+
| UNCOMPRESS(body)              |
+-------------------------------+
| hoge hoge hoge hoge hoge hoge |
+-------------------------------+
1 row in set (0.00 sec)

これで、アプリ側でCOMPRESSとUNCOMPRESSつけるのが若干めんどいですがカラム単位で圧縮することができます。

理想的にはアプリを改修することなく、圧縮・解凍できるようにMySQL側で自動でCOMPRESSとUNCOMPRESSができるようにしたいですね。

実現方法はQuery Rewrite Pluginぐらいしかないのかしら。なんかいい方法ないかなぁ。

追記

generated columnを使えば自動的にUNCOMPRESSはできるみたい。

MySQLでgenerated columnを使って圧縮したデータを自動的に解凍する - CreateField Blog

*1:具体的な負荷は検証してません。ところで、マニュアルではsuch as zlibっていってますが、他の圧縮ライブラリが使えるように実装されているんですかね?LZ4やsnappyなどの伸長速度優先のアルゴリズムが利用できれば、高速に伸長できて良さそうです。そのうち調べるかも、調べないかも

Deploy to HerokuボタンでGitベースのWiki gollumを無料で簡単に作れるようにした

GitHubのWikiが検索できなくて不便だなぁとか思ったりしてたら、このWikiはGitベースでできており、gollumというオープンソースで公開されていることを知りました。

github.com

そこで、ちょろっと試すためにHerokuで動くようにしてみました。 GitHubのトークンとHerokuのアカウントがあれば数分で個人用のWikiがつくれます。既存のGitHubのWikiを指定して開くこともできます。gollumには一応簡単な検索機能はあるようです。

Deploy

naoa/gollum-on-heroku · GitHub

  • デモ

Gollum on Heroku

追加機能

Herokuでの利用を想定して以下の機能を追加しています。

  • GitHubへの同期

gollumでは更新があるとローカルのgitリポジトリにcommitされます。そのため、Herokuではdynoが再起動されると更新が消えてしまいます。

そこでサーバ起動時にGitHubのリポジトリをcloneし、Wikiが更新されると自動的にGitHubにpushするようにしています。*1

  • Basic認証

gollumのフロントエンドはSinatraで非常にシンプルに実装されており、認証機能などはありません。環境変数で全ページもしくは編集機能のみをBasic認証を設定できるようにしています。

  • 複数のgitリポジトリを利用可

1つのgitリポジトリだけでなく、複数のgitリポジトリを指定して起動できるようにしています。

必要なもの

環境変数で以下を指定する必要があります。

  • GitHubのPersonal access tokens GITHUB_TOKEN

これを指定しておかないと、Heroku上で直接更新しても再起動時に消えてしまいます。*2 GitHubのsettings -> Personal access tokensから取得することができます。

  • リモートのgitリポジトリのURL GIT_REPO_URL_1 GIT_REPO_URL_2~

wikiによって生成されたmdファイルなどが保存されるgitリポジトリを指定します。 既存のGitHubの任意のリポジトリのwikiを直接指定することもできます。*3 複数指定することができます。複数指定した場合、リポジトリ名ごとにURLが割り当てられます。*4

  • 編集者のアカウントAUTHOR_NAME emailアドレス AUTHOR_EMAIL

git config user.name, user.emailとかで設定するやつを設定しておきます。 これがないとGitHubのコミット履歴がunknownになっちゃいます。*5

  • Basic認証用のアカウント BASIC_AUTH_USERNAME BASIC_AUTH_PASSWORD

認証が必要な場合。編集機能のみを保護したい場合はBASIC_AUTH_MANAGE_ONLYにtrueを設定します。

  • その他gollumの起動オプション GOLLUM_~

gollumの起動オプションを指定できます。詳細は以下参照。

https://github.com/gollum/gollum#configuration

おわりに

gollumは非常にシンプルでちょっとした個人用のwikiをさくっと使うためにはいいかもしれません。 またSintatraベースでカスタマイズしやすそうです。

共同で利用するには、OAUTH認証を追加したりsession['gollum.author']を設定したり、もう少し手を加える必要がありそうです。

*1:サイズがでかくなってくるとcloneに時間がかかるようになり、起動が遅くなるかも。

*2:ローカルなどで更新してDeployするだけであれば、なくてもいいかもしれません。

*3:GitHubのwikiってリポジトリ名に.wikiってつけてgit cloneなどすると直接取得できるんですね。初めて知りました。たとえば、https://github.com/naoa/test.wiki

*4:その場合、今のところルートはなにもありません。

*5:ちなみにGitHubのコミットログってアカウントの認証関係なく、適当にemailアドレス設定すると勝手に人の名前使えそうですね。

Railsで高速全文検索エンジンMroongaを使うためのチュートリアル

はじめに

MySQLでオープンソースの日本語対応の高速な全文検索エンジンGroongaが使えるMroongaを使って簡単に全文検索機能付きのRailsアプリケーションを作成する方法を紹介します。Railsのデモアプリケーションと実際に使えるサンプルの検索用のメソッドを使って具体的に説明します。

前準備

まず、RailsとMySQLとMroongaが使えるようにしてください。 これらはすでに情報がたくさんあると思うので、さほど苦なく用意できると思います。最近のMariaDBであればデフォルトでバンドルされていたりします。

簡単に試すことができるようにRubyとMySQLとMroongaが自動で環境構築されるVagrantファイルを用意しておきました。

naoa/start-mroonga-with-rails · GitHub

vagrantとubuntu/trusty64のboxとvagrant-omnibusあたりをあらかじめ用意しておき、以下のようにして仮想環境を構築してゲストOSにログインします。

% git clone https://github.com/naoa/start-mroonga-with-rails
% cd start-mroonga-with-rails
% vagrant up
% vagrant ssh

ちなみにRubyのビルドとかが入っているのでかなり時間がかかります。以下のコマンドを実行するとMySQLにMroongaが認識されていることがわかります。

% mysql -uroot -ppassword -e "SHOW ENGINES;"

Railsのインストール

bundlerを使ってRailsをインストールします。別にbundler管理下じゃなくてもいいです。すでにRails環境がある場合は不要です。

% cd /vagrant/
% bundle init
% Gemfile 
gem "rails" #コメントアウト除去
% bundle install

デモアプリケーションの作成

Railsチュートリアルの第2章にあるデモアプリケーションを作ります。

% bundle exec rails new demo_app -d mysql
% cd demo_app
% vi Gemfile
gem 'therubyracer', platforms: :ruby #コメントアウト除去
% bundle install

上記のVagrantfileではmysqlのrootユーザのパスワードをpasswordに設定しているので、config/database.yamlにパスワードを設定します。

% vi config/database.yml
password: password

チュートリアルの手順に沿って、UserとMicropostの簡単なCRUDアプリケーションを作ります。

% bundle exec rake db:create
% bundle exec rails generate scaffold User name:string email:string
% bundle exec rails generate scaffold Micropost content:string user_id:integer
% bundle exec rake db:migrate
% bundle exec rails s -b 0.0.0.0

これでブラウザでhttp://localhost:30000/microposts (上記のvagrantのやつの場合)にアクセスすることにより簡単なポスト機能のRailsアプリケーションが動作していることが確認できます。

このMicropostにMroongaを使った全文検索機能を追加してみます。

Migration関連の拡張Gemのインストール

MySQLのデフォルトでは、InnoDBというストレージエンジンが利用されます。

MySQLでストレージエンジンを指定するにはテーブルオプションにENGINE=を書きます。また、Mroongaでは、カラムとインデックスのコメントを利用することにより転置索引の見出し語をカスタマイズするためのトークナイザーや文字列を正規化するノーマライザーを変更することができます。

現状のActiveRecordではmigrationスクリプトのcreate_tableにテーブルオプションを書く機能はあるのですが、schema.rbにダンプする機能はありません。また、カラムコメントやインデックスコメントについても対応していません。

schema.rbにダンプできなくてもマイグレーションスクリプトによりSQL自体は実行できるのですが、rake db:resetのようなschema.rbを元にテーブルを復元するようなことはできなくなります。このため、SQLで直接テーブル定義を変更するような場合は、schema_format:sqlにする必要があります。この場合、schema.rbではなくstructure.sqlで管理することになります。

これらに対応するため、activereord-mysql-awesomeactiverecord-mysql-commentのgemをインストールします。両方ともRails4.2.0時点では正常に動作します。

% vi Gemfile
gem 'activerecord-mysql-awesome'
gem 'activerecord-mysql-comment'
% bundle install

なお、activerecord-mysql-commentactivereord-mysql-awesomeよりも後に書いてください(一部の追加アクセサが上書きされちゃうため)。

ストレージエンジン変更

micropostsのテーブルをMroongaストレージエンジンに変更します。Railsはテーブルオプションの変更に対応していないので、一旦、ロールバックで削除します。別にdrop_tableで削除してもかまいません。

% bundle exec rake db:rollback

マイグレーションスクリプトに以下のようにENGINE=Mroongaのテーブルオプションを追記します。

class CreateMicroposts < ActiveRecord::Migration
  def change
    create_table :microposts, options: 'ENGINE=Mroonga' do |t|
      t.string :content
      t.integer :user_id
      t.timestamps
    end
  end
end

これでマイグレートすれば、Mroongaストレージエンジンでmicropostsテーブルが作成されます。以下のコマンドでMySQLのテーブル定義を確認することができます。stringだと255文字と短いのでtextとかにしておいてもいいかもしれません。

% bundle exec rake db:migrate
% mysql -uroot -ppassword demo_app_development -e "SHOW CREATE TABLE microposts;"

+------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table      | Create Table                                                                                                                                                                                                                                                                                                                                                                                                   |
+------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| microposts | CREATE TABLE `microposts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=Mroonga AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci |
+------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

全文インデックスの追加

Mroongaストレージエンジンで高速な全文検索機能を使うには、以下のようにしてFULLTEXT INDEXを追加します。

% bundle exec rails g migration AddFullTextIndexToMicroposts
class AddFullTextIndexToMicroposts < ActiveRecord::Migration
  def change
    add_index :microposts, :content, type: :fulltext
  end
end

これでマイグレートすれば、全文インデックスが追加されます。

% bundle exec rake db:migrate
% mysql -uroot -ppassword demo_app_development -e "SHOW CREATE TABLE microposts;"
+------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

| Table      | Create Table                                                                                                                                                                                                                                                                                                                                                                                                   |

+------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| microposts | CREATE TABLE `microposts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  FULLTEXT KEY `index_microposts_on_content` (`content`)
) ENGINE=Mroonga AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci |
+------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

schema.rbでも以下のようにきちんとテーブルオプションとインデックスが出力されています。

  create_table "microposts", force: :cascade, options: "ENGINE=Mroonga DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci" do |t|
    t.string   "content",    limit: 255
    t.integer  "user_id",    limit: 4
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "microposts", ["content"], name: "index_microposts_on_content", type: :fulltext

これでデータベースの準備はできました。次はRailsアプリケーションに検索機能を追加します。

Viewに検索フォームの追加

microposts#indexに非常に簡単な検索フォームを追加します。app/views/microposts/index.html.erbに以下を追記します。

<h2>Search Microposts</h2>

<%= render 'search_form' %>

以下のapp/views/microposts/_search_form.html.erbを追加します。

<%= form_tag microposts_path, method: :get do |f| %>
  <div class="field">
    <%= text_field_tag :keyword %>
  </div>
  <div class="actions">
    <%= submit_tag "Search", :name => nil %>
  </div>
<% end %>

これでSearchボタンをクリックするとフォームの内容がparams[:keyword]としてmicropostsコントローラのindexアクションに渡されます。

コントローラの修正

デモアプリケーションでは、app/controllers/microposts_controller.rbのindexアクションは以下のようになっており、Micropostの全てを出力するようになっています。

  def index
    @microposts = Micropost.all
  end

これを検索フォームのパラメータにより全文検索するように変更します。

  def index
    query = params[:keyword]
    columns = "content"
    @microposts = Micropost.where("MATCH(#{columns}) AGAINST('#{query}' IN BOOLEAN MODE)")
  end

MroongaではWHERE句にMATCH([col_name1, col_name2…]) AGAINST('検索クエリ' IN BOOLEAN MODE)の構文で全文検索することができます。通常はIN BOOLEAN MODEをつけてください。デフォルトではIN NATURAL LANGUAGE MODEの自然文検索になります*1。だいたいの用途ではIN BOOLEAN MODEでいいはずです。

これで検索フォームの内容で全文検索した結果が得られます。いくつかデータをポストしてみて試してみると検索したレコードのみが取得できることがわかると思います。

なお、このままでは検索フォームに値なしの場合0件になっているので注意です。フォームに値なしの場合をハンドリングすべきです。また、詳細なロジックを書く場合、モデルに書いた方が良いでしょう。

モデル共通のスコープ化

MATCH … AGAINST … IN BOOLEAN MODEとか長いの毎回書きたくないと思うので、ActiveSupport::Concernの機能で全モデル共通のスコープ化させておきます。

  • app/models/concerns/mroonga.rb
module Mroonga
  extend ActiveSupport::Concern
  included do
    scope :mrn_search, ->(query, columns) do
      return if query.nil?
      where("MATCH(#{columns}) AGAINST('#{query}' IN BOOLEAN MODE)")
    end
  end
end

全文検索したいモデルでincludeします。

  • app/models/micropost.rb
class Micropost < ActiveRecord::Base
  include Mroonga
end

これでコントローラー側はシンプルになります。ちなみにこの例のスコープの第二引数のcolumnsは、Arrayじゃなくて”,”区切りのStringです。

  • app/controlers/microposts.rb
  def index
    query = params[:keyword]
    columns = "content"
    @microposts = Micropost.mrn_search(query, columns)
  end

scopeでチェインしてActiveRecord::Relationが返るので、通常のActiveRecordと同様にソートなどのメソッドチェインやkaminariなどのページングライブラリなどがそのまま利用可能です。

なお検索フォームには、単語だけでなく、Googleなどと同様にAND OR NOTのブール演算式で複数の単語の組み合わせで全文検索したりフレーズ検索したりすることができます。MroongaではAND+ORORNOT-の演算子をつけます。たとえば、今日の両方が含まれるものを検索する場合には今日 +雨と入力します。デフォルトでは演算子なしはORになっているので注意してください。ANDに変更するには、先頭に*D+というDプラグマをつけます。詳細はこちらを参照してください。

ここまでで基本的な全文検索の機能は使えると思います。

この後はいくつかのオプション機能や関数を使う方法を紹介します。

スニペット

Mroongaでは全文検索でヒットしたキーワードの周辺のスニペット(断片)を抽出するmroonga_snippet関数が提供されています。

http://mroonga.org/ja/docs/reference/udf/mroonga_snippet.html

ちょっとすぐに試す気にはなれない、非常に長いシンタックスですね。そこで検索クエリとカラム指定だけで自動で組んでくれるスコープ例を作っておきました。app/model/concern/mroonga.rbmrn_snippetmrn_extract_keywordsをコピペして使ってみてください。

https://github.com/naoa/start-mroonga-with-rails/blob/master/mroonga.rb#L34-L89

これでMicropostモデルにmrn_snippet(query, columns)をチェインさせるだけでそのカラム全文の代わりに検索ワードがタグで囲まれた150バイト分のスニペットが得られるようになります。

  • app/controllers/microposts_controller.rb
  def index
    query = params[:keyword]
    columns = "content"
    @microposts = Micropost.mrn_search(query, columns).mrn_snippet(query, columns)
  end
...<span class="keyword">今日</span>は雨だな...

上記のMySQLのmroonga_snippet関数は検索クエリではなく検索ワードごとにタグを指定する必要があるのですが、mrn_snippetスコープは検索クエリから自動でワードを正規表現で抽出するようにしています。なお、だいたいは動くと思いますがあまり厳密にはやっていないので気になるところがあれば、適宜修正してください。

これで、検索結果一覧で検索ワードのみを太字やハイライトさせてその周辺のみを表示させることができます。

f:id:naoa_y:20150225115014p:plain

他の検索機能

Mroongaではデフォルトの演算子を変更するDプラグマやカラムごとの重みを変更するWプラグマや複数のワードが近い距離で出現している文書のみを抽出する近傍検索演算子*Nなどがあります。GitHubにざっくりとこれらを使えるオプションを全部盛りしたスコープ例mrn_searchを作っておいたので、よければコピペして試してみてください。これもだいたい動くと思いますが、あまりテストしていないので不具合があれば、適宜修正してください。

たとえば、近傍検索だと以下のようにオプションを指定します。

  • app/controllers/microposts_controller.rb
@microposts = Micropost
  .mrn_search(query, columns, near: {distance: 5, words: ['今日', '']})

これで今日5の距離以内に出現する文書のみが検索されます。距離はデフォルトではほぼ文字数に相当します。MeCabなどの形態素解析のトークナイザーを使う場合は単語の数だけ離れた距離になります。

トークナイザー、ノーマライザーの変更

MroongaのデフォルトのトークナイザーTokenBigramでは、アルファベット記号数字は同一字種ひとまとまりにしてトークナイズされます。たとえばDatabaseという単語に対して、tabaという検索クエリではヒットしません。アルファベットや記号を文字単位でヒットさせたい場合はTokenBigramSplitSymbolAlpha等を使います。トークナイザーの種類はこちらを参照してください。

まず、現在のインデックスを削除します。

% bundle exec rails g migration RemoveFullTextIndexFromMicroposts
class RemoveFullTextIndexFromMicroposts < ActiveRecord::Migration
  def up
    remove_index "microposts", name: "index_microposts_on_content"
  end
  def down
    add_index "microposts", ["content"], name: "index_microposts_on_content", type: :fulltext
  end
end
% bundle exec rake db:migrate

トークナイザーを変更するにはインデックスコメントで指定します。

% bundle exec rails g migration AddSFullTextIndexWithParserToMicroposts
class AddSFullTextIndexWithParserToMicroposts < ActiveRecord::Migration
  def change
    add_index "microposts", ["content"], name:"index_microposts_on_content", type: :fulltext, comment: 'parser "TokenBigramSplitSymbolAlpha"'
  end
end
% bundle exec rake db:migrate

activerecord-mysql-commentによってschema.rbにも反映されています。

  create_table "microposts", force: :cascade, options: "ENGINE=Mroonga DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci" do |t|
    t.string   "content",    limit: 255
    t.integer  "user_id",    limit: 4
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "microposts", ["content"], name: "index_microposts_on_content", type: :fulltext, comment: "parser \"TokenBigramSplitSymbolAlpha\""

これで、Databaseという単語に対してtabaという検索クエリでもヒットさせることができるようになります。

念のため、以下のコマンドでGroonga側で本当にトークナイザーが認識できているか確認できます。これはmroonga_commandというMroongaの拡張関数を使ってGroonga側のコマンドを発行しています。

 mysql -uroot -ppassword demo_app_development -e "select mroonga_command('table_list');"
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| mroonga_command('table_list')                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| [[["id","UInt32"],["name","ShortText"],["path","ShortText"],["flags","ShortText"],["domain","ShortText"],["range","ShortText"],["default_tokenizer","ShortText"],["normalizer","ShortText"]],[259,"microposts","demo_app_development.mrn.0000103","TABLE_PAT_KEY|PERSISTENT","Int32",null,null,null],[265,"microposts-index_microposts_on_content","demo_app_development.mrn.0000109","TABLE_PAT_KEY|PERSISTENT","ShortText",null,"TokenBigramSplitSymbolAlpha","NormalizerMySQLUnicodeCI"]] |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

この他のMroongaのオプションの指定方法の詳細は以下を参照ください。Mroongaでコメント句で指定するものをそのままマイグレーションのコメントプションにかけば良いだけです。

http://blog.createfield.com/entry/2014/10/29/084941

https://github.com/naoa/activerecord-mysql-comment

RailsでGroongaを使う他の方法

RailsでGroongaを使う他の方法として、GroongaのRubyバインディングであるRroongaやRroongaをActiveRecordライクに利用できるActiveGroongaを利用する方法があります。

これらは、SQLiteと同様にライブラリとして全文検索の機能を提供しますが、サーバの機能は提供しないため、Railsと同じサーバに全文検索機能をのせる必要があります。

サーバを立てなくて良いというメリットでもあるので、大量アクセスをさばく必要がないようなRailsアプリケーションの場合はこちらの利用を検討してみはいかがでしょうか。

おわりに

上記のように、Mroongaを使えば、Railsアプリケーションに簡単に全文検索機能を追加することができます。MySQLなのでActiveRecord用の資産はほぼ使えますし、既存のコードを書き換える必要はほとんどありません。

MySQLでRailsを使っていて高速な日本語対応の全文検索機能が欲しいと思ったらMroongaを使ってみてはいかがでしょうか。

また、最近、PostgreSQLの拡張機能としてPGroongaが開発されています。こちらは少し構文が変わりますが、上記と同様にSQLで管理できます。そのうち、ActiveRecordで足りない部分を補足したりチュートリアルを書くかも?書かないかも?

HerokuでPGroongaが使えるようになるといいな!

*1:MySQLのデフォルト。