AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP. ai.. ai. ml.. ai. ml. PHP.. ai. ml. PHP. векторный поиск.. ai. ml. PHP. векторный поиск. искусственный интеллект.. ai. ml. PHP. векторный поиск. искусственный интеллект. Машинное обучение.. ai. ml. PHP. векторный поиск. искусственный интеллект. Машинное обучение. поиск по тексту.. ai. ml. PHP. векторный поиск. искусственный интеллект. Машинное обучение. поиск по тексту. трансформеры.. ai. ml. PHP. векторный поиск. искусственный интеллект. Машинное обучение. поиск по тексту. трансформеры. эмбеддинги.

Это вторая часть статьи.
Часть 1: Практика без Python и data science

AI в PHP: не теория, а место, с которого можно начать

В своей прошлой статье я описал на довольно общем уровне почему тема AI вроде бы везде, но при этом почти не пересекается с повседневной PHP-разработкой. Не потому что PHP “не подходит”, а потому что сам разговор обычно идёт мимо наших задач и привычного способа мышления. Ну и, конечно, о том, что почти нет материала, который объясняет AI именно для PHP-разработчиков, их задач и их мышления.

После публикации мне несколько раз задали один и тот же вопрос, в разных формулировках:

Окей, допустим. А с чего конкретно начать?

И это, пожалуй, самый интересный вопрос из тех, что я получил. Ниже я попытаюсь дать на него ответ.

Про “точку входа”

Когда вы слышите “использование AI в проекте”, скорее всего в вашей голове сразу возникает слишком много лишнего: инфраструктура, обучение моделей, эксперименты, отдельные сервисы, новые роли в команде и т.д.

Но если честно, в большинстве PHP-проектов нам это просто не нужно (хотя разобраться в этом самому – жутко интересная задача).

Но всё же, нам с вами не нужно:

  • обучать модели с нуля

  • разбираться в градиентном спуске

  • собирать датасеты и т.д.

Нужно понять, что AI ≠ Data Science. Друзья, в большинстве PHP-проектов никто не обучает модели, пожалуйста, запомните это! По крайней мере в прикладных PHP-проектах, где мы говорим об использовании готовых моделей, а не об исследованиях и обучении.

Нам нужно вз��ть готовый инструмент и аккуратно встроить его в уже существующую логику. Так же, как мы это делаем с любой другой библиотекой.

И вот тут начинается интересное.

Что это вообще за зверь такой TransformersPHP и с чем его едят?

Про использование LLM через API знают уже все. Это полезно и удобно, но в таком виде AI остаётся чем-то внешним: сервисом, к которому ты просто отправляешь текст. Внутри – туман.

В какой-то момент мне захотелось посмотреть на более “приземлённый” уровень: не генерация текста, а представление семантики: эмбеддинги, задачи классификации и поиска. Именно на этом уровне решается большинство прикладных задач в бэкенд-системах: поиск по тексту, сравнение и автоматическая классификация, а не диалоговое взаимодействие человека с моделью.

И здесь неожиданно выяснилось, что есть инструменты, которые позволяют делать это напрямую в PHP, без Python-стека. Один из них – TransformersPHP.

Важно сразу понять: это не попытка превратить PHP в Python и не универсальное решение. Это библиотека для inference (инференс) – использования уже обученных моделей.

Как по мне, TransformersPHP – один из самых интересных и показательных проектов в современной PHP ML-экосистеме. Отдельное спасибо его автору – Kyrian Obikwelu за то что создал этот проект и продолжает над ним работать. В общем это библиотека, которая позволяет использовать трансформер-модели (BERT, RoBERTa, DistilBERT и др.) напрямую из PHP, без Python и без внешних API.

По сути, это PHP-ориентированная обертка над идеями Hugging Face Transformers, адаптированная под PHP-экосистему и реальные прикладные сценарии.

Ключевая особенность библиотеки – локальный инференс. Модели загружаются и выполняются на стороне PHP-приложения (через ONNX Runtime), что открывает важные архитектурные возможности:

  • отсутствие сетевых вызовов к LLM API

  • полный контроль над данными (это может быть важно для privacy (конфиденциальности) в вашей работе)

  • предсказуемое и стабильное время обработки запроса

  • возможность оффлайн-работы (после первого запуска и загрузки модели)

