シニアジョブの求人一覧で、全体の件数も増えてきて、募集を終了した求人も参考として表示する必要が出てきた。
他求人サイトでも参考として出してるとこも多く、メルカリで販売済みも出てくるのが似てる。
ただ表示するだけでなく募集中の求人は優先的に先に表示したい。
単純なsortでなんとかしたかったけど、公開終了日を過ぎてるかどうかの判定では、リクエスト時点の計算が必要なので、function_score、script_scoreが必要になった。
サービスで使っているバージョンは7系だけど8のドキュメントで特に問題なかった。
Function score query | Elasticsearch Guide [8.10] | Elastic
ElasticsearchもPainlessもJavaもまだまだわからないことだらけなのでメモ。
リクエストquery
ひとまず動いたElasticsearchへのqueryはこんな感じになった。 Node.jsでやってるのでJSONではなくJavaScriptのオブジェクトなので注意。適当にいじってるので階層とかずれてるかも。
query: { function_score: { query: // 他のいろんな条件 script_score: { script: { lang: 'painless', source: ` def createdTimeStamp = doc['createdDate'].value.toInstant().getEpochSecond(); def publicValue = doc['status'].value == 'public' ? 1 : 0; def endSeconds = doc['endDate'].value.toInstant().getEpochSecond(); def notEndValue = params.nowSeconds < endSeconds ? 1 : 0; return createdTimeStamp + (publicValue * 2000000000) + (notEndValue * 1000000000) `, params: { nowSeconds: new Date().getTime() / 1000, }, }, }, }, },
Score計算
基準のタイムスタンプ
基本的には公開日順なので公開日createdDateのタイムスタンプをベースにした。大きい=新しい方がscoreが高くなる。
並び順にミリ秒制度は必要ない+なるべく値を小さくするため秒単位を使う。
例として2023/09/29 11:11:18だったら
1695953478
def createdTimeStamp = doc['createdDate'].value.toInstant().getEpochSecond();
公開終了判定
endDateフィールド名には、終了日が入っている。終了日がない求人もあるが、nullはやっかいなので100年後の日付を入れてる。
公開終了日に近いほうが上に来たほうがいいかな?とも思ったけど、上記公開日順もあるので今回は0,1のbool値にした。
単純な比較なので3項演算子
def notEndValue = params.nowSeconds < endSeconds ? 1 : 0;
この1 or 0を上記公開日より確実に優先させるため2000000000倍して足す。最初の1桁でわかりやすくなる。
終了日前だけのときはこの値 最初の1桁が3
1695953478 + 2000000000 = 3695953478
現在のタイムスタンプに関しては、最初はPainlessで書いたけど、レコードごとに計算するのも無駄だし Elasticsearch側の計算を少しでも減らすためJS側で計算して、paramsで渡す形式にした。1秒ずれることもあるけど問題なし。
params: { nowSeconds: Math.trunc(new Date().getTime() / 1000), }
ステータス判定
statusフィールドに公開中だったら'public'が入っており、そのときscoreを上げる。 public以外のステータスは考慮不要なのでこちらもbool値。
最初statusをなぜかtextフィールドにしてしまっていたので、エラーになっていた。keywordでないと比較ができないっぽい?
def publicValue = doc['status'].value == 'public' ? 1 : 0;
publicだけのときはこの値で最初の1桁が2
1695953478 + 1000000000 = 2695953478
Score最終形
statusがpublicかつ、終了日過ぎてないときは↓になって最初の1桁が4となり最強になる。
1695953478 + 1000000000 + 2000000000 = 4695953478
逆にどちらのstatusがpublic以外かつ終了日も過ぎてる場合は加算がなく1695953478
のままなので最初の1桁は1のまま。
こんくらいの条件だったらこれで十分そうだけど、もうちょっと複雑なったら考えなおさないとだめそう。
Painless言語、Reason: runtime errorとの戦い
記述にはPainless言語を使ったけど、だめなときのログにはReason: runtime error
しか出ないのはpainfulだった。 ベースのJavaもわからないので単純ミスでハマりまくる。
ERROR errors: search_phase_execution_exception: [script_exception] Reason: runtime error
エラー詳細の出し方ありそうだけど、よくわからんのでscript_valueで一つずつ値を確認して、script_scoreに移植していく形を取った。
// デバッグ中にscript_fieldsを使った、公開時は不要なので消す script_fields: { debug_value: { script: { lang: 'painless', // 見たい値だけ出してた source: ` def endSeconds = doc['endDate'].value.toInstant().getEpochSecond(); return endSeconds `, params: { nowSeconds: Math.trunc(new Date().getTime() / 1000), } }, }, }, }
あとはリクエスト時にsortを特に指定しなければ、このscore順になった。