仕事面で 2017 年を振り返ると、いろいろやったけど自分でなんか作ったというのはほとんどない。 人のふんどしで相撲をとっていた一年(転職してからは半年強)だと言える。SaaS として提供されているツールを導入したり、 OSS の分析ツールを導入・構築したり、会社の仕組みを調整したりしてただけだった。各ライブラリを作ってくれた人には感謝しかない 🙏🏻

組織方面

  • チーム横断の定例 MTG 働きかけ
    • 人が増えて「あの人何やってるかわからない」「仕事を横からいきなり依頼される」などの問題が出てきたため、チーム横断の定例ミーティングを開催してお互いの状況を確認したり依頼しそうなことがあれば前もって共有するように
  • 全体ミーティングフォーマット整え&司会業
    • かつては社長が考えていることを聞くだけの場だったが、チームごとに資料を作ってみんなで発表し、議論をする場に変えた
  • Slack 導入
    • Slack に変えるまでも別のチャットツール使ってたけど、平アカウントでは大したことできず、窮屈な感じがした
    • Slack は平アカウントでも外部ツール連携したり API 使ってなんかやったりできて便利
    • ベンチャーには平社員でも必要なことをやれるようなシステムの方が向いてると思う。 Slack はデザインだけじゃなくてそういうところが優れている。フラットで雰囲気が明るい。使うのが楽しくなる。
  • Kibela 導入
    • Wiki と Blog 、 Board ( Group )の概念がちょうどよい。 Qiita:Team で厳しかったところが解消されている。
    • Kibela 導入以前、情報共有は Issue Tracker に何でも書いてる感じだったが、 Issue Tracker は Issue Tracker なので close することができない問題を扱うのに向いていない
    • タスクには落とし込めないけど社内で見解を表明しておきたい事柄を社内ブログに書いて問題意識をみんなと共有する文化を構築できた
    • Kibela は PlantUML で図を書けるのがとにかくすばらしい。込み入った処理フローをシーケンス図にすることで設計・実装がはかどる。
  • HRT について説く
  • OKR 導入
    • OKR を設定してやっていきましょうという風にした
      • とはいえ自分は HR の専門家ではないのでちゃんと運用して行くにはそういう人に入ってもらわないと厳しいと思ってる

エンジニアリング方面

  • t_wada (テスト文化根付かせ)業
    • No Test, No Merge
  • CI が回る仕組み構築業
    • テストコードは別の人が書いてたけど回せてなくて fail しっ放しになってたので気合いで通るようにして CircleCI で Pull Request ごとにビルドするようにした✌🏻
  • Pull Request テンプレート導入
    • どんな問題を解決する Pull Request なのか、何をやったのか、完了条件を明記する✅
  • Pull Request レビューフォーマット提案
  • gitignore されていた Gemfile.lock をリポジトリに突っ込み業
    • Gemfile でバージョンが固定されてた😢
  • Embulk で分析用データ書き出し業
  • autodoc で API ドキュメント自動生成の仕組み構築
  • Git Flow から GitHub Flow へブランチ戦略変更
    • 1日に何回もデプロイするような製品はこっちの方が向いてる
  • Rubocop 導入& .rubocop.yml 番人業
    • 👮🏻‍♂️
  • CircleCI から勝手に deploy される仕組み導入
  • docker-compose 導入
    • Docker は使われてたがクラスターの管理は手運用だったので docker-compose 使うようにした
  • AWS ECS 導入
  • 社内 Gyazo 導入
  • Redash 導入
    • 経営陣しか数値に関心を持ってなかったので全員が見るように毎朝 Slack に KPI を通知するようにした
    • 複雑なクエリを組んでテーブルごとに値を集計しているだけでは見えてこない値を追えるようになった
    • 独自に KPI/KGI を設定して Growth Hack に取り組むエンジニアも
  • リードレプリカ作ってデータ分析がやりやすくなる仕組み導入
    • RDS で Multi AZ にはなっていたがリードレプリカがなく重いクエリを投げられなかった
    • 複雑な JOIN クエリも書けるようになりデータ分析し放題
    • 来年は BigQuery とかも使えるようにしてさらに分析が捗るようにしたい
  • Itamae でプロビジョニング( Linux アカウントの管理)
    • Itamae 一発で Linux アカウント追加できるようにしてサーバーサイドのエンジニアしか DB にクエリを投げられない状況を改善
  • cronbot 導入
    • 他人が作ったスケジュールも更新できて便利
    • KPI 通知は redashbot と cronbot を組み合わせて実現
  • iOS と Android のダウンロード数自動取得
    • iOS 側はタイムゾーンがずれる、 Android 側は更新が異常に遅いという問題があるものの、ある程度の目安となる数値が毎朝自動で Slack に通知されるように
  • お問い合わせがあったときに Slack に通知する仕組み導入
    • お問い合わせはカスタマーサポートの人が一手に引き受ける感じだったけどみんなが関心を持って見るようになった
    • カスタマーサポートの人からエスカレーションされる前にエンジニアが回答
    • 不具合あったときはいち早く対応可能に
  • Ruby app の前段に CloudFront 導入
    • app サーバーへのリクエストが半減
    • Nginx でキャッシュしきれてなかった静的ファイルを CloudFront でキャッシュするようになり爆速に
  • サイト全面 HTTPS 化
    • CSS/JS が並列で配信されるようになり爆速に

