A recommendation engine does not need a separate machine-learning platform. You can build a practical semantic recommender with Laravel, PostgreSQL, pgvector, and the Laravel AI SDK.
This approach represents products, articles, or other content as embedding vectors. It then compares those vectors with cosine similarity. The closest records become recommendations.
Laravel, a php web framework, provides the application structure, database integration, queues, and API layer. The AI SDK provides a consistent interface for embedding providers such as OpenAI. Anthropic can handle text generation when you want to explain or personalize the recommendations.
The architecture: Content to recommendations
The system has four parts:
- Store descriptive content for each item.
- Generate an embedding for that content.
- Store the vector in PostgreSQL with
pgvector. - Search for nearby vectors when a user requests recommendations.
Suppose your application sells books. You can embed each book's title, author, category, and description:
Title: Building APIs with Laravel
Author: Jane Doe
Category: Web Development
Description: A practical guide to designing, testing, and deploying Laravel APIs.
The embedding model converts this text into a fixed-length array of floating-point numbers. Similar content produces vectors that are close together in vector space.
This differs from a keyword query. A search for “backend development” can find a book described as “building APIs,” even when the exact words do not match.

Installation and provider setup: OpenAI for embeddings
Install the Laravel AI SDK with Composer:
composer require laravel/ai
Publish the package configuration and migrations:
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
Add your provider key to .env:
OPENAI_API_KEY=your-openai-api-key
ANTHROPIC_API_KEY=your-anthropic-api-key
The current Laravel AI SDK supports OpenAI for embeddings. Anthropic is supported for text generation, but it is not an embedding provider. Use OpenAI, Gemini, Cohere, Jina, or another supported embedding provider for vector creation.
You can use Anthropic later to generate a short explanation such as, “Recommended because you read books about Laravel APIs.” Keep that text-generation step separate from vector retrieval.
Embedding generation: Create vectors with the AI SDK
Use the Embeddings class when generating vectors explicitly. The following example uses OpenAI’s text-embedding-3-small model with 1,536 dimensions:
<?php
namespace App\Services;
use App\Models\Product;
use Laravel\Ai\Embeddings;
use Laravel\Ai\Enums\Lab;
class ProductEmbeddingService
{
public function generate(Product $product): array
{
$input = implode("\n", [
"Title: {$product->title}",
"Category: {$product->category}",
"Description: {$product->description}",
]);
$response = Embeddings::for([$input])
->dimensions(1536)
->generate(
provider: Lab::OpenAI,
model: 'text-embedding-3-small',
);
return $response->embeddings[0];
}
}
Generate embeddings after creating or updating a product. For larger catalogs, dispatch this work to a queue instead of blocking the HTTP request.
$product->update([
'embedding' => app(ProductEmbeddingService::class)
->generate($product),
]);
A queued job is a better production pattern:
ProcessProductEmbedding::dispatch($product);
This keeps provider latency away from customer-facing requests. It also lets you retry temporary provider failures through Laravel’s queue system.
The AI SDK can generate multiple embeddings in one request. Batch generation is useful when importing a catalog or rebuilding vectors after changing your embedding model.
Database migration: Store vectors with pgvector
PostgreSQL needs the pgvector extension. Laravel provides native schema support for vector columns.
Create a migration for the products table:
<?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::table('products', function (Blueprint $table) {
$table->vector('embedding', dimensions: 1536)->index();
});
}
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('embedding');
});
}
};
The dimension count must match the embedding model. A 1536-dimension column cannot store vectors generated with a model that returns 768 dimensions.
Calling index() creates an HNSW index with cosine distance. This improves nearest-neighbor searches as the table grows.
For local development, install pgvector in your PostgreSQL environment. PostgreSQL databases on Laravel Cloud include pgvector.
Cast the column on the Eloquent model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\AsVector;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected function casts(): array
{
return [
'embedding' => AsVector::class,
];
}
}
The cast handles conversion between PHP arrays and PostgreSQL vector values.
Similarity search: Find related items
A recommendation query starts with a reference item. You can use the reference product’s embedding to find other products with similar content.
Create a service that excludes the current product and applies normal business filters:
<?php
namespace App\Services;
use App\Models\Product;
use Laravel\Ai\Embeddings;
use Laravel\Ai\Enums\Lab;
use Illuminate\Support\Collection;
class RecommendationService
{
public function forProduct(Product $product, int $limit = 6): Collection
{
return Product::query()
->where('id', '!=', $product->id)
->where('is_active', true)
->whereVectorSimilarTo(
column: 'embedding',
value: $product->embedding,
minSimilarity: 0.65,
)
->limit($limit)
->get();
}
public function forQuery(string $query, int $limit = 6): Collection
{
$embedding = Embeddings::for([$query])
->dimensions(1536)
->generate(
provider: Lab::OpenAI,
model: 'text-embedding-3-small',
)
->embeddings[0];
return Product::query()
->where('is_active', true)
->whereVectorSimilarTo(
column: 'embedding',
value: $embedding,
minSimilarity: 0.65,
)
->limit($limit)
->get();
}
}
The whereVectorSimilarTo method compares vectors using cosine similarity. The minSimilarity value ranges from 0.0 to 1.0. A higher threshold returns fewer, closer matches.
Start with a threshold around 0.60 or 0.65. Test it against real catalog data. A threshold that works for technical articles may be too strict for products with short descriptions.
Laravel also accepts a plain string instead of an embedding array:
$products = Product::query()
->whereVectorSimilarTo(
'embedding',
'books about building Laravel APIs',
minSimilarity: 0.65,
)
->limit(6)
->get();
Laravel generates the query embedding through the configured provider. Explicit generation gives you more control over the provider, model, caching, and error handling.
SimilaritySearch: Give agents access to recommendations
The AI SDK’s SimilaritySearch tool is designed for agents. It searches an Eloquent model by vector similarity and lets an agent retrieve relevant records from your database.
use App\Models\Product;
use Laravel\Ai\Tools\SimilaritySearch;
public function tools(): iterable
{
return [
SimilaritySearch::usingModel(
model: Product::class,
column: 'embedding',
minSimilarity: 0.65,
limit: 6,
query: fn ($query) => $query
->where('is_active', true),
),
];
}
At runtime, the tool embeds the user’s query and uses whereVectorSimilarTo under the hood. This works well when an agent needs to combine recommendations with a conversation.
Use the direct query builder for predictable product recommendations. Use SimilaritySearch when an agent should decide when to search and how to use the returned records.

