クックパッドマートにおける実世界での配送を意識した注文の検証処理【連載:クックパッドマート開発の裏側 vol.1】

はじめに

こんにちは、買物事業部の勝間(@ryo_katsuma)です。 今日から5日間は、買物事業部のメンバーで連載記事を書かせていただきます。

買物事業部は、クックパッドの生鮮食品ECサービスの新規事業「クックパッドマート」の開発を行っている事業部です。 クックパッドマートのサービスについての説明や、立ち上げ期の舞台裏については昨年の長野によるエントリをご参照ください。

クックパッドマートは、iOS、Androidアプリのリリース、商品受け取り場所におけるスマートロックの設置、注文当日配送の実現など、 サービスをより多くの人に便利に使っていただくためのいろいろな新しい取り組みを行ってきました。 今回はこれらの取り組みについて多くの方にぜひ知っていただきたいと思い、連載形式で紹介させていただきます。

ちなみに明日以降は、以下のような内容を予定しています。

注文の検証処理

vol.1の本稿では、クックパッドマートにおける注文の検証処理(validation)について紹介します。 注文の検証処理とは、クックパッドマート内での用語になりますが、その名の通りユーザーがカートに入れた商品について、「注文可能かどうか」を判定するための検証処理になります。

iTunes Storeのようなデジタルコンテンツの販売の場合、クレジットカードなど決済手段に問題がない限り「注文できるかどうか」の検証処理に複雑なケースはあまり多くなく、せいぜい販売上限数を考慮するくらいではないかと思います。 一方で、物流が絡むECにおいては、現実世界での非常に細かな制限が多く絡み、注文の検証処理には多くのことを考慮する必要があります。

ここからは、クックパッドマートにおける注文の検証において、どのようなことを考慮しているかをご紹介します。

前提

まず、クックパッドマートにおけるデータの関連性についてご紹介します。 説明を簡略化するために実際の概念とは一部異なるものもありますが、注文処理においては以下の概念が重要な登場人物になります。

product

  • いわゆる「商品」の概念
  • ユーザーが購入し、販売店や生産者の方に用意いただく食材を指す

location

  • クックパッドマートにおける「受け取り場所」の概念
  • ユーザーは常に1つの受け取り場所を指定している

delivery

  • location毎の注文締切、受け取り開始、受け取り終了などの「配送スケジュール」
  • たとえば「4/8 2:00注文締切」「4/8 16:00 ~ 23:00 商品受け取り可能」などのデータを持つ

order

  • いわゆる「注文」の概念
  • 注文毎に配送スケジュールのdeliveryに紐付き、各orderは複数のproductを持つ

データ間の関連性

rails的に表現すると、このような関連性を持っています。

class Delivery
  belons_to :location
end

class Location
  has_many :deliveries
end

class Product
end

class Order
  has_many :products
  belongs_to :delivery
end

また、商品の集荷配送については、後ほど詳細を述べますが下記のような流れになります。

  • 販売店は注文情報に従い、コンテナに商品を入れて準備
  • ドライバーは指示書に従い、複数の販売店から商品が積載されたコンテナを集荷し、温度管理された配送資材(以下、シッパー)に入れる
  • ドライバーは指示書に従い、複数の受け取り場所でシッパーからコンテナを取り出し、冷蔵庫に設置する
f:id:ryokatsuma:20190405192518p:plain

注文の受付可能数

前述の通り、在庫の概念が無いデジタルコンテンツではなく、実在する販売店や生産者の方に用意いただく商品なので扱える数は有限です。 また、商品の多くはクックパッドマートだけではなく、実店舗でも販売されているので、その数には上限が設けられている必要があります。

そこで、商品には購入可能数が設定されてる必要があります。ここではsales_limit_per_dayとすると、購入可能数を超えているかどうかのチェックはこのようになります。

product.sales_limit_per_day < delivery.orders.where(product: product).count

これで購入可能数の確認は十分でしょうか?答えはNoです。

すべての配送場所を考慮

ある販売店の商品は、1つの受け取り場所だけではなく、N箇所の受け取り場所に配送されます。 言い換えると、あるdeliveryの配送日と同じ配送日の別のdeliveryが存在することになります。 たとえば、4/8配送分の販売店Aの豚肉は、受け取り場所1のユーザーで購入されていなくても、受け取り場所2のユーザーで購入されている可能性があります。

そこで、ある配送日における全配送場所のdeliveryを考慮すると、受付可能数のチェックはこのようにする必要があります。

deliveries = Delivery.find_by(date: delivery.date)
product.sales_limit_per_day < Order.where(delivery: deliveries, product: product).count

これで購入可能数のチェックはOKになりましたが、注文の検証としてはまだ不十分です。

受け取り場所のスペースを考慮

受け取り場所において、注文した商品は冷蔵庫の中のコンテナ内に設置されます。

f:id:ryokatsuma:20190405192532j:plain

受け取り場所の冷蔵庫設置面積は基本的に広げることはできないので、冷蔵庫の数は簡単に増やすことがはできません。言い換えると、受け取り場所における冷蔵庫内のコンテナの数も有限になります。 例えば、注文商品の種類が多く、コンテナに空きスペースが存在しない場合は、冷蔵庫に商品を設置することができなくなるので、配送しても冷蔵できない状態になるので注文を受け付けてはいけないことになります。