自分でまともな OSS を作れないことにコンプレックスを感じていた時期もあったが( OSS コミュニティでの活動が評価軸となるような職場では全然評価されない)、自分が作れなくても他の人が作ってくれるので、それをいかに組み合わせて有効活用し、価値を生み出せるかに注力すればいいかなぁと思うようになった。

もちろん、 OSS 使っててバグを見つけたり不便なところあったら改善する Pull Request なんかは出していきたいと思ってる。ただ自分は頭がよくないし、抽象的な思考は苦手で個別具体的なコードを書くことしかできないので、自分で OSS を生み出すことは諦めて個別具体的な事象に特化してやっていく方が自分的にも世の中的にも幸せだよね、という風に割り切れるようになった。

こういう割り切りができるようになったのは Kaizen Platform で仕事する機会を得たからだよなぁと思う。 OSS への考え方に限らず、コードを書く部分以外で組織を変革したりだとかオペレーションの仕組みを変えたりだとかは全部 Kaizen Platform で学んだ気がする。1年11ヶ月と短い期間だったけれど、いまの自分の血となり肉となっていると思う。

Kaizen を辞めたときの記事で以下のように書いてたけど、いまのところ失敗を糧にしていい方向に向かってるのではないかと思う。

Kaizen でのリモートワーク失敗経験をどう今後の人生に生かすか。以下のツイートを繰り返し眺めながら悔い改めていきたいと思う。

Kaizen Platform という会社について - portal shit!

というわけでいまは YAMAP という会社で働いています。元同僚の pyama86 さんに比べたら知名度では全然負けててミジンコみたいなもんだと思うけど、そのうち逆転できるようにプロダクトの完成度を高めていって pyama86 さんの方が YAMAP のパクりであるような雰囲気を醸成していきたい。今後ともよろしくお願いいたします🙏🏻

YAMAP

cho45 さんの以下の記事を参考に関連記事を表示するようにしてみた。

TF-IDFとコサイン類似度による類似エントリー機能の実装 | tech - 氾濫原

ほとんど cho45 さんの記事に書いてある SQL を実行しているだけだけど、関連記事の表示用に Lokka 側に Similarity というモデルを追加して、以下のようなスキーマにしてる。

similar-entries-erd.png

Similarity テーブルの更新は cho45 さんの記事にあるように SQLite で行った計算の結果を反映することで行う。以下のような Rake タスクを定義した。

desc "Detect and update similar entries"
task similar_entries: %i[similar_entries:extract_term similar_entries:vector_normalize similar_entries:export]

