おいしい健康 開発者ブログ

株式会社おいしい健康で働くエンジニア・デザイナーが社内の様子をお伝えします。

ペアプログラミングを導入して分かったこと

こんにちは。おいしい健康エンジニアの近藤です。

WWDC2018で他のエンジニアと情報交換させていただいた中で、他社ではペアプログラミングを導入して開発速度が2倍に上がったというお話を聞き、おいしい健康でも少しずつペアプログラミングを導入しています。今回はペアプログラミングを導入してみて分かったペアプログラミングのメリット、デメリットを紹介いたします。

f:id:oishi-kenko:20180817173215j:plain ※画像はペアプロの相方が出社するまでの間の場所取りとして活躍している人形です。

おいしい健康でのペアプログラミングのやり方

おいしい健康では、複雑なモデル周りの設計をするときや、比較的難易度の高いアプリの画面を実装するときに、ペアプログラミングをしながら最初に実装方針を決めています。
また、最近では、毎日エンジニア同士でランダムにペアを決めて、ペア同士は必ず隣に座るという運用も始めました。これによってフリーアドレスでもエンジニア同士で気軽に相談できるようにしました。こちらはペアプログラミングに限らず、テディベア効果も狙ったものになります。

ペアプログラミングのメリット

設計や実装のスピードが格段に上がる

ペアプログラミング中は途中で集中力が途切れたり、Slackを見たりする暇がなく、強制的にプログラミングが進むため、格段にスピードが上がります。

レビューによる手戻りが少ない

最初にペアプログラミングで実装方針を決めているため、大きな手戻りが発生することがありません。レビューでは細かい修正点だけを見ていけばよく、レビュー時間の短縮にも繋がります。

知識の底上げ、新メンバーのフォローアップ

新メンバーが新しく開発に入るタイミングでペアプログラミングをすることで、最初に詰まりやすいポイントに対してすぐにフォローすることができ、エンジニア全体の知識の底上げにも繋がります。

ペアプログラミングのデメリット

ナビゲータ役のエンジニアへの負担

弊社のペアプログラミングでは主にベテランエンジニアがナビゲータ、新メンバーがドライバーとして行っていることが多いため、ベテランエンジニアに負担が行ってしまうことがあります。こちらはペアプログラミングを続けていくことによって、エンジニア全体の知識が平準化されることで徐々にナビゲータ役のエンジニアが増えていけば解決されるかも知れません。

ペア席が取りづらい

おいしい健康ではフリーアドレスを導入しているため、外部ディスプレイ席を隣同士で2台確保しづらい場合があります。またフリーアドレスのメリットも少し薄れてしまいます。

単純作業の場合には向かない

比較的難易度の高い設計や実装の場合はペアプログラミングが効果を発揮しますが、ただ手を動かすだけの単純作業の場合は、一人で作業をした方が効率が良い場合があります。

さいごに

いくつかデメリットもありましたが、全体的にはペアプログラミングを導入したあとの方が実装スピードも上がり、エンジニア全体の知識の底上げにも繋がり良かったと思います。 開発速度が2倍とまでは行きませんでしたが、開発効率や品質向上の施策の一つとしてはオススメです。

参加してわかった WWDC ラボでの過ごしかた

はじめに

エンジニアの濵田です。昨年に引き続き、カリフォルニア州サンノゼで開かれた WWDC 2018 に参加してきました。

前回の WWDC 2017 参加で悔しかったことの一つに、「ラボ」と呼ばれるイベントにほとんど顔をださなかったことがあります。あとから、「ラボには絶対行ったほうがいい」という話をきいて、惜しいことしたなと残念でした。幸運にも再度 WWDC に参加することができたので、今回はラボ参加を優先して5日間を過ごしました 💪。

効率よくラボを楽しむにはいくつかコツがあることもわかったので、参加する上でのチップスや会場での流れを簡単にまとめます。

続きを読む

Linuxでプロセスごとに開いているファイルディスクリプタの数を調べる

こんにちは 山下(@tomorrowkey) です。

Linuxをセットアップする時にすぐに数を大きくするfile descriptorの数ですが、どのプロセスがどのくらいファイルを開いているか確認する方法を知らなかったので調べました。 早速最終的なコマンドから。例えばunicornが開いているファイルの数を調べるならこんな感じ。

for i in $(ps aux | grep "[u]nicorn" | awk '{print $2}'); do  ls /proc/$i/fd | wc -l; done