そのため、「注文しようとしている商品は受け取り場所のコンテナに設置できるかどうか」を判定する必要があります。 そこで、クックパッドマートでは、販売商品1つづつ簡易的に体積を測定し(縦x横x高さ)、注文しようとしている商品の体積は、受け取り場所のコンテナの総体積に収まるかどうかを確認しています。

f:id:ryokatsuma:20190405192553p:plain

実際は、コンテナは配送のオペレーションの観点で販売店や生産者ごとに分けられている(複数の販売店, 生産者の商品が1つのコンテナに混在することは無い)ので、冷蔵庫の中で

  • 空きコンテナを確保できるか
  • コンテナの中に空き容量があるか

を判定しています。

ContainerBox.find_available(
  shop: product.shop,
  delivery: delivery,
  capacity: product.volume
).any?

ちなみに、コンテナの空き容量確認で現在は体積を利用していますが、ここは議論の余地があると考えています。 たとえば、商品は上積み(= 商品の上に商品を載せる)を禁止させたほうが商品が痛むリスクを減らすことができるはずですが、「商品に上積みすれば体積的にはコンテナに積載可能」な状態になってしまいます。そのため、本質的には「商品の接地面積を測定し、その総数がコンテナの設置面積を超えなければ積載可能」とでもしたほうが良いと考えています。 ただ、実際は商品それぞれ接地面積を簡単に測定することは難しいため、オペレーションコストを考えて体積を利用して運用を行っています。

さて、これで注文の検証は十分でしょうか?

配送時に温度を考慮

クックパッドマートで扱う商品は、商品の品質を下げないようにするために「予冷品(肉や魚など)」「未予冷品(野菜など)」「常温品(パンなど)」と複数の温度カテゴリに属し、各温度帯の商品はそれぞれ別のシッパーで配送を行っています。

f:id:ryokatsuma:20190405171806j:plain

たとえば、(これは実際に自分たちで配送実験を行うことで初めて理解したことなのですが、)夏の朝採れ野菜は相当高い温度に上がっています。この野菜を肉や魚など予冷品と同じシッパーで配送した場合、シッパー内の温度が上がってしまい、予冷品の品質に大きな影響が出ることが分かりました。部内では夏の朝採れ野菜温度高すぎ問題として有名な問題です。

また、パンも配送時に冷やしすぎると(約-2度〜5度)、小麦のデンプンがアルファ化老化して品質に大きな影響が出ることが知られています。 このように、商品の配送時にはその商品の「温度帯ごとに分けたシッパー」で配送することが、商品の品質保持の観点で必要になります。

一方で、各ドライバーが配送できるシッパーの積載量は有限です。 先の冷蔵庫の例と同じく、ドライバーも当日急に多く確保することは難しいので、あらかじめ確保したドライバーたちの車の積載量を超えるシッパーは配送できません。 つまり、1ドライバーが配送できるコンテナの総量も有限になるので、注文対象商品の温度カテゴリのシッパーに余裕があるかどうかを判定する必要があります。 注文対象商品の集荷配送を行うドライバーはあらかじめ決定できるので、このように書けます。

Shipper.find_available(
  driver: routing.driver,
  delivery: delivery,
  temperature_category: product.temperature_category
).any?

これらすべての確認を行うことで、「この商品は注文可能かどうか」が判定できます。なかなか道が長いですね。

まとめ

クックパッドマートにおける現在の注文の検証処理について解説を行いました。 実際はさらに細かな考慮がもう少しありますが、概ねこのような確認を注文処理の直前に行っています。 「かなり複雑すぎる処理をしているな、、」と思われた方もいるかと思いますが、実際に実装している僕もそう思います。

また、これらの処理はどんどんアップデートを行っています。 実際、温度カテゴリも最初は全く考慮せずにすべて冷蔵で配送していたものが、パンの扱いが出てきたことで2つの温度カテゴリになり、 夏の朝採れ野菜温度高すぎ問題に対応するために3つの温度カテゴリに対応しました。 このように現実に起きる問題に柔軟に対応するためは、的確にオブジェクト指向プログラミングで設計、実装を行う必要があり、ここは難しくも楽しいところでもあります。

一方で、このような複雑な処理も、おいしい食材をユーザーさんに確実に届けるために必要な条件だと考えています。 実際、リリースしてから半年近く経過していますが、冷蔵庫に設置できないなどの配送事故や、商品が傷んでいたといった事故は起きずに済んでいるので、引き続きおいしい食材を安全に届けるために技術によるサポートを行っていきたいと思います。

この記事を通して、クックパッドマートのサービス開発にご興味を持っていただけた方がいらっしゃいましたら、ぜひ一緒にサービスを作りましょう!

www.wantedly.com

お知らせ

クックパッドマートでは、4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催予定です。

cookpad.connpass.com

今回の記事のような生鮮ECそのものの仕組みや、流通の仕組みの開発に興味がある方、クックパッドマートのエンジニアと直接話してみたい方はぜひご応募ください!お待ちしています!

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/