36.Ruby|instance_of?/is_a?(kind_of?)

instance_of?(klass) -> bool

オブジェクトがクラス klass の直接のインスタンスである時、真を返します。

obj.instance_of?(c) が成立する時には、常に obj.kind_of?(c) も成立します。

[PARAM] klass:
Classかそのサブクラスのインスタンスです。

class C < Object
end
=> nil

class S < C
end
=> nil

obj = S.new
=> #<S:0x00007fdee60d2628>

p obj.instance_of?(S)
=> true

p obj.instance_of?(C)
=> false

docs.ruby-lang.org

is_a?(mod) -> bool

kind_of?(mod) -> bool

kind_of?is_a?エイリアス(なので同じ処理)。

オブジェクトが指定されたクラス mod かそのサブクラスのインスタンスであるとき真を返します。

また、オブジェクトがモジュール mod をインクルードしたクラスかそのサブクラスのインスタンスである場合にも真を返します。 Module#includeだけではなく、Object#extendやModule#prependによってサブクラスのインスタンスになる場合も含みます。上記のいずれでもない場合に false を返します。

[PARAM] mod:
クラスやモジュールなど、Moduleかそのサブクラスのインスタンスです。

module M
end
=> nil

class C < Object
   include M
end
=> C

class S < C
end
=> nil

obj = S.new
=> #<S:0x00007f94c892e370>

p obj.is_a?(S)
=> true

p obj.is_a?(C)
=> true

p obj.is_a?(Object)
=> true

p obj.is_a?(M)
=> true

p obj.is_a?(Hash)
=> false

 ↑ ※Moduleをクラスの中にインクルードしている。 インクルードされたModuleはインスタンスメソッドと同じ様に実行できる。

参考

Module

Moduleの使い方

  • 定数やメソッドをまとめる

  • クラスに組み込んで多重継承を実現する

  • 名前空間を提供する

35.Ruby|strptime/strftime

strptime

「文字列」をDate._strptimeを用いて、Timeオブジェクトに変換。

require 'time'
Time.strptime('2001-02-03T04:05:06+09:00', '%Y-%m-%dT%H:%M:%S%z')
#=> 2001-02-03 06:05:06 +0900
require 'time'
Time.strptime('91/5/18 4:13:00', '%Y/%m/%d %T'){|y|
    if y > 100 then y
    elsif y >= 69 then y + 1900
    else y + 2000
    end
}
#=> 1991-05-18 04:13:00 +0900

docs.ruby-lang.org

strftime

「時刻」を「format 文字列」に従って文字列に変換した結果を返す。

p t = Time.new(2001,2,3,4,5,6,"+09:00")  # => 2001-02-03 04:05:06 +0900
p t.strftime("%m/%d/%Y")       # => "02/03/2001"
p t.strftime("%m/%-d/%_6Y")    # => "02/3/  2001"
p t.strftime("%I:%M%p")        # => "04:05AM"
p t.strftime("%I:%M%#p")       # => "04:05am"

docs.ruby-lang.org

注意

以下の場合はrequireをする必要はない。

p Time.now

以下の場合はrequire 'time'しないとエラーになる。

p Time.parse('2017-10-04 12:34:56')

34.Sidekiq

プロダクトに導入されているが、問題が多すぎて散々迷惑を被っているSidekiqについてお勉強。
今までは無料版を使っていたが、流石に酷すぎてProを導入する運びとなった。設定などはこれから。
(ただSidekiqそのものが悪いというよりは、よく分からないまま導入し使用されてきた点が問題な気がする)

Sidekiqとは

Railsアプリで非同期処理を行う為のライブラリ。

github.com

Simple, efficient background processing for Ruby.
Sidekiq uses threads to handle many jobs at the same time in the same process.
It does not require Rails but will integrate tightly with Rails to make background processing dead simple.

前提

・Redisが必要(なおRedisについては下記にまとめた)。 tnet.hatenadiary.com

使い方

github.com

補足

system.blog.uuum.jp

retry

