> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lunarphp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Search & Product Discovery

> Build product search with Lunar, covering search setup, querying, faceted filtering, and URL-based product resolution.

## Overview

Product search lets customers find products by typing keywords, browsing faceted filters, and navigating directly to products via URLs. This guide walks through building search and discovery features using Lunar's `Search` facade and `Url` model.

The examples below use standard Laravel controllers and Blade templates. The same concepts apply whether the storefront is built with Livewire, Inertia, or a headless API.

## Search Setup

Lunar's search system is provided by the `lunarphp/search` add-on package. It is built on top of [Laravel Scout](https://laravel.com/docs/scout) with a driver-based architecture that supports Meilisearch, Typesense, and a database fallback. The `Search` facade provides a consistent API regardless of which driver is active.

### Installing the Search Package

```bash theme={null}
composer require lunarphp/search
```

This installs the `Search` facade, search indexers, and data classes used throughout this guide.

### Installing a Search Driver

For production storefronts, install either Meilisearch or Typesense. The database driver works out of the box but lacks faceting and advanced relevance features.

```bash theme={null}
# Meilisearch
composer require meilisearch/meilisearch-php

# Typesense
composer require typesense/typesense-php
```

Set the driver in the application's `.env` file:

```bash theme={null}
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=your-master-key
```

### Indexing Products

After configuring the driver, import existing products into the search index:

```bash theme={null}
php artisan scout:import "Lunar\Models\Product"
```

Lunar automatically keeps the index in sync as products are created, updated, or deleted.

### Searchable Models

The following models are searchable by default:

| Model                        | Description                                             |
| :--------------------------- | :------------------------------------------------------ |
| `Lunar\Models\Product`       | Products with attributes, SKUs, brand, and product type |
| `Lunar\Models\Collection`    | Collections with their attribute data                   |
| `Lunar\Models\Brand`         | Brands                                                  |
| `Lunar\Models\Customer`      | Customers (typically for admin use)                     |
| `Lunar\Models\Order`         | Orders (typically for admin use)                        |
| `Lunar\Models\ProductOption` | Product options                                         |

## Searching Products

Use the `Search` facade to query products. The `query()` method accepts a search string, and `get()` returns a `Lunar\Search\Data\SearchResults` object. Products are the default model, so no additional configuration is needed.

```php theme={null}
use Lunar\Search\Facades\Search;

$results = Search::query('running shoes')->get();
```

To search a different model, use `model()` to specify it:

```php theme={null}
use Lunar\Models\Collection;
use Lunar\Search\Facades\Search;

$results = Search::model(Collection::class)->query('hoodies')->get();
```

### Search Results

The `SearchResults` object contains everything needed to render a results page:

| Property     | Type            | Description                           |
| :----------- | :-------------- | :------------------------------------ |
| `query`      | `?string`       | The search query that was executed    |
| `count`      | `int`           | Total number of matching results      |
| `page`       | `int`           | Current page number                   |
| `perPage`    | `int`           | Results per page                      |
| `totalPages` | `int`           | Total number of pages                 |
| `hits`       | `SearchHit[]`   | Array of search result hits           |
| `facets`     | `SearchFacet[]` | Array of available facets with counts |
| `links`      | `View`          | Pagination links                      |

Each `SearchHit` contains:

| Property     | Type                   | Description                                             |
| :----------- | :--------------------- | :------------------------------------------------------ |
| `document`   | `array`                | The indexed document data (id, name, brand, SKUs, etc.) |
| `highlights` | `SearchHitHighlight[]` | Highlighted matches (Typesense only)                    |

### Building a Search Controller

```php theme={null}
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Lunar\Search\Facades\Search;

class SearchController extends Controller
{
    public function index(Request $request)
    {
        $query = $request->get('q', '');

        if (empty($query)) {
            return view('search.index', ['results' => null, 'query' => '']);
        }

        $results = Search::query($query)
            ->perPage(24)
            ->get();

        return view('search.index', [
            'results' => $results,
            'query' => $query,
        ]);
    }
}
```

### Displaying Search Results

```blade theme={null}
<form action="{{ route('search') }}" method="GET">
    <input
        type="search"
        name="q"
        value="{{ $query }}"
        placeholder="Search products..."
    >
    <button type="submit">Search</button>
</form>

@if($results)
    <p>{{ $results->count }} results for "{{ $results->query }}"</p>

    <div class="product-grid">
        @foreach($results->hits as $hit)
            <a href="{{ route('products.show', $hit->document['id']) }}">
                @if(! empty($hit->document['thumbnail']))
                    <img src="{{ $hit->document['thumbnail'] }}" alt="{{ $hit->document['name'] ?? '' }}">
                @endif

                <h3>{{ $hit->document['name'] ?? 'Untitled' }}</h3>

                @if(! empty($hit->document['brand']))
                    <p>{{ $hit->document['brand'] }}</p>
                @endif
            </a>
        @endforeach
    </div>

    {{ $results->links }}
@endif
```

