burak-sevinc/scout-redis-search

A Redis Search engine for Laravel Scout.

Maintainers

Package info

github.com/burak-sevinc/scout-redis-search

pkg:composer/burak-sevinc/scout-redis-search

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-08-31 17:42 UTC

This package is auto-updated.

Last update: 2026-08-31 18:15:10 UTC


README

A Redis Search engine for Laravel Scout. Models are stored as Redis hashes and queried through the RediSearch FT.SEARCH API.

The package supports Laravel 10-13, PHP 8.2+, Predis and PhpRedis.

Read the full documentation at scout-redis-search.buraksevinc.dev.

Features

  • Prefix search: Ad matches Ada
  • Fuzzy search with one to three edit-distance levels
  • Field-scoped full-text search
  • where, whereIn and whereNotIn filters
  • Numeric comparison operators: =, !=, >, >=, <, <=
  • Sorting, limits and pagination
  • Readable or raw RediSearch schemas
  • Custom Redis connections and document key prefixes
  • Laravel Scout index, import and flush commands
  • Redis-free unit testing and reusable integration-test helpers

Requirements

This package needs Redis with the RediSearch module. Standard Redis does not include FT.CREATE or FT.SEARCH.

For local development, Redis Stack is the quickest option:

services:
  redis:
    image: redis/redis-stack:latest
    ports:
      - "6379:6379"
      - "8001:8001"
    volumes:
      - redis-data:/data

volumes:
  redis-data:

Start it with:

docker compose up -d

RedisInsight will be available at http://localhost:8001.

Installation

Install Laravel Scout, a Redis client, and this package:

composer require laravel/scout predis/predis burak-sevinc/scout-redis-search

Publish the package configuration:

php artisan vendor:publish --tag=scout-redis-search-config

Configure Scout and Redis in .env:

SCOUT_DRIVER=redis-search
REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

SCOUT_REDIS_SEARCH_CONNECTION=default
SCOUT_REDIS_SEARCH_PREFIX=scout:
SCOUT_REDIS_SEARCH_SEPARATOR=:
SCOUT_REDIS_SEARCH_PREFIX_MATCHING=true
SCOUT_REDIS_SEARCH_PREFIX_MIN_LENGTH=2

SCOUT_REDIS_SEARCH_CONNECTION may point to any connection from config/database.php. Leave it empty to use Laravel's default Redis connection.

Make A Model Searchable

Add Scout's Searchable trait, choose an index name, and return scalar search data:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

final class User extends Model
{
    use Searchable;

    public function searchableAs(): string
    {
        return 'users';
    }

    public function toSearchableArray(): array
    {
        return [
            'id' => $this->getKey(),
            'name' => $this->name,
            'email' => $this->email,
            'status' => $this->status,
            'created_at' => $this->created_at?->timestamp,
        ];
    }
}

Arrays and objects are JSON encoded before being stored. Fields used by RediSearch should normally be scalars that match their configured schema type.

Configure An Index

Define every index in config/scout-redis-search.php. The readable format is recommended:

'schemas' => [
    'users' => [
        'options' => ['LANGUAGE', 'English'],
        'fields' => [
            'id' => ['type' => 'NUMERIC', 'sortable' => true],
            'name' => [
                'type' => 'TEXT',
                'weight' => 2.0,
                'sortable' => true,
            ],
            'email' => 'TAG',
            'status' => 'TAG',
            'created_at' => [
                'type' => 'NUMERIC',
                'sortable' => true,
            ],
            '__scout_key' => 'TAG',
        ],
    ],
],

Supported field types are TEXT, TAG, NUMERIC, GEO, VECTOR, and GEOSHAPE. Structured fields support these options:

Config key RediSearch argument
weight WEIGHT
separator SEPARATOR
phonetic PHONETIC
sortable SORTABLE
unf UNF
nostem NOSTEM
noindex NOINDEX
casesensitive CASESENSITIVE
withsuffixtrie WITHSUFFIXTRIE

For advanced RediSearch features, use raw argument lists:

'schemas' => [
    'users' => [
        'name', 'TEXT', 'WEIGHT', 2.0,
        'email', 'TAG',
        '__scout_key', 'TAG',
    ],
],

__scout_key is written automatically and is recommended for every index. Filterable exact-value fields should be TAG; range fields should be NUMERIC; fields passed to orderBy must include SORTABLE.

Create And Populate The Index

Create the configured RediSearch index, then import existing records:

php artisan scout:index users
php artisan scout:import "App\Models\User"

New, updated, and deleted models are synchronized by Laravel Scout. If Scout queues are enabled, run your queue worker as usual.

When the schema changes, recreate and re-import the index:

php artisan scout:delete-index users
php artisan scout:index users
php artisan scout:import "App\Models\User"

scout:flush removes the model documents and recreates the empty configured index:

php artisan scout:flush "App\Models\User"

Searching

Use the standard Laravel Scout API:

$users = User::search('Ada')->get();
$users = User::search('Ada')->take(10)->get();
$users = User::search('Ada')->paginate(20);

Prefix Matching

Prefix matching is enabled by default. A query such as Ad is compiled to Ad*, so it matches Ada and Adam.

Change it globally with environment values:

