December 09, 2025

December 09, 2025

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 \GuzzleHttp\Client();

            $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

Next up, we’ll build a AI Category Recommendation for Drupal 11

Next
This is the most recent post.
Older Post

0 comments:

Post a Comment