TransformersPHP поддерживает типовые задачи NLP, такие как: получение эмбеддингов, классификацию текста, семантическое сравнение и прочее.

Что мне нравится больше всего

Самое ценное ощущение – отсутствие разрыва контекста.

Я остаюсь в PHP, я пишу тот же код, у меня тот же деплой и у меня те же подходы к архитектуре. Ничего не изменилось. Модель для меня в этом случае – не некий “магический объект”, а просто ещё один источник данных. Да, непривычный, но вполне объяснимый – не хуже и не лучше других.

И это сильно меняет отношение к всей теме. Вы согласны?

Запуск модели за 10 минут

Один из самых важных моментов – это первый запуск.
Если он сложный, на этом всё обычно и заканчивается.

В случае с TransformersPHP ощущение как раз обратное: это больше похоже на работу с обычной зависимостью.

Полное руководство по установке можно найти на сайте документации.

Мы же с вами для простоты запустим докер контейнер со всеми необходимыми зависимостями. Да, Docker file выглядит объёмно – но это одноразовая инфраструктура. Сам запуск и первый демо-пример действительно укладываются в несколько минут.

Что нам нужно (требования)

  • PHP 8.1 или выше

  • Composer

  • Расширение PHP FFI

  • JIT-компиляция (опционально, для повышения производительности)

  • Увеличенный лимит памяти (для сложных задач, таких как генерация текста)

Структура проекта

/project/
  ├── app/
  │    ├── demo.php
  │    ├── semantic-search.php
  ├── docker/
  │    ├── Dockerfile
  ├── docker-composer.yaml
  ├── composer.json
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 1

Установка (Docker)

Файл: docker-compose.yml
networks:
  ai-for-php-developers:
    driver: bridge

