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
  • 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() wraps MULTI/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- from APP_NAME), so several applications can share one Redis. The prefix also applies to pub/sub channels, below.

  • Clusters are configured with a clusters key 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"}

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

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).

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"]

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"]

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-mongodb is now mongodb/laravel-mongodb, maintained by MongoDB, with the MongoDB\Laravel\Eloquent\Model namespace.

  • 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 database and collection engines.