<Info>
  Search hits contain the indexed document data, not Eloquent models. The indexed fields for products include `id`, `status`, `product_type`, `brand`, `thumbnail`, `skus`, and any attributes marked as searchable. To load full Eloquent models from search results, collect the IDs and query the database.
</Info>

### Loading Eloquent Models from Results

When full model data is needed (for example, to use the `Pricing` facade or access relationships), load the models from the hit IDs:

```php theme={null}
$productIds = collect($results->hits)->pluck('document.id');

$products = Product::whereIn('id', $productIds)
    ->with([
        'variants.prices.currency',
        'media',
        'brand',
        'defaultUrl',
    ])
    ->get()
    ->keyBy('id');
```

```blade theme={null}
@foreach($results->hits as $hit)
    @php
        $product = $products[$hit->document['id']] ?? null;
    @endphp

    @if($product)
        <a href="{{ route('products.show', $product->defaultUrl?->slug) }}">
            <img
                src="{{ $product->getFirstMediaUrl('images', 'medium') }}"
                alt="{{ $product->attr('name') }}"
            >
            <h3>{{ $product->attr('name') }}</h3>
        </a>
    @endif
@endforeach
```

## Sorting Results

Use the `sort()` method with a `field:direction` string to control result ordering.

```php theme={null}
$results = Search::query('shoes')
    ->sort('created_at:desc')
    ->get();
```

The sortable fields for products are `created_at`, `updated_at`, `skus`, and `status`. Additional sortable fields can be configured in a custom indexer.

### Letting Customers Choose a Sort Order

```php theme={null}
$sort = $request->get('sort', '');

$engine = Search::query($request->get('q', ''));

if (in_array($sort, ['created_at:asc', 'created_at:desc'])) {
    $engine->sort($sort);
}

$results = $engine->get();
```

## Faceted Filtering

Facets allow customers to narrow results by attributes like brand, size, or color. Facets are configured per model in the search configuration and are only available when using Meilisearch or Typesense.

### Configuring Facets

Publish the search configuration and define facets for the `Product` model:

```php theme={null}
// config/lunar/search.php
return [
    'facets' => [
        \Lunar\Models\Product::class => [
            'brand' => [],
        ],
    ],
];
```

Each key in the facets array corresponds to a field in the product's search index. Additional facets can be added for any indexed field.

### Displaying Facets

After executing a search, the `facets` property on the results contains the available facet groups with value counts.

```blade theme={null}
@foreach($results->facets as $facet)
    <div>
        <h4>{{ $facet->label }}</h4>

        @foreach($facet->values as $value)
            <label>
                <input
                    type="checkbox"
                    name="filters[{{ $facet->field }}][]"
                    value="{{ $value->value }}"
                    @checked($value->active)
                >
                {{ $value->label }} ({{ $value->count }})
            </label>
        @endforeach
    </div>
@endforeach
```

Each `SearchFacet` has:

| Property | Type                 | Description                       |
| :------- | :------------------- | :-------------------------------- |
| `label`  | `string`             | Display label for the facet group |
| `field`  | `string`             | The indexed field name            |
| `values` | `SearchFacetValue[]` | Available values with counts      |

Each `SearchFacetValue` has:

| Property | Type     | Description                              |
| :------- | :------- | :--------------------------------------- |
| `label`  | `string` | Display label for the value              |
| `value`  | `string` | The value to filter by                   |
| `count`  | `int`    | Number of matching results               |
| `active` | `bool`   | Whether this filter is currently applied |

### Applying Facet Filters

Pass filters as an associative array to the `filter()` method. Multiple values for the same facet use OR logic.

```php theme={null}
$engine = Search::query($request->get('q', ''));

if ($filters = $request->get('filters')) {
    $engine->filter($filters);
}

$results = $engine->get();
```

Filters can also be added individually:

```php theme={null}
$engine->addFilter('brand', ['Nike', 'Adidas']);
$engine->addFilter('status', 'published');
```

## URL-Based Product Resolution

Lunar stores SEO-friendly slugs in the `Lunar\Models\Url` model. Products, collections, and brands all support URL resolution through the `HasUrls` trait.