namespace :similar_entries do
  require 'sqlite3'
  desc "Extract term"
  task :extract_term do
    require 'natto'
    nm = Natto::MeCab.new
    db = SQLite3::Database.new('db/tfidf.sqlite3')
    create_table_sql =<<~SQL
      DROP TABLE IF EXISTS tfidf;
      CREATE TABLE tfidf (
        `id` INTEGER PRIMARY KEY,
        `term` TEXT NOT NULL,
        `entry_id` INTEGER NOT NULL,
        `term_count` INTEGER NOT NULL DEFAULT 0, -- エントリ内でのターム出現回数
        `tfidf` FLOAT NOT NULL DEFAULT 0, -- 正規化前の TF-IDF
        `tfidf_n` FLOAT NOT NULL DEFAULT 0 -- ベクトル正規化した TF-IDF
      );
      CREATE UNIQUE INDEX index_tf_term ON tfidf (`term`, `entry_id`);
      CREATE INDEX index_tf_entry_id ON tfidf (`entry_id`);
    SQL
    db.execute_batch(create_table_sql)

    entries = Entry.published.all(fields: [:id, :body])
    entry_frequencies = {}
    entries.each do |entry|
      words = []
      body_cleansed = entry.body.
        gsub(/<.+?>/, '').
        gsub(/!?\[.+?\)/, '').
        gsub(/(```|<code>).+?(```|<\/code>)/m, '')
      begin
        nm.parse(body_cleansed) do |n|
          next if !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 %w[これ こと とき よう そう やつ とこ ところ 用 もの はず みたい たち いま 後 確か 中 気 方 頃 上 先 点 前 一 内 lt gt ここ なか どこ まま わけ ため 的 それ あと].include?(n.surface)
          words << n.surface
        end
      rescue ArgumentError
        next
      end
      frequency = words.inject(Hash.new(0)) {|sum, word| sum[word] += 1; sum }
      entry_frequencies[entry.id] = frequency
    end
    entry_frequencies.each do |entry_id, frequency|
      frequency.each do |word, count|
        db.execute("INSERT INTO tfidf (`term`, `entry_id`, `term_count`) VALUES (?, ?, ?)", [word, entry_id, count])
      end
    end
  end

  desc "Vector Normalize"
  task :vector_normalize do
    db = SQLite3::Database.new('db/tfidf.sqlite3')

    load_extension_sql =<<~SQL
      -- SQRT や LOG を使いたいので
      SELECT load_extension('/usr/local/Cellar/sqlite/3.21.0/lib/libsqlitefunctions.dylib');
    SQL
    db.enable_load_extension(true)
    db.execute(load_extension_sql)

    update_tfidf_column_sql = <<~SQL
      -- エントリ数をカウントしておきます
      -- SQLite には変数がないので一時テーブルにいれます
      CREATE TEMPORARY TABLE entry_total AS
          SELECT CAST(COUNT(DISTINCT entry_id) AS REAL) AS value FROM tfidf;

      -- ワード(ターム)が出てくるエントリ数を数えておきます
      -- term と entry_id でユニークなテーブルなのでこれでエントリ数になります
      CREATE TEMPORARY TABLE term_counts AS
          SELECT term, CAST(COUNT(*) AS REAL) AS cnt FROM tfidf GROUP BY term;
      CREATE INDEX temp.term_counts_term ON term_counts (term);

      -- エントリごとの合計ワード数を数えておきます
      CREATE TEMPORARY TABLE entry_term_counts AS
          SELECT entry_id, LOG(CAST(SUM(term_count) AS REAL)) AS cnt FROM tfidf GROUP BY entry_id;
      CREATE INDEX temp.entry_term_counts_entry_id ON entry_term_counts (entry_id);

      -- TF-IDF を計算して埋めます
      -- ここまでで作った一時テーブルからひいて計算しています。
      UPDATE tfidf SET tfidf = IFNULL(
          -- tf (normalized with Harman method)
          (
              LOG(CAST(term_count AS REAL) + 1) -- term_count in an entry
              /
              (SELECT cnt FROM entry_term_counts WHERE entry_term_counts.entry_id = tfidf.entry_id) -- total term count in an entry
          )
          *
          -- idf (normalized with Sparck Jones method)
          (1 + LOG(
              (SELECT value FROM entry_total) -- total
              /
              (SELECT cnt FROM term_counts WHERE term_counts.term = tfidf.term) -- term entry count
          ))
      , 0.0);
    SQL
    db.execute_batch(update_tfidf_column_sql)

    vector_normalize_sql = <<~SQL
      -- エントリごとのTF-IDFのベクトルの大きさを求めておきます
      CREATE TEMPORARY TABLE tfidf_size AS
          SELECT entry_id, SQRT(SUM(tfidf * tfidf)) AS size FROM tfidf
          GROUP BY entry_id;
      CREATE INDEX temp.tfidf_size_entry_id ON tfidf_size (entry_id);

      -- 計算済みの TF-IDF をベクトルの大きさで割って正規化します
      UPDATE tfidf SET tfidf_n = IFNULL(tfidf / (SELECT size FROM tfidf_size WHERE entry_id = tfidf.entry_id), 0.0);
    SQL
    db.execute_batch(vector_normalize_sql)
  end

  desc "Export calculation result to MySQL"
  task :export do
    db = SQLite3::Database.new('db/tfidf.sqlite3')
    create_similar_candidate_sql = <<~SQL
      DROP TABLE IF EXISTS similar_candidate;
      DROP INDEX IF EXISTS index_sc_parent_id;
      DROP INDEX IF EXISTS index_sc_entry_id;
      DROP INDEX IF EXISTS index_sc_cnt;
      CREATE TABLE similar_candidate (
        `id` INTEGER PRIMARY KEY,
        `parent_id` INTEGER NOT NULL,
        `entry_id` INTEGER NOT NULL,
        `cnt` INTEGER NOT NULL DEFAULT 0
      );
      CREATE INDEX index_sc_parent_id ON similar_candidate (parent_id);
      CREATE INDEX index_sc_entry_id ON similar_candidate (entry_id);
      CREATE INDEX index_sc_cnt ON similar_candidate (cnt);
    SQL
    db.execute_batch(create_similar_candidate_sql)

    extract_similar_entries_sql = <<~SQL
      -- 類似していそうなエントリを共通語ベースでまず100エントリほど出します
      INSERT INTO similar_candidate (`parent_id`, `entry_id`, `cnt`)
          SELECT ? as parent_id, entry_id, COUNT(*) as cnt FROM tfidf
          WHERE
              entry_id <> ? AND
              term IN (
                  SELECT term FROM tfidf WHERE entry_id = ?
                  ORDER BY tfidf DESC
                  LIMIT 50
              )
          GROUP BY entry_id
          HAVING cnt > 3
          ORDER BY cnt DESC
          LIMIT 100;
    SQL

    search_similar_entries_sql = <<~SQL
      -- 該当する100件に対してスコアを計算してソートします
      SELECT
          ? AS entry_id,
          entry_id AS similar_entry_id,
          SUM(a.tfidf_n * b.tfidf_n) AS score
      FROM (
          (SELECT term, tfidf_n FROM tfidf WHERE entry_id = ? ORDER BY tfidf DESC LIMIT 50) as a
          INNER JOIN
          (SELECT entry_id, term, tfidf_n FROM tfidf WHERE entry_id IN (SELECT entry_id FROM similar_candidate WHERE parent_id = ?)) as b
          ON
          a.term = b.term
      )
      WHERE similar_entry_id <> ?
      GROUP BY entry_id
      ORDER BY score DESC
      LIMIT 10;
    SQL

    results = {}
    Entry.published.all(fields: [:id]).each do |entry|
      db.execute(extract_similar_entries_sql, [entry.id, entry.id, entry.id])
      db.results_as_hash = true
      similarities = db.execute(search_similar_entries_sql, [entry.id, entry.id, entry.id, entry.id])
      results[entry.id] = similarities
    end

    Similarity.destroy

    results.each do |entry_id, similarities|
      if similarities.present?
        similarities.each do |s|
          conditions = { entry_id: s["entry_id"], similar_entry_id: s["similar_entry_id"] }
          similarity = Similarity.new(conditions)
          similarity.score = s["score"]
          similarity.save
        end
      end
    end
  end
