Integrating the OpenAI Embeddings API with Laravel for Intelligent Search and Recommendations
In today’s digital landscape, delivering fast, relevant results is a key differentiator for eCommerce platforms and SaaS products. Traditional keyword‑based search often falls short when users expect semantic understanding and personalized suggestions. This tutorial shows you how to harness the power of OpenAI Embeddings API inside a Laravel application to create a smart, AI‑driven search and recommendation engine.
Why Use OpenAI Embeddings?
- Semantic Understanding: Convert text, product titles, or descriptions into high‑dimensional vectors that capture meaning, not just keywords.
- Scalable Similarity Search: Use vector similarity (cosine, dot‑product) to find the most relevant items in milliseconds.
- Personalization: Combine user behavior vectors with product embeddings to generate tailored recommendations.
- AI Automation: Reduce manual tagging and categorisation effort—perfect for startups and enterprises looking for rapid digital transformation.
Prerequisites
Before we dive in, make sure you have the following:
- Laravel 9+ installed (we’ll use Laravel 10 in the examples).
- Composer and PHP 8.1+.
- An OpenAI API key.
- MySQL or PostgreSQL database for storing vectors (JSON column works well).
- Basic knowledge of Laravel service providers, queues, and Eloquent models.
Step 1 – Set Up the Laravel Project
composer create-project laravel/laravel openai‑search-demo
cd openai‑search-demo
php artisan serve
Open the project in your favorite IDE. Add the OpenAI PHP client library:
composer require openai-php/client
Step 2 – Configure Environment Variables
In .env, add your OpenAI key and a default model for embeddings:
OPENAI_API_KEY=sk-********************
OPENAI_EMBEDDING_MODEL=text-embedding-ada-002
Also, set a dedicated queue connection for background vector generation (Redis is recommended):
QUEUE_CONNECTION=redis
Step 3 – Create a Service Wrapper for the Embeddings API
Generate a service class that abstracts the API call:
php artisan make:service OpenAIEmbeddingService
Replace the generated file app/Services/OpenAIEmbeddingService.php with:
namespace App\Services;
use OpenAI; // Provided by openai-php/client
use Illuminate\Support\Facades\Log;
class OpenAIEmbeddingService
{
protected $client;
protected $model;
public function __construct()
{
$this->client = OpenAI::client(config('services.openai.key'));
$this->model = config('services.openai.embedding_model');
}
/**
* Generate an embedding vector for a given text.
*/
public function embed(string $text): array
{
try {
$response = $this->client->embeddings()->create([
'model' => $this->model,
'input' => $text,
]);
// OpenAI returns an array of embeddings; we need the first one.
return $response->data[0]->embedding;
} catch (\Exception $e) {
Log::error('OpenAI embedding error: '.$e->getMessage());
return [];
}
}
}
Register the service in config/services.php:
'openai' => [
'key' => env('OPENAI_API_KEY'),
'embedding_model' => env('OPENAI_EMBEDDING_MODEL', 'text-embedding-ada-002'),
],
Step 4 – Design the Product Model to Store Vectors
Assume an eCommerce product table with name, description, and a JSON column embedding:
php artisan make:model Product -m
In the migration file:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description');
$table->json('embedding')->nullable(); // Stores vector as JSON array
$table->timestamps();
});
Run migrations:
php artisan migrate
Step 5 – Generate Embeddings Asynchronously
Embedding generation can be time‑consuming. Use Laravel queues to process it in the background.
php artisan make:job GenerateProductEmbedding
Job implementation (app/Jobs/GenerateProductEmbedding.php):
namespace App\Jobs;
use App\Models\Product;
use App\Services\OpenAIEmbeddingService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class GenerateProductEmbedding implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $productId;
public function __construct(int $productId)
{
$this->productId = $productId;
}
public function handle(OpenAIEmbeddingService $embeddingService)
{
$product = Product::find($this->productId);
if (!$product) return;
$text = $product->name . ' ' . $product->description;
$vector = $embeddingService->embed($text);
if (!empty($vector)) {
$product->embedding = $vector;
$product->save();
}
}
}
Dispatch the job whenever a product is created or updated:
use App\Jobs\GenerateProductEmbedding;
public function store(Request $request)
{
$product = Product::create($request->only(['name','description']));
GenerateProductEmbedding::dispatch($product->id);
return response()->json($product, 201);
}
Step 6 – Implement Semantic Search
When a user searches, we embed the query and find the nearest product vectors.
php artisan make:controller SearchController
Controller logic (app/Http/Controllers/SearchController.php):
namespace App\Http\Controllers;
use App\Models\Product;
use App\Services\OpenAIEmbeddingService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class SearchController extends Controller
{
public function search(Request $request, OpenAIEmbeddingService $embeddingService)
{
$query = $request->input('q');
$queryVector = $embeddingService->embed($query);
if (empty($queryVector)) {
return response()->json(['error' => 'Embedding failed'], 500);
}
// PostgreSQL example using <-> operator for cosine similarity
$results = Product::select('*')
->whereNotNull('embedding')
->orderByRaw('embedding <-> ?', [json_encode($queryVector)])
->limit(10)
->get();
return response()->json($results);
}
}
For MySQL 8.0+, you can store vectors as JSON and compute similarity in PHP after fetching a limited set, or use the VECTOR data type via external extensions. The key idea remains: compare the query embedding with stored embeddings.
Step 7 – Build a Recommendation Engine
Recommendations can be built by blending user interaction vectors (e.g., recent clicks) with product embeddings. A simple approach:
- Collect user‑item interaction data (views, purchases).
- Create a user profile vector by averaging embeddings of interacted products.
- Find nearest product vectors that the user hasn’t interacted with.
Example service:
php artisan make:service RecommendationService
namespace App\Services;
use App\Models\Product;
use App\Models\User; // Assume a pivot table user_product_interaction
use Illuminate\Support\Facades\DB;
class RecommendationService
{
public function recommendForUser(int $userId, int $limit = 5): array
{
$interacted = DB::table('user_product_interaction')
->where('user_id', $userId)
->pluck('product_id')
->toArray();
$vectors = Product::whereIn('id', $interacted)
->pluck('embedding')
->map(fn($e) => json_decode($e, true))
->toArray();
if (empty($vectors)) return [];
// Simple average
$userVector = array_fill(0, count($vectors[0]), 0);
foreach ($vectors as $vec) {
foreach ($vec as $i => $val) {
$userVector[$i] += $val;
}
}
$userVector = array_map(fn($v) => $v / count($vectors), $userVector);
// Find nearest products not yet interacted with
$candidates = Product::whereNotIn('id', $interacted)
->whereNotNull('embedding')
->get();
$scored = $candidates->map(function ($product) use ($userVector) {
$embedding = json_decode($product->embedding, true);
$score = $this->cosineSimilarity($userVector, $embedding);
$product->score = $score;
return $product;
})->sortByDesc('score')->take($limit);
return $scored->values()->all();
}
private function cosineSimilarity(array $a
Join the Conversation
0 Comments