services:
  app:
    build:
      context: .
      dockerfile: docker/Dockerfile
    volumes:
      - .:/var/www
    ports:
      - "8088:8088"
    command: php -S 0.0.0.0:8088 -t app
    networks:
      - ai-for-php-developers
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 2
Файл: docker/Dockerfile
# ------------------------------
# Install system dependencies
# ------------------------------
RUN apt-get update && apt-get install -y 
    libzip-dev 
    zip 
    unzip 
    git 
    libxml2-dev 
    libcurl4-openssl-dev 
    libpng-dev 
    libonig-dev 
    && rm -rf /var/lib/apt/lists/*

# ------------------------------
# Install PHP extensions
# ------------------------------
RUN docker-php-ext-install zip pdo_mysql bcmath xml mbstring curl gd pcntl

# ------------------------------
# Enable FFI
# ------------------------------
RUN apt-get update && apt-get install -y 
    libffi-dev 
    pkg-config 
    && rm -rf /var/lib/apt/lists/*
RUN docker-php-ext-install ffi
RUN echo "ffi.enable=1" > /usr/local/etc/php/conf.d/ffi.ini

# ------------------------------
# Install ONNX Runtime
# ------------------------------
ENV ONNXRUNTIME_VERSION=1.17.1

RUN curl -L https://github.com/microsoft/onnxruntime/releases/download/v${ONNXRUNTIME_VERSION}/onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}.tgz 
    | tar -xz 
    && cp onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}/lib/libonnxruntime.so* /usr/lib/ 
    && ldconfig 
    && rm -rf onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}


# ------------------------------
# Install Composer
# ------------------------------
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Set working directory
WORKDIR /var/www

# Copy existing application directory contents
COPY . /var/www

# Configure PHP
RUN echo "memory_limit = 512M" >> /usr/local/etc/php/conf.d/docker-php-ram-limit.ini
RUN echo "max_execution_time = 300" >> /usr/local/etc/php/conf.d/docker-php-max-execution-time.ini

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 3

И для установки самого TransformersPHP

Файл: composer.json
{
    "type": "project",
    "minimum-stability": "stable",
    "prefer-stable": true,
    "require": {
        "codewithkyrian/transformers": "~0.6.2"
    },
    "config": {
        "allow-plugins": {
            "codewithkyrian/platform-package-installer": true
        }
    }
}
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 4

Команда для запуска окружения

docker compose build --pull
docker compose up -d
docker compose exec app /bin/bash -c "composer install"
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 5

Идея простая: чтобы пример можно было поднять локально без ручной настройки окружения.

Базовый демо-пример

Начнём с самого простого: анализа настроений.

Если всё, что описано выше сработало хорошо и установка прошла нормально, можно запустить пример, описанный ниже (примите во внимание, что первый запуск может занять несколько секунд):

docker compose exec app php app/demo.php
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 6

Пример использования выглядит концептуально просто: вы загружаете предобученную модель и применяете ее к тексту так же, как это делали бы в Python – но уже внутри PHP-кода. TransformersPHP предлагает простой pipeline API для задач вроде анализа настроений, классификации текста, семантического сравнения и т.д. В примере ниже модель определяет тональность двух фраз и показывает метку и score.

Файл: app/demo.php

require_once __DIR__ . '/../vendor/autoload.php';

use function CodewithkyrianTransformersPipelinespipeline;

// Выделить конвейер для анализа настроений
$classifier = pipeline('sentiment-analysis');

$out = $classifier(['I love transformers!']);
echo 'I love transformers!';
echo print_r($out, true);

$out = $classifier(['I hate transformers!']);
echo 'I hate transformers!';
echo print_r($out, true);
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 7

Результат, конечно, же вполне ожидаемый:

I love transformers!
Array ( 
  [label] => POSITIVE 
  [score] => 0.99978870153427 
)

I hate transformers!
Array ( 
  [label] => NEGATIVE 
  [score] => 0.99863630533218 
)
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 8

Цель этого примера – снять у вас психологический барьер: эта модель в PHP — это просто ещё один объект, с которым можно работать.

Этот же пример можно запустить онлайн.

Важно понимать архитектурную роль TransformersPHP.

Эта библиотека не конкурирует с большими LLM-сервисами вроде GPT или Claude. Она закрывает другой, очень важный слой:

  • быстрые эмбеддинги

  • локальная классификация

  • семантический поиск

  • lightweight NLP без внешних зависимостей

В связке с PHP это выглядит особенно логично. PHP остается центральным слоем бизнес-логики, а трансформеры становятся встроенным инструментом, а не удаленным сервисом. TransformersPHP – это хороший пример того, как современный ML постепенно перестает быть “чужим” для PHP и становится частью его нативной экосистемы, пусть и через аккуратные инженерные мосты вроде ONNX.

Реальный кейс: семантический поиск по событиям

Учебные примеры хороши, но быстро надоедают. Гораздо интереснее посмотреть на задачу, которая реально встречается в бэкенде. Сейчас мы с вами сделаем кое-что поинтересней.

Для запуска этого примера используйте следующую команду

docker compose exec app php app/semantic-search.php
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 9

Сценарий

Есть события с коротким текстовым описанием. Пользователь ищет, например:

  • “санкции против IT-компаний”

  • “космическая гонка среди стран региона”

При этом, как вы понимаете, в самих данных таких формулировок может не быть вообще. Классический поиск по словам здесь начинает усложняться: синонимы, морфология, разные языки, костыли поверх костылей и прочее.

Небольшое мысленное упражнение

Допустим, у нас есть лента событий или материалов, где каждое событие описано парой предложений. Что-то вроде:

  • введены новые ограничения в отношении технологических корпораций

  • страны региона наращивают инвестиции в спутниковые программы

  • обострение конфликта на политической почве в нескольких провинциях

Теперь пользователь вводит запрос: “космическая гонка среди стран региона”.

Ни одно из этих слов буквально не обязано встречаться в описании событий. Упс… И это нормально – люди редко формулируют мысли так же, как их описывают системы.

Идея решения

Вместо того чтобы пытаться угадать слова, можно попробовать искать по смыслу. Не в философском смысле, а в инженерном:

  • описание события → вектор,

  • запрос пользователя → вектор,

  • дальше – обычный поиск ближайших значений.

То есть, нам нужно использовать эмбеддинги как универсальный индекс смысла.

Таким образом мы:

  1. Берём массив событий {id, title, description}

  2. Считаем эмбеддинг только по description (заголовок часто слишком короткий, неинформативный и может добавлять шум)

  3. Эмбеддим пользовательский запрос

  4. Ищем ближайшие векторы

  5. Сортируем и возвращаем результат

Без обучения моделей и без сложной инфраструктуры. Эмбеддинги для событий считаются один раз и могут храниться где угодно. Запрос пользователя обрабатывается в момент поиска. Дальше – сортировка и вывод результатов.

И… вуаля!
Никакого обучения моделей.
Никакой магии.
Просто другой способ представить текст.

Логика работы

Поместим логику работы в отдельный класс SemanticEventSearch. Этот класс не претендует на звание лучшего кода на планете, и написан только в в демонстрационных целях – поэтому опустим замечания по его качеству.

Класс SemanticEventSearch
final class SemanticEventSearch
{
    private string $model = 'Xenova/paraphrase-multilingual-MiniLM-L12-v2';
    private string $cachePath;
    private string $defaultQuery = 'санкции против IT-компаний';
    private int $topN;

    private ?string $query = null;

    /** @var list<array{id:int,title:string,description:string}> */
    private array $events;

    /** @var array<int, list<float|int>> */
    private array $eventEmbeddingsById = [];

    private $embedder;

    /**
     * Create a new semantic search instance.
     *
     * @param int $topN Number of results to return.
     */
    public function __construct(int $topN = 3)
    {
        $this->cachePath = __DIR__ . '/../embeddings.events.json';
        $this->events = [];
        $this->embedder = null;
        $this->topN = $topN;
    }

    /**
     * Inject events that will be indexed/searched.
     *
     * @param list<array{id:int,title:string,description:string}> $events
     * @return $this
     */
    public function setEvents(array $events): self
    {
        $this->events = $events;
        $this->eventEmbeddingsById = [];
        return $this;
    }

    /**
     * Set the embeddings model identifier.
     *
     * Switching model invalidates in-memory embeddings.
     *
     * @param string $model
     * @return $this
     */
    public function setModel(string $model): self
    {
        $this->model = $model;
        $this->embedder = null;
        $this->eventEmbeddingsById = [];
        return $this;
    }

    /**
     * Set the query to be searched.
     *
     * @param string $query
     * @return $this
     */
    public function setQuery(string $query): self
    {
        $q = trim($query);
        $this->query = $q === '' ? null : $q;
        return $this;
    }

    /**
     * Run the end-to-end semantic search pipeline (cache -> embed query -> score -> top-N).
     *
     * @return array{query:string,results:list<array{score:float,event:array{id:int,title:string,description:string}}>}
     * @throws RuntimeException If events are not set or embeddings output is unexpected.
     */
    public function run(): array
    {
        if (count($this->events) === 0) {
            throw new RuntimeException('Events list is empty. Call setEvents() before run().');
        }

        if ($this->embedder === null) {
            $this->embedder = pipeline('embeddings', $this->model);
        }

        $this->loadEmbeddingsFromCacheIfCompatible();
        $this->ensureAllEventEmbeddings();

        $query = $this->query ?? $this->defaultQuery;
        $queryVec = $this->embedText($query);

        $results = $this->search($queryVec);
        return [
            'query' => $query,
            'results' => $results,
        ];
    }

    /**
     * Compute an embedding vector for a single text.
     *
     * @param string $text
     * @return list<float|int>
     * @throws RuntimeException
     */
    private function embedText(string $text): array
    {
        $emb = ($this->embedder)($text, normalize: true, pooling: 'mean');
        if (!is_array($emb) || !isset($emb[0]) || !is_array($emb[0])) {
            throw new RuntimeException('Unexpected embeddings output format');
        }

        return $emb[0];
    }

    /**
     * Cosine similarity between two vectors.
     *
     * @param list<float|int> $a
     * @param list<float|int> $b
     * @return float
     */
    private function cosineSimilarity(array $a, array $b): float
    {
        $n = min(count($a), count($b));

        $dot = 0.0;
        $normA = 0.0;
        $normB = 0.0;

        for ($i = 0; $i < $n; $i++) {
            $x = (float) $a[$i];
            $y = (float) $b[$i];

            $dot += $x * $y;
            $normA += $x * $x;
            $normB += $y * $y;
        }

        if ($normA <= 0.0 || $normB <= 0.0) {
            return 0.0;
        }

        return $dot / (sqrt($normA) * sqrt($normB));
    }

    /**
     * Load a JSON file and decode to array.
     *
     * @param string $path
     * @return array|null
     */
    private function loadJsonFile(string $path): ?array
    {
        if (!is_file($path)) {
            return null;
        }

        $raw = file_get_contents($path);
        if ($raw === false) {
            return null;
        }

        $data = json_decode($raw, true);
        return is_array($data) ? $data : null;
    }

    /**
     * Encode and save data to JSON file.
     *
     * @param string $path
     * @param array $data
     * @throws RuntimeException
     */
    private function saveJsonFile(string $path, array $data): void
    {
        $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
        if ($json === false) {
            throw new RuntimeException('Failed to encode JSON');
        }

        $ok = file_put_contents($path, $json);
        if ($ok === false) {
            throw new RuntimeException('Failed to write cache file: ' . $path);
        }
    }

    /**
     * Load cached event embeddings only if they were produced by the current model.
     *
     * @return void
     */
    private function loadEmbeddingsFromCacheIfCompatible(): void
    {
        $cached = $this->loadJsonFile($this->cachePath);

        if (!is_array($cached) || !isset($cached['model'], $cached['events']) || !is_array($cached['events'])) {
            return;
        }

        if ($cached['model'] !== $this->model) {
            return;
        }

        foreach ($cached['events'] as $row) {
            if (isset($row['id'], $row['embedding']) && is_array($row['embedding'])) {
                $this->eventEmbeddingsById[(int) $row['id']] = $row['embedding'];
            }
        }
    }

    /**
     * Ensure embeddings exist for all events and persist them to cache.
     *
     * @return void
     * @throws RuntimeException
     */
    private function ensureAllEventEmbeddings(): void
    {
        $missing = [];
        foreach ($this->events as $event) {
            $id = (int) $event['id'];
            if (!isset($this->eventEmbeddingsById[$id])) {
                $missing[] = $event;
            }
        }

        if (count($missing) === 0) {
            return;
        }

        foreach ($missing as $event) {
            $id = (int) $event['id'];
            $text = (string) $event['description'];

            $this->eventEmbeddingsById[$id] = $this->embedText($text);
        }

        $toCache = [
            'model' => $this->model,
            'events' => array_values(array_map(
                fn(array $event): array => [
                    'id' => (int) $event['id'],
                    'embedding' => $this->eventEmbeddingsById[(int) $event['id']],
                ],
                $this->events
            )),
        ];

        $this->saveJsonFile($this->cachePath, $toCache);
    }

    /**
     * Score all events against the query embedding and return the top-N results.
     *
     * @param list<float|int> $queryVec
     * @return list<array{score:float,event:array{id:int,title:string,description:string}}>
     */
    private function search(array $queryVec): array
    {
        $scored = [];

        foreach ($this->events as $event) {
            $id = (int) $event['id'];
            $score = $this->cosineSimilarity($queryVec, $this->eventEmbeddingsById[$id]);

            $scored[] = [
                'score' => $score,
                'event' => $event,
            ];
        }

        usort($scored, static fn(array $a, array $b): int => $b['score'] <=> $a['score']);

        return array_slice($scored, 0, $this->topN);
    }

    /**
     * Render results as plain text.
     *
     * @param string $query
     * @param list<array{score:float,event:array{id:int,title:string,description:string}}> $results
     * @return void
     */
    public function render(string $query, array $results): void
    {
        echo "Query: {$query}nn";
        foreach ($results as $row) {
            $event = $row['event'];
            $score = (float) $row['score'];

            echo "[" . number_format($score, 4) . "] #{$event['id']} {$event['title']}n";
            echo "  {$event['description']}nn";
        }
    }
}
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 10