end

やってることとしては、全エントリーを拾ってきて本文を MeCab で品詞分解して名詞だけを取り出し記事ごとの term 一覧を作り、そこから TF-IDF を求めてベクトル正規化し、最後に関連していそうなエントリを探し出して similarities テーブル(こちらは SQLite のテーブルではない)を更新している。詳しいアルゴリズムはバカなのでわからないが、 cho45 さんが書いているやり方を Lokka のスキーマに素直に適用した感じ。

結構この処理は遅いので parallel.gem を使って高速化できないか試してみたが、スレッドによる並行処理ではあまり速くできなかった。 4 コアある CPU のうち一つが 100% で処理を実行してもまだ 3 コアは余っている。プロセスを増やして並列処理するのがよさそうだが、分散をプロセスレベルで行おうとすると MySQL server has gone というエラーが出る。 DataMapper が MySQL とのコネクションをロストするようである。 ActiveRecord であれば reconnect するだとか回避方法があるようなのだけど DataMapper は情報が少なく、対応方法が見つけられなかったので一旦並列処理はあきらめた。

何回か動かしてみて大体正しく関連記事を表示できてそうなのでさくらの VPS で稼働させたいところなのだけど、関連記事の更新はいまのところ手動でやっている。本番 DB の entries テーブルを dump してきて Mac に取り込み、 similarities テーブルを更新して今度はローカルで similarities テーブルを dump して本番にインポートするという手順をとっている。

これにはいろいろ理由があって、一つには利用している mecab-ipadic-neologd (新語にも対応している MeCab の辞書)が空きメモリ 1.5GB 以上でないとインストールできずさくらの VPS にインストールできなかったから。もう一つには cho45 さんのブログにもあるけど SQLite で LOGSQRT を使うためには libsqlitefunction.so の読み込みが必要で、 load_extension() できるようにしないといけないが、そのためには sqlite3 をソースからビルドする必要があり若干面倒だった( Mac では Homebrew で sqlite を入れた)。

