Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save toshi0607/133f7e6c2c030617493574e9a7f81365 to your computer and use it in GitHub Desktop.
Save toshi0607/133f7e6c2c030617493574e9a7f81365 to your computer and use it in GitHub Desktop.
アーキテクチャ大全 (未完) のサンプル

アーキテクチャ大全のサンプル

これくらいの実例とともにソリューション選択できるようにしていきたい。

カテゴリ毎の件数表示

コンテキスト

ドリルダウンの検索導線において、カテゴリごとにヒットする検索結果を表示することで、カスタマがそのリンクを押す/押さない動機を 強めることができます。

indeed https://jp.indeed.com/

ソリューション

ヒット件数を求めるにはカテゴリごとの検索結果を集計する必要があり、たいていの場合、負荷の高いクエリとなります。

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の機能を使うと、

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回で、以下のように複数のカテゴリごとの件数表示データを取得できます。

{
  "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をレンダリングする際には、 取得した件数をマージします。件数が取得できなければ、カテゴリのみ表示します。

{
  "saraly_ranges": [
    {
      "key": "salary200",
      "label": "200万円"
    },
    {
      "key": "salary300",
      "label": "300万円"
    }
  ],
  "workplaces": [
    {
      "key": "place001",
      "label": "渋谷"
    },
    {
      "key": "place002",
      "label": "新宿"
    }
  ]
}

ただし想定する障害ケースが、一連のユーザ導線に影響があると意味がないので、デグラデーションを作り込むかどうかは 一連のユーザ導線単位で決めます。

Appendix. 例で使用したスキーマ

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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment