Automating Content Tagging in Laravel Using OpenAI Embeddings and Cron Jobs

It is tedious, inconsistent, and frequently incorrect to manually tag blog posts. With AI embeddings, Laravel can automatically determine the topic of a blog post and assign the appropriate tags without the need for human intervention.

This guide demonstrates how to create a complete Laravel AI auto-tagging system using:

  • The OpenAI Embeddings API
  • Laravel Jobs & Queues
  • Cron Scheduler
  • Tag → Vector Matching
  • Automatic Tag Assignment

This is one of the most useful AI enhancements for any Laravel-based CMS or blog system.

What We’re Constructing

You’ll construct:

  • Table of Tag Vector – The meaning of each tag (such as “PHP”, “Laravel”, “Security”, and “AI”) will be represented by an embedding vector created by AI.
  • A Generator for Post Embedding – We generate an embedding for the post content whenever a new post is saved.
  • A Matching Algorithm – The system determines which post embeddings are closest by comparing them with tag embeddings.
  • A Cron Job -The system automatically assigns AI-recommended tags every hour (or on any schedule).

This is ideal for:

  • Custom blogs made with Laravel
  • Headless CMS configurations
  • Tagging categories in e-commerce
  • Auto-classification of knowledge bases
  • Websites for documentation

Now let’s get started.

Step 1: Create Migration for Tag Embeddings

Run:

php artisan make:migration create_tag_embeddings_table

Migration:

    public function up()
    {
        Schema::create('tag_embeddings', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('tag_id')->unique();
            $table->json('embedding'); // store vector
            $table->timestamps();
        });
    }

Run:

php artisan migrate

Step 2: Generate Embeddings for Tags

Create a command:

php artisan make:command GenerateTagEmbeddings

Add logic:

`public function handle()         {             $tags = Tag::all();              foreach ($tags as $tag) {                 $vector = $this->embed($tag->name);                  TagEmbedding::updateOrCreate(                     ['tag_id' => $tag->id],                     ['embedding' => json_encode($vector)]                 );                  $this->info("Embedding created for tag: {$tag->name}");             }         }          private function embed($text)         {             $client = new GuzzleHttpClient();              $response = $client->post("https://api.openai.com/v1/embeddings", [                 "headers" => [                     "Authorization" => "Bearer " . env('OPENAI_API_KEY'),                     "Content-Type" => "application/json",                 ],                 "json" => [                     "model" => "text-embedding-3-large",                     "input" => $text                 ]             ]);              $data = json_decode($response->getBody(), true);              return $data['data'][0]['embedding'] ?? [];         }`

Run once:

php artisan generate:tag-embeddings

Now all tags have AI meaning vectors.

Step 3: Save Embeddings for Each Post

Add to your Post model observer or event.

`$post->embedding = $this->embed($post->content);         $post->save();`

Migration for posts:

`$table->json('embedding')->nullable();`

Step 4: Matching Algorithm (Post → Tags)

Create a helper class:

`class EmbeddingHelper         {             public static function cosineSimilarity($a, $b)             {                 $dot = array_sum(array_map(fn($i, $j) => $i * $j, $a, $b));                 $magnitudeA = sqrt(array_sum(array_map(fn($i) => $i * $i, $a)));                 $magnitudeB = sqrt(array_sum(array_map(fn($i) => $i * $i, $b)));                 return $dot / ($magnitudeA * $magnitudeB);             }         }`

Step 5: Assign Tags Automatically (Queue Job)

Create job:

php artisan make:job AutoTagPost

Job logic:

`public function handle()         {             $postEmbedding = json_decode($this->post->embedding, true);              $tags = TagEmbedding::with('tag')->get();              $scores = [];             foreach ($tags as $te) {                 $sim = EmbeddingHelper::cosineSimilarity(                     $postEmbedding,                     json_decode($te->embedding, true)                 );                 $scores[$te->tag->id] = $sim;             }              arsort($scores); // highest similarity first              $best = array_slice($scores, 0, 5, true); // top 5 matches              $this->post->tags()->sync(array_keys($best));         }`

Step 6: Cron Job to Process New Posts

Add to app/Console/Kernel.php:

`protected function schedule(Schedule $schedule)         {             $schedule->command('ai:autotag-posts')->hourly();         }`

Create command:

php artisan make:command AutoTagPosts

Command logic:

`public function handle()         {             $posts = Post::whereNull('tags_assigned_at')->get();              foreach ($posts as $post) {                 AutoTagPost::dispatch($post);                 $post->update(['tags_assigned_at' => now()]);             }         }`

Now, every hour, Laravel processes all new posts and assigns AI-selected tags.

Step 7: Test the Full Flow

  • Create tags in admin
  • Run: php artisan generate:tag-embeddings
  • Create a new blog post
  • Cron or queue runs
  • Post automatically gets AI-selected tags

Useful enhancements

  • Weight tags by frequency
  • Use title + excerpt, not full content
  • Add confidence scores to DB
  • Auto-create new tags using AI
  • Add a manual override UI
  • Cache embeddings for performance
  • Batch process 1,000+ posts

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.