関連記事の更新は自分が記事を書いたときにしか発生しないのでいまの手動運用でもまぁ問題ないが、このブログは Docker でも動くようにしてあるので Docker イメージを作ればさくら VPS でも問題なく動かせそうな気はする。正月休みにでもチャレンジしたい。

感想

関連記事表示、結構面白くてちゃんと関連性の高いエントリーが表示される。例えば人吉に SL に乗り行った記事の関連記事にはちゃんと山口に SL に乗りに行ったときの記事 が表示される。いまのところ Google Adsense の関連コンテンツよりも精度が高いようである。

無限に自分の黒歴史を掘り返すことができるのでおすすめです。

DEAD FISH

fish-shell に移行 してこれで .zshrc のお守り業から解放されたと思ってたが、最近異常にシェルの新規セッションの開始が遅くて死にそうになってた。特にやばいのが git mergetool したときで、これは内部的には沢山の fish のプロセスが起動して diff を調べて最終的に vimdiff で表示しているように見えた(僕は git config editor=vim です)。下手するとちょっとしたコンフリクトを修正するために vimdiff が起動するまで 15 分くらい待たないといけないことがあって、 20 年くらい前の Photoshop 作業の現場1みたいな感じになってた。これはかなりやばくて、生産性ががた落ちになる。 vimdiff が開くまでに待っている間に他のことやり始めて、気がつくと平気で夕方になってたりする。

fish-shell のバージョンを 2.7.0 に上げたことが原因かと思って、かつて快適に使えていた fish のバージョンに下げたりしてみたが解決しなかったが、以下の記事にたどり着いて $fish_user_paths への値の push をやめたところ解決した。

fish shellの起動が遅くなった時の解決方法 - Qiita

fish-shell での PATH の通し方として以下のようなのがよく出てくる。 Homebrew で keg only なやつを入れたときにも表示される。

set -U fish_user_paths $HOME/Library/Python/2.7/bin $fish_user_paths

しかしこれをやると $fish_user_paths にどんどんパスが積まれていってしまう。どうもこれが原因で遅くなるようだった。自分の環境でも echo $fish_user_paths してみたところかなり酷いことになっていた…。

↑の Qiita 記事では set -U fish_user_paths するときに第三引数を消せとあったが、それでは根本的な問題の解決にならない( Python やら Ruby やらいろんなものの実行ファイルを PATH に通したいはず)。自分は以下の方法で解決した。

set -x PATH /usr/local/bin $PATH

bash や zsh で export PATH=/usr/local/bin:$PATH とやるのと同じオーソドックスなやり方だと思う。

これだけではこれまでに散々肥えた $fish_user_paths は残ったままなので適当に set -U fish_user_paths '' とかやってあげると空になって起動が速くなる。

fish-shell の set -U はセッションをまたいでグローバルかつ永続的に設定される変数定義の方法っぽいので下手するとその環境で fish を使い始めてからずーっと残ってしまう可能性がある(端末を再起動したらリセットされるかも知れないけど)。

まとめ

  • set -U は基本的にしない
  • $fish_user_paths は設定ファイルの中では使わない
  • PATH を通したいときは set -x PATH を使う

  1. 何らかの処理を実行して処理が完了されるまでにめっちゃ時間がかかるのでその間にたばこを吸いに行くことが可能だったらしい 

DSC_4054

以下の文章は正月に「2017 年の Lokka へのコントリビュート目標」というタイトルで書いたまま下書きになってたものです。もう 2017 年も終わりそうだけど公開しておきます。


RubyKaigi 2016 で komagata さんと Lokka についてしゃべったのだけど、 Lokka の開発も停滞してしまっていて(ブログとしては大体の機能そろっていて完成しているとも言える)、 "lokka" でググると JavaScript 製の GraphQL クライアントがヒットして、 GitHub のスター数ではこっちの方が多かったりする。

kadirahq/lokka: Simple JavaScript Client for GraphQL

このままで紛らわしいからその名前こっちに寄越せ、とか言われかねない。もっと Lokka を盛り上げていきたい。

なので一度仕切り直しで今後の方針とかをどうするかを決めた方がよいのではないかと思っている。まずは Issue の棚卸が必要ではないかと思う。

加えて自分でも結構いろいろ lokka-plugin を作っているのだけど、個人のリポジトリに適当に上げてあるだけだとユーザーとしては利用しづらいと思う。そこで GitHub の Lokka org に lokka-plugins というリポジトリを作って、とりあえずそこにコードを集約するようにしたらどうかと思う。前に Rebuild で Jenkins の川口さんが話していたやり方。

