🚀 Instant Search in Laravel with Scout + Typesense + React

This guide shows how to implement advanced real-time search in Laravel using Scout and Typesense.

📋 Table of Contents


🧠 Introduction

Instant search is a must-have for modern web apps. This guide shows how to build a production-ready solution in Laravel with:

  • Laravel Scout
  • Typesense
  • React (Inertia.js)

🛠 Laravel Scout Setup

Install Scout:

composer require laravel/scout

Publish config:

php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

🐳 Adding Typesense to Docker

Update docker-compose.yml:

typesense:
  image: typesense/typesense:0.25.1
  container_name: blog_typesense
  restart: unless-stopped
  ports:
    - "8108:8108"
  environment:
    - TYPESENSE_API_KEY=your_typesense_api_key
    - TYPESENSE_DATA_DIR=/data
    - TYPESENSE_ENABLE_CORS=true
  volumes:
    - typesense_data:/data
  networks:
    - laravel-network

volumes:
  typesense_data:
    driver: local

Start Typesense:

docker-compose up -d typesense

📦 Install the Typesense Scout Driver

composer require typesense/laravel-scout-typesense-engine

⚙️ Scout & Typesense Configuration

In .env:

SCOUT_DRIVER=typesense
SCOUT_ENABLED=true

TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
TYPESENSE_API_KEY=your_typesense_api_key

In config/scout.php:

'typesense' => [
    'client-settings' => [
        'api_key' => env('TYPESENSE_API_KEY'),
        'nodes' => [[
            'host' => env('TYPESENSE_HOST', 'localhost'),
            'port' => env('TYPESENSE_PORT', '8108'),
            'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
        ]],
        'nearest_node' => [
            'host' => env('TYPESENSE_HOST', 'localhost'),
            'port' => env('TYPESENSE_PORT', '8108'),
            'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
        ],
        'connection_timeout_seconds' => 2,
        'healthcheck_interval_seconds' => 30,
        'num_retries' => 3,
        'retry_interval_seconds' => 1,
    ],
    'model-settings' => [
        App\Models\Post::class => [
            'collection-schema' => [
                'fields' => [
                    ['name' => 'title',         'type' => 'string'],
                    ['name' => 'content',       'type' => 'string'],
                    ['name' => 'tags_names',    'type' => 'string[]'],
                    ['name' => 'published_at',  'type' => 'int32'],
                ],
                'default_sorting_field' => 'published_at',
            ],
            'search-parameters' => [
                'query_by' => 'title,content,tags_names',
                'query_by_weights' => '5,3,2,1',
                'sort_by' => 'published_at:desc',
                'filter_by' => 'published_at:<' . now()->timestamp,
                'drop_tokens_threshold' => 1,
                'typo_tokens_threshold' => 100,
                'prefix' => true,
            ],
        ],
    ],
]

🧩 Make Your Model Searchable

In Post.php:

use Laravel\Scout\Searchable;

class Post extends Model
{
    use Searchable;