プロセスでエラーが起きた時、
retry が有効な場合で、retry数がまだMAX retry数内(デフォルトは25)の場合は、ジョブ情報が retry queue に追加されます。
retry queue に保持される情報は、通常のジョブ情報に、エラー情報とリトライ回数などの情報が加えらえれたものになります。
上記で話した scheduledのキューを処理する poller は、retryのキュー情報も見るようになっているので、retryに入れられた情報は、
自然とジョブキューへ追加されます。

関連

qiita.com

Active Job は Rails におけるバックグラウンドジョブを動かすための共通インタフェースです。
バックグラウンドジョブを動かす Sidekiq、Resque、Delayed Job をアダプタとして利用できます。

Rails で提供されるのはジョブをメモリに保持するインプロセスのキューイングシステムだけなので Rails を再起動するとジョブは全て失われます。
(アダプタを指定しなかった場合のデフォルト動作)(参考)

Sidekiq Pro:通常版との違い

概観

selmertsx.hatenablog.com

TL;DR
sidekiq proでは、server processが死んでも jobの復活がサポートされる
sidekiq proにおいて、redis が死んでも、1000件程度のジョブならclientが保持し続けて、redisが復活したタイミングでenqueue してくれる
↑の状況において、client processが死ねば、蓄積された1000件のジョブは全て消える
sidekiq enterpriseでは、unique な jobになるように諸々やってくれるが、完全に保証してくれるものでは無いので、そこんところを考えたjob設計にすること。
Jobが失われないようにする仕組みについて
sidekiqにおいてjobが失われるケースは下記の3点

redisに接続できないケース
client processが死んだケース
server processが死んだケース
この中でSidekiq Proが対策をしているのは redisに接続できない と server processが死んだ の2つのケース。

Reliability

sidekiq.org

Enable more complex job workflows with Batches and Callbacks
Better server reliability in the face of Ruby VM crashes
Better client reliability in the face of Redis networking problems
Pause queues (e.g. only process a queue during business hours)
Expire unprocessed jobs after a deadline
Send job processing metrics to Statsd
High performance API extensions using Redis's Lua support
Search for jobs in the Web UI

Ruby VMとは
 RubyJavaなどの言語と同様、その言語用のVM仮想マシン)上で実行される。その仮想環境のこと。
 (なおCやGo言語などはマシン語コンパイルされ、VMではなくコンピュータが直接実行する。)

github.com

Setup
TL;DR To use the Reliability features in Sidekiq Pro, add this to your initializer:

Sidekiq::Client.reliable_push! unless Rails.env.test?

Sidekiq.configure_server do |config|
  config.super_fetch!
  config.reliable_scheduler!
end
Using super_fetch
Sidekiq uses BRPOP to fetch a job from the queue in Redis. This is very efficient and simple but it has one drawback: the job is now removed from Redis. If Sidekiq crashes while processing that job, it is lost forever. This is not a problem for many but some businesses need absolute reliability when processing jobs.

Sidekiq does its best to never lose jobs but it can't guarantee it; the only way to guarantee job durability is to not remove it from Redis until it is complete. For instance, if Sidekiq is restarted mid-job, it will try to push the unfinished jobs back to Redis but networking issues can prevent this.

Sidekiq Pro offers an alternative fetch strategy, super_fetch, for job processing using Redis' RPOPLPUSH command which keeps jobs in Redis. To enable super_fetch:

Sidekiq.configure_server do |config|
  # This needs to be within the configure_server block
  config.super_fetch!
end

・BRPOPとは
 ・ブロッキングするリストのPOPプリミティブのこと。←?
  ・要は「データが入力されると、リストの右側から取り出して来る」方式のことを指すようだ。
  ・指定されたリストからPOPする要素が無いときは接続をブロックする(LPOPのブロッキング版)。
  ・要素は空ではない最初のリストの先頭からPOPされ、指定された順番で指定されたキーがチェックされる。

・上記を一部和訳 & 意訳 & 要約する。

BRPOP方式はシンプルで効率的だが、一度Sidekiqがクラッシュするとジョブが永遠に失われる。

ジョブが失われない様にする為には、完了するまでジョブの情報をRedisに残しておく必要がある。だが、基本的にこれは保証できない。
例えば何らかの要因でジョブが実行途中に中断され、中途半端な実行状態のジョブを再実行させようとした際、
SidekiqはそのジョブをRedisに戻そうとするが、ネットワーク上の障害によってそれが妨げられる可能性もある。

