laravel-recommender maintained by solutionforest
Laravel Recommender
A simple recommendation engine for Laravel. Self-hosted “for you” feeds — collaborative filtering, time-decayed trending, and explainable blends. Requires PHP 8.3+ and Laravel 11–13.
If your product already stores user↔item interactions in your tables, this package turns them into a personalised ranked feed — without Amazon Personalize, a Python microservice, or shipping events to a third party.
Not only likes and views. Any event name works (share, bookmark, comment, sponsor, …). Defaults use view / like as examples; add a third (or tenth) type in config + your InteractionSource.
| You have… | This package gives you… |
|---|---|
| Interactions in your tables (like / view / share / …) | A recommendation engine: “acted together” + trending |
| A Laravel API or Livewire UI | Recommender::page() → cards / Laravel paginator JSON |
| Cache store + a queue worker | Fast cached lists, seen-set paging, explainable rankings |
Built for real product feeds (blog posts, inspiration / reviews, shops).
Why use it?
- Simple recommendation engine — item–item CF + trending + your own rules; no ML ops stack.
- Stays in your stack — reads existing tables; only adds 3 small
rec_*tables + cache keys. - Cheap on the request path — scroll serves from cache; no
ORDER BY RAND(), no external API call. - Explainable —
recommender:explainshows which sources scored each card. - Tunable — weights, exploration, diversity caps — not a black-box model.
- Runnable samples —
examples/(blog,products,eventswith view/like/join,events-advancedwith district + type affinity,reviews+ pet-type source), covered by tests.
How we recommend
No neural net. A small, transparent recommendation engine: assemble a ranked list from a few clear signals, then serve it from cache.
your interactions → background jobs → personal ranked list → next page of cards
(any events) (learn) (in cache) (no repeats)
- Learn in the background (not on every scroll)
- Every ~10 minutes: fold new interactions into a trending “hot” score (recent events count more; each event type has its own weight).
- Nightly: build neighbours — “people who engaged with this also engaged with that” (item–item collaborative filtering).
- Build a personal list when the user’s feed is missing or stale
Mix weighted sources (co-liked items, trending, plus any rules you add — e.g. same pet type, related shops), run your filter (published / not blocked), optional boosters (quality), sprinkle a little exploration (fresh items), then diversity caps (e.g. max 1 author per page). - Serve pages from that list
Recommender::page()returns the next unseen cards. Guests / brand-new users get a shared trending list until they have history. - You can see why
php artisan recommender:explain {feed} {userId}prints each card’s source scores.
Diagrams and request flow: How it works.
Formulas behind “hot”, neighbours, and quality: How we score (maths).
How to adjust the ratio
All knobs live in config/recommender.php under your feed (e.g. feeds.posts). Bigger number = that signal counts more. Weights are relative to each other.
1. Event types & weights (trending “hot”)
Event names are arbitrary strings. Scaffold defaults are view / like, but you can add more:
'events' => [
'view' => 1.0, // weak / browse
'like' => 4.0, // stronger
'share' => 6.0, // third type — any name you yield from InteractionSource
// 'bookmark' => 5.0,
],
Only events listed here with weight > 0 count toward trending. Default story is “a like ≈ 4 views”; raise like / add share if stronger actions should dominate.
These feed recommender:update-scores → the hot score. After changing them, re-run scores (and forget trending cache) so old hot values catch up.
Quality (Wilson) uses separate lists (also any of your event names):
'positive_events' => ['like', 'share'], // “good” actions
'exposure_events' => ['view'], // “seen” / denominator
In InteractionSource, yield new Interaction($userId, $itemId, 'share', $at) (same pattern for every type).
2. Personal “liked together” vs trending vs your own rules
'sources' => [
CoInteractionSource::class => 0.6, // people who liked X also liked Y
TrendingSource::class => 0.2, // hot items
// YourSource::class => 0.3, // e.g. same pet / related shop
],
'boosters' => [
QualityBooster::class => 0.2, // like-rate quality bump
],
Raise a source weight → that source’s cards rank higher in the blend.
exploration_ratio (e.g. 0.15) is different: it reserves slots for fresh/explore items, not the same as a source weight.
3. Sponsor wants certain posts / shops to show more
Two pieces (see examples/blog):
- Put the author/shop ids in config
- Give them a source (inject into the pool) and a booster (push score up)
'sponsored_author_ids' => [42, 99], // or sponsored_shop_ids for shops
'sources' => [
// …
SponsoredShopSource::class => 0.20, // raise → more sponsored candidates enter
],
'boosters' => [
// …
SponsoredShopBooster::class => 0.25, // raise → those items rank higher
],
'diversity' => ['author' => 1], // still cap so one sponsor cannot fill the whole page
Booster alone only helps items already in the pool — Source + Booster is the usual paid pattern.
After you change weights
Recommender::forgetUser('posts', $userId);
Recommender::forgetTrending('posts');
Then recommender:explain posts {userId} to verify. More recipes: Tuning.
Start here
👉 docs/tutorial.md — step-by-step scaffold checklist (make-feed → edit → cook → schedule). Easiest path for a first install.
| Doc | When |
|---|---|
| Scaffold tutorial | First install — numbered checklist |
| Getting started | Same story, slower / more analogy |
| How it works | Pipeline & request flow |
| How we score (maths) | Formulas for CF, trending, Wilson quality |
| Tuning | Weights & recipes |
| Reference | API, jobs, config · What is Context? |
| Operations | Go-live, FAQ, scale |
Install
composer require solutionforest/laravel-recommender:^0.0.14
php artisan migrate
php artisan recommender:make-feed
recommender:make-feed is an interactive Laravel Prompts wizard (presets: blog / products / events / reviews / custom, optional third event). Pass -n + flags for CI. It publishes config/recommender.php if needed, scaffolds the three classes under app/Recommendation/..., and registers the feed.
Full walkthrough: Scaffold tutorial. Or copy an example from examples/ (blog · products · events · events-advanced · reviews).
On Packagist. Current: ^0.0.14 (PHP 8.3+ / Laravel 11–13).
Changelog: CHANGELOG.md · Releases: GitHub Releases
60-second shape
// 1–3. Wire InteractionSource + ItemFilter + CardTransformer (copy examples/blog)
// 4. config/recommender.php → feeds.posts → those classes + weights
// 5. Serve:
$result = Recommender::page('posts', $user?->id, (int) $request->query('page', 1));
return response()->json($result->toPaginatorArray(['path' => $request->url()]));
php artisan recommender:update-scores posts
php artisan recommender:build-neighbours posts
php artisan recommender:explain posts {userId}
php artisan recommender:status
Schedule: scores ~every 10 min · neighbours nightly · prune daily.
Details: getting started → schedule.
Cache / locks (configurable via RECOMMENDER_CACHE_STORE or CACHE_STORE):
| Choice | When |
|---|---|
| redis | Production default — fastest hot path + atomic locks |
| database | OK on Laravel 11–13 if you ran php artisan cache:table (includes cache_locks) |
| array / file | Local / CI; fine for one process |
php artisan recommender:status shows whether locks are available. Details: reference → Cache.
For AI agents / Cursor
This package ships agent guidance so coding agents can wire feeds correctly in your app:
| File | Purpose |
|---|---|
AGENTS.md |
Short rules (package + consuming apps) |
skills/laravel-recommender/SKILL.md |
Cursor Skill — install into .cursor/skills/ of the app |
mkdir -p .cursor/skills
cp -R vendor/solutionforest/laravel-recommender/skills/laravel-recommender .cursor/skills/
Then ask the agent to add a for-you / inspiration feed — it should use recommender:make-feed, not invent Personalize or ORDER BY RAND().
License
MIT © Solution Forest Limited