The Python service you no longer have to write
Laravel 13 puts embeddings, reranking and vector similarity in the query builder. For a good share of AI work, the separate Python service has stopped earning its keep.
For years the answer to "we want AI in this Laravel application" had a predictable shape: keep Laravel for the workflow, stand up a Python service for the model work, and put a queue between them.
We have built exactly that, more than once — the verification service behind our identity work is a PHP application talking to a Python service over a queue, and it was the right call at the time.
The reason was never that PHP could not make an HTTP request to a model provider. It was that everything around the call — embedding libraries, vector maths, the tokenisers, the client SDKs that got features first — lived in Python. So you paid for a second runtime, a second deployment, a second dependency tree and an interface between two halves of one feature.
Laravel 13 changes that arithmetic for a meaningful slice of this work.
The interesting part is in the query builder
Laravel's AI SDK gets described as "Laravel can call models now", which undersells it. The part that changes how you design is that the primitives are wired into surfaces you already use.
Embeddings come off a string:
use Illuminate\Support\Str;
$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings();
Or in a batch, which is one API call rather than one per item:
use Laravel\Ai\Embeddings;
$response = Embeddings::for([
'Napa Valley has great wine.',
'Laravel is a PHP framework.',
])->generate();
$response->embeddings;
And similarity is a where clause:
$documents = Document::query()
->where('team_id', $user->team_id)
->whereVectorSimilarTo('embedding', $request->input('query'))
->limit(10)
->get();
Look closely at that second line, because it is the whole argument.
Permissions stop being a separate problem
We have written before that permissions belong in retrieval rather than in the prompt — that instructing a model not to reveal a document already sitting in its context window is a request, not a control.
That advice used to cost something to follow. With retrieval in a separate service, enforcing per-user access meant either passing the permission model across the boundary or filtering afterwards, which means the restricted document was retrieved before it was discarded.
When similarity is a clause on an Eloquent query, the tenant scope sits directly beside it and the restricted rows are never candidates. The correct design became the path of least resistance, which is the only way a security practice reliably survives.
Retrieve then rerank, without the second service
The other recommendation from that article — do not rely on vector search alone, because exact terms matter — also gets cheaper.
$articles = Article::query()
->whereFullText('body', $request->input('query'))
->limit(50)
->get()
->rerank('body', $request->input('query'), limit: 10);
Full-text search narrows thousands of rows to fifty at database speed; reranking then orders those fifty by semantic relevance. You get keyword precision and semantic ordering, and unlike vector search, reranking needs no pre-computed embeddings at all.
A useful detail: pass a raw string to whereVectorSimilarTo and the query builder embeds it for
you, with caching on by default. Repeated queries do not repeatedly pay the embedding provider.
The default minimum similarity is 0.6 — treat that as a starting point to tune against your own
evaluation set, not a constant.
What we actually use it for
Translation and verification, mostly — the same two jobs we were writing Python for.
Neither is a chatbot. Both are the ordinary shape of production AI work: take a structured input, hand it to a model under a strict schema, get a structured result back, and store it with enough detail that somebody can ask why later.
Being able to do that without a second service is not a small convenience. It means the verification result is written inside the same database transaction as the record it belongs to. It means one deployment, one log stream, one set of credentials. It means the queue worker running the model call is the queue worker you already operate.
What does not change
Nothing in the engineering discipline. The framework moved where the code runs; it did not move the hard parts.
A strict output schema is still the highest-leverage decision. A model that can return free text eventually will. Bind the output to a schema and validate it, so a bad response is a validation failure rather than a subtly wrong string travelling into your database.
It still belongs on a queue. A model call is seconds, not milliseconds, and it fails in ways HTTP requests do not. Accept the work, return a reference, notify when it lands.
Cost is still a number you should be able to see. Meter spend per operation type from the first day. An AI feature whose unit cost you infer from a monthly bill is one you cannot make decisions about.
You still need an evaluation set. A framework upgrade does not tell you that last week's prompt change made retrieval measurably worse.
When we would still write the Python service
This is not a claim that Python is finished. We would still reach for it when:
- The work is heavy computer vision or OCR, where the mature libraries remain Python's
- There is an existing Scrapy or Selenium pipeline the AI work attaches to
- A model or provider is only properly supported by a Python client
- The workload genuinely wants its own runtime, scaled and deployed separately
What has changed is the default. The separate service used to be the obvious starting point and now it is a decision that has to justify itself — which, for a Laravel team shipping translation, classification, extraction or search, it increasingly cannot.