OSS 、コードが素晴らしいことも大事だけど、利用しやすくないとユーザーに使ってもらえなくて盛り上がって行かないと思う。プラグインを使いやすい、作りやすいようにして裾野を広げていきたい。

ほかにも手軽に使ってもらうためにはいくつかやらないといけないことあると思う。 gem 化は是非とも必要だと思う。 Lokka 動かすために本体のソースコードごと管理しなければいけないのはやっぱり結構敷居が高いと思う。ディレクトリ作って Gemfile に gem 'lokka' と書いて bundle install し、 theme と db config さえ置けば動くようになるのがよさそう。ファイルをアップロードする仕組みについてもどうにかしたい。 Heroku 運用が前提のためファイルシステムを使うことができず本体にそういう仕組みがなかったのだと思う。 Amazon S3 や Google Cloud Storage 、 Dropbox 使えるようにするとかやり方を考えたい。

これらを推し進めるために、以下のことをやりたい。

  1. Lokka 開発者ミートアップを開催
  2. Slack にチームを作ってコミュニケーションできるようにする

1 は RubyKaigi のときに komagata さんに提案したけどそのあと動けてなかった。とりあえずは自分が komagta さんのところに会いに行くだけになるかもだけど、 Issue の棚卸と今後の方向性を固める会をやった方がよさげだと思う。仕事でも OSS でも意思や目的を共有しないと Project は先に進まないと思う。

2 に関しては Lokka は Lingr でコミュニケーションしていたが、いまは皆さん Slack を仕事で使っていると思うし Slack の方がコミュニケーションしやすいはず。というわけで Slack チームを立ち上げたい。1

という感じで 2017 年も Lokka の開発に関与していきたい。


転職して東京に行く機会がなくなり、結局 Lokka 開発者会議をやることはできなかった。プラグインの gem 化はおろかリポジトリへの集約も DataMapper => ActiveRecord への移行も手を付けられなかったが、それ以外で Lokka の改善は結構頑張ったと思う。

今年やった Lokka の改善

  • パーマリンク生成高速化 https://github.com/lokka/lokka/pull/220
    • Lokka はどのフォーマットで permalink を生成するかを DB に保存している
      • リンクを一つ生成する度に permalink のフォーマットを調べるためのクエリが流れる
    • ビュー内で各記事へのリンクは多数
      • めっちゃクエリが流れる 😱
    • request_store.gem を使ってリクエストごとに一回だけクエリが流れるようにする
      • テーマにもよるが記事一覧でパーマリンク生成のために 10 回くらい DB アクセスが発生していたところは 1/10 になる
      • 管理画面の記事一覧では 100 記事表示しているので 1/100 になる(爆速になった!!!、!)
  • 管理画面をスマートフォン最適化 https://github.com/lokka/lokka/pull/225
    • スマートフォンから管理画面が見やすくなるよう CSS を修正
    • 寝床からでもブログ書けるようになった!!!、!
  • ファイルアップロード機能を追加 https://github.com/lokka/lokka/pull/226
    • S3 にバケットを作ってもらいさえすれば GitHub のようにドラッグ&ドロップで画像をアップロード出来るように
    • めっちゃお手軽お気軽に画像アップロードできるようになって最高便利!!!、!
  • MySQL 絵文字対応 https://github.com/lokka/lokka/pull/230 😃
    • 今の時代、絵文字が使えないのはつらい 😅
  • Ruby 2.4 対応 https://github.com/lokka/lokka/pull/231
    • Lokka で Sinatra 、 DataMapper の次に依存度が高い PadrinoHelper のバージョンを上げることに成功(めっちゃ大変だった!!!、!)
      • 自分としては Rails 3 を Rails 4 に上げるくらいの働きをしたと思ってる😎

locale が i18n.gem がサポートしてるやつじゃないと 500 エラーになるという問題があって、こちらも自分のブログでは直してあるので修正する Pull Request を出したい。

P_BLOG のときもそうだったけど、どうも自分はユーザーの少なくなってきた CMS を細々と改造して使っていくのが好きみたいだ。このブログの開発・運用から学ぶことも多くて仕事にも役立っているので、まだしばらくは使い続けていきたいと思う。 Lokka は永遠に不滅です。


  1. 調べてみたらすでに lokka.slack.com は存在するみたいなんだけどこれって Lokka for CMS のやつですかね? 

ブログに人気記事を表示するようにしてみた。やり方はめっちゃ雑で、 Nginx の access_log を集計して Bot や Crawler 、 RSS Reader からのアクセス、画像や CSS 、 JS ファイルへのアクセスを除外してアクセス数を集計して結果をテキストファイルに出力し、 Ruby で parse してフッターに表示してる。こんな感じ。

人気のエントリー

仕組み

こんな感じのシェルスクリプトを置いて cron で実行してる。

#!/bin/bash

zcat -f /path/to/access.log* \
  | grep -vE 'useragent:.+?(bot|Feed\s?Fetcher|Crawler|Fastladder|Feed|Ruby|Aol\sReader|proximic|Hatena\sAntenna|Mediapartners-Google|subscribe)' \ # bot や Crawler を除外
  | cut -f5 | sed -e 's/request_uri://' \ # request_path だけ抜き出し
  | grep -vE '(favicon\.ico|index\.atom|\.js|\.json|\.css|\.jpe?g|\.png|\.gif|\.txt|\.php|\/admin|^\-$|^\/$)' \ # HTML 以外へのリクエストを除外
  | sort | uniq -c | sort -nr | head -100 | sed -r 's/^[ \t]+//g' \ # 集計して上位 100 件だけを得る
  | tee /path/to/public/access-ranking.txt # テキストファイルに書き出し

zcat -f しているのは gzip 済みのログファイルも cat したいため。このやり方だと現存するログファイルからしか調べられないので logrotate で設定している期間(自分の場合は 30 日)の集計しかできない。またサーバーを複数並べて運用しているようなアプリケーションではアクセスログがばらけるのでこんな雑なやり方は使えない。

Nginx のログのフォーマットは LTSV にしているので grep でのフィルタリングがやりやすい。まず User-Agent で bot っぽいアクセスを除外したあと、ログから request_uri のフィールドだけを切り出し、静的ファイルなどへのアクセスを除外したあと sort -> uniq -c -> sort -nr してる。

Ruby ( Lokka ) の方では以下のようなコードを書いて access-ranking.txt を読み込んでる。これをやらないと記事のタイトル表示やリンクが生成できないため。

class Entry
  class << self
    def popular(count = 5)
      access_ranking = File.open(File.join(Lokka.root, 'public', 'access-ranking.txt'))
      slugs = {}
      access_ranking.each.with_index(1) do |line, index|
        access_count, path = *line.split(" ")
        slug = path.split("/")[-1]
        slugs[access_count] = slug
        break if index == count
      end
      all(slug: slugs.values, limit: count).sort_by {|entry| slugs.values.index(entry.slug) }
    end
  end
end

フッターは適度にキャッシュしているのでスピードはそんなに遅くならない。

感想

アクセスランキングを表示してみて、意外と Twitter やはてブでバズった記事へのアクセスは継続的には多くないことがわかる。最近だと ARC'TERYX や SIERRA DESIGNS のパーカーの記事が人気があるようだ。これはおそらく寒くなってきててそういうキーワードで検索してたどり着く人が多いのだろう。 GarageBand でのアナログレコード録音の方法は前から人気ある。はてブとかは大して付いてないが、 Yahoo! 知恵袋や 2ch の過去記事・まとめサイトからのアクセスが多いようである。謎なのが痔ろうの記事へのアクセス数の多さ。痔ろうの症状・治療方法を結構詳細に書いたので Google 先生が良記事判定してくれているのかも知れない。家の記事ははてブでバズって 2000 ブックマーク以上付いたが、それでもやっと 5 位という感じ。バズっても短期的なアクセスしか得ることができず(人の噂も 75 日!!!、!)、長期的に細々とトラフィックを集めるためには特定の属性の人にだけ響く詳細な記事を書くのがよいのかもしれない。

旅行や街歩きに出かけたとき、移動軌跡が地図上に表示されるとその時何をしていたのかが思い出せて便利になると思う。例えば知らない街に旅行に行った数日後にこんな状況になったことはないだろうか。「昼飯を食ったあとに食器屋に行って良い皿を見たんだけど迷って買わなかったんだよなぁ、あれはなんて店なんだろう、ネット通販やってるかな🤔」。自分はよくある。こういうときに位置情報付きの行動ログが残ってれば店名を探り当てやすくなるし、目当ての商品にたどり着ける可能性が高まる。

自分は今のところ Twitter 、 Instagram への投稿と Swarm へのチェックインを IFTTT 経由で Day One に自動投稿しているので、それらの情報が手がかりにならないでもないが、それでもそのときの自分が Twitter なり Instagram なり Swarm なりに何らかの情報を投稿していないと Day One には何も残らない。やはり何らかの行動ログ的なものが記録されている方がいい。

