Redis, MongoDB and Search
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Laravel 13.x (13.34.0) on PHP 8.3 to 8.5, checked on 2026-10-05, with Redis 8.10,
MongoDB 8.3.11, PostgreSQL 17 with pgvector 0.8.7, mongodb/laravel-mongodb 5.11, predis/predis 3.6 and
laravel/scout 11.8. Each store gets one section here on how Laravel uses it; the stores themselves are covered on
Redis, MongoDB and
Vector Databases and RAG.
Redis
Laravel talks to Redis through the phpredis C extension (the default, REDIS_CLIENT=phpredis) or the pure-PHP
Predis library (composer require predis/predis, REDIS_CLIENT=predis), configured in the redis section of
config/database.php. Caches, sessions, queues, broadcasting and rate limiters can all use it; this section uses it
directly with the Redis facade:
<?php
use Illuminate\Support\Facades\Redis;
$r = Redis::connection();
echo 'client: ', config('database.redis.client'), ' (', get_class($r->client()), '), prefix "', config('database.redis.options.prefix'), '"', PHP_EOL;
$r->del('views:kindred');
$r->set('greeting', 'hello', 'EX', 60);
echo 'get: ', $r->get('greeting'), ', ttl: ', $r->ttl('greeting'), PHP_EOL;
// 1000 increments: one round trip each vs one pipeline
$t = microtime(true);
for ($i = 0; $i < 1000; $i++) { $r->incr('views:kindred'); }
$plain = (microtime(true) - $t) * 1000;
$t = microtime(true);
$r->pipeline(function ($pipe) { for ($i = 0; $i < 1000; $i++) { $pipe->incr('views:kindred'); } });
$piped = (microtime(true) - $t) * 1000;
printf("1000 INCR: %.1f ms one by one, %.1f ms pipelined; counter=%s%s", $plain, $piped, $r->get('views:kindred'), PHP_EOL);
// a sorted set leaderboard
$r->del('popular');
$r->zadd('popular', ['kindred' => 42, 'dispossessed' => 17, 'sower' => 30]);
echo 'zrevrange: ', json_encode($r->zrevrange('popular', 0, 1, ['withscores' => true])), PHP_EOL;
// atomic Lua script
$n = $r->eval("local v = redis.call('incrby', KEYS[1], ARGV[1]); redis.call('expire', KEYS[1], 60); return v", 1, 'rate:ada', 5);
echo 'eval: ', $n, PHP_EOL;
client: phpredis (Redis), prefix "laravel-database-"
get: hello, ttl: 60
1000 INCR: 37.6 ms one by one, 0.7 ms pipelined; counter=2000
zrevrange: {"kindred":42,"sower":30}
eval: 5
$ REDIS_CLIENT=predis php redis-demo.php
client: predis (Predis\Client), prefix "laravel-database-"
get: hello, ttl: 60
1000 INCR: 50.9 ms one by one, 2.9 ms pipelined; counter=2000
Source: Laravel — Interacting With Redis and Pipelining Commands
-
A pipeline sends all commands in one round trip: 1000 increments took 0.7 ms instead of 37.6 ms with phpredis, even with Redis in a container on the same host. Across a network the difference grows with latency. Pipelines are not transactions;
Redis::transaction()wrapsMULTI/EXEC, and a Lua script (eval) runs atomically on the server, as the "increment and set expiry" above does. -
phpredis was faster in both modes and supports serializers and compression (
igbinary,lz4); Predis needs no extension, which helps on hosts where extensions cannot be installed. -
Every key gets the
REDIS_PREFIX(laravel-database-fromAPP_NAME), so several applications can share one Redis. The prefix also applies to pub/sub channels, below. -
Clusters are configured with a
clusterskey and'cluster' => 'redis'(native Redis Cluster); multi-key commands then require all keys in one hash slot ({book:1}:views,{book:1}:likes). The theory of pipelining, scripting and clustering is on Redis: Pipelining, Transactions and Scripting.
Publish and subscribe, with a subscriber process started first:
<?php
// subscriber.php (a long-running process, usually an Artisan command)
use Illuminate\Support\Facades\Redis;
Redis::connection('default')->subscribe(['loans'], function (string $message, string $channel) {
echo "received on $channel: $message", PHP_EOL;
});
// publisher.php
foreach (['{"loan":"01a1...","event":"created"}', '{"loan":"01a1...","event":"returned"}'] as $m) {
echo 'subscribers reached: ', Redis::publish('loans', $m), PHP_EOL;
}
subscribers reached: 1
subscribers reached: 1
received on laravel-database-loans: {"loan":"01a1...","event":"created"}
received on laravel-database-loans: {"loan":"01a1...","event":"returned"}
Source: Laravel — Pub / Sub
The channel arrived as laravel-database-loans: a non-Laravel subscriber must listen on the prefixed name. Pub/sub
is fire-and-forget (a message published while no one listens is lost); durable delivery needs queues or Redis
Streams (Redis: Streams and Pub/Sub), and browser notifications use
broadcasting (Broadcasting and Reverb).
MongoDB
The mongodb/laravel-mongodb package (maintained by MongoDB) adds a mongodb database driver, an Eloquent model
class for documents, and Mongo-aware query builder methods. It needs the mongodb PHP extension:
<?php
// config/database.php, inside 'connections'
return [
'mongodb' => [ // mongodb/laravel-mongodb
'driver' => 'mongodb',
'dsn' => env('MONGODB_URI', 'mongodb://localhost:27017'),
'database' => 'bookshelf',
],
];
<?php
// app/Models/ReadingLog.php
namespace App\Models;
use MongoDB\Laravel\Eloquent\Model;
/** A schemaless document per reading session, stored in MongoDB. */
class ReadingLog extends Model
{
protected $connection = 'mongodb';
protected $table = 'reading_logs'; // the collection
protected $fillable = ['member', 'book_id', 'pages', 'device', 'highlights'];
protected function casts(): array
{
return ['pages' => 'integer'];
}
}
<?php
use App\Models\ReadingLog;
use Illuminate\Support\Facades\DB;
ReadingLog::truncate();
ReadingLog::create(['member' => 'ada', 'book_id' => 1, 'pages' => 42, 'device' => ['type' => 'e-reader', 'os' => 'kobo'], 'highlights' => ['"I lost an arm on my last trip home."']]);
ReadingLog::create(['member' => 'ada', 'book_id' => 3, 'pages' => 18, 'device' => ['type' => 'phone']]);
ReadingLog::create(['member' => 'ben', 'book_id' => 1, 'pages' => 77, 'device' => ['type' => 'e-reader', 'os' => 'kindle']]);
$log = ReadingLog::where('device.type', 'e-reader')->orderBy('pages', 'desc')->first();
echo 'first e-reader log: ', $log->member, ' ', $log->pages, ' pages, _id ', $log->id, ' (', get_debug_type($log->getKey()), ')', PHP_EOL;
echo 'whereNotNull(highlights): ', ReadingLog::whereNotNull('highlights')->count(), PHP_EOL;
echo 'pages per member: ', json_encode(ReadingLog::raw(fn ($c) => $c->aggregate([
['$group' => ['_id' => '$member', 'pages' => ['$sum' => '$pages']]],
['$sort' => ['_id' => 1]],
])->toArray())), PHP_EOL;
ReadingLog::where('member', 'ada')->increment('pages', 10);
echo 'after increment: ', ReadingLog::where('member', 'ada')->sum('pages'), PHP_EOL;
echo 'server: MongoDB ', DB::connection('mongodb')->getMongoDB()->command(['buildInfo' => 1])->toArray()[0]['version'], PHP_EOL;
first e-reader log: ben 77 pages, _id 6ac412acb1cb36ce760d1dc4 (string)
whereNotNull(highlights): 1
pages per member: [{"_id":"ada","pages":60},{"_id":"ben","pages":77}]
after increment: 80
server: MongoDB 8.3.11
Source: Laravel — MongoDB and MongoDB — Laravel MongoDB
Documents need no migration: nested arrays (device) are stored as embedded documents and queried with dot notation
(device.type). The key is an ObjectId exposed as a 24-character string id. raw() gives the underlying
collection for aggregation pipelines (MongoDB: Aggregation Pipeline);
the package also provides relationships between documents and SQL models, embedded models, cache, queue and session
drivers, GridFS file storage, a Scout engine and Atlas vector search.
Installing it surfaced a dependency issue worth knowing. The first build used the 2.1.4 extension, and composer
require resolved mongodb/mongodb 2.1.2; composer audit then reported a high-severity advisory for it ("Reject
. and NUL bytes in database and collection names", fixed in 2.4.1). The fixed library requires extension ^2.4, so
the advisory could only be resolved by building the 2.5.3 extension and running composer update mongodb/mongodb -W,
which installed 2.5.0. An old PHP extension on a server can pin a vulnerable Composer package without any warning
except composer audit (Composer and Packages).
Full-text search
Relational databases search words with full-text indexes. The query builder’s whereFullText() compiles to each
database’s syntax; PostgreSQL is on Database and Query Builder,
and MySQL needs the index first:
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\{DB, Schema};
Schema::connection('my')->table('books', fn (Blueprint $t) => $t->fullText('title'));
$q = DB::connection('my')->table('books')->whereFullText('title', 'sower heaven');
echo $q->toRawSql(), PHP_EOL, json_encode($q->pluck('title')), PHP_EOL;
$b = DB::connection('my')->table('books')->whereFullText('title', '+parable -heaven', ['mode' => 'boolean']);
echo $b->toRawSql(), PHP_EOL, json_encode($b->pluck('title')), PHP_EOL;
select * from `books` where match (`title`) against ('sower heaven' in natural language mode)
["Parable of the Sower","The Lathe of Heaven"]
select * from `books` where match (`title`) against ('+parable -heaven' in boolean mode)
["Parable of the Sower"]
Source: Laravel — Full-Text Search
Natural language mode matched either word; boolean mode required parable and excluded heaven. Full-text search
finds words, stems and phrases, not meaning: "a novel about climate collapse" matches nothing unless those words
appear in the title.
Semantic search with vectors
Vector search stores an embedding (a list of numbers produced by an embedding model) per record and finds the
records whose vectors are closest to the query’s. Laravel 13 has vector() columns, vectorIndex() and
whereVectorSimilarTo() for PostgreSQL with pgvector (and MariaDB 11.7+). The run below uses hand-made
three-dimensional vectors, scored on "time travel, anarchism, climate", so that the result can be checked by eye:
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\{DB, Schema};
$schema = Schema::connection('pgv');
$schema->ensureVectorExtensionExists();
$schema->dropIfExists('book_embeddings');
$schema->create('book_embeddings', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->vector('embedding', dimensions: 3); // real embeddings have 768-3072 dimensions
$table->vectorIndex('embedding'); // HNSW, cosine distance
});
$db = DB::connection('pgv');
// toy 3-dimensional "embeddings": [time travel, anarchism, climate]
$db->table('book_embeddings')->insert([
['title' => 'Kindred', 'embedding' => '[0.95, 0.05, 0.10]'],
['title' => 'The Dispossessed', 'embedding' => '[0.05, 0.95, 0.20]'],
['title' => 'Parable of the Sower', 'embedding' => '[0.10, 0.30, 0.90]'],
['title' => 'The Lathe of Heaven', 'embedding' => '[0.40, 0.20, 0.60]'],
]);
$query = [0.2, 0.25, 0.95]; // "a novel about climate collapse"
$q = $db->table('book_embeddings')->select('title')->whereVectorSimilarTo('embedding', $query, minSimilarity: 0.6);
echo $q->toRawSql(), PHP_EOL;
foreach ($q->get() as $row) { echo ' ', $row->title, PHP_EOL; }
select "title" from "book_embeddings" where ("embedding" <=> '[0.2,0.25,0.95]') <= 0.4 order by ("embedding" <=> '[0.2,0.25,0.95]') asc
Parable of the Sower
The Lathe of Heaven
pgvector 0.8.7
minSimilarity: 0.6 became <=> ... <= 0.4: pgvector’s <=> operator is cosine distance (1 minus similarity),
and the same expression orders the results. With a real model, the vectors come from the Laravel AI SDK
(Str::of($text)->toEmbeddings()), and passing a plain string to whereVectorSimilarTo() embeds the query
automatically; that needs an embedding provider and network access, which this run did not use
(AI SDK, MCP and Boost). The AI SDK’s Reranking class reorders a
candidate list by relevance, the usual second step after a fast full-text or vector retrieval. Choosing dimensions,
index types and hybrid search is covered on PostgreSQL with
pgvector and Hybrid Search and Reranking.
Laravel Scout
Scout adds a search() method to models and keeps a search index in sync through model events. Its engines are the
database engine (SQL LIKE and full-text on the application’s own database), the collection engine (filtering in
PHP, for tests), and hosted engines: Algolia, Meilisearch, Typesense and Turbopuffer. Bookshelf’s Book uses the
database engine:
<?php
// app/Models/Book.php (excerpt)
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Attributes\SearchUsingFullText;
use Laravel\Scout\Searchable;
class Book extends Model
{
use Searchable;
#[SearchUsingFullText(['title'])] // database engine: full-text on title, LIKE on the rest
public function toSearchableArray(): array
{
return ['title' => $this->title, 'isbn' => $this->isbn];
}
}
database: Parable of the Sower | isbn: The Dispossessed
select * from "books" where ("books"."isbn"::text ilike '%sower%' or (to_tsvector('english', "books"."title")) @@ plainto_tsquery('english', 'sower')) order by ts_rank(to_tsvector('english', books.title), plainto_tsquery('sower')) desc
collection: Parable of the Sower | isbn: The Dispossessed
select * from "books" order by "books"."id" desc
paginate: ["The Lathe of Heaven","The Dispossessed"]
Source: Laravel — Scout Database Engine and Collection Engine
Book::search('sower')->get() returned the same books from both engines, but the SQL shows the difference: the
database engine searched with ilike and to_tsvector and ranked by ts_rank, while the collection engine loaded
every book and filtered in PHP, which is fine for tests and unusable on a large table. search()->paginate(),
where() filters and query(fn ($q) => $q->with('author')) work with all engines. With a hosted engine, php artisan
scout:import "App\Models\Book" builds the index and changes are synced on save, ideally through the queue
(SCOUT_QUEUE=true); typo tolerance, facets, synonyms and geo-search are the reasons to choose one.
Where older books and tutorials are outdated
-
jenssegers/laravel-mongodbis nowmongodb/laravel-mongodb, maintained by MongoDB, with theMongoDB\Laravel\Eloquent\Modelnamespace. -
Predis as the default Redis client (Laravel 5.x) changed to phpredis in Laravel 6; Predis 1.x tutorials predate Predis 2 and 3.
-
Search with
like '%term%'(Kromann, Nixon) cannot use ordinary indexes and finds substrings, not words; use full-text indexes, Scout or vectors depending on the need. -
Semantic search with a separate vector database only is no longer the only option: pgvector and Laravel 13’s
whereVectorSimilarTo()keep vectors next to the relational data. -
Scout without a database engine (Scout 8 and earlier needed Algolia or a community driver) now ships the
databaseandcollectionengines.