Skip to content

Instantly share code, notes, and snippets.

@thesubhendu
Created November 1, 2024 10:55
Show Gist options
  • Save thesubhendu/32e9372051f1906811185264b05f1076 to your computer and use it in GitHub Desktop.
Save thesubhendu/32e9372051f1906811185264b05f1076 to your computer and use it in GitHub Desktop.
AI-Driven Recommendation Engine in Laravel

Use cases:

Display matching jobs to applicants in Job websites, recommended videos , posts, products etc. One way to do it is using Sql queries which get difficult if there are too many parameters to consider. AI comes to rescue. Simplest way to build recommendation engine is using Vector embeddings .

Here I will use Open AI to generate embeddings, we can use other LLM models as well.

Todos

  • Laravel project with two models with data seeded applicants and Jobs
  • Database setup with PGVector and pg laravel package, open ai php package
    • jobs table embeddings col
  • To Text on each model, Open AI embedding service and vector search service
  • Batch embeddings
  • Display Results

Implementation

Postgres Installation

Setup in your OS Pgvector, https://www.postgresql.org/download/

Models and Database

Setup Postgres using Postgress.app

php artisan make:model Applicant -mf php artisan make:model Job -mf

jobs

  • title
  • description
  • requirements
  • benefits
  • salary
  • company_name
  • type (remote, hybrid, onsite)
  • embeddings $table->vector('embedding', 1536);
Migration
class CreateJobsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('jobs', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description');
            $table->text('requirements');
            $table->text('benefits');
            $table->string('company_name');
            $table->enum('type', ['remote', 'hybrid', 'onsite']);
            $table->vector('embeddings', 1536);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('jobs');
    }
}

applicants

  • full_name
  • summary
  • experiences
  • skills
  • expected_salary
  • preferences (location, skill, type)

Packages

composer require openai-php/client pgvector/pgvector

Indexing

[[Indexing PostgresSQL Vector]]

Code

App/Models/Job