とはいえ旅行や町歩きのときに意識的に行動ログの取得を開始するのは難しいと思う。旅行中、街を散策する前に iPhone を取り出して何らかのアプリケーションを起動し「開始」ボタンを押したりできるだろうか? 気がついたときには随分行動してしまったあとで「今から記録をとってもね😔」という感じになるのが現実だろう。この手のツールはバックグラウンドで勝手にログをとっといてくれるのが重要だと思う。

何か良いアプリはないものかと App Store で調べてみたところ、 SilentLog というやつが見つかった。評価は良かったがバッテリーの減りが速いとある。インストールしてみたところ確かにバッテリーの減りが速くなる。よくよく考えたら Google Maps のタイムラインや Day One でもバックグラウンドでの位置情報の取得はやってるので似たようなことを複数のアプリでやるのは効率が悪い。

とはいえ移動したログを一番かっちょよく見られるのは SilentLog だった。こんな風に一日のタイムラインを手軽に振り返ることができる。朝家を出て電車に乗り、9時から18時まで職場で仕事をしてなどがすぐわかる。休みの日に出かけた場所も意識せずに記録されていく。その日 iPhone で撮った写真も表示される。

SilentLog SilentLog SilentLog

特に良いのが1日の行動ログをほかのアプリやサービスに書き出せること。 Google Maps のタイムラインはエクスポート機能がないし、 Day One の Activity Feed は一定期間たつと消えてしまうので残しておきたいなら意識して記事を作成しないといけない上に、記事にしたとしても位置情報の履歴は残らない。

Google Maps Timeline Day One Activity Feed

その点、 SilentLog は行動ログが地図上に表示された画像を手軽に書き出せる。とても便利なのだが、この機能を使うには App 内課金で有料チケットを購入する必要があるようだった。 240円/30日で年払いだと 1900 円とのこと。

位置情報の収集や書き出し、気持ち悪いと感じる人には気持ち悪いこと極まりないだろう。またもし個人を特定できるかたちで流出して誰かに見られてしまったら非常にまずい(浮気中の人など)。加えて SilentLog を作っている会社 はユーザーの位置情報データを売っているようである。おそらく「年収 XXX 万円くらいの 30-40 代の男性が毎週何曜の何時に新宿駅に何人くらいいる」みたいなデータをデパートとかに売ったりするのだろう。 DMP の位置情報版といったところか。個人を特定できないようにはなってると思うがあまりよい気持ちはしない。

とはいえ自分でも忘れるような行動の履歴をスマートフォンアプリが取っておいてくれてあとで振り返られるというのは絶対に便利だと思う。日記は書いておくと便利だけど書くのが非常に面倒くさい。行動のログから自動で簡易的な日記にしてくれてバッテリーの消耗が穏やかなアプリがあったらうれしいなぁ。(結構個人のプライバシーを尊重する) Apple がそういうのつくってくれないかな。

先週、 Alfred で通貨換算をやる方法(💵 Alfred で手軽に通貨換算 💴 )について書いたけどその続き。

Numi icon

Numi という Alfred に似たソフトがある。 Soulver と違い多言語対応はしておらず、自然言語による入力は日本語未対応なのだが、 Soulver もまともに使えるのは英語利用時だけなので実質的には機能に差がない。タブを使えたりする分、 Numi の方が高機能かも知れない。さらに Numi は今のところ無料である(ベータ版のため)。ベータ版とはいえほとんど困ることなく利用できている。

Numi うどん

このソフトがすごいのが、 Alfred Workflow を入れると Numi による計算を Alfred から n <式> と入力することでできるようになるということ(ただし Numi.app を起動しておく必要あり)。こんな感じ。

Numi Currency conversion

いちいち Numi.app に切り替えて新規ドキュメントを作成し、ということをしなくて済む。とても手軽に計算できる。通貨換算のほかに、単位の換算もできる。 Google Fincance の Converter サービスを使うやり方よりも高機能だ。

Numi Unit conversion

このソフトを知ってから Soulver を使う機会がめっきり減ってしまった。 Alfred からも計算できるし、結果を残しておきたいような複雑な計算をするときは Numi.app に切り替えて使うこともできる。文章を書いたり考え事をしているときにささっと計算できて頭の切り替えコストが低い。 AWS を利用しているプログラマー諸氏なら EC2 インスタンスの料金なんかを計算するときにもとても便利だと思う。おすすめです。


この記事は Alfred 3 Advent Calendar 2017 - Adventar 12 日目の記事でした。明日は(明日も) @tsurusuke さんです。