November 10, 2025

November 10, 2025

One of the strongest functions in any web application is search, however, the conventional search is restricted. It will only get an exact match of the key words and this fails to get the exact results when the user enters the query in different wordings.

What will happen when your Symfony application can comprehend the content of the query, rather than match text?

In that case, semantic search with AI can be introduced.

In this tutorial, we will create a semantic search functionality in the Symfony application with the help of the embeddings API of OpenAI and PHP. You will be able to transform text into vector embeddings, store them and compare them to be relevant, making your search results much smarter.

The present post is the continuation of our AI + PHP CMS Framework Series.

Prerequisites

Before we start, make sure you have:

  • Symfony 6 or 7
  • PHP 8.1+
  • Composer
  • A MySQL or SQLite database
  • An OpenAI API key

Step 1: Create a New Symfony Command

We’ll use a console command to generate embeddings for your existing content (articles, pages, etc.).

Inside your Symfony project, run:

php bin/console make:command app:generate-embeddings

This will create a new file in src/Command/GenerateEmbeddingsCommand.php.

Replace its contents with the following:

src/Command/GenerateEmbeddingsCommand.php

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Article;

#[AsCommand(
    name: 'app:generate-embeddings',
    description: 'Generate AI embeddings for all articles'
)]
class GenerateEmbeddingsCommand extends Command
{
    private $em;
    private $apiKey = 'YOUR_OPENAI_API_KEY';
    private $endpoint = 'https://api.openai.com/v1/embeddings';

    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $articles = $this->em->getRepository(Article::class)->findAll();
        foreach ($articles as $article) {
            $embedding = $this->getEmbedding($article->getContent());
            if ($embedding) {
                $article->setEmbedding(json_encode($embedding));
                $this->em->persist($article);
                $output->writeln("✅ Generated embedding for article ID {$article->getId()}");
            }
        }

        $this->em->flush();
        return Command::SUCCESS;
    }

    private function getEmbedding(string $text): ?array
    {
        $payload = [
            'model' => 'text-embedding-3-small',
            'input' => $text,
        ];

        $ch = curl_init($this->endpoint);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                "Content-Type: application/json",
                "Authorization: Bearer {$this->apiKey}"
            ],
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($payload)
        ]);

        $response = curl_exec($ch);
        curl_close($ch);

        $data = json_decode($response, true);
        return $data['data'][0]['embedding'] ?? null;
    }
}

This command takes every article from the database, sends its content to OpenAI’s Embedding API, and saves the resulting vector in a database field.

Step 2: Update the Entity

Assume your entity is App\Entity\Article.

We’ll add a new column called embedding to store the vector data.

src/Entity/Article.php

    #[ORM\Column(type: 'text', nullable: true)]
    private ?string $embedding = null;

    public function getEmbedding(): ?string
    {
        return $this->embedding;
    }

    public function setEmbedding(?string $embedding): self
    {
        $this->embedding = $embedding;
        return $this;
    }

Then update your database:

    php bin/console make:migration
    php bin/console doctrine:migrations:migrate

Step 3: Create a Search Endpoint

We'll now include a basic controller that takes a search query, turns it into an embedding, and determines which article is the most semantically similar.

src/Controller/SearchController.php

    namespace App\Controller;

    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Routing\Annotation\Route;
    use Doctrine\ORM\EntityManagerInterface;
    use App\Entity\Article;

    class SearchController extends AbstractController
    {
        private $apiKey = 'YOUR_OPENAI_API_KEY';
        private $endpoint = 'https://api.openai.com/v1/embeddings';

        #[Route('/search', name: 'ai_search')]
        public function search(Request $request, EntityManagerInterface $em): Response
        {
            $query = $request->query->get('q');
            if (!$query) {
                return $this->json(['error' => 'Please provide a search query']);
            }

            $queryVector = $this->getEmbedding($query);
            $articles = $em->getRepository(Article::class)->findAll();

            $results = [];
            foreach ($articles as $article) {
                if ($article->getEmbedding()) {
                    $score = $this->cosineSimilarity(
                        $queryVector,
                        json_decode($article->getEmbedding(), true)
                    );
                    $results[] = [
                        'id' => $article->getId(),
                        'title' => $article->getTitle(),
                        'similarity' => $score,
                    ];
                }
            }

            usort($results, fn($a, $b) => $b['similarity'] <=> $a['similarity']);
            return $this->json(array_slice($results, 0, 5)); // top 5 results
        }

        private function getEmbedding(string $text): array
        {
            $payload = [
                'model' => 'text-embedding-3-small',
                'input' => $text,
            ];

            $ch = curl_init($this->endpoint);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_HTTPHEADER => [
                    "Content-Type: application/json",
                    "Authorization: Bearer {$this->apiKey}"
                ],
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => json_encode($payload)
            ]);

            $response = curl_exec($ch);
            curl_close($ch);

            $data = json_decode($response, true);
            return $data['data'][0]['embedding'] ?? [];
        }

        private function cosineSimilarity(array $a, array $b): float
        {
            $dot = 0; $magA = 0; $magB = 0;
            for ($i = 0; $i < count($a); $i++) {
                $dot += $a[$i] * $b[$i];
                $magA += $a[$i] ** 2;
                $magB += $b[$i] ** 2;
            }
            return $dot / (sqrt($magA) * sqrt($magB));
        }
    }

Now, even if the articles don't contain the exact keywords, your /search?q=php framework tutorial endpoint will return those that are most semantically similar to the query.

Step 4: Try It Out

Run the below command.

php bin/console app:generate-embeddings

This generates embeddings for all articles.

Now visit the following URL.

http://your-symfony-app.local/search?q=learn symfony mvc

The top five most pertinent articles will be listed in a JSON response, arranged by meaning rather than keyword.

Real-World Applications

  • A more intelligent search within a CMS or knowledge base
  • AI-supported matching of FAQs
  • Semantic suggestions ("you might also like..."
  • Clustering of topics or duplicates in admin panels

Tips for Security and Performance

  • Reuse and cache embeddings (avoid making repeated API calls for the same content).
  • Keep your API key in.env.local (OPENAI_API_KEY=your_key).
  • For better performance, think about using a vector database such as Pinecone, Weaviate, or Qdrant if you have thousands of records.

Next up, we’ll build a Product Description Generator in Magento — powered by AI — to automate product content creation.

Next
This is the most recent post.
Older Post

0 comments:

Post a Comment