public function toEmbeddingText(Job $job)  
{  
  $text = <<<TEXT  
	Job Title : {$job->title} 
	Salary: {$job->salary} 
	Company: {$job->company_name} 
	Location: {$job->city}, {$job->state}  
	Description: {$job->salary},
	Skills: {$job->skills}  
	TEXT;  
    return $text;  
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Applicant extends Model
{

    public function toEmbeddingText(): string
    {
        $text = <<<TEXT
  
        Summary: {$this->summary}
        Experience: 
        
        {$this->experiences->implode('description', "\n")}

        Skills: 
        
        {$this->skills->implode('name', ', ')}

        Expected Salary: {$this->expected_salary}
        Preferences: 
            Location: {$this->preferences->implode('location', ', ')}
            Skills: {$this->preferences->implode('skill', ', ')}
            Type: {$this->preferences->implode('type', ', ')}

        TEXT;

        return $text;
    }
}

Embedding Service

<?php  

use OpenAI;  
class EmbeddingService  
{  
    private OpenAI\Client $openai;  
  
    public string $embeddingModel='text-embedding-3-small';  
  
    public function __construct()  
    {  
          
        $this->openai = OpenAI::client(config('services.openai.apikey'));  
    }  
  
    public function getClient()  
    {  
        return $this->openai;  
    }  
  
  
   public function generateEmbedding(string $text) : array  
    {  
        $response = $this->openai->embeddings()->create([  
            'model' => $this->embeddingModel,  
            'input' => $text,  
        ]);  
  
        return $response->embeddings[0]->embedding;  
    }  
}

Vector search service

<?php  
use App\Models\Job;  
use Pgvector\Laravel\Distance;  
class VectorDBService  
{  
	public function querySimilarItems(Applicant $applicant, int $topK = 5): Collection  
    {  
	    if(!$applicant->embedding) {
	    
		    $embedding =
		     $embeddingService->generateEmbedding($applicant);
		     
		    $applicant->update(['embedding'=> $embedding]);
	    }
	    
        return Job::query()
        ->nearestNeighbors('embedding',
         $applicant->embedding, Distance::L2)
        ->take($topK)
        ->get();  
    }  
 
  
}

Batch Embedding (save cost)

Steps
  1. Generate [[JsonL File for Open AI batch Embedding]]
  2. Upload it (JsonL file) , create a batch, save batch id to db
id
batch_id string
status 
results_file_path

[Batch Status](// https://platform.openai.com/docs/guides/batch/4-checking-the-status-of-a-batch Track the status (pending, completed, failed)) 3. check if status of batch is completed, if yes download results JsonL file and process each it and bulk insert in DB

Generate JsonL File
private function generateJsonLine(Job $model): string  
{  
    $text = $model->toEmbeddingText();  
    $data = [  
        'custom_id' => (string)$model->getKey(),  
        'method'=> 'POST',  
        'url' => '/v1/embeddings',  
        'body' => [  
            'model' => 'text-embedding-3-small',  
            'input' => $text  
        ],  
  
    ];  
    return json_encode($data, JSON_UNESCAPED_UNICODE);  
}

This statement creates an index on the embedding column in the items table to optimize similarity search using the ivfflat index type, which is available in pgvector for PostgreSQL.

Here’s a breakdown:

  1. Index Type (ivfflat): • ivfflat is an index type in pgvector that supports fast similarity searches on vector embeddings. • It organizes vectors into “inverted file lists” (IVF) based on clusters, allowing quick approximate nearest-neighbor searches. This is especially useful when you have a large dataset and need efficient matching.
  2. Distance Metric (vector_l2_ops): • vector_l2_ops specifies the distance metric used for comparing vectors. This option calculates the L2 (Euclidean) distance between embeddings, which is the straight-line distance in vector space. • If you’re looking for exact matches (more literal similarity), Euclidean distance is generally effective.
  3. Other Options (vector_ip_ops and vector_cosine_ops): • vector_ip_ops uses the inner product (or dot product) to measure similarity. It’s suitable for cases where vector magnitude matters, and you’re interested in finding vectors pointing in the same direction with similar magnitudes. • vector_cosine_ops measures cosine similarity, ignoring magnitude and focusing on the angle between vectors. ==It’s useful when you want to measure conceptual similarity without worrying about vector length, often preferred in recommendation engines==.
  4. Parameter (lists = 100): • This specifies the number of “lists” (clusters) in the ivfflat index. A higher value can increase precision at the cost of speed, as it narrows down the number of items scanned.

This configuration essentially speeds up your similarity queries by clustering vectors and selecting the most appropriate distance metric based on your similarity goals. Both HNSW (Hierarchical Navigable Small World) and IVFFlat (Inverted File Index with Flat Quantization) are popular indexing algorithms for fast approximate nearest neighbor (ANN) searches in large-scale vector data, like embeddings. However, they differ significantly in structure, speed, memory usage, and ideal use cases. Here’s a comparison:

1. HNSW (Hierarchical Navigable Small World)

Structure: HNSW builds a graph-like structure where each vector (node) connects to other nearby vectors. Multiple hierarchical layers allow efficient traversal from “high-level” to “fine-grained” connections, improving search speed.

Performance: Known for high recall (accuracy) with very fast search times, often providing near-exact results even in approximate mode.

Memory Usage: Requires more memory than IVFFlat because it stores the graph structure, especially with many connections per node.

Indexing Speed: Indexing can be slower compared to IVFFlat due to the complex graph construction but is usually a one-time cost.

Best For: When memory availability is high and search speed/accuracy is crucial, such as in real-time recommendation engines or high-precision search applications.

2. IVFFlat (Inverted File Index with Flat Quantization)

Structure: IVFFlat clusters vectors into “lists” (or clusters) based on a chosen metric (e.g., L2 distance). During search, only a subset of lists is checked, which speeds up query times at the cost of some accuracy.

Performance: Typically faster indexing and requires less memory than HNSW but may sacrifice some accuracy depending on the number of lists and the search parameters.

Memory Usage: Lower memory footprint compared to HNSW, which is useful when memory is a constraint.

Indexing Speed: Faster than HNSW, making it better suited for dynamic datasets where indexing needs to be done frequently.

Best For: Large datasets with memory constraints or applications where very high recall is not strictly required. It’s often preferred for static or semi-static datasets in recommendation systems, as it balances speed, accuracy, and memory usage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment