スタッフ、会場提供のVoyage Groupさま、参加者のみなさま、お疲れ様でした。スライドのビュー数も一晩で2300超えと、今までSlideshareに上げたスライドの中では一番伸びがいい気がします。PyCon JPの時の発表を日本語で再演するのでいいから出てください、と言われてたのですが、末永さんの検索本もちょうど直後に出てて説明省けたし、転置インデックスの検索の説明はざっと削って、あらたに進捗をちょびっと足しました。
SphinxCon記念で、Sphinxユーザに便利かもしれないリポジトリ作ったよ。 https://t.co/XnvZy5OBZG #sphinxjp
— 渋川よしき (@shibu_jp) October 23, 2014
イベントの前に、こんなリポジトリをでっち上げました。Markdown形式でOSSライセンス文を集めたリポジトリがあったので、手でフォーマットを直しました。Sphinxで何かしらのツールのドキュメントを書くときは、この中のファイルをコピーして入れるとお手軽ですね。LICENSE.rstという名前で入れておいて、index.rstに .. include:: LICENSE.rst
を入れておくのが僕のマイブームです。後から考えれば、PanDoc使えばよかった!
パフォーマンスチューニング
Webフロントエンドに従事するお前らはいい加減高頻度イベントとレイアウトとスタイリングの付き合い方を考えろ [JavaScript] on @Qiita http://t.co/OFQG1nz2Ih
— ダメレオン (@damele0n) October 24, 2014
怒りにまかせて書きました。ありがとうございました。
— ダメレオン (@damele0n) October 24, 2014
とりあえず、今回はSphinxとつなぐところまでトライすればいいかな、と思って組み込みをやってみたのですが、Sphinxのドキュメントをビルドしてみると30分間CPU使用率100%のまま帰ってこなくなるというのがわかったのが発表前日の夜。職場のとなりの席の人が荒ぶっている中、パフォーマンスがダメダメな状態を放置しておくと燃やされてしまうかもしれないので、清水川さんやPython文法詳解で有名な石本さんことぁっぉさんやaodag氏からオンラインでアドバイスをもらいつつがんばってパフォーマンスを改善しました。
会場に向かう副都心線の中で時間計測コード(time.time()で時間を取る)を埋め込んで分かった即座に見つけたボトルネックが、投入された全文字列を結合するstr = str + substr
というコード。文字列だったら全部配列にいれてjoinするところですが、Oktaviaでは内部は文字コードを振りなおして、使用する文字のビット数を削減するというデータ容量の最適化を行っているため、strという名前でも実は数値の配列です (rawmode=False
を付与すれば文字コード再割当てはしないで単なる文字列になる)。とりあえずlist1 += list2
にしたら1400倍速くなりました。なにこれ。Pythonむずい。このコードはPyPyよりもCPythonの方が早いみたい。
次に遅い箇所も、適当な時間計測コード特定できたのですが、詳細がつかめなかったのでプロファイラ(cProfile.runctx()
)を部分適用して見てみたところ、Python 2.7でxrange
ではなくてrange
が使われてしまっていることが分かりました。_xrange = getattr(__builtins__, 'xrange', range)
というコードで、xrange
があればそっちを、なければrangeを取ってくるというPython 2/3両対応コードにしていて、PyPyは正しく動いていましたが、CPythonではNG。テスト的なスクリプトを書いてみてわかったのは、インタプリタに直接渡されるスクリプトではxrange
が取れるのに、そこからimportで呼ばれる子供のモジュールではrange
になってしまうことが分かりました。なにこれ。Pythonむずい。最終的にはsys.version_info
でバージョン番号を取ってきて2ならxrange
、3ならrange
というコードにしたら動きました。
最後の大物のボトルネックがBitVector._rank32
。この内部ではビット演算を多様しまくっているのですが、石本さん情報によるとPythonのビット演算は「C言語風になるようにがんばってエミュレートしている」状態で、パフォーマンス自体は良くない模様。しかたなく、お昼ごはん食べつつ、今までやったことなかったんですがC拡張の作成にトライしてみました。発表時点ではコンパイルは通ったけどテストは通らずという状態でしたが、帰りの電車の中でテストにパスしました。
最後のチューニングはCPythonだけのもので、ここのスコアが聞いてCPythonがPyPyの倍の速度というのが今の状況です。PyPyではどうするのがいいかな、とPython仲間に聞いたり調べたりしたところ、PyPyでは以下の方法があるけどどれもダメということが分かりました。とりあえずPyPyは放置で。
- CFFIもしくはctypes(中身はどちらもほぼ一緒)を使ってCのコードをスクリプトから呼び出す。Sphinxドキュメント規模で200万回呼ばれるような関数(BitVector.get/BitVector.set)もあったりするので呼び出し部分がボトルネックになるのは明白。
- cppyyモジュールを利用する。ただし、デフォルトでは使えるように鳴ってないので、PyPyの再ビルドが必要。
- RPythonで書く。ただしこれはPyPyにハードコードされた拡張モジュールの作成には使えるが、ダイナミックにロードするようなモジュールには使えない。
- C/Python APIも一応使える。ただし、RPythonで書かれているのでクソ遅い。あと、メモリ再配置が行われるので、ポインタが正しくオブジェクトを指しているかわからない(@mopemopeさん情報)
Oktaviaの今後
いちおうは動くようにはなりましたが、標準のSphinxの検索とは違って、ページ遷移せずにJavaScriptだけで検索結果を出すという仕様なので、今Sphinxで提供されているHTMLのテンプレートだと使いにくいです。入力欄は画面上部に固定ぐらいが使いやすいはず。テンプレートまで修正しないと、Sphinx組み込みというのは中途半端なので、Sphinxへのマージをがんばるのはやめようかな、と考え中です。
@shibu_jp 良いと思います。その方が別のソリューションも出てくるかもしれないし、がっちり組み込むよりOktaviaの更新もしやすいので良さそう。
— Takayuki Shimizukawa (@shimizukawa) October 27, 2014
あと現在考えているのが、フォーマットの変更。JavaScriptで書かれた汎用圧縮アルゴリズムもあるので、インデックス部分はこれで圧縮をしかけようかなぁと思っています。あとは末永さんに本にあったgolomb符号でもいいかも。現在はゼロが続くようなやつは自前でランレングス圧縮展開してるんですが、中途半端なのでしっかりとしたアルゴリズムでやりたいです。これでダウンロードサイズは半分にできそうです。転送量自体の削減はgzip圧縮転送すればいいんですが、ダウンロード後のキャッシュとかもろもろ考えるとデータそのものが小さい方がうれしいですもんね。
後は、メタ情報はいろいろ足せるものの、現在のウェブの検索コードはセクションでの分類ぐらいしか使っていないので、データ側にメタ情報の使い方も入れておいて、フィルタリングのチェックボックスを付与したりということがデータ側の変更だけでできるようにしたいですね。
あと、せっかくオンメモリでインデックスを持っているという特性があるので、改行コードを押すまで検索実行をやめるのではなく、一文字タイプするごとに検索を走らせてもいいかな、と思っています。
Sphinx組み込みの改善
今はデモ用にざっと作業しただけの状況なので、make cleanしてからmake htmlしないとインデックスが作られない(IndexGeneratorには変更されたコンテンツしか渡されてこない)という手抜きがあるので、ここはOktavia側で改善します。ステミングの設定も無視しているのでそちらも追加が必要です。Sphinx側に加える変更としては、app.addSearchIndexGeneratorみたいなAPIを追加すると共に、設定に検索エンジン切り替えオプションの追加あたりですね。既存のクラスも抽象クラスをつくっておいて、それを継承して実装できるようにした方がいいですね。実際に検索エンジンをフルスクラッチで起こす人はいないだろうけど、外部のエンジンとかデータベースにデータを流すだけというのはお手軽にできそうですし。検索エンジンではなくて、文章を分析したりする人向けのWordCollectorインタフェースとしても面白そうです。
@shibu_jp うぇーい
— Takayuki Shimizukawa (@shimizukawa) October 27, 2014
ということで、のんびり取り組もうと思います。