REST API endpoint: Return recommendations from Laravel
If you want to build rest api with php, Laravel gives you the routing, validation, serialization, and authentication primitives you need.
Create a controller:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Product;
use App\Services\RecommendationService;
use Illuminate\Http\JsonResponse;
class ProductRecommendationController extends Controller
{
public function __invoke(
Product $product,
RecommendationService $recommendations,
): JsonResponse {
$items = $recommendations
->forProduct($product, limit: 6)
->map(fn (Product $item) => [
'id' => $item->id,
'title' => $item->title,
'category' => $item->category,
'description' => $item->description,
]);
return response()->json([
'product_id' => $product->id,
'recommendations' => $items,
]);
}
}
Register the endpoint in routes/api.php:
<?php
use App\Http\Controllers\Api\ProductRecommendationController;
use Illuminate\Support\Facades\Route;
Route::get(
'/products/{product}/recommendations',
ProductRecommendationController::class,
);
The endpoint now returns:
{
"product_id": 42,
"recommendations": [
{
"id": 18,
"title": "Testing Laravel Applications",
"category": "Web Development",
"description": "A guide to testing Laravel applications."
}
]
}
Add authentication when recommendations contain private or user-specific data. Laravel Sanctum provides a practical option for protecting API routes.
Production considerations: Keep recommendations useful
Embedding quality depends on input quality. Build a stable text representation for each item. Include fields that describe meaning, but avoid internal IDs or volatile metadata.
Re-embed an item when its meaningful content changes. Store the embedding model and version if you expect to change providers later. A new model may produce vectors with different dimensions and cannot share the same vector column.
Cache repeated query embeddings when users submit the same natural-language request. The AI SDK supports embedding caching in config/ai.php and on individual requests.
Monitor the recommendation endpoint like any other production feature. Track latency, provider failures, empty result rates, and click-through behavior. Laravel Nightwatch can help you inspect application performance and logs.
You can also combine vector search with traditional filters. Restrict results by tenant, category, inventory status, language, or publication date before returning them to the client.
The next step: Add feedback and reranking
A vector search engine gives you a strong first stage. The next layer can use user behavior, inventory rules, popularity, and freshness.
For more complex catalogs, retrieve a larger candidate set with vector search. Then rerank those candidates with the AI SDK or a business scoring function. Laravel’s search documentation covers vector search, reranking, and combining semantic search with traditional filters.
Embeddings make recommendations understandable as a Laravel feature. Store vectors with Eloquent, query them with the database, and expose the result through familiar php developer tools and API controllers.
As recommendation data grows, you can add feedback loops, agent-based explanations, and personalized ranking without changing the foundation. If you build something with Laravel and the AI SDK, we’d love to hear what you discover in the Laravel community.