シニアジョブの求人一覧で、全体の件数も増えてきて、募集を終了した求人も参考として表示する必要が出てきた。
seniorjob.jp
他求人サイトでも参考として出してるとこも多く、メルカリで販売済みも出てくるのが似てる。
ただ表示するだけでなく募集中の求人は優先的に先に表示したい。
単純な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
www.elastic.co
エラー詳細の出し方ありそうだけど、よくわからんので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順になった。