| @技術/プログラミング

Rust 製の全文検索システム Tantivy を Ruby から使える Tantiny を導入したことを書いた。

結構手軽に使えるのだがやはり日本語のトークナイズ(形態素解析)ができないのでいまいちなところがあった。 Tantivy には lindera-tantivy というものがあって、 Lindera は kuromoji のポートなので、これを使うと日本語や中国語、韓国語の形態素解析ができる。 Tantiny に導入できないか試してみたが、自分の Rust 力では到底無理だった。

ちなみに関連記事の表示でも日本語の形態素解析は行っている。

MeCab に neologd/mecab-ipadic-neologd を組み合わせてナウな日本語に対応させつつ形態素解析している。

この仕組みを作ってトークナイズは Ruby で自前で行い、 Tantiny および Tantivy にはトークナイズ済みの配列を食わせるだけにした( Tantiny はトークナイズ済みのテキストを受け付けることもできる)。トークナイズを自前で行うことで辞書ファイルで拾いきれないような固有名詞もカバーできる。例えば 山と道 なんかは MeCab と mecab-ipadic-neologd にトークナイズさせると に分割されてしまう。自前のトークナイザーで単語として認識させていている。おかげで「山と道」をちゃんと検索できるようになっている

なお、自前のトークナイザーはこんなコードになっている。

class Tokenizer
  attr_reader :text

  class << self
    def run(text)
      self.new(text).tokenize
    end
  end

  def initialize(text)
    @text = text
  end

  def cleansed_text
    @cleansed_ ||= text.
      gsub(/<.+?>/, '').
      gsub(/!?\[(.+)?\].+?\)/, '\1').
      gsub(%r{(?:```|<code>)(.+?)(?:```|</code>)}m, '\1')
  end

  def words_to_ignore
    @words_to_ignore ||= %w[
      これ こと とき よう そう やつ とこ ところ 用 もの はず みたい たち いま 後 確か 中 気 方
      頃 上 先 点 前 一 内 lt gt ここ なか どこ まま わけ ため 的 それ あと
    ]
  end

  def preserved_words
    @preserved_words ||= %w[
      山と道 ハイキング 縦走 散歩 プログラミング はてブ 鐘撞山 散財 はてなブックマーク はてな
    ]
  end

  def nm
    require 'natto'
    @nm ||= Natto::MeCab.new
  end

  def words
    @words ||= []
  end

  def tokenize
    preserved_words.each do |word|
      words << word if cleansed_text.match?(word)
    end

    nm.parse(cleansed_text) do |n|
      next unless n.feature.match?(/名詞/)
      next if n.feature.match?(/(サ変接続|数)/)
      next if n.surface.match?(/\A([a-z][0-9]|\p{hiragana}|\p{katakana})\Z/i)
      next if words_to_ignore.include?(n.surface)
      words << n.surface
    end

    words
  end
end

preserved_words が手製の辞書だ。 はてなはてブ も辞書登録しておかないと MeCab だとバラバラに分割されてしまって検索できなかった。

難点としては記事更新後に自動でインデックスの更新が行われず、 cron によるバッチ処理でインデックス更新を行っている[1]。なので検索インデックスにデータが反映されるまでにタイムラグがある。 Tantiny でやれれば記事作成・更新時のコールバックとして処理できるのでリアルタイムに変更を検索インデックスに反映させることができるが、個人の日記なのでタイムラグありでも大きな問題にはならない。

本当は Tantiny で lindera-tantivy を使えるようにして Pull Request がカッチョイイのだが、とりあえずは自分は目的が達成できたので満足してしまった。 5 年くらい前から Rust 勉強したいと思っているが、いつまでも経っても Rust を書けるようにはならない。

