Building a Serverless Search Engine with Laravel and Cloudflare Vectorize
April 15, 2025
developmentI've been writing up different ways to build search with Laravel. Elasticsearch gives me a separate search service. PostgreSQL with pgvector keeps vector retrieval in the database. Cloudflare Vectorize puts that part in a managed service.
Laravel still runs the application. A Worker can generate embeddings with Workers AI and query Vectorize. The search service is the part I don't have to operate myself.
Split up the jobs
The indexing path starts with content: extract a page, generate an embedding, and insert the vector with an ID that can be mapped back to the page.
The query path starts with the search text: generate an embedding using the same model, ask Vectorize for nearby vectors, and turn the matching IDs into results.
Workers AI generates the embeddings. Vectorize stores and searches them. They are separate services, even when the same Worker calls both.
R2 can hold content and KV can hold metadata or cached results if the design needs them. You don't need to add both just to make a vector query.
Match the model and index
Cloudflare's Workers AI and Vectorize example uses @cf/baai/bge-base-en-v1.5, which outputs 768 dimensions. An index for that model can be created with:
npx wrangler vectorize create search-index --dimensions=768 --metric=cosine
The Worker needs an AI binding and a VECTORIZE binding pointing to the index. Other embedding models can use different dimensions; the index needs to match the output you are storing.
Inside a Worker with those bindings, the query operation looks like this:
const embedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
text: [query],
});
const matches = await env.VECTORIZE.query(embedding.data[0], {
topK: 10,
returnMetadata: 'all',
});
That's a query fragment, not a complete endpoint. The handler still needs to validate the request, scope the search, and return the result. Content also needs to have been embedded and inserted before there's anything to find.
Connecting it to Laravel
Laravel can coordinate indexing through queued jobs and call an authenticated Worker endpoint for search. Keep the credentials on the server. The Worker returns matching IDs and scores; the application uses those IDs to retrieve the page data it needs to show.
For multiple websites, scope retrieval before selecting the top results, using the index's supported namespace or metadata filtering. Filtering one site's matches out of a global top ten afterward could leave you with an empty list even when that site has relevant pages.
Content changes need to update the corresponding vector. Deleted pages need their vectors removed. A failed embedding request needs a retry rather than a silent claim that the page was indexed.
What vector search tells you
A similarity score ranks candidates according to the embedding. It doesn't tell you that a result satisfies every part of a request. A search for women's red running shoes still has attributes that may need explicit filters.
Generating an answer from retrieved content is another step again. That requires a generation model and a way to supply the retrieved text. Calling a text model with a few snippets isn't, by itself, an implementation of Cloudflare's managed retrieval product.
The appeal of this approach is letting Cloudflare operate the vector service while Laravel handles the application workflow. I would still measure the full request, including embedding generation and the trip back to the app, before making a claim about speed or cost.
For keyword search through Scout, see the Meilisearch version. For this approach, the Vectorize documentation is the place to follow the API details.