とあるサイトで全文検索を実装する必要があったので、技術メモを。
今回の環境は
・さくらインターネット(スタンダード)
・MySQL4.1(EUC)
・PHP5
・データは3万件強ほど
といったところ。
全文検索実装にあたり候補に挙げたプログラムは、
・Namazu
・Senna
・Hyper Estraier
・Googleなど外部のサイト内検索
・LIKE検索
・Ngram+FULLTEXT
といったところ。
順に。
■Namazu
PHPのモジュールがあり、さくらで実現可能だが触ったことがなく、Perlも苦手なため他の候補に。
■Senna
サーバのルート権限がないとキツいようなので他の候補に。
■Hyper Estraier
こちらやこちらやこちらなどを参考に形態素解析「mecab」また「chasen」とともに、何度かインストールを試みるも成功せず。
PHPモジュールが別途必要だったり、インデクスは作成されるが中身が空だったり、mecabインストール後にの「mecab.so」が見つからなかったり(・・・)、など、やはり敷居が高く、勉強する時間も足りないため他の候補に。
■Googleなど外部のサイト内検索
制作当初はこれを予定していた。
GoogleならばGoogle Ajax SearchのJSONファイルをPHPでパースし、それを検索用に整形する。
Yahooの場合もAPI的な使い方。
しかしこれでは検索件数が限られる(Gooleは1クエリが中途半端な8件・・・)し、そもそもインデクスされないと話にならないし、なによりサイト内検索という重要な部分を外部サイトに依存するのも・・・。
ということで他の候補に。
■LIKE検索
MySQLのLIKEで地味に検索・・・。
3万件といえど、これでは動作と付加が重く他の候補に。
■Ngram+FULLTEXT
これはこちらの記事が目からウロコで、最終手段として頭のスミに入れておいてよかった。
考えてみると、LIKE検索以外のこれまでの候補では、MySQLにあるデータのみを検索することが難しい。
データ以外のページもヒットしてしまうため、ヒットさせない処理も必要となり工数がさらに増えてしまう。
それもあって今回はこれしかないと。
ここでいうNgramというものがどういうことかは先述のリンク先の記事を見ていただくとして。
またしてもこれには大きな壁が立ちはだかった。
MySQLの既存のテーブルに「ngram」というカラムを用意し、FULLTEXTインデクスを適用。
そこにNgram方式で擬似形態素解析を施したテキストを全てのレコードに突っ込んだ。
検索してみると・・・出てこない。
どうやら文字コードが問題のよう。
MySQLはEUC、サイトはShift-JISで書かれており、NgramテキストはEUCで入っているので、検索語はShift-JISをEUCにエンコードし、さらにNgram化してみるもヒットしない。
ローカル環境(UTF-8)でやるとすんなり成功するのだが・・・。
散々悩んだ挙句、文字コードの垣根を越えるべくMySQL側(ngramカラム)と検索語を16進数へ変換してみると、なんとか成功。
ここまでくるのにどれだけかかったことやら・・・腕のなさを痛感。
しかし。
検索結果には大量のレコードが引っ張り出されてしまう。
機械的な検索だからしょうがないが、これでは自然な検索ができない。
Ngramにて分割された文字は、たとえば「東京都」なら「東 東京 京都 都」となる。
2文字ずつで分割しないと都道府県名でヒットし辛いのだが、これでは「東京」も「京都」もヒットしてしまう。
なんとかならないものか・・・。
Ngramでは機械的すぎて不向きなので、なんとか形態素解析したくも、mecabやchasenは前述「Hyper Estraier」で失敗済み。
そんな時にハッと思いついたのがYahoo!のAPI「日本語形態素解析Webサービス」。
Yahooさまさまです。
MySQL側とHTML側で文字コードが違うため、このAPIを使用して形態素解析を行い、さらに16進数に変換しMySQLへ格納。
恥ずかしいほどの力技だが、これでやっとそれらしい全文検索ができあがった。
少し詳しめに書くと手順は以下。
まずINSERT時に任意のカラムへ入れるテキストデータを全て1文に繋げ、UTF-8に変換&urlencodeしてYahoo API投げる。
↓
帰ってきたXMLをsimplexml関数でパースし、単語のみを抽出。
↓
単語をUTF-8→Shift-JISにmb_convert_encodingし、さらにunpack関数で16進数に変換し、スペース区切りでFULLTEXT適用カラムへINSERT。
これで準備はOK。
↓
検索時、検索語をまたUTF-8に変換&urlencodeしてYahoo! APIへ投げる。
↓
帰ってきたXMLをsimplexml関数でパースし、単語のみを抽出。
↓
単語をUTF-8→Shift-JISにmb_convert_encodingし、さらにunpack関数で16進数に変換。
↓
MATCH AGAINST構文でMySQLを検索
↓
レコードを表示。遂行完了。
かなりの紆余曲折&応急処置ではあるけど、なんとかそれなりのことができた。
しかし、やはり外部のサイトに依存するのは安全とは言えないため、引き続きいろいろと検討する必要はある。
また、このYahoo! APIは1日に50,000リクエストまでという制限があり、それも危険。
環境が限定されている中で理想の機能を実現することはこのように難しい場合がある。
本来は根本の設計段階でこういうものも想定しなければならない(自戒)。