    public function toSearchableArray(): array
    {
        $this->load(['tags']);
        
        return [
            'id' => (string) $this->id,
            'title' => $this->title,
            'content' => $this->content,
            'tags_names' => $this->tags->pluck('name')->toArray(),
            'published_at' => $this->published_at->timestamp,
        ];
    }

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

📥 Index Your Data

php artisan scout:import App\Models\Post

🔙 Backend Suggestion Endpoint

// routes/api.php
Route::get('/suggest-posts', [BlogController::class, 'suggestPosts']);

// BlogController.php
public function suggestPosts(Request $request)
{
    $query = $request->input('query');
    
    if (trim($query) === '') {
        return response()->json([]);
    }

    $results = Post::getSuggestions($query);

    return response()->json($results);
}

// Post.php
public static function getSuggestions(string $query)
{
    return self::search($query)
        ->query(fn (Builder $query) => $query->where('published_at', '<=', now()))
        ->take(5)
        ->get()
        ->map(fn ($post) => [
            'id' => $post->id,
            'title' => $post->title,
            'slug' => $post->slug,
        ])
        ->values();
}

💻 Frontend: Instant Search in React

React search bar example:

const [search, setSearch] = useState(filters?.search);
const [sort, setSort] = useState(filters?.sort);
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
const [suggestionsOpen, setSuggestionsOpen] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
const inputRef = useRef<HTMLInputElement>(null);

const handleSearch = useCallback((value: string) => {
    setSearch(value);
    setHighlightedIndex(-1);
}, []);

const handleSort = useCallback((value: string) => {
    setSort(value);
}, []);

const handleSuggestionSelect = useCallback(
    (suggestion: Suggestion) => {
        setSuggestionsOpen(false);
        setHighlightedIndex(-1);
        router.visit(route('blog.show', { post: suggestion.slug }));
    },
    [],
);

const handleKeyDown = useCallback(
    (e: React.KeyboardEvent) => {
        if (e.key === 'ArrowDown') {
            e.preventDefault();
            setHighlightedIndex((prev) => Math.min(prev + 1, suggestions.length - 1));
        } else if (e.key === 'ArrowUp') {
            e.preventDefault();
            setHighlightedIndex((prev) => Math.max(prev - 1, 0));
        } else if (e.key === 'Enter' && highlightedIndex >= 0) {
            e.preventDefault();
            const selected = suggestions[highlightedIndex];
            if (selected) {
                handleSuggestionSelect(selected);
            }
        } else if (e.key === 'Escape') {
            setSuggestionsOpen(false);
        }
    },
    [suggestions, highlightedIndex, handleSuggestionSelect],
);

useEffect(() => {
    const timeout = setTimeout(() => {
        if (search && search.length >= 2) {
            axios
                .get(route('blog.suggest-posts', { query: search }))
                .then((res) => res.data)
                .then(setSuggestions);
        } else {
            setSuggestions([]);
        }

        const params = {
            search,
            sort,
        };

        router.reload({
            only: ['posts'],
            data: params,
        });
    }, 400);
    return () => clearTimeout(timeout);
}, [sort, search]);

useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
        if (!inputRef.current?.contains(e.target as Node)) {
            setSuggestionsOpen(false);
        }
    };
    window.addEventListener('click', handleClickOutside);
    return () => window.removeEventListener('click', handleClickOutside);
}, []);

Input with suggestions dropdown:

<div className="relative w-full" ref={inputRef}>
    <Input
        className="w-full"
        placeholder={__('Search posts...')}
        value={search}
        onChange={(e) => handleSearch(e.target.value)}
        onFocus={() => setSuggestionsOpen(true)}
        onKeyDown={handleKeyDown}
        aria-label={__('Search posts')}
        aria-expanded={suggestionsOpen}
        aria-controls="search-suggestions"
        role="combobox"
    />
    {suggestionsOpen && suggestions.length > 0 && (
        <ul
            id="search-suggestions"
            className="absolute z-10 mt-1 w-full rounded-md border bg-white shadow-md dark:bg-zinc-900"
            role="listbox"
        >
            {suggestions.map((s, i) => (
                <li
                    key={s.id}
                    role="option"
                    aria-selected={i === highlightedIndex}
                    className={`cursor-pointer px-4 py-2 ${
                        i === highlightedIndex ? 'bg-muted font-medium' : 'hover:bg-muted'
                    }`}
                    onMouseDown={() => handleSuggestionSelect(s)}
                >
                    {s.title}
                </li>
            ))}
        </ul>
    )}
</div>

✅ Conclusion

You now have a production-grade instant search feature with:

  • Backend indexing via Laravel Scout & Typesense
  • Blazing-fast frontend suggestions via React + Inertia.js

Follow me on LinkedIn for more Laravel and DevOps content!

Would you like to learn more about instant search? Leave a comment below!

Comments (0)
Leave a comment

© 2026 All rights reserved.