Sidekiq Proでは上記を解決する為の、代替的なジョブ取得方法を提案出来る。
それがsuper_fetchだ。Redisに対してRPOP LPUSHコマンドを用いる事でこれを実現する。

33.Ruby|ループの種類・使い分け

処理が遅くてトランザクションが中断される問題が頻発していて、リファクタリングをする機会に恵まれた。 この機会にRubyのループ処理について不足している知識を補い整理しておきたい。

forよりもeach

forとeachの違い

まず大きな違いとして、eachはブロック内外でスコープが仕切られているのに対して、 forはブロック内外でスコープが変わらない。

forを原則使わない方が良い理由としては、下記。 for内部はeachにより実装されており(わざわざforを使う理由が薄く)、 新しいスコープが導入されない為、外側からもアクセスできてしまう(無闇にアクセスできない方が良い)。 techracho.bpsinc.jp

スコープを作らないグループ:forやwhile

for

for 変数 in オブジェクト do
  繰り返し処理
end
for i in [1, 2, 3] do  # forではdoは省略可能
  num = i
end

p num
  • forではループ変数(i)もスコープが変わらず、forの外でも参照できる。
for i in [1, 2, 3]
end

p i
  • 出力結果
3

while

a = [1, 2, 3]
i = 0

while i < a.size
  num = a[i]
  i += 1
end

p num

スコープを作るグループ:eachやloop

each

例えばeachだとスコープを作るので、下記のようにスコープ外から呼び出すとエラーとなる。

[1, 2, 3].each do |i|
  num i
end

p num
  • 出力結果
20201123_loop_test:2:in `block in <main>': undefined method `num' for main:Object (NoMethodError)

eachはブロック内外でスコープが仕切られているが、forはブロック内外でスコープが変わらない。

しかしeachは飽くまでも

eachよりもmap

下記に詳しく書いていくが、mapよりもeachの方が冗長な記述になりやすく、 またループの内外で余計に処理を追加しなくてはいけない。

eachとmapの違い

eachが「元の配列」・mapが「処理後の配列」を返す。

each

list = (1..5).to_a.freeze

double = []  # 事前に変数を定義する必要
list.each do |i|
  double << i * 2  # ループの中でdoubleを組み立てる必要
end

p "2倍したリスト:#{double}"

map

list = (1..5).to_a.freeze

mapped = list.map do |i|
  i * 2  # 実行したい処理だけで済む
end

p "2倍したリスト:#{mapped}"
  • 出力結果
"2倍したリスト:[2, 4, 6, 8, 10]"

techracho.bpsinc.jp

map処理を簡潔に書きたい場合

例えば下記のような処理があったとして。

user_names = users.map do |user|
  user.name
end

下記のように簡潔に書くこともできる。 この&(アンパサンド)は、そのメソッドをブロックとして展開することを意味する。

user_names = users.map(&:name)

なおmapメソッドは、Enumerableモジュールに実装されたメソッド。 docs.ruby-lang.org - なおEnumerableモジュールとは、集合を表すクラスに数え上げや検索などのメソッドを提供するパッケージのようなものを指す。

https://tech-camp.in/note/technology/4423/

loop

loopは無限ループを作るのに適している。 意図的にループを終了(break)させない限り、永久に処理を繰り返す。

loop ブロック
# 例)
loop do
  繰り返し処理
  終了条件によってループ中断( break )
end

eachよりもfind_each

find_eachは分割してレコードを取得して処理をしてくれるので、eachよりもメモリの消費量を少なく抑えられるケースがある。 デフォルトで1000件ずつの処理をする。

qiita.com

大量のデータを処理する際、DBから取得してきたデータを each のループで回してしまうと、 ループを回す直前にデータを全てメモリ上に展開してしまいます

取得するデータ量が多い場合は、 find_each を使ってDBからデータを分割して取得するようにし、 メモリ上に展開されるデータ数を絞る方がパフォーマンスが良い場合があります