Подготовим данные

Предположим, что это наши данные, собранные из разных источников. Для простоты поместим их в массив.

Массив $events
$events = [
    [
        'id' => 1,
        'title' => 'Ограничения против технологических корпораций',
        'description' => 'Введены новые экономические меры в отношении крупных технологических компаний.',
    ],
    [
        'id' => 2,
        'title' => 'Развитие космических программ',
        'description' => 'Несколько стран региона увеличили финансирование национальных спутниковых проектов.',
    ],
    [
        'id' => 3,
        'title' => 'Эскалация политического конфликта',
        'description' => 'Обострение конфликта на политической почве в нескольких провинциях.',
    ],
    [
        'id' => 4,
        'title' => 'Ограничения против ИТ-сектора',
        'description' => 'Правительство объявило о новых ограничениях для компаний, работающих в сфере информационных технологий.',
    ],
    [
        'id' => 5,
        'title' => 'Рост инфляции и пересмотр ключевой ставки',
        'description' => 'Центральный банк повысил ключевую ставку на фоне ускорения инфляции и роста цен на импортные товары.',
    ],
    [
        'id' => 6,
        'title' => 'Запуск программы поддержки малого бизнеса',
        'description' => 'Власти объявили о льготных кредитах и налоговых послаблениях для малого и среднего бизнеса в регионах.',
    ],
    [
        'id' => 8,
        'title' => 'Утечка данных в сфере онлайн-ритейла',
        'description' => 'Интернет-магазин расследует утечку персональных данных клиентов после компрометации учётных записей сотрудников.',
    ],
    [
        'id' => 9,
        'title' => 'Прорыв в медицине: новый метод диагностики',
        'description' => 'Исследователи представили метод ранней диагностики заболеваний по биомаркерам, сокращающий время анализа.',
    ],
    [
        'id' => 10,
        'title' => 'Сезонный рост заболеваемости',
        'description' => 'В нескольких городах отмечен рост заболеваемости респираторными инфекциями, клиники усилили приём пациентов.',
    ],
    [
        'id' => 12,
        'title' => 'Засуха и риски для сельского хозяйства',
        'description' => 'Из-за продолжительной засухи фермеры прогнозируют снижение урожайности, обсуждаются меры поддержки аграриев.',
    ],
    [
        'id' => 13,
        'title' => 'Финал крупного спортивного турнира',
        'description' => 'В решающем матче сезона команда одержала победу в дополнительное время, установив новый рекорд по посещаемости.',
    ],
    [
        'id' => 14,
        'title' => 'Трансфер игрока и усиление состава',
        'description' => 'Клуб подписал контракт с новым нападающим, рассчитывая усилить атакующую линию перед серией дерби.',
    ],
    [
        'id' => 15,
        'title' => 'Новые правила для маркетплейсов',
        'description' => 'Регулятор предложил требования к маркировке товаров и прозрачности комиссий на торговых онлайн-платформах.',
    ],
    [
        'id' => 17,
        'title' => 'Сбои в поставках полупроводников',
        'description' => 'Производители электроники предупредили о задержках поставок чипов из-за ограничений экспорта и перегрузки заводов.',
    ],
    [
        'id' => 18,
        'title' => 'Открытие фестиваля современного искусства',
        'description' => 'В столице стартовал фестиваль современного искусства с выставками, перформансами и лекциями художников.',
    ],
    [
        'id' => 19,
        'title' => 'Крупная сделка на рынке недвижимости',
        'description' => 'Инвестфонд приобрёл портфель коммерческой недвижимости, планируя реконструкцию и повышение энергоэффективности.',
    ],
    [
        'id' => 20,
        'title' => 'Исследование океана и новые данные',
        'description' => 'Научная экспедиция собрала данные о течениях и температуре воды, уточнив прогнозы по изменению климата.',
    ],
];
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 11

