Recommendation systems do not need a separate machine learning platform. You can build a practical content-based system inside Laravel with embeddings, PostgreSQL, and vector search.
This guide builds an API that recommends products based on semantic similarity. The same pattern works for articles, courses, videos, support documents, or listings.
Laravel provides the application layer, queue system, validation, routing, and database access. The Laravel AI SDK generates embeddings. PostgreSQL with pgvector stores and compares them.
Laravel is a productive php web framework because these pieces fit into a familiar Eloquent workflow.
Recommendation Architecture: Embed, Store, Query
The system has four stages:
- Convert each product description into an embedding.
- Store the embedding beside the product.
- Embed a product or user preference query.
- Return products with the closest vectors.
An embedding is a numeric representation of meaning. Products about “lightweight hiking backpacks” can be close to products described as “durable outdoor day packs,” even when they use different words.
This is content-based recommendation. It does not require historical user behavior. You can add clicks, purchases, and ratings later.

Step 1: Install the AI SDK and Configure PostgreSQL
Create a Laravel application or use an existing one. Then install the AI SDK:
composer require laravel/ai
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
Configure an embedding provider in .env:
OPENAI_API_KEY=your-api-key
The provider and default model can be configured in config/ai.php. This guide uses OpenAI’s text-embedding-3-small model with 1,536 dimensions.
You need PostgreSQL with the vector extension. Laravel Cloud PostgreSQL databases include pgvector, or you can enable it locally:
CREATE EXTENSION IF NOT EXISTS vector;
Laravel’s native vector features currently target PostgreSQL with pgvector. The Laravel search documentation also covers MongoDB vector search through the Laravel MongoDB package.
Step 2: Store Product Embeddings
Create a migration for the products you want to recommend:
php artisan make:model Product -m
Define the product fields and vector column:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::ensureVectorExtensionExists();
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description');
$table->string('category')->nullable();
$table->unsignedInteger('price_cents')->nullable();
$table->boolean('is_published')->default(true);
$table->vector('embedding', dimensions: 1536)
->nullable()
->index();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
Calling index() creates an HNSW index with cosine distance. This improves similarity queries as your catalog grows.
The dimension count must match the embedding model. If you change providers or models, update the column and regenerate existing vectors.
Run the migration:
php artisan migrate
Now configure the Eloquent model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = [
'title',
'description',
'category',
'price_cents',
'is_published',
'embedding',
];
protected $hidden = [
'embedding',
];
protected function casts(): array
{
return [
'is_published' => 'boolean',
'embedding' => 'array',
];
}
public function embeddingText(): string
{
return implode("\n", [
"Title: {$this->title}",
"Category: {$this->category}",
"Description: {$this->description}",
]);
}
}
Keep the text used for embeddings consistent. If titles, categories, and descriptions matter during recommendations, include all three.
Step 3: Generate Embeddings with a Queue Job
Embedding generation calls an external provider. It should not block a product creation request.
Create a queued job:
php artisan make:job GenerateProductEmbedding
Implement the job with the Laravel AI SDK:
<?php
namespace App\Jobs;
use App\Models\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Laravel\Ai\Embeddings;
use Laravel\Ai\Enums\Lab;
class GenerateProductEmbedding implements ShouldQueue
{
use Queueable;
public function __construct(
public Product $product,
) {}
public function handle(): void
{
$response = Embeddings::for([
$this->product->embeddingText(),
])
->dimensions(1536)
->generate(
provider: Lab::OpenAI,
model: 'text-embedding-3-small',
);
$this->product->update([
'embedding' => $response->embeddings[0],
]);
}
}
Dispatch the job after creating or updating a product:
GenerateProductEmbedding::dispatch($product);
For bulk imports, generate multiple embeddings in one request:
$response = Embeddings::for([
$firstProduct->embeddingText(),
$secondProduct->embeddingText(),
])->generate();
Batching reduces network overhead. The AI SDK also supports embedding caching, which helps avoid paying for identical inputs repeatedly.
Step 4: Query Similar Products
Laravel adds vector-aware query methods to Eloquent. The main method is whereVectorSimilarTo.
Create a recommendation service:
php artisan make:class Services/RecommendationService
Then add two recommendation paths:
<?php
namespace App\Services;
use App\Models\Product;
use Illuminate\Support\Collection;
class RecommendationService
{
public function similarTo(Product $product, int $limit = 8): Collection
{
return Product::query()
->where('id', '!=', $product->id)
->where('is_published', true)
->whereNotNull('embedding')
->whereVectorSimilarTo(
'embedding',
$product->embedding,
minSimilarity: 0.4,
)
->limit($limit)
->get();
}
public function forPreference(string $query, int $limit = 8): Collection
{
return Product::query()
->where('is_published', true)
->whereNotNull('embedding')
->whereVectorSimilarTo(
'embedding',
$query,
minSimilarity: 0.4,
)
->limit($limit)
->get();
}
}
When you pass an embedding array, Laravel compares it with the stored vectors.
When you pass a string, Laravel generates the query embedding automatically using the configured provider:
Product::query()
->whereVectorSimilarTo(
'embedding',
'minimalist desk for a small home office',
minSimilarity: 0.4,
)
->limit(8)
->get();
The similarity threshold depends on your data. Start with 0.4, inspect results, and tune it using real interactions.