開いているファイルディスクリプタ/proc/$PID/fdにあるので、数を数えればOKです。
調べたことなど書きます。

dockerでfile descriptorの数を設定する

検証にはdockerを使います。 起動時に--ulimitというオプションを渡すだけで制限が可能です。 検証用なので極端に小さく設定します。

$ docker run --ulimit="nofile=64" --rm -it ruby:2.4.2-jessie /bin/bash
root@ea6e45c79d10:/# ulimit -n
64

irbを起動しただけのfile descriptorの数を調べる

プロセスIDを調べます。

# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  20244  3208 pts/0    Ss   00:35   0:00 /bin/bash
root         7  0.1  0.1  20244  3196 pts/1    Ss   00:35   0:00 /bin/bash
root        14  1.6  0.4  47672  9632 pts/0    Sl+  00:35   0:00 irb
root        16  0.0  0.1  17500  2072 pts/1    R+   00:35   0:00 ps aux

file descriptorの数を調べます。

# ls /proc/14/fd | wc -l
9

9個でした。

今度はlsofで数を確認します。 rubyのdockerイメージにはlsofがインストールされていないので、インストールします。

# apt-get update && apt-get install -y lsof
# lsof -p 14
COMMAND PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
irb      14 root  cwd    DIR   0,46     4096   52302 /
irb      14 root  rtd    DIR   0,46     4096   52302 /
irb      14 root  txt    REG  254,1   146248 3145942 /usr/local/bin/ruby
irb      14 root  mem    REG  254,1   171800 1444564 /lib/x86_64-linux-gnu/libtinfo.so.5.9
irb      14 root  mem    REG  254,1   151120 1444515 /lib/x86_64-linux-gnu/libncurses.so.5.9
irb      14 root  mem    REG  254,1   297040 1444546 /lib/x86_64-linux-gnu/libreadline.so.6.3
irb      14 root  mem    REG  254,1   260648 3146915 /usr/local/lib/ruby/2.4.0/x86_64-linux/readline.so
irb      14 root  mem    REG  254,1   292976 3146919 /usr/local/lib/ruby/2.4.0/x86_64-linux/stringio.so
irb      14 root  mem    REG  254,1    33400 3146874 /usr/local/lib/ruby/2.4.0/x86_64-linux/enc/trans/transdb.so
irb      14 root  mem    REG  254,1   183168 3146831 /usr/local/lib/ruby/2.4.0/x86_64-linux/enc/encdb.so
irb      14 root  mem    REG  254,1  1738176 1444479 /lib/x86_64-linux-gnu/libc-2.19.so
irb      14 root  mem    REG  254,1  1051056 1444509 /lib/x86_64-linux-gnu/libm-2.19.so
irb      14 root  mem    REG  254,1    35176 1444487 /lib/x86_64-linux-gnu/libcrypt-2.19.so
irb      14 root  mem    REG  254,1    14664 1444492 /lib/x86_64-linux-gnu/libdl-2.19.so
irb      14 root  mem    REG  254,1   137384 1444543 /lib/x86_64-linux-gnu/libpthread-2.19.so
irb      14 root  mem    REG  254,1 15803280 3145980 /usr/local/lib/libruby.so.2.4.2
irb      14 root  mem    REG  254,1   140928 1444461 /lib/x86_64-linux-gnu/ld-2.19.so
irb      14 root    0u   CHR  136,0      0t0       3 /dev/pts/0
irb      14 root    1u   CHR  136,0      0t0       3 /dev/pts/0
irb      14 root    2u   CHR  136,0      0t0       3 /dev/pts/0
irb      14 root    3r  FIFO   0,10      0t0   54293 pipe
irb      14 root    4w  FIFO   0,10      0t0   54293 pipe
irb      14 root    5r  FIFO   0,10      0t0   54294 pipe
irb      14 root    6w  FIFO   0,10      0t0   54294 pipe
irb      14 root    7u   CHR  136,0      0t0       3 /dev/pts/0
irb      14 root    8u   CHR  136,0      0t0       3 /dev/pts/0

FDカラムに注目すると…

  • cwd -> current working directory
  • rtd -> root directory
  • txt -> program text
  • mem -> memory-mapped device
  • \[0-9]+r\ -> read-onlyで開いているファイル
  • \[0-9]+w\ -> write-onlyで開いているファイル
  • \[0-9]+u\ -> read-writeで開いてるファイル