SCOUT_REDIS_SEARCH_PREFIX_MATCHING=true
SCOUT_REDIS_SEARCH_PREFIX_MIN_LENGTH=2

Or control it for one query:

User::search('Ad')->options([
    'prefix_matching' => false,
])->get();

User::search('Ada')->options([
    'prefix_min_length' => 3,
])->get();

Fuzzy Search

Set fuzzy from 1 to 3. When fuzzy matching is active, prefix matching is not applied to that query.

$users = User::search('Ado')
    ->options(['fuzzy' => 1])
    ->get();

Higher levels tolerate more differences but can broaden the result set and cost more to execute.

Search Specific Fields

Limit full-text matching to one or more TEXT fields:

$users = User::search('Ada')
    ->options(['fields' => ['name']])
    ->get();

Filters

Use TAG fields for exact filters:

$users = User::search('Ada')
    ->where('status', 'active')
    ->whereIn('email', ['ada@example.com', 'grace@example.com'])
    ->whereNotIn('status', ['blocked'])
    ->get();

Use NUMERIC fields for ranges and comparisons:

$users = User::search('')
    ->where('created_at', '>=', now()->subMonth()->timestamp)
    ->where('id', '!=', 10)
    ->get();

An empty search string with filters performs a filter-only search.

Sorting

RediSearch requires sorted fields to be marked SORTABLE in the schema:

$users = User::search('Ada')
    ->orderByDesc('created_at')
    ->get();

The engine currently accepts one Scout orderBy clause per search and throws a clear exception when more are provided.

Query Options

Options are set with Scout's options() method:

User::search('Ada Lovelace')->options([
    'fields' => ['name'],
    'prefix_matching' => true,
    'prefix_min_length' => 2,
    'fuzzy' => 0,
    'verbatim' => false,
    'in_order' => true,
    'language' => 'English',
    'dialect' => 2,
])->get();
Option Description
fields Restrict matching to the listed fields
prefix_matching Enable or disable suffixing terms with *
prefix_min_length Minimum term length before prefix matching
fuzzy Fuzzy matching level from 0 to 3
verbatim Disable stemming with VERBATIM
in_order Require terms to appear in order
language Set the query language
dialect Set the RediSearch query dialect
raw_query Send the search string as RediSearch query syntax

raw_query bypasses query escaping and should only be used with trusted input:

User::search('@name:(Ada|Grace)')
    ->options(['raw_query' => true, 'dialect' => 2])
    ->get();

Low-Level Callback

Scout callbacks receive the Redis connection contract, compiled query string, and complete FT.SEARCH argument list. The callback must return a normal RediSearch response:

use BurakSevinc\ScoutRedisSearch\Contracts\RedisSearchConnection;

$users = User::search('Ada', function (
    RedisSearchConnection $redis,
    string $query,
    array $arguments,
) {
    // Inspect or alter $arguments here.
    return $redis->command('FT.SEARCH', $arguments);
})->get();

Testing Applications

The package includes helpers for integration tests. Add the trait to your Laravel test class:

<?php

namespace Tests\Feature;

use App\Models\User;
use BurakSevinc\ScoutRedisSearch\Testing\InteractsWithRedisSearch;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

final class UserSearchTest extends TestCase
{
    use InteractsWithRedisSearch, RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();

        $this->recreateRedisSearchIndex('users');
    }

    protected function tearDown(): void
    {
        $this->dropRedisSearchIndex('users');

        parent::tearDown();
    }

    public function test_it_finds_a_user_by_name_prefix(): void
    {
        User::factory()->create(['name' => 'Ada Lovelace'])->searchable();

        $results = User::search('Ad')->get();

        $this->assertCount(1, $results);
        $this->assertSame('Ada Lovelace', $results->first()->name);
        $this->assertRedisSearchIndexExists('users');
    }
}

Available helpers:

  • recreateRedisSearchIndex(string $index)
  • dropRedisSearchIndex(string $index)
  • assertRedisSearchIndexExists(string $index)
  • assertRedisSearchIndexMissing(string $index)
  • redisSearchEngine()

Use a dedicated Redis database or container for tests. Recreating an index deletes all hashes belonging to that index.

A complete copy-ready consumer test is available at examples/laravel/tests/Feature/UserSearchTest.php. The setup notes for running it are in examples/laravel/README.md.

Package Development

Package unit tests use an in-memory fake connection and do not require Redis:

composer install
composer test
composer test:unit
composer lint
composer check

Generate a local coverage report when a PHP coverage driver is installed:

composer test:coverage

The public RedisSearchConnection contract can be replaced in the Laravel container when a project needs custom command transport or instrumentation.

Troubleshooting

Unknown command FT.CREATE

The connected Redis server does not have the RediSearch module. Use Redis Stack or install RediSearch on the server.

No schema is configured

The key in schemas must exactly match the model's searchableAs() result or the name passed to within().

A filter returns no records

Confirm that exact-value fields are TAG, ranges are NUMERIC, and the same field is returned by toSearchableArray(). Recreate and re-import the index after changing its schema.

Sorting fails

Add 'sortable' => true to the field, recreate the index, and import the models again.

Search results lag behind writes

When Scout queueing is enabled, the queue worker must process the indexing job before the record becomes searchable.

License

Laravel Scout Redis Search is open-sourced software licensed under the MIT license.