Step 5: Expose Recommendations Through a REST API
Laravel makes it straightforward to build rest api with php. Enable API routing and Sanctum authentication:
php artisan install:api
Create the controller:
php artisan make:controller RecommendationController
Add endpoints for both recommendation types:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Services\RecommendationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class RecommendationController extends Controller
{
public function __construct(
protected RecommendationService $recommendations,
) {}
public function similar(
Request $request,
Product $product,
): JsonResponse {
$limit = min($request->integer('limit', 8), 50);
$items = $this->recommendations->similarTo(
$product,
$limit,
);
return response()->json([
'product_id' => $product->id,
'recommendations' => $items,
]);
}
public function fromPreference(Request $request): JsonResponse
{
$validated = $request->validate([
'query' => ['required', 'string', 'max:500'],
'limit' => ['sometimes', 'integer', 'min:1', 'max:50'],
]);
$items = $this->recommendations->forPreference(
$validated['query'],
$validated['limit'] ?? 8,
);
return response()->json([
'query' => $validated['query'],
'recommendations' => $items,
]);
}
}
Register the routes in routes/api.php:
<?php
use App\Http\Controllers\RecommendationController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () {
Route::get(
'/recommendations/products/{product}',
[RecommendationController::class, 'similar'],
);
Route::post(
'/recommendations/preferences',
[RecommendationController::class, 'fromPreference'],
);
});
Laravel automatically adds the /api prefix to routes in routes/api.php.
Test the product endpoint:
curl "https://example.com/api/recommendations/products/42?limit=6" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"
Test the preference endpoint:
curl -X POST "https://example.com/api/recommendations/preferences" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"quiet mechanical keyboard for programming","limit":6}'
The result can power a “You might also like” component in Vue, React, Livewire, or any mobile client.
Production Improvements: Filters, Fallbacks, and Monitoring
Vector similarity should usually work with traditional filters. For example, filter by tenant, stock status, price range, or category before applying vector search:
Product::query()
->where('is_published', true)
->where('category', $category)
->whereBetween('price_cents', [$minimum, $maximum])
->whereVectorSimilarTo('embedding', $query)
->limit(8)
->get();
Add a fallback for products without embeddings or queries that return no strong matches. Popular products, recent products, or category bestsellers provide a better experience than an empty response.
Do not generate embeddings inside every recommendation request when you can avoid it. Store product embeddings during ingestion. Cache repeated preference queries when appropriate.
Finally, observe provider latency, token usage, queue failures, and recommendation click-through rates. Laravel’s ecosystem includes tools such as Horizon for queues and Nightwatch for application monitoring.
The Laravel AI SDK also includes a higher-level SimilaritySearch tool for agents. For a direct recommendation API, querying your Eloquent model gives you better control over filters, authorization, and response shape.
A recommendation engine starts with a simple loop: represent your catalog, store the vectors, retrieve nearby items, and measure the results. Laravel keeps that loop inside the same application structure your team already uses, supported by familiar php developer tools and a clear path from prototype to production.