lsofでfile descriptorの数を調べるにはFDが数字から始まるものを数えるとよさそう。 その他の値についてはこちら https://linux.die.net/man/8/lsof

lsofを使えばどのファイルを開いているかまで分かります。

たくさんのファイルを開いてfile descriporの数を調べる

スクリプトでファイルをたくさん開きます。

files = []
(1..(2 ** 10)).each do |i|
  puts i
  files << File.open("#{i}", "w")
  sleep 0.5
end
1
2
3
# ...
54
55
56
Errno::EMFILE: Too many open files @ rb_sysopen - 56
    from (irb):4:in `initialize'
  from (irb):4:in `open'
  from (irb):4:in `block in irb_binding'
    from (irb):2:in `each'
  from (irb):2
  from /usr/local/bin/irb:11:in `<main>'

無事ファイルの開き過ぎでエラーを吐きました。 irbをそのままに、違うターミナルでfile descriptorの数を調べます。

# for i in $(ps aux | grep "[i]rb" | awk '{print $2}'); do  ls /proc/$i/fd | wc -l; done
64

lsofで確認します。

# lsof -p $(ps aux | grep "[i]rb" | awk '{print $2}' | tr -d " ")
COMMAND PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
irb      14 root  cwd    DIR   0,46     4096   52302 /
irb      14 root  rtd    DIR   0,46     4096   52302 /
irb      14 root  txt    REG  254,1   146248 3145942 /usr/local/bin/ruby
irb      14 root  mem    REG  254,1   171800 1444564 /lib/x86_64-linux-gnu/libtinfo.so.5.9
irb      14 root  mem    REG  254,1   151120 1444515 /lib/x86_64-linux-gnu/libncurses.so.5.9
irb      14 root  mem    REG  254,1   297040 1444546 /lib/x86_64-linux-gnu/libreadline.so.6.3
irb      14 root  mem    REG  254,1   260648 3146915 /usr/local/lib/ruby/2.4.0/x86_64-linux/readline.so
irb      14 root  mem    REG  254,1   292976 3146919 /usr/local/lib/ruby/2.4.0/x86_64-linux/stringio.so
irb      14 root  mem    REG  254,1    33400 3146874 /usr/local/lib/ruby/2.4.0/x86_64-linux/enc/trans/transdb.so
irb      14 root  mem    REG  254,1   183168 3146831 /usr/local/lib/ruby/2.4.0/x86_64-linux/enc/encdb.so
irb      14 root  mem    REG  254,1  1738176 1444479 /lib/x86_64-linux-gnu/libc-2.19.so
irb      14 root  mem    REG  254,1  1051056 1444509 /lib/x86_64-linux-gnu/libm-2.19.so
irb      14 root  mem    REG  254,1    35176 1444487 /lib/x86_64-linux-gnu/libcrypt-2.19.so
irb      14 root  mem    REG  254,1    14664 1444492 /lib/x86_64-linux-gnu/libdl-2.19.so
irb      14 root  mem    REG  254,1   137384 1444543 /lib/x86_64-linux-gnu/libpthread-2.19.so
irb      14 root  mem    REG  254,1 15803280 3145980 /usr/local/lib/libruby.so.2.4.2
irb      14 root  mem    REG  254,1   140928 1444461 /lib/x86_64-linux-gnu/ld-2.19.so
irb      14 root    0u   CHR  136,0      0t0       3 /dev/pts/0
irb      14 root    1u   CHR  136,0      0t0       3 /dev/pts/0
irb      14 root    2u   CHR  136,0      0t0       3 /dev/pts/0
irb      14 root    3r  FIFO   0,10      0t0   54293 pipe
irb      14 root    4w  FIFO   0,10      0t0   54293 pipe
irb      14 root    5r  FIFO   0,10      0t0   54294 pipe
irb      14 root    6w  FIFO   0,10      0t0   54294 pipe
irb      14 root    7u   CHR  136,0      0t0       3 /dev/pts/0
irb      14 root    8u   CHR  136,0      0t0       3 /dev/pts/0
irb      14 root    9w   REG  254,1        0 3149355 /1
irb      14 root   10w   REG  254,1        0 3149365 /2
irb      14 root   11w   REG  254,1        0 3149369 /3
irb      14 root   12w   REG  254,1        0 3149372 /4
irb      14 root   13w   REG  254,1        0 3149422 /5
irb      14 root   14w   REG  254,1        0 3149423 /6
irb      14 root   15w   REG  254,1        0 3149425 /7
irb      14 root   16w   REG  254,1        0 3149455 /8
irb      14 root   17w   REG  254,1        0 3149457 /9
irb      14 root   18w   REG  254,1        0 3149458 /10
irb      14 root   19w   REG  254,1        0 3149459 /11
irb      14 root   20w   REG  254,1        0 3149460 /12
irb      14 root   21w   REG  254,1        0 3149461 /13
irb      14 root   22w   REG  254,1        0 3149462 /14
irb      14 root   23w   REG  254,1        0 3149463 /15
irb      14 root   24w   REG  254,1        0 3149464 /16
irb      14 root   25w   REG  254,1        0 3149465 /17
irb      14 root   26w   REG  254,1        0 3149466 /18
irb      14 root   27w   REG  254,1        0 3149467 /19
irb      14 root   28w   REG  254,1        0 3149468 /20
irb      14 root   29w   REG  254,1        0 3149469 /21
irb      14 root   30w   REG  254,1        0 3149470 /22
irb      14 root   31w   REG  254,1        0 3149471 /23
irb      14 root   32w   REG  254,1        0 3149472 /24
irb      14 root   33w   REG  254,1        0 3149473 /25
irb      14 root   34w   REG  254,1        0 3149474 /26
irb      14 root   35w   REG  254,1        0 3149475 /27
irb      14 root   36w   REG  254,1        0 3149476 /28
irb      14 root   37w   REG  254,1        0 3149477 /29
irb      14 root   38w   REG  254,1        0 3149478 /30
irb      14 root   39w   REG  254,1        0 3149479 /31
irb      14 root   40w   REG  254,1        0 3149480 /32
irb      14 root   41w   REG  254,1        0 3149481 /33
irb      14 root   42w   REG  254,1        0 3149482 /34
irb      14 root   43w   REG  254,1        0 3149483 /35
irb      14 root   44w   REG  254,1        0 3149484 /36
irb      14 root   45w   REG  254,1        0 3149485 /37
irb      14 root   46w   REG  254,1        0 3149486 /38
irb      14 root   47w   REG  254,1        0 3149487 /39
irb      14 root   48w   REG  254,1        0 3149488 /40
irb      14 root   49w   REG  254,1        0 3149489 /41
irb      14 root   50w   REG  254,1        0 3149490 /42
irb      14 root   51w   REG  254,1        0 3149491 /43
irb      14 root   52w   REG  254,1        0 3149492 /44
irb      14 root   53w   REG  254,1        0 3149493 /45
irb      14 root   54w   REG  254,1        0 3149494 /46
irb      14 root   55w   REG  254,1        0 3149495 /47
irb      14 root   56w   REG  254,1        0 3149496 /48
irb      14 root   57w   REG  254,1        0 3149497 /49
irb      14 root   58w   REG  254,1        0 3149498 /50
irb      14 root   59w   REG  254,1        0 3149499 /51
irb      14 root   60w   REG  254,1        0 3149500 /52
irb      14 root   61w   REG  254,1        0 3149501 /53
irb      14 root   62w   REG  254,1        0 3149502 /54
irb      14 root   63w   REG  254,1        0 3149503 /55

64個ファイルを開いていることが確認できました。

以上です。

Swift でドロップキャップな TextView を作る

こんにちは、関口@tanukiti1987 です。

今、おいしい健康では iOS アプリを開発しています。 そこで、ドロップキャップを使った表現が何箇所か登場する(予定)なのですが、 UITextView を純粋に使っただけでは、そういった表現が出来ずに、ちょっとした実装が必要になってきます。

今回は、そのちょっとした実装方法をご紹介していきます。

ドロップキャップ(drop-cap) とは

そもそも、ドロップキャップがなんなんだ。という話があると思います。 僕自身も実装を担当することになり、この言葉を知りました。

ドロップキャップは、こういうやつです。

f:id:oishi-kenko:20171115223923p:plain

1文字目が大きく表示され、以降1文字目を回り込むように文章が展開されていく例のやつです。

css の世界では

p:first-letter{
    font-size: 2em;
}

という指定をしてあげることで、このドロップキャップの表現が可能です。 いつもは嫌いだけど、この時ばかりは最高じゃないか、CSS

Swift ではどうやるのか

ここまで紹介しましたが、さきにも述べたように swift では :first-letter みたいなベンリ attribute はないので、自前でがんばっていくことになります。

使っていくのは、 UITextView UIImageView の2つです。

  • 一文字目を UIImageView としてコードで生成する
  • TextKit の exclusion path を指定することで 作った UIImageView の周りに文字を回り込ませる

この2点を行うことで、ドロップキャップを実現していきます。

文字を UIImageView として生成する

文字を画像として生成するには UIGraphics を使って、描画していきます。

今回は、以下のような関数を作ってみました。

@IBOutlet weak var dropCapImageView: UIImageView!

func createDropCapImage(text: String) -> UIImage {
    let size = CGSize(width: dropCapImageView.frame.size.width, height: dropCapImageView.frame.size.height)
    UIGraphicsBeginImageContext(size)

    let fontSize: UIFont = UIFont.systemFont(ofSize: 42.0)

    let style: NSMutableParagraphStyle = NSMutableParagraphStyle()
    style.alignment = NSTextAlignment.center
    style.lineBreakMode = NSLineBreakMode.byClipping
    style.minimumLineHeight = 30

    let attributes = [
        NSAttributedStringKey.font: fontSize,
        NSAttributedStringKey.paragraphStyle: style,
        NSAttributedStringKey.foregroundColor: UIColor.black,
        NSAttributedStringKey.backgroundColor: UIColor.clear
    ]

    text.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height), withAttributes: attributes)
    let image: UIImage = UIGraphicsGetImageFromCurrentImageContext() as UIImage!
    UIGraphicsEndImageContext()

    return image
}

font 情報を諸々設定したあとで text.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height), withAttributes: attributes) で文字を書き、 UIGraphicsGetImageFromCurrentImageContext() as UIImage! で画像を生成しています。

TextKit の exclusion path で文字を回り込ませる

先ほど作った画像を、UITextView の上に載せ、文字を回り込ませていきます。

こんな感じにviewを重ねます。

f:id:oishi-kenko:20171115230254p:plain

constraint などはよしなに調整してください。 今回のアプローチの少しイケてないところは、ドロップキャップの大きさを変えたり、テキストの attributes を変えると、レイアウトの微調整も必要になってくるところですね。。

ともあれ、他の方法もないので、この方法で突き進みます。

xibファイルが用意できたら、文字を回り込ませるような処理を書いていきます。

@IBOutlet weak var textView: UITextView!
@IBOutlet weak var dropCapImageView: UIImageView!
let text: String = "吾輩(わがはい)は猫である。名前はまだ無い。どこで生れたかとんと見当(けんとう)がつかぬ。"

textView.text = String(text[text.index(text.startIndex, offsetBy: 1)...])
setTextAttributes()

dropCapImageView.image = createDropCapImage(text: String(text[text.startIndex]))
let exclusionRect = CGRect(x: 0, y: 0, width: dropCapImageView.frame.size.width, height: dropCapImageView.frame.size.height)
let path: UIBezierPath = UIBezierPath(rect: exclusionRect)
textView.textContainer.exclusionPaths = [path]

また、ここで忘れてはいけないのは、1文字目は既に画像としてViewに含ませているので、2文字目以降の文字を UITextView に乗せていくというところです。

UITextView に2文字目以降の文字を代入し、 UIImageView に1文字目の画像を載せていきます。 最後に画像の周辺にテキストを回り込ませるための exclution path を生成し、 UITextView.textContainer.exclusionPaths に登録します。

ここまでいけば、完成です!

このあたりのコードを実装していけば、冒頭ご紹介したような

f:id:oishi-kenko:20171115232122p:plain

このような表示ができるはずです。

おしまいに

今回ご紹介したコードはこちらにおいておきました。

github.com

Xcode9.1 で作っていますので、古くて動かなくなったらご愛嬌ということで。 是非、みなさまのご参考になればと思います。

おしまいのおしまいに

おいしい健康では、一緒に Swift で iOS アプリを作ってくれる仲間を募集しています! 新しいプロジェクトであり、会社のコアプロダクトを作っているところですので、自由度も高く、もりもり開発していけます。

何より、この記事の内容を見て、思うところがある方とはぜひお話してみたいところです ( ̄ー ̄)

Swift初心者の関口がお送りしました〜。

Apache Solrでサジェスト機能を実装する

こんにちは。おいしい健康に入社して2ヶ月目の44akiです。

今回、おいしい健康のアプリで検索窓の入力サジェスト機能を導入する機会があり、 Apache SolrのSpellCheckComponentを使って実現しました。 おいしい健康のサービス全体でいえば小さな機能ではありますが、前職では検索エンジンベンダーでエンジニアをやっていたので、こういうところには無駄に拘ってしまいます。

この記事の内容は、Apache Solrのバージョン4.10.4で動作確認をしています。

f:id:oishi-kenko:20171107173455p:plain

候補語のデータについて

入力サジェスト機能を実現する場合、その候補語となるデータの取得元は大きく分けて2つあり、それぞれにメリット、デメリットがあります。

  1. 検索対象のデータから形態素解析で取得する
    検索対象のデータをmecab等の形態素解析エンジンで分かち書きして、候補語のデータとして使用する方法です。おいしい健康の場合はレシピ情報がこのデータにあたります。検索対象データを形態素解析して取得した候補語のため、必ず検索にヒットする候補語が取得できるというメリットがありますが、形態素解析エンジンの辞書データをもとに分かち書きされるため、辞書データの精度が低いと、候補語が名詞の途中で途切れたり、意図しない候補語が表示される可能性があります。

  2. 検索ログから取得する
    ユーザが実際に検索したキーワードをログとして保存しておき、候補語のデータとして使用する方法です。検索ログから候補語を取得しているため、ユーザーが実際の入力するようなキーワードをサジェストすることが出来ます。また、過去1ヶ月間の検索ログなど、集計対象の期間を絞ることで、キーワードのトレンドを反映し、秋ならサンマ、冬に近づくとサバを候補語の上位に表示するなどが実現できるようになります。ただし、検索ログベースのため、検索ログを取得する環境を構築する必要がある、検索にヒットしない候補語を除外する必要がある等、実装のコストは増えてしまいます。

今回は2の検索ログベースでサジェスト機能を実装しました。

schema.xml

先ずはApache Solrのスキーマを定義していきます。
候補語の表示用として name フィールドを定義します。

<fields>
    <field name="_version_" type="long" indexed="true" stored="true"/>
    <field name="id" type="string" multiValued="false" stored="true" indexed="true"/>
    <field name="name" type="string" multiValued="false" stored="true" indexed="true"/>
</fields>

また、検索用に text_ja_romaji の型を定義しています。 ローマ字に変換して検索を行うことで、入力途中のキーワードでもサジェストが行えるようにしています。

<fieldType name="text_ja_romaji" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="false">
  <analyzer>
    <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
    <filter class="solr.CJKWidthFilterFactory"/>
    <filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="true"/>
    <filter class="solr.ShingleFilterFactory" minShingleSize="2" maxShingleSize="99" outputUnigramsIfNoShingles="true" tokenSeparator=""/>
    <filter class="solr.LowerCaseFilterFactory"/>
  </analyzer>
</fieldType>

全体的な流れとしては、 JapaneseTokenizerFactory でKuromojiによる形態素解析を行い、 JapaneseReadingFormFilterFactory のオプションでuseRomajiを指定することで形態素解析で取得した読みをローマ字に変換します。最後に ShingleFilterFactory により形態素解析で分割された文字列を連結します。

ローマ字変換のイメージ>
おいしい健康→おいしい 健康→oishi kenko→oishikenko

solrconfig.xml

次にサジェスト用のsearchComponentとrequestHandlerを定義します。

今回はSolrの solr.SpellCheckComponent を使います。

<!-- Auto Complete component -->
<searchComponent class="solr.SpellCheckComponent" name="autocomplete_ja">
    <lst name="spellchecker">
      <str name="name">autocomplete_ja</str>
      <str name="classname">org.apache.solr.spelling.suggest.Suggester</str>
      <str name="lookupImpl">org.apache.solr.spelling.suggest.fst.AnalyzingLookupFactory</str>
      <str name="buildOnCommit">true</str>
      <str name="comparatorClass">score</str>
      <str name="field">name</str>
      <str name="suggestAnalyzerFieldType">text_ja_romaji</str>
      <bool name="exactMatchFirst">true</bool>
    </lst>
    <str name="queryAnalyzerFieldType">text_ja_romaji</str>
</searchComponent>

suggestAnalyzerFieldTypeとqueryAnalyzerFieldTypeに先程定義したtext_ja_romajiを指定することでローマ字での前方一致検索を行っています。

comparatorClassで候補語のソート順を指定します。score、freq、もしくは独自で作成したcomparatorClassが指定可能です。今回のように検索ログベースで、検索回数順にソートする場合は、通常はcomparatorClassを作成する必要がありますが、今回は前述のnameフィールドに「キーワード:検索回数」のようなフォーマットでインデックスし、アプリ側で取得したキーワードと検索回数をもとに検索回数順にソートすることで、簡易的に検索回数順ソートを実装しました。

最後に、上記のsearchComponentを「/autocomplete_ja」のパスで受けられるように、requestHandlerを定義します。

<!-- Auto Complete handler -->
<requestHandler name="/autocomplete_ja" class="org.apache.solr.handler.component.SearchHandler">
    <lst name="defaults">
      <str name="spellcheck">true</str>
      <str name="spellcheck.dictionary">autocomplete_ja</str>
      <str name="spellcheck.collate">true</str>
      <str name="spellcheck.count">100</str>
      <str name="spellcheck.onlyMorePopular">true</str>
    </lst>
    <arr name="components">
      <str>autocomplete_ja</str>
    </arr>
</requestHandler>

終わりに

今回はApache Solrの定義の部分のみの説明でしたが、実際には、Fluentd、BigQueryによる検索ログ取得、検索にヒットしない候補語の削除等も行いました。Apache SolrのSpellCheckComponentを使うことで比較的簡単に入力サジェストの実装を行うことができました。

MySQLにユーザーと権限を設定する

MySQLにユーザーを追加する機会がありましたので、ユーザー作成と権限追加について復習しました。
MySQL 5.6で動作確認しています。

dockerを使い、動作確認用のコンテナを起動します。 簡単に捨てられる環境が使えるのは便利ですね。

$ docker run -e MYSQL_ROOT_PASSWORD=password -p 13306:3306 --rm mysql:5.6

一通りログが流れきったら使えるようになります。 mysqlコマンドで母艦からmysqlにアクセスします。

$ mysql -u root -p -h 127.0.0.1 -P 13306

まずはユーザー一覧を表示します。

mysql> select Host, User from mysql.user;
+-----------+------+
| Host      | User |
+-----------+------+
| %         | root |
| localhost | root |
+-----------+------+

何も設定していないのでrootしかいない状態です。検証用のユーザーを作成します。

mysql> create user writable identified by 'hogehogehoge';

hogehogehogewritableユーザーのパスワードです。ユーザーを作成したので権限を確認してみます。

mysql> show grants for writable;
+---------------------------------------------------------------------------------------------------------+
| Grants for writable@%                                                                                   |
+---------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'writable'@'%' IDENTIFIED BY PASSWORD '*E8DD65E018E30F27D962FB9BFA2F4E8206DC3AF8' |
+---------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

USAGEは権限なしを表します。この辺に権限の説明があります。 https://dev.mysql.com/doc/refman/5.6/ja/privileges-provided.html

rootの権限は*.*を対象にALL PRIVILEGESをもっているので、すべての権限をもっています。

mysql> show grants for root;
+--------------------------------------------------------------------------------------------------------------------------------+
| Grants for root@%                                                                                                              |
+--------------------------------------------------------------------------------------------------------------------------------+
| GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY PASSWORD '*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19' WITH GRANT OPTION |
+--------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

権限を付与するためのデータベースを作成します。

mysql> create database hoge;
Query OK, 1 row affected (0.00 sec)

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| hoge               |
| mysql              |
| performance_schema |
+--------------------+
4 rows in set (0.00 sec)

hogeデータベースが作成されました。

この時点でwritableユーザーでログインしてデータベース一覧を表示してもinformation_schemaしか表示されません。

$ mysql -u writable -p -h 127.0.0.1 -P 13306
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
+--------------------+
1 row in set (0.00 sec)

writableユーザーにhogeデータベースの全ての権限を付与します。

mysql> grant all privileges on hoge.* to writable@'%';
Query OK, 0 rows affected (0.00 sec)

mysql> show grants for writable;
+---------------------------------------------------------------------------------------------------------+
| Grants for writable@%                                                                                   |
+---------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'writable'@'%' IDENTIFIED BY PASSWORD '*E8DD65E018E30F27D962FB9BFA2F4E8206DC3AF8' |
| GRANT ALL PRIVILEGES ON `hoge`.* TO 'writable'@'%'                                                      |
+---------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

これでwritableユーザーが自由にhogeデータベースを操作できるようになりました。 再びwritableユーザーでログインしてデータベース一覧を見るとhogeデータベースが表示されます。

$ mysql -u writable -p -h 127.0.0.1 -P 13306
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| hoge               |
+--------------------+
2 rows in set (0.00 sec)

テーブルを作り、レコードをいくつか追加します。

mysql> create table users(
    ->   id int auto_increment not null primary key,
    ->   name varchar(32)
    -> );
Query OK, 0 rows affected (0.08 sec)

mysql> insert into users(name) values('Taro'),('Hanako'),('Jiro');
Query OK, 3 rows affected (0.02 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> select * from users;
+----+--------+
| id | name   |
+----+--------+
|  1 | Taro   |
|  2 | Hanako |
|  3 | Jiro   |
+----+--------+
3 rows in set (0.00 sec)

再びrootに戻り、今度はreadonlyユーザーを追加します。 読み込みだけできればいいので、SELECT権限だけ付与します。

mysql> create user readonly identified by 'hogehogehoge';
Query OK, 0 rows affected (0.00 sec)

mysql> grant select on hoge.* to readonly@'%';
Query OK, 0 rows affected (0.00 sec)

mysql> show grants for readonly;
+---------------------------------------------------------------------------------------------------------+
| Grants for readonly@%                                                                                   |
+---------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'readonly'@'%' IDENTIFIED BY PASSWORD '*E8DD65E018E30F27D962FB9BFA2F4E8206DC3AF8' |
| GRANT SELECT ON `hoge`.* TO 'readonly'@'%'                                                              |
+---------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

ユーザーの作成ができたので、動作確認してみます。

$ mysql -u readonly -p -h 127.0.0.1 -P 13306

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| hoge               |
+--------------------+
2 rows in set (0.00 sec)

mysql> use hoge;
Database changed
mysql> show tables;
+----------------+
| Tables_in_hoge |
+----------------+
| users          |
+----------------+
1 row in set (0.00 sec)

mysql> select * from users;
+----+--------+
| id | name   |
+----+--------+
|  1 | Taro   |
|  2 | Hanako |
|  3 | Jiro   |
+----+--------+
3 rows in set (0.00 sec)

mysql> insert into users(name) values('Saburou');
ERROR 1142 (42000): INSERT command denied to user 'readonly'@'172.17.0.1' for table 'users'

select文で内容を表示することはできましたが、insert文を実行することはできませんでした。

以上です。

esa のトップページを活発にするなら esapad があるじゃない

(\( ⁰⊖⁰)/) < こんばんは!

おいしい健康でいろんな開発を担当しています、関口 id:tanukiti1987 です。

社名やサービス名となっているおいしい「健康」ですが、今日の晩ご飯はカップラーメンを食べてしまいました。

全粒粉入りの麺だということだったので、それでご勘弁願いたいです。

情報共有ツールとしての esa

弊社では、諸々の事情により社内の情報共有ツールに esa を使っています。

社内wiki として非常に整理された機能を持っており、合わせて WIP でモノを書いていこう!荒々のうちにアウトプットしていこうぜ。のコンセプトが非常に気に入っております。

GoogleAnalytics を仕込むこともでき、みんながいつどれくらい活用しているかだって見れちゃいます。 自前のS3バケットに画像などを保存していくこともできます。

できることが結構多い!最高!

esapad is 何

esa はレスポンシブにデザインされていて、ある程度広い画面で見ていれば 「Recently updated」という項目が見れて、最近のみんなの動きが見れるのですが、ノートPCで特に意識せずにブラウジングしているくらいでは、この項目がまー、出てこない。

そこで esapad です。

github.com

READMEとなるページに特定のタグを埋め込んでおき、 この rubyコードを実行すると、指定したページに stock な記事、flow な記事、最近スターされた記事などをリストでリプレイスしてくれます。

弊社ではこんな感じで活用しています。

f:id:oishi-kenko:20170818204523p:plain

全然見せられそうなのがないので、 esapad に対応した!というblog 記事だけモザイクなしです..

この esapad を定期的にジョブ管理システムに実行させるようにして、対応がサクッと完了します。

弊社ではトップページを開いたときに常にこの stock/flow の記事を見られるようにして、動きを少しでも出すようにしています。

動きがでることで更に活発になっていくことを願って、やってみました。

最後に

esapad は id:hogelog さんが作ったものですが、 gem を公開してくださいよ〜。とツンツンしていたら僕 contributor に加えてくださり、僕が少しだけ手を加えたものです。

こんなことやってみたいんだけど。。使ってみたけどバグってる。。などありましたら、お気軽に issue を立てたり、pull request したりしてください :)

それでは、よい週末を〜。