Использование примера

Здесь всё просто – запускаем наш код и ждём результата.

require_once __DIR__ . '/../vendor/autoload.php';

use function CodewithkyrianTransformersPipelinespipeline;

final class SemanticEventSearch {...}

$events = [...];

$query = 'санкции против IT-компаний';

$search = new SemanticEventSearch(topN: 3);
$search->setModel('Xenova/paraphrase-multilingual-MiniLM-L12-v2');
$search->setEvents($events);
$search->setQuery($query);
$out = $search->run();
$search->render($out['query'], $out['results']);
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 12

Логика работы (по шагам)

Если вам не хочется разбираться в деталях реализации SemanticEventSearch, ниже – упрощённое пошаговое описание. Более подробный разбор кода выходит за рамки этой статьи.

Логика работы кода
  • Снаружи задаём:

    • список событий (setEvents)

    • модель (setModel)

    • запрос (setQuery)

    • topN через конструктор

  • Дальше — обычная логика в run():

    • Поднимаем embedder (pipeline(’embeddings’, model)) если ещё не поднят.

    • .transformers-cache:

      • при первом использовании модель и файлы токенизации/весов скачиваются и кладутся в .transformers-cache

      • дальше они берутся оттуда, чтобы не качать заново и работать быстрее

    • embeddings.events.json:

      • это наш локальный кэш эмбеддингов событий

      • пытаемся его прочитать и использовать только если model в кэше совпадает с текущей моделью

    • Если для каких-то событий эмбеддингов нет:

      • считаем эмбеддинги для description

      • сохраняем обратно в embeddings.events.json

    • Эмбеддим запрос (эмбеддинги нормализуются, чтобы косинусная близость была стабильной и сравнимой)

    • Считаем близость запроса к каждому событию (cosine similarity)

    • Сортируем, берём top‑N, возвращаем результаты

    • Рендер (вывод) делается снаружи через render(query, results)