### Defining a Catch-All Route

A common pattern is to define a catch-all route that resolves the URL to the correct model type:

```php theme={null}
use App\Http\Controllers\ResolverController;

// Place this after all other routes
Route::get('/{slug}', [ResolverController::class, 'show'])->where('slug', '.*');
```

### Resolving the URL

```php theme={null}
<?php

namespace App\Http\Controllers;

use Lunar\Models\Brand;
use Lunar\Models\Collection;
use Lunar\Models\Product;
use Lunar\Models\Url;

class ResolverController extends Controller
{
    public function show(string $slug)
    {
        $url = Url::where('slug', $slug)->firstOrFail();

        $element = $url->element;

        return match ($element::class) {
            Product::class, Product::modelClass() => $this->showProduct($element),
            Collection::class, Collection::modelClass() => $this->showCollection($element),
            Brand::class, Brand::modelClass() => $this->showBrand($element),
            default => abort(404),
        };
    }

    protected function showProduct(Product $product)
    {
        $product->load([
            'variants.prices.currency',
            'media',
            'brand',
        ]);

        return view('products.show', compact('product'));
    }

    protected function showCollection(Collection $collection)
    {
        return view('collections.show', compact('collection'));
    }

    protected function showBrand(Brand $brand)
    {
        return view('brands.show', compact('brand'));
    }
}
```

### Working with URLs on Models

```php theme={null}
use Lunar\Models\Product;

$product = Product::find(1);

// Get the default URL
$defaultUrl = $product->defaultUrl;
echo $defaultUrl->slug; // e.g., "blue-running-shoes"

// Get all URLs
$urls = $product->urls;

// Build a link
$href = '/' . $product->defaultUrl?->slug;
```

## Searchable Attributes

Product attributes marked as `searchable` in the admin panel are automatically included in the search index. This means customers can search by any attribute value, such as material, color, or specifications.

The search index for a product includes:

| Field             | Source                            |
| :---------------- | :-------------------------------- |
| `id`              | Product ID                        |
| `status`          | Product status (published, draft) |
| `product_type`    | Product type name                 |
| `brand`           | Brand name                        |
| `skus`            | Array of variant SKUs             |
| `thumbnail`       | Thumbnail URL                     |
| `created_at`      | Unix timestamp                    |
| Attribute handles | Values from searchable attributes |

<Tip>
  To make an attribute searchable, edit the attribute in the Lunar admin panel and enable the "Searchable" option. After changing searchable attributes, reimport the search index with `php artisan scout:import "Lunar\Models\Product"`.
</Tip>

## Routes

```php theme={null}
use App\Http\Controllers\ResolverController;
use App\Http\Controllers\SearchController;

Route::get('/search', [SearchController::class, 'index'])->name('search');
Route::get('/{slug}', [ResolverController::class, 'show'])->where('slug', '.*');
```

## Putting It All Together

Here is a complete search controller with faceted filtering and sorting:

```php theme={null}
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Lunar\Models\Product;
use Lunar\Search\Facades\Search;

class SearchController extends Controller
{
    public function index(Request $request)
    {
        $query = $request->get('q', '');

        if (empty($query)) {
            return view('search.index', [
                'results' => null,
                'query' => '',
            ]);
        }

        $engine = Search::query($query)
            ->perPage(24);

        if ($filters = $request->get('filters')) {
            $engine->filter($filters);
        }

        if ($sort = $request->get('sort')) {
            if (in_array($sort, ['created_at:asc', 'created_at:desc'])) {
                $engine->sort($sort);
            }
        }

        $results = $engine->get();

        // Load full Eloquent models for display
        $productIds = collect($results->hits)->pluck('document.id');

        $products = Product::whereIn('id', $productIds)
            ->with([
                'variants.prices.currency',
                'media',
                'brand',
                'defaultUrl',
            ])
            ->get()
            ->keyBy('id');

        return view('search.index', [
            'results' => $results,
            'products' => $products,
            'query' => $query,
            'activeFilters' => $request->get('filters', []),
            'activeSort' => $request->get('sort', ''),
        ]);
    }
}
```

## Next Steps

* Review the [Search reference](/1.x/reference/search) for the full list of searchable models and configuration options.
* Review the [Search add-on](/1.x/addons/search) for advanced search engine configuration.
* Review the [Extending Search](/1.x/extending/search) guide for customizing indexers and adding custom fields to the search index.
* Review the [Product Listing Page guide](/1.x/guides/product-listing-page) for collection-based product browsing as an alternative to search.
