Feature: Recommendation Service

This commit introduces a RecommendationService that is used when indexing products.

The service is utilizing a recommendation rules interface, so many rules can be created and applied during indexing the products
This commit is contained in:
2026-09-01 12:06:29 +03:00
parent 53d8a5aefe
commit 0f07751559
11 changed files with 507 additions and 0 deletions
@@ -0,0 +1,47 @@
<?php
namespace Modules\Core\Catalog\Services;
use Illuminate\Support\Collection;
use Lunar\Models\Product;
use Modules\Core\Catalog\Contracts\RecommendationRule;
/**
* Runs each rule in config('catalog.recommendation_rules'), in order,
* topping up from each successive rule until $limit distinct products are
* collected or every rule is exhausted — e.g. 3 from SameCategoryRule
* (the product's category only has 3 other products) + 1 from RandomRule.
* No rule is special-cased as "the fallback" here; a store gets fallback
* behaviour purely by how it orders its own config (e.g. SameCategoryRule
* before RandomRule). Never returns the same product twice even if two
* rules would both suggest it (see RecommendationRule's $exclude), and
* never returns fewer than $limit unless the store genuinely doesn't have
* that many other products at all.
*/
class RecommendationService
{
/**
* @return Collection<int, Product>
*/
public function recommend(Product $product, int $limit = 4): Collection
{
$recommendations = collect();
foreach (config('catalog.recommendation_rules', []) as $ruleClass) {
if ($recommendations->count() >= $limit) {
break;
}
$exclude = [$product->id, ...$recommendations->pluck('id')];
$remaining = $limit - $recommendations->count();
/** @var RecommendationRule $rule */
$rule = app($ruleClass);
$recommendations = $recommendations->merge(
$rule->recommend($product, $remaining, $exclude)
);
}
return $recommendations->take($limit)->values();
}
}