Результат

На выходе мы получаем не совпадение слов, а совпадение по смыслу. Первый результат – наиболее подходящий по схожести с нашим запросом про “санкции против IT-компаний”. И всё это – без сложной математики и без танцев с бубнами.

Query: санкции против IT-компаний

[0.4288] #4 Ограничения против ИТ-сектора
  Правительство объявило о новых ограничениях для компаний, работающих в сфере информационных технологий.

[0.3356] #15 Новые правила для маркетплейсов
  Регулятор предложил требования к маркировке товаров и прозрачности комиссий на торговых онлайн-платформах.

[0.2598] #8 Утечка данных в сфере онлайн-ритейла
  Интернет-магазин расследует утечку персональных данных клиентов после компрометации учётных записей сотрудников.
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 13

Где это можно применять в проде

В целом это уже похоже на продуктовый подход, а не на эксперимент. К тому же вы можете легко заметить, что такой подход:

  • слабо привязан к конкретной формулировке запроса

  • хорошо работает на коротких описаниях

  • легко комбинируется с обычными фильтрами (дата, регион, тип события)

То есть это не “AI ради AI”, а вполне конкретная прикладная логика. Та самая, которую можно объяснить, отладить и поддерживать.

Ограничения и подводные камни

Важно не обманываться: это не серебряная пуля.

Модели весят немало. Производительность нужно учитывать. Некоторые задачи проще и надёжнее решаются без AI обычным SQL.

Но это уже нормальный инженерный разговор – про trade-off’ы, а не про магию или чёрный ящик.

Куда двигаться дальш��

Как для меня, то TransformersPHP – это хороший пример того, что AI можно использовать напрямую в PHP-проектах без смены стека и без Python.

В своей книге “AI для PHP-разработчиков” (открытой и бесплатной) я как раз и разбираю подобные кейсы: где это имеет смысл, как выбирать модели и как не превратить проект в набор экспериментальных фич.

Ссылка на книгу “AI для PHP-разработчиков”.

Кстати, все примеры можно скачать и запустить через готовую среду Docker.

Или же вы также можете запускать все примеры из книги напрямую.

Если тема откликается – буду рад обсуждению и фидбэку.
Особенно интересно, какие задачи вы уже решаете или хотели бы решать с помощью AI в PHP-проектах.

Автор: samako

Источник