ちなみに、 find_eachは in_batches で取得したデータをeachで返しています

× データ数が多いと一度に全てをメモリ上に展開してしまうためパフォーマンスが悪い

Log.all.each do |log|
  # 処理を実行
end

◯ DBから分割して取得してくれるので、必要とするメモリの量を抑えながら実行できる

Log.find_each do |log|
  # 処理を実行
end

each_slice

each_sliceは要素を複数ブロックに分けた上で繰り返し処理をさせたい場合に使う。

doc.okkez.net

n 要素ずつブロックに渡して繰り返します。 要素数が n で割り切れないときは、最後の回だけ要素数が減ります。 例)

(1..10).each_slice(3) {|a| p a}
    # => [1, 2, 3]
    #    [4, 5, 6]
    #    [7, 8, 9]
    #    [10]

pluck

qiita.com

32.「CODE COMPLETE 上」

20201010

p.32~

3.1.3.コンストラクションの準備に有無を言わせない根拠

p.34

コンストラクションを開始する前に欠陥を取り除いた場合は、
プロセスの最後や、システムテストの最中や、リリース後に欠陥を取り除いた場合よりも、
作業の手戻りが10~100倍少ないことがわかっている。

多くの企業が、欠陥修正を先延ばしにせず、早い段階で着手することに重点を置くだけで、
開発コストやスケジュールを半分以下に軽減できることに気付いている。

普段リリース後の欠陥の修正にあたふたしている立場なので、これは非常に参考になった。

こういった視点を持つことの重要性を心しておきたい。

31.Elasticsearch|search API

応答については、次の部分がわかります。

took - Elasticsearchが検索の実行にかかった時間(ミリ秒)
timed_out - 検索がタイムアウトしたかどうかを示す
_shards - 検索されたシャードの数と検索に成功/失敗したシャードの数を示す
hits - 検索結果
hits.total - 検索基準に一致したドキュメントの数
hits.hits - 検索結果の実際の配列(デフォルトで最初の10個のドキュメント)
hits.sort - 結果のソートキー(スコアでソートする場合は欠落)
hits._score と max_score - 今のところこれらのフィールドは無視

www.elastic.co

30.Elasticsearch|match_phrase/スコアリング

match_phraseで検索をかけた時、何を基準としてスコアリングされるのか、仕事で問われる機会があったので学習。

  • analyzerの基本

blog.chocolapod.net

www.elastic.co

medium.com

  • _analyze
GET Index名/_analyze
{
  "field": "~Field名~",
  "text": "~Value~"
}

下記が返ってくる

token: tokenの内容
start_offset: 入力値の先頭から数えたtokenの始まる位置
end_offset: 入力値の先頭から数えたtokenの終わる位置
type: tokenのタイプ
position: token列内でのtokenの位置
  • 下記で、対象Indexのマッピング(どのDocumentをどう設定しているか)を確認できる
GET Index名/_mappings
  • Standard tokenizer

www.elastic.co

lucene.apache.org

The Standard Tokenizer supports Unicode standard annex UAX#29 word boundaries with the following token types: , , <SOUTHEAST_ASIAN>, , and .

