カテゴリ毎の件数表示
ドリルダウンの検索導線において、カテゴリごとにヒットする検索結果を表示することで、カスタマがそのリンクを押す/押さない動機を強めることができる。
https://gyazo.com/1e4b60b7f28300a2373372575e2694eb
判断ポイント
カテゴリの中でのドリルダウン
0件のカテゴリを表示するか否か
件数の正確さ
組み合わせでの
ソリューション
ヒット件数を求めるにはカテゴリごとの検索結果を集計する必要があり、たいていの場合、負荷の高いクエリとなる。
code:sql
SELECT SR_OUT.name, CASE WHEN cnt IS NULL THEN 0 ELSE cnt END
FROM salary_ranges SR_OUT
LEFT OUTER JOIN (
SELECT SR.name, COUNT(SR.name) AS cnt
FROM job_postings JP
JOIN salary_ranges SR ON JP.salary BETWEEN SR.lower AND SR.upper
GROUP BY SR.name
) AS PER_SR ON SR_OUT.name = PER_SR.name
`
job_postingsのフルスキャンは避けられない。
検索エンジン
上記の「推定年収」「雇用形態」といったカテゴリが多くなるとそれだけで、SQL発行回数が増える。job_descriptionsをElasticsearchやSolrのような検索エンジンでインデックス化し、そちらからデータを取得することで性能的なアドバンテージがある。
例えば、ElasticSearchのAggregationの機能を使うと、
code:json
GET index/job_postings/_search
{
"from" : 0, "size" : 0,
"aggs": {
"salary_ranges": {
"range": {
"field": "salary",
"ranges": [
{"from": 200, "to": 299},
{"from": 300, "to": 399},
{"from": 400, "to": 499},
{"from": 500, "to": 599},
{"from": 600, "to": 699}
]
}
},
"workplaces": {
"terms": {
"field": "workplace"
}
}
}
}
このようなクエリ1回で、以下のように複数のカテゴリごとの件数表示データを取得できます。
code:json
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 0,
"hits": []
},
"aggregations": {
"workplaces": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "新宿",
"doc_count": 1
},
{
"key": "渋谷",
"doc_count": 1
}
]
},
"salary_ranges": {
"buckets": [
{
"key": "200.0-299.0",
"from": 200,
"to": 299,
"doc_count": 1
},
{
"key": "300.0-399.0",
"from": 300,
"to": 399,
"doc_count": 0
},
{
"key": "400.0-499.0",
"from": 400,
"to": 499,
"doc_count": 1
},
{
"key": "500.0-599.0",
"from": 500,
"to": 599,
"doc_count": 0
},
{
"key": "600.0-699.0",
"from": 600,
"to": 699,
"doc_count": 0
}
]
}
}
}
キャッシュ
件数はリクエストごとに計算するのでなく、キャッシュしておくことで検索負荷を大幅に下げることができる。
A. 定期的に集計して保存しておく
集計用のテーブルを作って、定期的にカテゴリごとの集計する。
B. クエリのリザルトキャッシュ
O/Rマッパーやデータベース、検索エンジンのリザルトキャッシュの機能を使って、キャッシュさせることができる (リザルトキャッシュの詳細は別章にて)。大抵の場合、透過的にキャッシュできるので、コードがシンプルになり、特別なミドルウェアや設計が必要でないことがメリットである。だが、結局集計のクエリが重いのであれば、キャッシュ有効期限切れの際のリクエストは遅くなってしまうので、そういうケースを許容できないのであれば、Aの定期集計にしておくのが無難だろう。
デグラデーション
負荷の高いときや、キャッシュの仕組みが停止している場合、件数のみださなくするようにデグラデーションさせる設計をしておくと、サービス全体のダウンを防ぐことができる。
例えばカテゴリのみを以下のようなJSONファイルに出力しておいて、HTMLをレンダリングする際には、取得した件数をマージする。件数が取得できなければ、カテゴリのみ表示する。
code:json
{
"saraly_ranges": [
{
"key": "salary200",
"label": "200万円"
},
{
"key": "salary300",
"label": "300万円"
}
],
"workplaces": [
{
"key": "place001",
"label": "渋谷"
},
{
"key": "place002",
"label": "新宿"
}
]
}
ただし想定する障害ケースが、一連のユーザ導線に影響があると意味がないので、デグラデーションを作り込むかどうかは一連のユーザ導線単位で決める。
Appendix. 例で使用したスキーマ
code:sql
CREATE TABLE job_descriptions(
id BIGINT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
salary BIGINT
);
CREATE TABLE salary_ranges(
name VARCHAR(100) NOT NULL,
lower BIGINT NOT NULL,
upper BIGINT NOT NULL
);
INSERT INTO job_descriptions(id, title, salary) VALUES
(1, 'AAA', 342),
(2, 'BBB', 442),
(3, 'CCC', 388),
(4, 'DDD', 242),
(5, 'EEE', 790),
(6, 'FFF', 678)
;
INSERT INTO salary_ranges(name, lower, upper) VALUES
('200万円', 200, 299),
('300万円', 300, 399),
('400万円', 400, 499),
('500万円', 500, 599),
('600万円', 600, 699);