[1]: mecab-ipadic-neologd を VPS 上でインストールできず(めっちゃメモリを使う)、手元の Mac で Docker コンテナ化して Docker Hub 経由でコンテナイメージを Pull して VPS 上で Docker 経由で動かしている(その辺について書いてる記事: ブログのコンテナ化を試みたけどやめた

| @技術/プログラミング

松浦福島初崎海岸のドクロのような岩

Mac の Homebrew のライブラリ群を久しぶりにアップデートした。 tmux と fish のバージョンを上げたら tmux が動かなくなってめっちゃ焦った。いろんなものを同時にバージョンアップするとどっちに原因があるのかわからなくて困る。結局、 brew reinstall tmux で事なきを得た。

次に VPS の Ubuntu のバージョンが古くなっていたのでアップグレードした。ついでにいろいろ気になってたところ(ログローテートがうまく動いていないところとか Nginx の設定ファイルの配置など)を直して回った。

OS のアップグレードに伴って Ruby の再インストールが必要になり、 Ruby 再インストール後にアプリケーションを deploy しようとすると mimemagic gem が yank されていたりでライブラリのアップデートが必要になった( MimeMagic は脆弱性があって Mercel に変更しないといけなかったが、変え忘れていたところがあった)。これによって引きずられるように gem のアップデートが必要になり、うっかり capistrano3-puma を v5 系にしたところ、 puma の起動ができなくて困った。どうも puma の v5 ではデーモン化オプションが削除されているようで、 capistrano で puma を再起動させたりはできないようだった。いろいろ面倒くさそうなので capistrano3-puma も puma も v4 系に固定して凌いだ。

Archives ページの npm パッケージも古くなってたので、 React や React Router 、 Webpack 、 Babel など各種ライブラリのバージョンを上げた。 React Router の v5 系から v6 系へのアップデートは結構大変だった。以下を読みながらやった。

withRouter などは React Router から機能が消えるのでそれをラップする関数を自分で書いてコンポーネントに mixin するような感じだった。以前に比べたらマイルドになっているとはいえ、 JavaScript 界隈はアップデートについて行くのが厳しい。

職業プログラマーじゃなくなったので開発環境の維持管理などがおろそかになりがちだし、 Vim やシェルのショートカットを忘れてしまうことがある。 Vim やシェルの操作は特殊技能のようなものなので忘れるともったいない。たまに触って忘れないようにしておきたい。

そういえば温かくなって庭の雑草が伸びてきたので庭の草むしりもやった。ゴールデンメンテナンスウィークだ

| @技術/プログラミング

いろいろスパム対策は行っているもののやはりまだスパムが届くので NG ワードの設定を簡単に行えるようにした。こんな感じ。

NG ワード設定

これまで一つのテキストフィールドにカンマ区切りで入力しないといけなかったのを、フィールドを分けて単語ごとに入力できるようにした。

モダンなやり方はしてなくて全部サーバーサイドの Ruby でやってる。動的に新規登録フィールドを増やしたりはできない。新規登録したいときは都度都度フォームを保存する必要があるが、十分速いので実用上は全く問題ない。

同様に、スパマーの餌食になっている過去記事のコメント欄を閉鎖する機能も便利にした。

コメント欄を閉じる記事の設定

仕事では全然プログラミングしていないので久々にコードを書いた。

| @Mac/iPhone

Touch Bar

リモートワーク中心の世の中なので Slack の Status で離席していることやミーティング中であることが分かると便利なはず。というわけで自分はなるべく Slack の Status を更新するようにしているが、 Slack アプリ内での Status の更新は面倒くさい。メニューを押して絵文字選んでひと言アップデートを入力とか毎度やってられない。ボタン一発で Status を更新したい。

MacBook Pro の Touch Bar は評判が悪い。自分もあまり便利だと思わないのだけど、一つだけ便利な使い方があって、それがこの Slack の Status アップデートボタンを配置するというもの。 Touch Bar に配置されたボタンを押すだけで食事中であることや退勤済であることを Slack の Status として表示できるようになる。めっちゃ便利。

なお、オリジナルのアイディアとソースコードは 9m さんのものです。

必要なもの

準備

1. Slack の API Token を発行する

2. 9m さんの gist を clone し、手元で動かせるようにする

$ ghq clone https://gist.github.com/af5894ced5cc1ac38bfd2687cad7c780.git slack_status
$ cd clack_status
$ bundle install
$ echo "SLACK_TOKEN=XXXX" > .env
$ bundle exec app.rb "🍺" "退勤しました"

ちゃんと設定できてれば以下のようになる。

コマンドラインから Slack Status をアップデートしている様子

3. Automator を開き、クイックアクションを設定

新規作成で「クイックアクション」を選ぶ。

Automator を開き「クイックアクション」を新規作成

アクションの中からシェルスクリプトを選ぶ。

シェルスクリプトを選ぶ

実行したい処理をシェルスクリプトで書く。

実行したい処理をシェルスクリプトとして記載

自分は以下のようにしている。

export PATH="~/.rbenv/shims:$PATH"
export LC_ALL=ja_JP.UTF-8
export LANG=ja_JP.UTF-8
cd /Users/morygonzalez/src/gist.github.com/slack_status
bundle exec ruby app.rb "🚽" "放尿 or 脱糞中です"

なお、赤枠で囲った「ワークフローが受け取る項目」は「入力なし」にしておかないとちゃんと動かないので注意。

入力なしを選択

設定完了したら名前を付けて保存する。自分の場合は Slack トイレ などのような名前にしている。この作業を追加したいコマンドの数だけ繰り返す。

4. キーボードショートカットの割り当て

システム環境設定 -> キーボード -> ショートカット -> サービス の順に進む。正しく Automator でアクションを設定できていれば「サービス」の一覧に表示されるので、割り当てたいショートカットキーを割り当てる。

ショートカットの設定

5. BetterTouchTool で Touch Bar をカスタマイズする

タッチバーに表示されるボタンのアイコンとラベル文字を選び、タップしたときにショートカットキーが実行されるようにする。

BetterTouchTool で Touch Bar をカスタマイズ

こうすることで Touch Bar から Automator のクイックアクションが実行され、めでたく Slack の Status がアップデートされるようになる。

ちなみに自分の Touch Bar はこんな感じ。

Touch Bar の様子

ほこりをかぶってる Touch Bar を是非有効活用してあげてください。

Touch Bar がないパソコンを使っている人向けの情報

Touch Bar のない Mac を使っている人はこのやり方を使えないので Slack の Google Calendar 連携機能を使うと良いと思う。設定に Status Sync という項目があるのでこいつを On にすると、 Google Calendar で予定が入っている時間になると Slack の Status を自動で更新してくれる。

Google Calender の Status Sync

会議中であることくらいしか共有できないので Touch Bar にいろんなボタンを配置するのに比べたら不便だけど、カレンダーに予定を入れておくだけで Slack の Status を更新できるようになるのは便利。

今後の課題

良くありがちなのが「仕事中」の Status のまま退勤してしまうというやつ。夜中や週末も仕事している異常な人になってしまう。スマートフォンからも同様にめっちゃ手軽に Slack の Status をアップデートしたいけどまだソリューションを見つけられていない。情報お持ちの方いたら教えてください。

| @技術/プログラミング

7 年間眠っていたブランチを起こして、 Lokka の ActiveRecord 化に取り組み始めた。元のブランチは hrysd さんが取り組んでいたやつだ。

現在の master の内容を取り込むのが大変だった。 active-record ブランチでは ActiveRecord 化と同時に様々な改良・改変が行われていて、 master の内容と思い切りコンフリクトするものがあったりして、コンフリクトの解消作業はかなり大変だった。

active-record の大きな変更点は以下。

  1. カスタムパーマリンク機能の削除
  2. 「もっと読む」機能の削除
  3. カテゴリーをネストさせる機能の削除
  4. ユーザー認証方法の変更(カラムの追加)

このうち 1 と 2 は削除された機能を復活させた。自分が使っていてなくなると困るし、特にカスタムパーマリンクは既存サイトでこの機能を使っているところがデッドリンクだらけになって散々な目に遭ってしまう。 4 に関しても、 master の認証方法と互換性を持たせないと既存ユーザーがログインできなくなるので古い認証方法でもログインできるようにした。

3 に関しては WordPress との互換性を考えると必要かもしれないが、自分で使ってなくてユースケースが思い浮かばないのでいらないかなという感じがする。そもそも Lokka は WordPress キラーとなるべく Fjord 社内で作られ始めたと認識しているが、 WordPress は相変わらず元気だし Lokka の利用状況的にも WordPress alternative を目指す必要はないと思う。

そのほか、 rake db:delete が動かなかったのを直したり bundle update をしてぶっ壊れたところを直したり、デフォルト以外のテーマが ActiveRecord 化してなかったのを対応させたり( dm-pagination から kaminari へ移行)して ActiveRecord 5 で概ね動くところまで持ってくることができた。

ActiveRecord は良くできていて、 DataMapper だと難しかった JOIN した上での集計クエリなどが書きやすい。ドキュメントが山ほどあるのもよい。 DataMapper は情報が少ないのが一番つらかった。一方で DataMapper だと気にする必要がなかった N+1 問題を自分で解決する必要がある。 View でうかつに参照するテーブルのデータを増やすと N+1 問題が発生して途端にパフォーマンスが劣化する。

また、誰がどんな DB で利用するかわからない状況で db/schema.rb を git で追跡してよいものかというのもひかっかる。 ActiveRecord を使う以上、 migration と schema.rb からは逃げられないのだが、 MySQL で使う人も PostgreSQL で使う人も SQLite で使う人もいて、それぞれの DB でマイグレーションを実行するごとに異なる schema.rb が吐き出されるので git で追跡すべきではないのではないかと思う。どんなデータベースで利用されるかを意識せずに開発できる、という点では DataMapper の方が CMS 開発向きだったと思う。

以前の Lokka であればあまり Ruby 知らない人でもとりあえず git clone して自分の好みのテーマを追加して Heroku に push すれば動かせたが、 ActiveRecord 化することで N+1 問題など Rails に強くないと触りにくい感じになってしまった。ただ、 Sass は Ruby を捨てて C に移行したし、 Slim なんかも JavaScript フロントエンド技術の盛り上がりの陰で開発は停滞している。こういう時勢になってくるとフロントエンドに強いマークアップエンジニア兼ウェブデザイナー的な人が Ruby 製の CMS を使う動機はなくなってしまう。 CMS を使ったサイト構築でも Sass や Slim を使って HTML コーディングの生産性を上げ、 Heroku を使って簡単に deploy できる、というのが komagata さん達が最初に想定してた Lokka のユースケースだと思うけど、 JavaScript によるフロントエンド技術が強力になりすぎて、生産性の高いフロントサイド開発のために Ruby を経由する必要がなくなってしまった。


これから Lokka はどうあるべきなのだろうか。モダンなフロントエンドフレームワークは強力だ。否が応でも JAMStack に対応していくしかないだろうと思う。つまり Sinatra で作るのは API (と管理画面)だけになり、フロントエンドは React や Vue.js で作るべきだろう。ちょっとしたサイトを JAMStack で構築したいが、 API に良いのがない、とはいえ Rails は使いたくない、というケースで Lokka を使うという感じだろうか。ただ、いまは Firebase なんかもあるのでそもそも API を自前で持つ必要はないのかもしれない。どのみちかなりニッチなユースケースになるだろう。

ちなみにこのブログの Archive ページは中途半端ながら React で作っていて割といい感じに動いている。 ActiveRecord 化が済んだら React でサイト全体を作り直してみたい。

| @技術/プログラミング

DataMapper のドキュメントを見たくてググったが出てくるのは Stack Overflow ばかりで公式サイトが検索結果に出てこない。 GitHub の DataMapper のリポジトリ( Archive されている)経由で見に行ってみると、なんと ROM ( Ruby Object Mapper ) のページにリダイレクトされた。

ROM は Hanami で使われる ORM で、 DataMapper よりもさらに ActiveRecord と使い心地が異なる。

Qiita の以下の記事を読むと使い方のイメージが湧く。

軽くてシンプルなのだろうがだいぶ特殊だ。

Lokka の使い手は少なくとも Heroku が使える人で、そういう人ならば ActiveRecord の方が Rails の本やドキュメントで学びやすいはずだ。というわけで早めに、真剣に ActiveRecord への移行を考えなければならない。

| @ブログ

background-image random pick up

写真や画像があると文章だけよりも記憶に定着しやすいらしい。なのでヘッダー画像のバリエーションを増やしてみることにした。これまで自分が撮ってきた写真の中で気に入ってるもの、ヘッダー画像にちょうど良さそうなやつを 14 枚選んで、これまで表示していた門司の「平民食堂」のやつと合わせて 15 枚をランダムピックアップして表示させている。本当ならランダムではなく記事ごとにイメージに合う画像を選定すべきなのだろうけどそこまではやってない。「このブログ主はこういう写真の場所に行ったりこういう写真をいいと思ってるんだな」ということが伝われば良いと思ってる。

ランダム表示に関して、最初は JavaScript で CSS の background-image を書き換える方式でやっていたが、 DOM の読み込み完了時に動く都合上、かくつきが出てしまう。それで画像の選定は Ruby で行って data-attribute として指定し、予め CSS にそれぞれの data-attribute に応じた background-image を書いておくことにした。これでかくつきはほぼほぼなくなった。

<div id="header" data-background-image="heimin">
</div>
#header[data-background-image="heimin"] {
  background-image: url(header-bg-heimin.jpg);
}

このほかにも CSS 周りを結構いじってて、パンくずリストが載る部分は文字が読みにくくなるのでグラデーションでシャドーを掛けてる。これまで float でやってた配置の制御を flexbox に置き換えたりもやった。今のデザインのベースは 10 年前に作ったので、当時はグラデーションなんかは CSS で実現できず横幅 1px の画像をリピートする方法を使ったが、これで画像を捨てて全て CSS で実現できそう。良い世の中になってきてる。