Standard Tokenizer は、(Unicode Standard Annex#29で指定されているように、Unicode Text Segmentationアルゴリズムに基づく)文法ベースのトークン化を提供し、ほとんどの言語でうまく機能します。

  • 参考
GET    /_cat/count/{index}      # インデックスのドキュメント数取得

medium.com

match_all ... 全てのドキュメントを返す
match ... キーワード検索
match_phrase ... 語順も考慮される
query_string ... Lucene形式での検索式
term ... 完全一致検索
terms ... 複数のtermを指定できる
range ... 数値は日付などの範囲検索
bool,must,should,must_not ... andやorなどの検索
filter ... スコアを返さない

Elasticsearchでは、全文検索を行うために文章を単語の単位に分割する処理機能を、Analyzerと呼びます。 ちなみにElasticsearch標準のAnalyzerでは日本語の形態素解析が行えないため、kuromojiプラグインをインストールする方法が書籍では紹介されています。

Analyzerは

Char filter ... 入力されたテキストを前処理するためのフィルタ
Tokeneizer ... テキストの分割処理
Token filter ... 分割されたテキスト(トークン)に対する処理を行うフィルタ
phrase match
フレーズ検索

fooとbaaの間に未知の単語をslop個許容したフレーズを検索
{
  "query" : {
    "match_phrase" : {
      "title" : {
        "query" : "foo baa",
        "slop" : 1
      }
    }
  }
}

qiita.com

  • Elasticsearchのスコアリングは、下記によって計算されるようだ。
以下の項目でスコアリングされます。

TF
IDF
fieldの長さ
つまり、
一つのDocumentに対して
一つの検索語を構成している単語が
どれだけ含有されているか
その単語はどれだけそのdocumentに対して特徴的なのか
を計算され、
検索対象のfieldの文字列の長さが短いのをもっと高く
評価する仕組みです。

思想的には
一つの文書の中で該当検索語がよくマッチしていて、しかもそれは他の文書ではあんまり見られない該当文書のユニークな検索語で、しかもそれが短い文書であれば、マッチ度は高いということになります。

qiita.com

  • TF(Term Frequency):
    一つのDocumentの中で、該当単語の出現頻度を表わす。

  • IDF(Inverse Document Frequency):
    文書頻度の逆数/ある単語が出現する文書数であるDFの逆数。対数を取ることで、重み付けに利用されることが多い。
    噛み砕いて言うと、レアなら特徴的なので高い値となる。

www.cse.kyoto-su.ac.jp

※分かりやすい  ↓ dev.classmethod.jp

  • tf/計算例
1.4142135 = tf(freq=2.0), with freq of:

DocumentのTermの個数から求められる関数で、2つ以上のDocumentがヒットした時に、Termがより多く含まれた方が上にくる。
ここでは、「java」というTermが2回(freq=2.0)登場している。

  • idf/計算例
1.2231436 = idf(docFreq=3, maxDocs=5)

Index内のDocumentの数と、指定したTermを含むDocumentの数から求められる関数。
指定したTermを含むDocumentが少ないほど、大きな数字となる。
ここでは、全Document(maxDocs=5)中、3回登場している(docFreq+3)。

kazuhira-r.hatenablog.com

sitest.jp

  • これは結構わかりやすいかも。
GET scoring/_search
{
  "query" : {
    "match" : {
      "name" : "one two"
    }
  }
}

{
  "_index": "scoring",
  "_type": "doc",
  "_id": "1",
  "_score": 0.79143506,
  "_source": {
    "name": "one two"
  }
}
{
  "_index": "scoring",
  "_type": "doc",
  "_id": "3",
  "_score": 0.6331481,
  "_source": {
    "name": "one two three"
  }
}
{
  "_index": "scoring",
  "_type": "doc",
  "_id": "4",
  "_score": 0.6331481,
  "_source": {
    "name": "one two three four"
  }
}
{
  "_index": "scoring",
  "_type": "doc",
  "_id": "2",
  "_score": 0.14893481,
  "_source": {
    "name": "one three"
  }
}
  • フィールド内のターム数が少ない方がスコアが高くなる。

notta55.hatenablog.com

検索語の出現数が多いと高評価。
ここで、レアな検索語であれば、出現数評価の重みをさらにUPする。
ここで、検索語の出現割合が高い文書であれば、さらに出現数評価の重みをUPする。

itdepends.hateblo.jp

  • explain(検索結果の_explanationを参照)
GET ~Index名~/_search
{
    "query": {
        "bool": {
                {
                    "match_phrase": {
                        "content": "~検索文字列~"
                    }
                }
        }
    },
    "size": 10,
    "from": 0,
    "explain": true
}
  • 参照図
その名も「TF-IDF」である。

Elasticsearchでの検索では、

より短い文章の中に*2より多くの検索語が出現するほど良い。検索語がレアなワードであればそれを多く含む文章が際立つ、結果として

評価が高くなるということだと理解した。

itdepends.hateblo.jp

  • Similarity

PerFieldSimilarity…? Similarity?

www.elastic.co

  • Similarity Module

FullTextSearchにおけるクエリとドキュメントの適合度を求めるモジュール

www.elastic.co