sed -n '1,280p' app/Services/SearchService.php

<?php

namespace App\Services;

use App\Enums\InventoryPolicy;
use App\Enums\ProductStatus;
use App\Models\Collection as ProductCollection;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\SearchQuery;
use App\Models\SearchSettings;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class SearchService
{
    /**
     * @param  array<string, mixed>  $filters
     */
    public function search(Store $store, string $query, array $filters = [], int $perPage = 24, string $sort = 'relevance'): LengthAwarePaginator
    {
        $query = trim($query);
        $perPage = max(1, min(50, $perPage));

        if ($query === '') {
            return $this->browse($store, $filters, $perPage, $sort);
        }

        $paginator = $this->baseSearchQuery($store, $query, $filters)
            ->tap(fn (QueryBuilder $builder) => $this->applySort($builder, $sort, true))
            ->paginate($perPage, ['products.id'], 'page', Paginator::resolveCurrentPage());

        $paginator->setCollection($this->hydrateProducts(
            collect($paginator->items())->pluck('id')->map(fn (mixed $id): int => (int) $id)->all()
        ));

        $this->log($store, $query, $filters, $paginator->total());

        return $paginator;
    }

    public function autocomplete(Store $store, string $prefix, int $limit = 5): Collection
    {
        $prefix = trim($prefix);
        $limit = max(1, min(10, $limit));

        if ($prefix === '') {
            return collect();
        }

        $ids = $this->baseSearchQuery($store, $prefix, [])
            ->orderByRaw('products_fts.rank')
            ->limit($limit)
            ->pluck('products.id')
            ->map(fn (mixed $id): int => (int) $id)
            ->all();

        return $this->hydrateProducts($ids);
    }

    public function suggestions(Store $store, string $prefix, int $limit = 5): Collection
    {
        $productLimit = max(1, min(10, $limit));

        $products = $this->autocomplete($store, $prefix, $productLimit)
            ->map(function (Product $product): array {
                $variant = $product->variants->first();

                return [
                    'type' => 'product',
                    'title' => $product->title,
                    'handle' => $product->handle,
                    'image_url' => $product->media->first()?->storage_key,
                    'price_amount' => $variant?->price_amount,
                    'currency' => $variant?->currency ?? $product->store?->default_currency,
                ];
            });

        $remaining = max(0, $productLimit - $products->count());

        $collections = $remaining > 0
            ? ProductCollection::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->where('status', 'active')
                ->where('title', 'like', '%'.str_replace(['%', '_'], ['\%', '\_'], trim($prefix)).'%')
                ->oldest('title')
                ->limit($remaining)
                ->get()
                ->map(fn (ProductCollection $collection): array => [
                    'type' => 'collection',
                    'title' => $collection->title,
                    'handle' => $collection->handle,
                    'image_url' => null,
                ])
            : collect();

        return $products->concat($collections)->values();
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return array{vendors: list<array{value: string, count: int}>, tags: list<array{value: string, count: int}>, price_range: array{min: int|null, max: int|null}}
     */
    public function facets(Store $store, string $query, array $filters = []): array
    {
        $ids = trim($query) === ''
            ? $this->activeProductQuery($store, $filters)->limit(1000)->pluck('products.id')->all()
            : $this->baseSearchQuery($store, $query, $filters)->limit(1000)->pluck('products.id')->all();

        $ids = collect($ids)->map(fn (mixed $id): int => (int) $id)->all();

        if ($ids === []) {
            return [
                'vendors' => [],
                'tags' => [],
                'price_range' => ['min' => null, 'max' => null],
            ];
        }

        $vendors = Product::withoutGlobalScopes()
            ->whereIn('id', $ids)
            ->whereNotNull('vendor')
            ->selectRaw('vendor as value, COUNT(*) as count')
            ->groupBy('vendor')
            ->orderBy('vendor')
            ->get()
            ->map(fn (Product $product): array => [
                'value' => (string) $product->value,
                'count' => (int) $product->count,
            ])
            ->all();

        $tags = Product::withoutGlobalScopes()
            ->whereIn('id', $ids)
            ->get(['tags'])
            ->flatMap(fn (Product $product): array => $product->tags ?? [])
            ->filter()
            ->countBy()
            ->sortKeys()
            ->map(fn (int $count, string $tag): array => [
                'value' => $tag,
                'count' => $count,
            ])
            ->values()
            ->all();

        $priceRange = ProductVariant::withoutGlobalScopes()
            ->whereIn('product_id', $ids)
            ->selectRaw('MIN(price_amount) as min_price, MAX(price_amount) as max_price')
            ->first();

        return [
            'vendors' => $vendors,
            'tags' => $tags,
            'price_range' => [
                'min' => $priceRange?->min_price === null ? null : (int) $priceRange->min_price,
                'max' => $priceRange?->max_price === null ? null : (int) $priceRange->max_price,
            ],
        ];
    }

    public function syncProduct(Product $product): void
    {
        DB::table('products_fts')
            ->where('product_id', $product->getKey())
            ->delete();

        DB::table('products_fts')->insert([
            'store_id' => $product->store_id,
            'product_id' => $product->getKey(),
            'title' => $product->title,
            'description' => trim(strip_tags((string) $product->description_html)),
            'vendor' => (string) $product->vendor,
            'product_type' => (string) $product->product_type,
            'tags' => collect(Arr::wrap($product->tags))->filter()->implode(' '),
        ]);
    }

    public function removeProduct(int $productId): void
    {
        DB::table('products_fts')
            ->where('product_id', $productId)
            ->delete();
    }

    public function reindex(Store $store): int
    {
        DB::table('products_fts')
            ->where('store_id', $store->getKey())
            ->delete();

        $count = 0;

        Product::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->orderBy('id')
            ->chunkById(100, function (Collection $products) use (&$count): void {
                $products->each(function (Product $product) use (&$count): void {
                    $this->syncProduct($product);
                    $count++;
                });
            });

        return $count;
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    private function browse(Store $store, array $filters, int $perPage, string $sort): LengthAwarePaginator
    {
        $paginator = $this->activeProductQuery($store, $filters)
            ->tap(fn (QueryBuilder $builder) => $this->applySort($builder, $sort, false))
            ->paginate($perPage, ['products.id'], 'page', Paginator::resolveCurrentPage());

        $paginator->setCollection($this->hydrateProducts(
            collect($paginator->items())->pluck('id')->map(fn (mixed $id): int => (int) $id)->all()
        ));

        return $paginator;
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    private function baseSearchQuery(Store $store, string $query, array $filters): QueryBuilder
    {
        $match = $this->toFtsQuery($store, $query);

        if ($match === '') {
            return $this->activeProductQuery($store, $filters);
        }

        return $this->activeProductQuery($store, $filters)
            ->join('products_fts', 'products_fts.product_id', '=', 'products.id')
            ->where('products_fts.store_id', $store->getKey())
            ->whereRaw('products_fts MATCH ?', [$match]);
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    private function activeProductQuery(Store $store, array $filters): QueryBuilder
    {
        $builder = DB::table('products')
            ->select('products.id')
            ->where('products.store_id', $store->getKey())
            ->where('products.status', ProductStatus::Active->value)
            ->whereNotNull('products.published_at');

        $this->applyFilters($builder, $filters);

        return $builder;
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    private function applyFilters(QueryBuilder $builder, array $filters): void
    {
        if (isset($filters['collection_id'])) {
            $builder->whereExists(function (QueryBuilder $query) use ($filters): void {
                $query
                    ->selectRaw('1')
                    ->from('collection_products')
                    ->whereColumn('collection_products.product_id', 'products.id')
                    ->where('collection_products.collection_id', (int) $filters['collection_id']);
            });
        }

        $vendors = collect(Arr::wrap($filters['vendor'] ?? []))
            ->filter(fn (mixed $vendor): bool => filled($vendor))
            ->map(fn (mixed $vendor): string => (string) $vendor)
            ->values()
            ->all();


response accepted json response status 202

database query builder count raw table validation JSON API testing

route group middleware dependency injection controller

# Search Results

## Controllers Dependency Injection and Controllers
Controllers | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/controllers.md

## Dependency Injection and Controllers

---

## Verification Introduction Database Preparation
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

### Database Preparation

Next, your `users` table must contain an `email_verified_at` column to store the date and time that the user's email address was verified. Typically, this is included in Laravel's default `0001_01_01_000000_create_users_table.php` database migration.

---

## Http Client Making Requests
Http Client | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-client.md

## Making Requests

To make requests, you may use the `head`, `get`, `post`, `put`, `patch`, and `delete` methods provided by the `Http` facade. First, let's examine how to make a basic `GET` request to another URL:

```php
use Illuminate\Support\Facades\Http;

$response = Http::get('http://example.com');
```

The `get` method returns an instance of `Illuminate\Http\Client\Response`, which provides a variety of methods that may be used to inspect the response:

```php
$response->body() : string;
$response->json($key = null, $default = null) : mixed;
$response->object() : object;
$response->collect($key = null) : Illuminate\Support\Collection;
$response->resource() : resource;
$response->status() : int;
$response->successful() : bool;
$response->redirect(): bool;
$response->failed() : bool;
$response->clientError() : bool;
$response->header($header) : string;
$response->headers() : array;
```

The `Illuminate\Http\Client\Response` object also implements the PHP `ArrayAccess` interface, allowing you to access JSON response data directly on the response:

```php
return Http::get('http://example.com/users/1')['name'];
```

In addition to the response methods listed above, the following methods may be used to determine if the response has a specific status code:

```php
$response->ok() : bool;                  // 200 OK
$response->created() : bool;             // 201 Created
$response->accepted() : bool;            // 202 Accepted
$response->noContent() : bool;           // 204 No Content
$response->movedPermanently() : bool;    // 301 Moved Permanently
$response->found() : bool;               // 302 Found
$response->badRequest() : bool;          // 400 Bad Request
$response->unauthorized() : bool;        // 401 Unauthorized
$response->paymentRequired() : bool;     // 402 Payment Required
$response->forbidden() : bool;           // 403 Forbidden
$response->notFound() : bool;            // 404 Not Found
$response->requestTimeout() : bool;      // 408 Request Timeout
$response->conflict() : bool;            // 409 Conflict
$response->unprocessableEntity() : bool; // 422 Unprocessable Entity
$response->tooManyRequests() : bool;     // 429 Too Many Requests
$response->serverError() : bool;         // 500 Internal Server Error
```

---

## Validation Validation Quickstart Validation Error Response Format
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

### Validation Error Response Format

When your application throws a `Illuminate\Validation\ValidationException` exception and the incoming HTTP request is expecting a JSON response, Laravel will automatically format the error messages for you and return a `422 Unprocessable Entity` HTTP response.

Below, you can review an example of the JSON response format for validation errors. Note that nested error keys are flattened into "dot" notation format:

```json
{
    "message": "The team name must be a string. (and 4 more errors)",
    "errors": {
        "team_name": [
            "The team name must be a string.",
            "The team name must be at least 1 characters."
        ],
        "authorization.role": [
            "The selected authorization.role is invalid."
        ],
        "users.0.email": [
            "The users.0.email field is required."
        ],
        "users.2.email": [
            "The users.2.email must be a valid email address."
        ]
    }
}
```

---

## Upgrade Upgrading To 12.0 From 11.x Database Database Constructor Signature Changes
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Database Constructor Signature Changes

**Likelihood Of Impact: Very Low**

In Laravel 12, several low-level database classes now require an `Illuminate\Database\Connection` instance to be provided via their constructors.

**These changes are primarily applicable to database package maintainers - it is extremely unlikely any of these changes affect normal application development.**

`Illuminate\Database\Schema\Blueprint`

The constructor of the `Illuminate\Database\Schema\Blueprint` class now expects a `Connection` instance as its first argument. This primarily affects applications or packages that manually instantiate `Blueprint` instances.

`Illuminate\Database\Grammar`

The constructor of the `Illuminate\Database\Grammar` class also now requires a `Connection` instance. In previous versions, the connection was assigned after construction using the `setConnection()` method. This method has been removed in Laravel 12:

```php
// Laravel <= 11.x
$grammar = new MySqlGrammar;
$grammar->setConnection($connection);

// Laravel >= 12.x
$grammar = new MySqlGrammar($connection);
````

In addition, the following APIs have been removed or deprecated:

- The `Blueprint::getPrefix()` method is deprecated.
- The `Connection::withTablePrefix()` method has been removed.
- The `Grammar::getTablePrefix()` and `setTablePrefix()` methods are deprecated.
- The `Grammar::setConnection()` method has been removed.

When working with table prefixes, you should now retrieve them directly from the database connection:

```php
$prefix = $connection->getTablePrefix();
```

If you maintain custom database drivers, schema builders, or grammar implementations, you should review their constructors and ensure a `Connection` instance is provided.

---

## Vite Working With Blade and Routes Aliases
Vite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/vite.md

### Aliases

It is common in JavaScript applications to [create aliases](#aliases) to regularly referenced directories. But, you may also create aliases to use in Blade by using the `macro` method on the `Illuminate\Support\Facades\Vite` class. Typically, "macros" should be defined within the `boot` method of a [service provider](/docs/{{version}}/providers):

```php
/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Vite::macro('image', fn (string $asset) => $this->asset("resources/images/{$asset}"));
}
```

Once a macro has been defined, it can be invoked within your templates. For example, we can use the `image` macro defined above to reference an asset located at `resources/images/logo.png`:

```blade
<img src="{{ Vite::image('logo.png') }}" alt="Laravel Logo">
```

---

## Routing Route Groups Middleware
Routing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md

### Middleware

To assign [middleware](/docs/{{version}}/middleware) to all routes within a group, you may use the `middleware` method before defining the group. Middleware are executed in the order they are listed in the array:

```php
Route::middleware(['first', 'second'])->group(function () {
    Route::get('/', function () {
        // Uses first & second middleware...
    });

    Route::get('/user/profile', function () {
        // Uses first & second middleware...
    });
});
```

---

## Database Testing Available Assertions assertDatabaseCount
Database Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md

#### assertDatabaseCount

Assert that a table in the database contains the given number of records:

```php
$this->assertDatabaseCount('users', 5);
```

---

## Http Tests Available Assertions Response Assertions assertAccepted
Http Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md

#### assertAccepted

Assert that the response has an accepted (202) HTTP status code:

```php
$response->assertAccepted();
```

---

## Upgrade Upgrading To 12.0 From 11.x Database Multi-Schema Database Inspecting
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Multi-Schema Database Inspecting

**Likelihood Of Impact: Low**

The `Schema::getTables()`, `Schema::getViews()`, and `Schema::getTypes()` methods now include the results from all schemas by default. You may pass the `schema` argument to retrieve the result for the given schema only:

```php
// All tables on all schemas...
$tables = Schema::getTables();

// All tables on the 'main' schema...
$tables = Schema::getTables(schema: 'main');

// All tables on the 'main' and 'blog' schemas...
$tables = Schema::getTables(schema: ['main', 'blog']);
```

The `Schema::getTableListing()` method now returns schema-qualified table names by default. You may pass the `schemaQualified` argument to change the behavior as desired:

```php
$tables = Schema::getTableListing();
// ['main.migrations', 'main.users', 'blog.posts']

$tables = Schema::getTableListing(schema: 'main');
// ['main.migrations', 'main.users']

$tables = Schema::getTableListing(schema: 'main', schemaQualified: false);
// ['migrations', 'users']
```

The `db:table` and `db:show` commands now output the results of all schemas on MySQL, MariaDB, and SQLite, just like PostgreSQL and SQL Server.

---

## Routing Rate Limiting Defining Rate Limiters Response-Based Rate Limiting
Routing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md

#### Response-Based Rate Limiting

In addition to rate limiting incoming requests, Laravel allows you to rate limit based on the response using the `after` method. This is useful when you only want to count certain responses toward the rate limit, such as validation errors, 404 responses, or other specific HTTP status codes.

The `after` method accepts a closure that receives the response and should return `true` if the response should be counted toward the rate limit, or `false` if it should be ignored. This is particularly useful for preventing enumeration attacks by limiting consecutive 404 responses, or allowing users to retry requests that fail validation without exhausting their rate limit on an endpoint that should only throttle successful operations:

```php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Symfony\Component\HttpFoundation\Response;

RateLimiter::for('resource-not-found', function (Request $request) {
    return Limit::perMinute(10)
        ->by($request->user()?->id ?: $request->ip())
        ->after(function (Response $response) {
            // Only count 404 responses toward the rate limit to prevent enumeration...
            return $response->status() === 404;
        });
});
```

---

## Vite Working With Blade and Routes Refreshing on Save
Vite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/vite.md

### Refreshing on Save

When your application is built using traditional server-side rendering with Blade, Vite can improve your development workflow by automatically refreshing the browser when you make changes to view files in your application. To get started, you can simply specify the `refresh` option as `true`.

```js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            // ...
            refresh: true,
        }),
    ],
});
```

When the `refresh` option is `true`, saving files in the following directories will trigger the browser to perform a full page refresh while you are running `npm run dev`:

- `app/Livewire/**`
- `app/View/Components/**`
- `lang/**`
- `resources/lang/**`
- `resources/views/**`
- `routes/**`

Watching the `routes/**` directory is useful if you are utilizing [Ziggy](https://github.com/tighten/ziggy) to generate route links within your application's frontend.

If these default paths do not suit your needs, you can specify your own list of paths to watch:

```js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            // ...
            refresh: ['resources/views/**'],
        }),
    ],
});
```

Under the hood, the Laravel Vite plugin uses the [vite-plugin-full-reload](https://github.com/ElMassimo/vite-plugin-full-reload) package, which offers some advanced configuration options to fine-tune this feature's behavior. If you need this level of customization, you may provide a `config` definition:

```js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            // ...
            refresh: [{
                paths: ['path/to/watch/**'],
                config: { delay: 300 }
            }],
        }),
    ],
});
```

---

## Responses Other Response Types JSON Responses
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

### JSON Responses

The `json` method will automatically set the `Content-Type` header to `application/json`, as well as convert the given array to JSON using the `json_encode` PHP function:

```php
return response()->json([
    'name' => 'Abigail',
    'state' => 'CA',
]);
```

If you would like to create a JSONP response, you may use the `json` method in combination with the `withCallback` method:

```php
return response()
    ->json(['name' => 'Abigail', 'state' => 'CA'])
    ->withCallback($request->input('callback'));
```

---

## Responses Streamed Responses Streamed Downloads
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

### Streamed Downloads

Sometimes you may wish to turn the string response of a given operation into a downloadable response without having to write the contents of the operation to disk. You may use the `streamDownload` method in this scenario. This method accepts a callback, filename, and an optional array of headers as its arguments:

```php
use App\Services\GitHub;

return response()->streamDownload(function () {
    echo GitHub::api('repo')
        ->contents()
        ->readme('laravel', 'laravel')['contents'];
}, 'laravel-readme.md');
```

---

## Upgrade Upgrading To 12.0 From 11.x Authentication Updated `DatabaseTokenRepository` Constructor Signature
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Updated `DatabaseTokenRepository` Constructor Signature

**Likelihood Of Impact: Very Low**

The constructor of the `Illuminate\Auth\Passwords\DatabaseTokenRepository` class now expects the `$expires` parameter to be given in seconds, rather than minutes.

---

## Vite Working With Blade and Routes Processing Static Assets With Vite
Vite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/vite.md

### Processing Static Assets With Vite

When referencing assets in your JavaScript or CSS, Vite automatically processes and versions them. In addition, when building Blade based applications, Vite can also process and version static assets that you reference solely in Blade templates.

However, in order to accomplish this, you need to make Vite aware of your assets by importing the static assets into the application's entry point. For example, if you want to process and version all images stored in `resources/images` and all fonts stored in `resources/fonts`, you should add the following in your application's `resources/js/app.js` entry point:

```js
import.meta.glob([
  '../images/**',
  '../fonts/**',
]);
```

These assets will now be processed by Vite when running `npm run build`. You can then reference these assets in Blade templates using the `Vite::asset` method, which will return the versioned URL for a given asset:

```blade
<img src="{{ Vite::asset('resources/images/logo.png') }}">
```

---

## Responses Streamed Responses Streamed JSON Responses
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

### Streamed JSON Responses

If you need to stream JSON data incrementally, you may utilize the `streamJson` method. This method is especially useful for large datasets that need to be sent progressively to the browser in a format that can be easily parsed by JavaScript:

```php
use App\Models\User;

Route::get('/users.json', function () {
    return response()->streamJson([
        'users' => User::cursor(),
    ]);
});
```

The `useJsonStream` hook is identical to the [useStream hook](#consuming-streamed-responses) except that it will attempt to parse the data as JSON once it has finished streaming:

```tsx tab=React
import { useJsonStream } from "@laravel/stream-react";

type User = {
    id: number;
    name: string;
    email: string;
};

function App() {
    const { data, send } = useJsonStream<{ users: User[] }>("users");

    const loadUsers = () => {
        send({
            query: "taylor",
        });
    };

    return (
        <div>
            <ul>
                {data?.users.map((user) => (
                    <li>
                        {user.id}: {user.name}
                    </li>
                ))}
            </ul>
            <button onClick={loadUsers}>Load Users</button>
        </div>
    );
}
```

```vue tab=Vue
<script setup lang="ts">
import { useJsonStream } from "@laravel/stream-vue";

type User = {
    id: number;
    name: string;
    email: string;
};

const { data, send } = useJsonStream<{ users: User[] }>("users");

const loadUsers = () => {
    send({
        query: "taylor",
    });
};
</script>

<template>
    <div>
        <ul>
            <li v-for="user in data?.users" :key="user.id">
                {{ user.id }}: {{ user.name }}
            </li>
        </ul>
        <button @click="loadUsers">Load Users</button>
    </div>
</template>
```

```svelte tab=Svelte
<script>
import { useJsonStream } from "@laravel/stream-svelte";

const stream = useJsonStream("users");

const loadUsers = () => {
    stream.send({
        query: "taylor",
    });
};
</script>

<div>
    <ul>
        {#if $stream.data?.users}
            {#each $stream.data.users as user (user.id)}
                <li>{user.id}: {user.name}</li>
            {/each}
        {/if}
    </ul>
    <button onclick={loadUsers}>Load Users</button>
</div>
```

---

## Database Testing Available Assertions expectsDatabaseQueryCount
Database Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md

#### expectsDatabaseQueryCount

The `expectsDatabaseQueryCount` method may be invoked at the beginning of your test to specify the total number of database queries that you expect to be run during the test. If the actual number of executed queries does not exactly match this expectation, the test will fail:

```php
$this->expectsDatabaseQueryCount(5);

// Test...
```

---

## Vite Working With Blade and Routes
Vite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/vite.md

## Working With Blade and Routes

---

## Testing Running Tests Running Tests in Parallel Parallel Testing and Databases
Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/testing.md

#### Parallel Testing and Databases

As long as you have configured a primary database connection, Laravel automatically handles creating and migrating a test database for each parallel process that is running your tests. The test databases will be suffixed with a process token which is unique per process. For example, if you have two parallel test processes, Laravel will create and use `your_db_test_1` and `your_db_test_2` test databases.

By default, test databases persist between calls to the `test` Artisan command so that they can be used again by subsequent `test` invocations. However, you may re-create them using the `--recreate-databases` option:

```shell
php artisan test --parallel --recreate-databases
```

---

## Responses Response Macros
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

## Response Macros

If you would like to define a custom response that you can re-use in a variety of your routes and controllers, you may use the `macro` method on the `Response` facade. Typically, you should call this method from the `boot` method of one of your application's [service providers](/docs/{{version}}/providers), such as the `App\Providers\AppServiceProvider` service provider:

```php
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Response;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Response::macro('caps', function (string $value) {
            return Response::make(strtoupper($value));
        });
    }
}
```

The `macro` function accepts a name as its first argument and a closure as its second argument. The macro's closure will be executed when calling the macro name from a `ResponseFactory` implementation or the `response` helper:

```php
return response()->caps('foo');
```

---

## Verification Routing Protecting Routes
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

### Protecting Routes

[Route middleware](/docs/{{version}}/middleware) may be used to only allow verified users to access a given route. Laravel includes a `verified` [middleware alias](/docs/{{version}}/middleware#middleware-aliases), which is an alias for the `Illuminate\Auth\Middleware\EnsureEmailIsVerified` middleware class. Since this alias is already automatically registered by Laravel, all you need to do is attach the `verified` middleware to a route definition. Typically, this middleware is paired with the `auth` middleware:

```php
Route::get('/profile', function () {
    // Only verified users may access this route...
})->middleware(['auth', 'verified']);
```

If an unverified user attempts to access a route that has been assigned this middleware, they will automatically be redirected to the `verification.notice` [named route](/docs/{{version}}/routing#named-routes).

---

## Responses Streamed Responses Event Streams (SSE) Consuming Event Streams
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

#### Consuming Event Streams

Event streams may be consumed using Laravel's `stream` npm package, which provides a convenient API for interacting with Laravel event streams. To get started, install the `@laravel/stream-react`, `@laravel/stream-vue`, or `@laravel/stream-svelte` package:

```shell tab=React
npm install @laravel/stream-react
```

```shell tab=Vue
npm install @laravel/stream-vue
```

```shell tab=Svelte
npm install @laravel/stream-svelte
```

Then, `useEventStream` may be used to consume the event stream. After providing your stream URL, the hook will automatically update the `message` with the concatenated response as messages are returned from your Laravel application:

```jsx tab=React
import { useEventStream } from "@laravel/stream-react";

function App() {
  const { message } = useEventStream("/chat");

  return <div>{message}</div>;
}
```

```vue tab=Vue
<script setup lang="ts">
import { useEventStream } from "@laravel/stream-vue";

const { message } = useEventStream("/chat");
</script>

<template>
  <div>{{ message }}</div>
</template>
```

```svelte tab=Svelte
<script>
import { useEventStream } from "@laravel/stream-svelte";

const eventStream = useEventStream("/chat");
</script>

<div>{$eventStream.message}</div>
```

The second argument given to `useEventStream` is an options object that you may use to customize the stream consumption behavior. The default values for this object are shown below:

```jsx tab=React
import { useEventStream } from "@laravel/stream-react";

function App() {
  const { message } = useEventStream("/stream", {
    eventName: "update",
    onMessage: (message) => {
      //
    },
    onError: (error) => {
      //
    },
    onComplete: () => {
      //
    },
    endSignal: "</stream>",
    glue: " ",
  });

  return <div>{message}</div>;
}
```

```vue tab=Vue
<script setup lang="ts">
import { useEventStream } from "@laravel/stream-vue";

const { message } = useEventStream("/chat", {
  eventName: "update",
  onMessage: (message) => {
    // ...
  },
  onError: (error) => {
    // ...
  },
  onComplete: () => {
    // ...
  },
  endSignal: "</stream>",
  glue: " ",
});
</script>
```

```svelte tab=Svelte
<script>
import { useEventStream } from "@laravel/stream-svelte";

const eventStream = useEventStream("/chat", {
    eventName: "update",
    onMessage: (event) => {
        //
    },
    onError: (error) => {
        //
    },
    onComplete: () => {
        //
    },
    endSignal: "</stream>",
    glue: " ",
    replace: false,
});
</script>
```

Event streams may also be manually consumed via an [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) object by your application's frontend. The `eventStream` method will automatically send a `</stream>` update to the event stream when the stream is complete:

```js
const source = new EventSource('/chat');

source.addEventListener('update', (event) => {
    if (event.data === '</stream>') {
        source.close();

        return;
    }

    console.log(event.data);
});
```

To customize the final event that is sent to the event stream, you may provide a `StreamedEvent` instance to the `eventStream` method's `endStreamWith` argument:

```php
return response()->eventStream(function () {
    // ...
}, endStreamWith: new StreamedEvent(event: 'update', data: '</stream>'));
```

---

## Structure The Root Directory The Database Directory
Structure | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/structure.md

### The Database Directory

The `database` directory contains your database migrations, model factories, and seeds. If you wish, you may also use this directory to hold an SQLite database.

---

## Routing Route Groups Controllers
Routing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md

### Controllers

If a group of routes all utilize the same [controller](/docs/{{version}}/controllers), you may use the `controller` method to define the common controller for all of the routes within the group. Then, when defining the routes, you only need to provide the controller method that they invoke:

```php
use App\Http\Controllers\OrderController;

Route::controller(OrderController::class)->group(function () {
    Route::get('/orders/{id}', 'show');
    Route::post('/orders', 'store');
});
```

---

## Http Tests Available Assertions Response Assertions assertNoContent
Http Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md

#### assertNoContent

Assert that the response has the given HTTP status code and no content:

```php
$response->assertNoContent($status = 204);
```

---

## Database Testing Available Assertions assertDatabaseHas
Database Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md

#### assertDatabaseHas

Assert that a table in the database contains records matching the given key / value query constraints:

```php
$this->assertDatabaseHas('users', [
    'email' => 'sally@example.com',
]);
```

---

## Responses Streamed Responses Event Streams (SSE)
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

### Event Streams (SSE)

The `eventStream` method may be used to return a server-sent events (SSE) streamed response using the `text/event-stream` content type. The `eventStream` method accepts a closure which should [yield](https://www.php.net/manual/en/language.generators.overview.php) responses to the stream as the responses become available:

```php
Route::get('/chat', function () {
    return response()->eventStream(function () {
        $stream = OpenAI::client()->chat()->createStreamed(...);

        foreach ($stream as $response) {
            yield $response->choices[0];
        }
    });
});
```

If you would like to customize the name of the event, you may yield an instance of the `StreamedEvent` class:

```php
use Illuminate\Http\StreamedEvent;

yield new StreamedEvent(
    event: 'update',
    data: $response->choices[0],
);
```

---

## Session Introduction Driver Prerequisites Database
Session | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/session.md

#### Database

When using the `database` session driver, you will need to ensure that you have a database table to contain the session data. Typically, this is included in Laravel's default `0001_01_01_000000_create_users_table.php` [database migration](/docs/{{version}}/migrations); however, if for any reason you do not have a `sessions` table, you may use the `make:session-table` Artisan command to generate this migration:

```shell
php artisan make:session-table

php artisan migrate
```

---

## Verification Routing Resending the Verification Email
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

### Resending the Verification Email

Sometimes a user may misplace or accidentally delete the email address verification email. To accommodate this, you may wish to define a route to allow the user to request that the verification email be resent. You may then make a request to this route by placing a simple form submission button within your [verification notice view](#the-email-verification-notice):

```php
use Illuminate\Http\Request;

Route::post('/email/verification-notification', function (Request $request) {
    $request->user()->sendEmailVerificationNotification();

    return back()->with('message', 'Verification link sent!');
})->middleware(['auth', 'throttle:6,1'])->name('verification.send');
```

---

## Search Laravel Scout Database Engine
Search | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/search.md

### Database Engine

Scout's built-in database engine performs full-text and `LIKE` searches against your existing database — no external service or extra infrastructure required. Simply add the `Searchable` trait to your model and define a `toSearchableArray` method that returns the columns you want to be searchable.

You may use PHP attributes to control the search strategy for each column. `SearchUsingFullText` will use your database's full-text index, `SearchUsingPrefix` will only match from the beginning of the string (`example%`), and any columns without an attribute use a default `LIKE` strategy with wildcards on both sides (`%example%`):

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Attributes\SearchUsingFullText;
use Laravel\Scout\Attributes\SearchUsingPrefix;
use Laravel\Scout\Searchable;

class Article extends Model
{
    use Searchable;

    #[SearchUsingPrefix(['id'])]
    #[SearchUsingFullText(['title', 'body'])]
    public function toSearchableArray(): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
        ];
    }
}
```

> [!WARNING]
> Before specifying that a column should use full-text query constraints, ensure that the column has been assigned a [full-text index](/docs/{{version}}/migrations#available-index-types).

Once the trait is added, you may search your model using Scout's `search` method. Scout's database engine will automatically order results by relevance, even on PostgreSQL:

```php
$articles = Article::search('Laravel')->get();
```

The database engine is a great choice when your search needs are moderate and you want the convenience of Scout's automatic index syncing without deploying an external service. It handles the most common search use cases well, including filtering, pagination, and soft-deleted record handling. For complete details, consult the [Scout documentation](/docs/{{version}}/scout#database-engine).

_[Truncated]_


Output:
Wall time: 3.6026 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Controllers Dependency Injection and Controllers\nControllers | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/controllers.md\n\n\n## Dependency Injection and Controllers\n\n---\n\n## Verification Introduction Database Preparation\nVerification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md\n\n\n### Database Preparation\n\nNext, your `users` table must contain an `email_verified_at` column to store the date and time that the user's email address was verified. Typically, this is included in Laravel's default `0001_01_01_000000_create_users_table.php` database migration.\n\n---\n\n## Http Client Making Requests\nHttp Client | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-client.md\n\n\n## Making Requests\n\nTo make requests, you may use the `head`, `get`, `post`, `put`, `patch`, and `delete` methods provided by the `Http` facade. First, let's examine how to make a basic `GET` request to another URL:\n\n```php\nuse Illuminate\\Support\\Facades\\Http;\n\n$response = Http::get('http://example.com');\n```\n\nThe `get` method returns an instance of `Illuminate\\Http\\Client\\Response`, which provides a variety of methods that may be used to inspect the response:\n\n```php\n$response->body() : string;\n$response->json($key = null, $default = null) : mixed;\n$response->object() : object;\n$response->collect($key = null) : Illuminate\\Support\\Collection;\n$response->resource() : resource;\n$response->status() : int;\n$response->successful() : bool;\n$response->redirect(): bool;\n$response->failed() : bool;\n$response->clientError() : bool;\n$response->header($header) : string;\n$response->headers() : array;\n```\n\nThe `Illuminate\\Http\\Client\\Response` object also implements the PHP `ArrayAccess` interface, allowing you to access JSON response data directly on the response:\n\n```php\nreturn Http::get('http://example.com/users/1')['name'];\n```\n\nIn addition to the response methods listed above, the following methods may be used to determine if the response has a specific status code:\n\n```php\n$response->ok() : bool;                  // 200 OK\n$response->created() : bool;             // 201 Created\n$response->accepted() : bool;            // 202 Accepted\n$response->noContent() : bool;           // 204 No Content\n$response->movedPermanently() : bool;    // 301 Moved Permanently\n$response->found() : bool;               // 302 Found\n$response->badRequest() : bool;          // 400 Bad Request\n$response->unauthorized() : bool;        // 401 Unauthorized\n$response->paymentRequired() : bool;     // 402 Payment Required\n$response->forbidden() : bool;           // 403 Forbidden\n$response->notFound() : bool;            // 404 Not Found\n$response->requestTimeout() : bool;      // 408 Request Timeout\n$response->conflict() : bool;            // 409 Conflict\n$response->unprocessableEntity() : bool; // 422 Unprocessable Entity\n$response->tooManyRequests() : bool;     // 429 Too Many Requests\n$response->serverError() : bool;         // 500 Internal Server Error\n```\n\n---\n\n## Validation Validation Quickstart Validation Error Response Format\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n### Validation Error Response Format\n\nWhen your application throws a `Illuminate\\Validation\\ValidationException` exception and the incoming HTTP request is expecting a JSON response, Laravel will automatically format the error messages for you and return a `422 Unprocessable Entity` HTTP response.\n\nBelow, you can review an example of the JSON response format for validation errors. Note that nested error keys are flattened into \"dot\" notation format:\n\n```json\n{\n    \"message\": \"The team name must be a string. (and 4 more errors)\",\n    \"errors\": {\n        \"team_name\": [\n            \"The team name must be a string.\",\n            \"The team name must be at least 1 characters.\"\n        ],\n        \"authorization.role\": [\n            \"The selected authorization.role is invalid.\"\n        ],\n        \"users.0.email\": [\n            \"The users.0.email field is required.\"\n        ],\n        \"users.2.email\": [\n            \"The users.2.email must be a valid email address.\"\n        ]\n    }\n}\n```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Database Database Constructor Signature Changes\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Database Constructor Signature Changes\n\n**Likelihood Of Impact: Very Low**\n\nIn Laravel 12, several low-level database classes now require an `Illuminate\\Database\\Connection` instance to be provided via their constructors.\n\n**These changes are primarily applicable to database package maintainers - it is extremely unlikely any of these changes affect normal application development.**\n\n`Illuminate\\Database\\Schema\\Blueprint`\n\nThe constructor of the `Illuminate\\Database\\Schema\\Blueprint` class now expects a `Connection` instance as its first argument. This primarily affects applications or packages that manually instantiate `Blueprint` instances.\n\n`Illuminate\\Database\\Grammar`\n\nThe constructor of the `Illuminate\\Database\\Grammar` class also now requires a `Connection` instance. In previous versions, the connection was assigned after construction using the `setConnection()` method. This method has been removed in Laravel 12:\n\n```php\n// Laravel <= 11.x\n$grammar = new MySqlGrammar;\n$grammar->setConnection($connection);\n\n// Laravel >= 12.x\n$grammar = new MySqlGrammar($connection);\n````\n\nIn addition, the following APIs have been removed or deprecated:\n\n
\n\n- The `Blueprint::getPrefix()` method is deprecated.\n- The `Connection::withTablePrefix()` method has been removed.\n- The `Grammar::getTablePrefix()` and `setTablePrefix()` methods are deprecated.\n- The `Grammar::setConnection()` method has been removed.\n\n
\n\nWhen working with table prefixes, you should now retrieve them directly from the database connection:\n\n```php\n$prefix = $connection->getTablePrefix();\n```\n\nIf you maintain custom database drivers, schema builders, or grammar implementations, you should review their constructors and ensure a `Connection` instance is provided.\n\n---\n\n## Vite Working With Blade and Routes Aliases\nVite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/vite.md\n\n\n### Aliases\n\nIt is common in JavaScript applications to [create aliases](#aliases) to regularly referenced directories. But, you may also create aliases to use in Blade by using the `macro` method on the `Illuminate\\Support\\Facades\\Vite` class. Typically, \"macros\" should be defined within the `boot` method of a [service provider](/docs/{{version}}/providers):\n\n```php\n/**\n * Bootstrap any application services.\n */\npublic function boot(): void\n{\n    Vite::macro('image', fn (string $asset) => $this->asset(\"resources/images/{$asset}\"));\n}\n```\n\nOnce a macro has been defined, it can be invoked within your templates. For example, we can use the `image` macro defined above to reference an asset located at `resources/images/logo.png`:\n\n```blade\n<img src=\"{{ Vite::image('logo.png') }}\" alt=\"Laravel Logo\">\n```\n\n---\n\n## Routing Route Groups Middleware\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n### Middleware\n\nTo assign [middleware](/docs/{{version}}/middleware) to all routes within a group, you may use the `middleware` method before defining the group. Middleware are executed in the order they are listed in the array:\n\n```php\nRoute::middleware(['first', 'second'])->group(function () {\n    Route::get('/', function () {\n        // Uses first & second middleware...\n    });\n\n    Route::get('/user/profile', function () {\n        // Uses first & second middleware...\n    });\n});\n```\n\n---\n\n## Database Testing Available Assertions assertDatabaseCount\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n\n#### assertDatabaseCount\n\nAssert that a table in the database contains the given number of records:\n\n```php\n$this->assertDatabaseCount('users', 5);\n```\n\n---\n\n## Http Tests Available Assertions Response Assertions assertAccepted\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### assertAccepted\n\nAssert that the response has an accepted (202) HTTP status code:\n\n```php\n$response->assertAccepted();\n```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Database Multi-Schema Database Inspecting\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Multi-Schema Database Inspecting\n\n**Likelihood Of Impact: Low**\n\nThe `Schema::getTables()`, `Schema::getViews()`, and `Schema::getTypes()` methods now include the results from all schemas by default. You may pass the `schema` argument to retrieve the result for the given schema only:\n\n```php\n// All tables on all schemas...\n$tables = Schema::getTables();\n\n// All tables on the 'main' schema...\n$tables = Schema::getTables(schema: 'main');\n\n// All tables on the 'main' and 'blog' schemas...\n$tables = Schema::getTables(schema: ['main', 'blog']);\n```\n\nThe `Schema::getTableListing()` method now returns schema-qualified table names by default. You may pass the `schemaQualified` argument to change the behavior as desired:\n\n```php\n$tables = Schema::getTableListing();\n// ['main.migrations', 'main.users', 'blog.posts']\n\n$tables = Schema::getTableListing(schema: 'main');\n// ['main.migrations', 'main.users']\n\n$tables = Schema::getTableListing(schema: 'main', schemaQualified: false);\n// ['migrations', 'users']\n```\n\nThe `db:table` and `db:show` commands now output the results of all schemas on MySQL, MariaDB, and SQLite, just like PostgreSQL and SQL Server.\n\n---\n\n## Routing Rate Limiting Defining Rate Limiters Response-Based Rate Limiting\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n#### Response-Based Rate Limiting\n\nIn addition to rate limiting incoming requests, Laravel allows you to rate limit based on the response using the `after` method. This is useful when you only want to count certain responses toward the rate limit, such as validation errors, 404 responses, or other specific HTTP status codes.\n\nThe `after` method accepts a closure that receives the response and should return `true` if the response should be counted toward the rate limit, or `false` if it should be ignored. This is particularly useful for preventing enumeration attacks by limiting consecutive 404 responses, or allowing users to retry requests that fail validation without exhausting their rate limit on an endpoint that should only throttle successful operations:\n\n```php\nuse Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nRateLimiter::for('resource-not-found', function (Request $request) {\n    return Limit::perMinute(10)\n        ->by($request->user()?->id ?: $request->ip())\n        ->after(function (Response $response) {\n            // Only count 404 responses toward the rate limit to prevent enumeration...\n            return $response->status() === 404;\n        });\n});\n```\n\n---\n\n## Vite Working With Blade and Routes Refreshing on Save\nVite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/vite.md\n\n\n### Refreshing on Save\n\nWhen your application is built using traditional server-side rendering with Blade, Vite can improve your development workflow by automatically refreshing the browser when you make changes to view files in your application. To get started, you can simply specify the `refresh` option as `true`.\n\n```js\nimport { defineConfig } from 'vite';\nimport laravel from 'laravel-vite-plugin';\n\nexport default defineConfig({\n    plugins: [\n        laravel({\n            // ...\n            refresh: true,\n        }),\n    ],\n});\n```\n\nWhen the `refresh` option is `true`, saving files in the following directories will trigger the browser to perform a full page refresh while you are running `npm run dev`:\n\n- `app/Livewire/**`\n- `app/View/Components/**`\n- `lang/**`\n- `resources/lang/**`\n- `resources/views/**`\n- `routes/**`\n\nWatching the `routes/**` directory is useful if you are utilizing [Ziggy](https://github.com/tighten/ziggy) to generate route links within your application's frontend.\n\nIf these default paths do not suit your needs, you can specify your own list of paths to watch:\n\n```js\nimport { defineConfig } from 'vite';\nimport laravel from 'laravel-vite-plugin';\n\nexport default defineConfig({\n    plugins: [\n        laravel({\n            // ...\n            refresh: ['resources/views/**'],\n        }),\n    ],\n});\n```\n\nUnder the hood, the Laravel Vite plugin uses the [vite-plugin-full-reload](https://github.com/ElMassimo/vite-plugin-full-reload) package, which offers some advanced configuration options to fine-tune this feature's behavior. If you need this level of customization, you may provide a `config` definition:\n\n```js\nimport { defineConfig } from 'vite';\nimport laravel from 'laravel-vite-plugin';\n\nexport default defineConfig({\n    plugins: [\n        laravel({\n            // ...\n            refresh: [{\n                paths: ['path/to/watch/**'],\n                config: { delay: 300 }\n            }],\n        }),\n    ],\n});\n```\n\n---\n\n## Responses Other Response Types JSON Responses\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n### JSON Responses\n\nThe `json` method will automatically set the `Content-Type` header to `application/json`, as well as convert the given array to JSON using the `json_encode` PHP function:\n\n```php\nreturn response()->json([\n    'name' => 'Abigail',\n    'state' => 'CA',\n]);\n```\n\nIf you would like to create a JSONP response, you may use the `json` method in combination with the `withCallback` method:\n\n```php\nreturn response()\n    ->json(['name' => 'Abigail', 'state' => 'CA'])\n    ->withCallback($request->input('callback'));\n```\n\n---\n\n## Responses Streamed Responses Streamed Downloads\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n### Streamed Downloads\n\nSometimes you may wish to turn the string response of a given operation into a downloadable response without having to write the contents of the operation to disk. You may use the `streamDownload` method in this scenario. This method accepts a callback, filename, and an optional array of headers as its arguments:\n\n```php\nuse App\\Services\\GitHub;\n\nreturn response()->streamDownload(function () {\n    echo GitHub::api('repo')\n        ->contents()\n        ->readme('laravel', 'laravel')['contents'];\n}, 'laravel-readme.md');\n```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Authentication Updated `DatabaseTokenRepository` Constructor Signature\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Updated `DatabaseTokenRepository` Constructor Signature\n\n**Likelihood Of Impact: Very Low**\n\nThe constructor of the `Illuminate\\Auth\\Passwords\\DatabaseTokenRepository` class now expects the `$expires` parameter to be given in seconds, rather than minutes.\n\n---\n\n## Vite Working With Blade and Routes Processing Static Assets With Vite\nVite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/vite.md\n\n\n### Processing Static Assets With Vite\n\nWhen referencing assets in your JavaScript or CSS, Vite automatically processes and versions them. In addition, when building Blade based applications, Vite can also process and version static assets that you reference solely in Blade templates.\n\nHowever, in order to accomplish this, you need to make Vite aware of your assets by importing the static assets into the application's entry point. For example, if you want to process and version all images stored in `resources/images` and all fonts stored in `resources/fonts`, you should add the following in your application's `resources/js/app.js` entry point:\n\n```js\nimport.meta.glob([\n  '../images/**',\n  '../fonts/**',\n]);\n```\n\nThese assets will now be processed by Vite when running `npm run build`. You can then reference these assets in Blade templates using the `Vite::asset` method, which will return the versioned URL for a given asset:\n\n```blade\n<img src=\"{{ Vite::asset('resources/images/logo.png') }}\">\n```\n\n---\n\n## Responses Streamed Responses Streamed JSON Responses\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n### Streamed JSON Responses\n\nIf you need to stream JSON data incrementally, you may utilize the `streamJson` method. This method is especially useful for large datasets that need to be sent progressively to the browser in a format that can be easily parsed by JavaScript:\n\n```php\nuse App\\Models\\User;\n\nRoute::get('/users.json', function () {\n    return response()->streamJson([\n        'users' => User::cursor(),\n    ]);\n});\n```\n\nThe `useJsonStream` hook is identical to the [useStream hook](#consuming-streamed-responses) except that it will attempt to parse the data as JSON once it has finished streaming:\n\n```tsx tab=React\nimport { useJsonStream } from \"@laravel/stream-react\";\n\ntype User = {\n    id: number;\n    name: string;\n    email: string;\n};\n\nfunction App() {\n    const { data, send } = useJsonStream<{ users: User[] }>(\"users\");\n\n    const loadUsers = () => {\n        send({\n            query: \"taylor\",\n        });\n    };\n\n    return (\n        <div>\n            <ul>\n                {data?.users.map((user) => (\n                    <li>\n                        {user.id}: {user.name}\n                    </li>\n                ))}\n            </ul>\n            <button onClick={loadUsers}>Load Users</button>\n        </div>\n    );\n}\n```\n\n```vue tab=Vue\n<script setup lang=\"ts\">\nimport { useJsonStream } from \"@laravel/stream-vue\";\n\ntype User = {\n    id: number;\n    name: string;\n    email: string;\n};\n\nconst { data, send } = useJsonStream<{ users: User[] }>(\"users\");\n\nconst loadUsers = () => {\n    send({\n        query: \"taylor\",\n    });\n};\n</script>\n\n<template>\n    <div>\n        <ul>\n            <li v-for=\"user in data?.users\" :key=\"user.id\">\n                {{ user.id }}: {{ user.name }}\n            </li>\n        </ul>\n        <button @click=\"loadUsers\">Load Users</button>\n    </div>\n</template>\n```\n\n```svelte tab=Svelte\n<script>\nimport { useJsonStream } from \"@laravel/stream-svelte\";\n\nconst stream = useJsonStream(\"users\");\n\nconst loadUsers = () => {\n    stream.send({\n        query: \"taylor\",\n    });\n};\n</script>\n\n<div>\n    <ul>\n        {#if $stream.data?.users}\n            {#each $stream.data.users as user (user.id)}\n                <li>{user.id}: {user.name}</li>\n            {/each}\n        {/if}\n    </ul>\n    <button onclick={loadUsers}>Load Users</button>\n</div>\n```\n\n---\n\n## Database Testing Available Assertions expectsDatabaseQueryCount\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n\n#### expectsDatabaseQueryCount\n\nThe `expectsDatabaseQueryCount` method may be invoked at the beginning of your test to specify the total number of database queries that you expect to be run during the test. If the actual number of executed queries does not exactly match this expectation, the test will fail:\n\n```php\n$this->expectsDatabaseQueryCount(5);\n\n// Test...\n```\n\n---\n\n## Vite Working With Blade and Routes\nVite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/vite.md\n\n\n## Working With Blade and Routes\n\n---\n\n## Testing Running Tests Running Tests in Parallel Parallel Testing and Databases\nTesting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/testing.md\n\n\n#### Parallel Testing and Databases\n\nAs long as you have configured a primary database connection, Laravel automatically handles creating and migrating a test database for each parallel process that is running your tests. The test databases will be suffixed with a process token which is unique per process. For example, if you have two parallel test processes, Laravel will create and use `your_db_test_1` and `your_db_test_2` test databases.\n\nBy default, test databases persist between calls to the `test` Artisan command so that they can be used again by subsequent `test` invocations. However, you may re-create them using the `--recreate-databases` option:\n\n```shell\nphp artisan test --parallel --recreate-databases\n```\n\n---\n\n## Responses Response Macros\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n## Response Macros\n\nIf you would like to define a custom response that you can re-use in a variety of your routes and controllers, you may use the `macro` method on the `Response` facade. Typically, you should call this method from the `boot` method of one of your application's [service providers](/docs/{{version}}/providers), such as the `App\\Providers\\AppServiceProvider` service provider:\n\n```php\n<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Support\\Facades\\Response;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass AppServiceProvider extends ServiceProvider\n{\n    /**\n     * Bootstrap any application services.\n     */\n    public function boot(): void\n    {\n        Response::macro('caps', function (string $value) {\n            return Response::make(strtoupper($value));\n        });\n    }\n}\n```\n\nThe `macro` function accepts a name as its first argument and a closure as its second argument. The macro's closure will be executed when calling the macro name from a `ResponseFactory` implementation or the `response` helper:\n\n```php\nreturn response()->caps('foo');\n```\n\n---\n\n## Verification Routing Protecting Routes\nVerification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md\n\n\n### Protecting Routes\n\n[Route middleware](/docs/{{version}}/middleware) may be used to only allow verified users to access a given route. Laravel includes a `verified` [middleware alias](/docs/{{version}}/middleware#middleware-aliases), which is an alias for the `Illuminate\\Auth\\Middleware\\EnsureEmailIsVerified` middleware class. Since this alias is already automatically registered by Laravel, all you need to do is attach the `verified` middleware to a route definition. Typically, this middleware is paired with the `auth` middleware:\n\n```php\nRoute::get('/profile', function () {\n    // Only verified users may access this route...\n})->middleware(['auth', 'verified']);\n```\n\nIf an unverified user attempts to access a route that has been assigned this middleware, they will automatically be redirected to the `verification.notice` [named route](/docs/{{version}}/routing#named-routes).\n\n---\n\n## Responses Streamed Responses Event Streams (SSE) Consuming Event Streams\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n#### Consuming Event Streams\n\nEvent streams may be consumed using Laravel's `stream` npm package, which provides a convenient API for interacting with Laravel event streams. To get started, install the `@laravel/stream-react`, `@laravel/stream-vue`, or `@laravel/stream-svelte` package:\n\n```shell tab=React\nnpm install @laravel/stream-react\n```\n\n```shell tab=Vue\nnpm install @laravel/stream-vue\n```\n\n```shell tab=Svelte\nnpm install @laravel/stream-svelte\n```\n\nThen, `useEventStream` may be used to consume the event stream. After providing your stream URL, the hook will automatically update the `message` with the concatenated response as messages are returned from your Laravel application:\n\n```jsx tab=React\nimport { useEventStream } from \"@laravel/stream-react\";\n\nfunction App() {\n  const { message } = useEventStream(\"/chat\");\n\n  return <div>{message}</div>;\n}\n```\n\n```vue tab=Vue\n<script setup lang=\"ts\">\nimport { useEventStream } from \"@laravel/stream-vue\";\n\nconst { message } = useEventStream(\"/chat\");\n</script>\n\n<template>\n  <div>{{ message }}</div>\n</template>\n```\n\n```svelte tab=Svelte\n<script>\nimport { useEventStream } from \"@laravel/stream-svelte\";\n\nconst eventStream = useEventStream(\"/chat\");\n</script>\n\n<div>{$eventStream.message}</div>\n```\n\nThe second argument given to `useEventStream` is an options object that you may use to customize the stream consumption behavior. The default values for this object are shown below:\n\n```jsx tab=React\nimport { useEventStream } from \"@laravel/stream-react\";\n\nfunction App() {\n  const { message } = useEventStream(\"/stream\", {\n    eventName: \"update\",\n    onMessage: (message) => {\n      //\n    },\n    onError: (error) => {\n      //\n    },\n    onComplete: () => {\n      //\n    },\n    endSignal: \"</stream>\",\n    glue: \" \",\n  });\n\n  return <div>{message}</div>;\n}\n```\n\n```vue tab=Vue\n<script setup lang=\"ts\">\nimport { useEventStream } from \"@laravel/stream-vue\";\n\nconst { message } = useEventStream(\"/chat\", {\n  eventName: \"update\",\n  onMessage: (message) => {\n    // ...\n  },\n  onError: (error) => {\n    // ...\n  },\n  onComplete: () => {\n    // ...\n  },\n  endSignal: \"</stream>\",\n  glue: \" \",\n});\n</script>\n```\n\n```svelte tab=Svelte\n<script>\nimport { useEventStream } from \"@laravel/stream-svelte\";\n\nconst eventStream = useEventStream(\"/chat\", {\n    eventName: \"update\",\n    onMessage: (event) => {\n        //\n    },\n    onError: (error) => {\n        //\n    },\n    onComplete: () => {\n        //\n    },\n    endSignal: \"</stream>\",\n    glue: \" \",\n    replace: false,\n});\n</script>\n```\n\nEvent streams may also be manually consumed via an [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) object by your application's frontend. The `eventStream` method will automatically send a `</stream>` update to the event stream when the stream is complete:\n\n```js\nconst source = new EventSource('/chat');\n\nsource.addEventListener('update', (event) => {\n    if (event.data === '</stream>') {\n        source.close();\n\n        return;\n    }\n\n    console.log(event.data);\n});\n```\n\nTo customize the final event that is sent to the event stream, you may provide a `StreamedEvent` instance to the `eventStream` method's `endStreamWith` argument:\n\n```php\nreturn response()->eventStream(function () {\n    // ...\n}, endStreamWith: new StreamedEvent(event: 'update', data: '</stream>'));\n```\n\n---\n\n## Structure The Root Directory The Database Directory\nStructure | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/structure.md\n\n\n### The Database Directory\n\nThe `database` directory contains your database migrations, model factories, and seeds. If you wish, you may also use this directory to hold an SQLite database.\n\n---\n\n## Routing Route Groups Controllers\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n### Controllers\n\nIf a group of routes all utilize the same [controller](/docs/{{version}}/controllers), you may use the `controller` method to define the common controller for all of the routes within the group. Then, when defining the routes, you only need to provide the controller method that they invoke:\n\n```php\nuse App\\Http\\Controllers\\OrderController;\n\nRoute::controller(OrderController::class)->group(function () {\n    Route::get('/orders/{id}', 'show');\n    Route::post('/orders', 'store');\n});\n```\n\n---\n\n## Http Tests Available Assertions Response Assertions assertNoContent\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### assertNoContent\n\nAssert that the response has the given HTTP status code and no content:\n\n```php\n$response->assertNoContent($status = 204);\n```\n\n---\n\n## Database Testing Available Assertions assertDatabaseHas\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n\n#### assertDatabaseHas\n\nAssert that a table in the database contains records matching the given key / value query constraints:\n\n```php\n$this->assertDatabaseHas('users', [\n    'email' => 'sally@example.com',\n]);\n```\n\n---\n\n## Responses Streamed Responses Event Streams (SSE)\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n### Event Streams (SSE)\n\nThe `eventStream` method may be used to return a server-sent events (SSE) streamed response using the `text/event-stream` content type. The `eventStream` method accepts a closure which should [yield](https://www.php.net/manual/en/language.generators.overview.php) responses to the stream as the responses become available:\n\n```php\nRoute::get('/chat', function () {\n    return response()->eventStream(function () {\n        $stream = OpenAI::client()->chat()->createStreamed(...);\n\n        foreach ($stream as $response) {\n            yield $response->choices[0];\n        }\n    });\n});\n```\n\nIf you would like to customize the name of the event, you may yield an instance of the `StreamedEvent` class:\n\n```php\nuse Illuminate\\Http\\StreamedEvent;\n\nyield new StreamedEvent(\n    event: 'update',\n    data: $response->choices[0],\n);\n```\n\n---\n\n## Session Introduction Driver Prerequisites Database\nSession | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/session.md\n\n\n#### Database\n\nWhen using the `database` session driver, you will need to ensure that you have a database table to contain the session data. Typically, this is included in Laravel's default `0001_01_01_000000_create_users_table.php` [database migration](/docs/{{version}}/migrations); however, if for any reason you do not have a `sessions` table, you may use the `make:session-table` Artisan command to generate this migration:\n\n```shell\nphp artisan make:session-table\n\nphp artisan migrate\n```\n\n---\n\n## Verification Routing Resending the Verification Email\nVerification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md\n\n\n### Resending the Verification Email\n\nSometimes a user may misplace or accidentally delete the email address verification email. To accommodate this, you may wish to define a route to allow the user to request that the verification email be resent. You may then make a request to this route by placing a simple form submission button within your [verification notice view](#the-email-verification-notice):\n\n```php\nuse Illuminate\\Http\\Request;\n\nRoute::post('/email/verification-notification', function (Request $request) {\n    $request->user()->sendEmailVerificationNotification();\n\n    return back()->with('message', 'Verification link sent!');\n})->middleware(['auth', 'throttle:6,1'])->name('verification.send');\n```\n\n---\n\n## Search Laravel Scout Database Engine\nSearch | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/search.md\n\n\n### Database Engine\n\nScout's built-in database engine performs full-text and `LIKE` searches against your existing database — no external service or extra infrastructure required. Simply add the `Searchable` trait to your model and define a `toSearchableArray` method that returns the columns you want to be searchable.\n\nYou may use PHP attributes to control the search strategy for each column. `SearchUsingFullText` will use your database's full-text index, `SearchUsingPrefix` will only match from the beginning of the string (`example%`), and any columns without an attribute use a default `LIKE` strategy with wildcards on both sides (`%example%`):\n\n```php\n<?php\n\nnamespace App\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Laravel\\Scout\\Attributes\\SearchUsingFullText;\nuse Laravel\\Scout\\Attributes\\SearchUsingPrefix;\nuse Laravel\\Scout\\Searchable;\n\nclass Article extends Model\n{\n    use Searchable;\n\n    #[SearchUsingPrefix(['id'])]\n    #[SearchUsingFullText(['title', 'body'])]\n    public function toSearchableArray(): array\n    {\n        return [\n            'id' => $this->id,\n            'title' => $this->title,\n            'body' => $this->body,\n        ];\n    }\n}\n```\n\n> [!WARNING]\n> Before specifying that a column should use full-text query constraints, ensure that the column has been assigned a [full-text index](/docs/{{version}}/migrations#available-index-types).\n\nOnce the trait is added, you may search your model using Scout's `search` method. Scout's database engine will automatically order results by relevance, even on PostgreSQL:\n\n```php\n$articles = Article::search('Laravel')->get();\n```\n\nThe database engine is a great choice when your search needs are moderate and you want the convenience of Scout's automatic index syncing without deploying an external service. It handles the most common search use cases well, including filtering, pagination, and soft-deleted record handling. For complete details, consult the [Scout documentation](/docs/{{version}}/scout#database-engine).\n\n_[Truncated]_"}]


Output:
Chunk ID: b4847b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2471
Output:
<?php

namespace App\Services;

use App\Enums\InventoryPolicy;
use App\Enums\ProductStatus;
use App\Models\Collection as ProductCollection;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\SearchQuery;
use App\Models\SearchSettings;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class SearchService
{
    /**
     * @param  array<string, mixed>  $filters
     */
    public function search(Store $store, string $query, array $filters = [], int $perPage = 24, string $sort = 'relevance'): LengthAwarePaginator
    {
        $query = trim($query);
        $perPage = max(1, min(50, $perPage));

        if ($query === '') {
            return $this->browse($store, $filters, $perPage, $sort);
        }

        $paginator = $this->baseSearchQuery($store, $query, $filters)
            ->tap(fn (QueryBuilder $builder) => $this->applySort($builder, $sort, true))
            ->paginate($perPage, ['products.id'], 'page', Paginator::resolveCurrentPage());

        $paginator->setCollection($this->hydrateProducts(
            collect($paginator->items())->pluck('id')->map(fn (mixed $id): int => (int) $id)->all()
        ));

        $this->log($store, $query, $filters, $paginator->total());

        return $paginator;
    }

    public function autocomplete(Store $store, string $prefix, int $limit = 5): Collection
    {
        $prefix = trim($prefix);
        $limit = max(1, min(10, $limit));

        if ($prefix === '') {
            return collect();
        }

        $ids = $this->baseSearchQuery($store, $prefix, [])
            ->orderByRaw('products_fts.rank')
            ->limit($limit)
            ->pluck('products.id')
            ->map(fn (mixed $id): int => (int) $id)
            ->all();

        return $this->hydrateProducts($ids);
    }

    public function suggestions(Store $store, string $prefix, int $limit = 5): Collection
    {
        $productLimit = max(1, min(10, $limit));

        $products = $this->autocomplete($store, $prefix, $productLimit)
            ->map(function (Product $product): array {
                $variant = $product->variants->first();

                return [
                    'type' => 'product',
                    'title' => $product->title,
                    'handle' => $product->handle,
                    'image_url' => $product->media->first()?->storage_key,
                    'price_amount' => $variant?->price_amount,
                    'currency' => $variant?->currency ?? $product->store?->default_currency,
                ];
            });

        $remaining = max(0, $productLimit - $products->count());

        $collections = $remaining > 0
            ? ProductCollection::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->where('status', 'active')
                ->where('title', 'like', '%'.str_replace(['%', '_'], ['\%', '\_'], trim($prefix)).'%')
                ->oldest('title')
                ->limit($remaining)
                ->get()
                ->map(fn (ProductCollection $collection): array => [
                    'type' => 'collection',
                    'title' => $collection->title,
                    'handle' => $collection->handle,
                    'image_url' => null,
                ])
            : collect();

        return $products->concat($collections)->values();
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return array{vendors: list<array{value: string, count: int}>, tags: list<array{value: string, count: int}>, price_range: array{min: int|null, max: int|null}}
     */
    public function facets(Store $store, string $query, array $filters = []): array
    {
        $ids = trim($query) === ''
            ? $this->activeProductQuery($store, $filters)->limit(1000)->pluck('products.id')->all()
            : $this->baseSearchQuery($store, $query, $filters)->limit(1000)->pluck('products.id')->all();

        $ids = collect($ids)->map(fn (mixed $id): int => (int) $id)->all();

        if ($ids === []) {
            return [
                'vendors' => [],
                'tags' => [],
                'price_range' => ['min' => null, 'max' => null],
            ];
        }

        $vendors = Product::withoutGlobalScopes()
            ->whereIn('id', $ids)
            ->whereNotNull('vendor')
            ->selectRaw('vendor as value, COUNT(*) as count')
            ->groupBy('vendor')
            ->orderBy('vendor')
            ->get()
            ->map(fn (Product $product): array => [
                'value' => (string) $product->value,
                'count' => (int) $product->count,
            ])
            ->all();

        $tags = Product::withoutGlobalScopes()
            ->whereIn('id', $ids)
            ->get(['tags'])
            ->flatMap(fn (Product $product): array => $product->tags ?? [])
            ->filter()
            ->countBy()
            ->sortKeys()
            ->map(fn (int $count, string $tag): array => [
                'value' => $tag,
                'count' => $count,
            ])
            ->values()
            ->all();

        $priceRange = ProductVariant::withoutGlobalScopes()
            ->whereIn('product_id', $ids)
            ->selectRaw('MIN(price_amount) as min_price, MAX(price_amount) as max_price')
            ->first();

        return [
            'vendors' => $vendors,
            'tags' => $tags,
            'price_range' => [
                'min' => $priceRange?->min_price === null ? null : (int) $priceRange->min_price,
                'max' => $priceRange?->max_price === null ? null : (int) $priceRange->max_price,
            ],
        ];
    }

    public function syncProduct(Product $product): void
    {
        DB::table('products_fts')
            ->where('product_id', $product->getKey())
            ->delete();

        DB::table('products_fts')->insert([
            'store_id' => $product->store_id,
            'product_id' => $product->getKey(),
            'title' => $product->title,
            'description' => trim(strip_tags((string) $product->description_html)),
            'vendor' => (string) $product->vendor,
            'product_type' => (string) $product->product_type,
            'tags' => collect(Arr::wrap($product->tags))->filter()->implode(' '),
        ]);
    }

    public function removeProduct(int $productId): void
    {
        DB::table('products_fts')
            ->where('product_id', $productId)
            ->delete();
    }

    public function reindex(Store $store): int
    {
        DB::table('products_fts')
            ->where('store_id', $store->getKey())
            ->delete();

        $count = 0;

        Product::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->orderBy('id')
            ->chunkById(100, function (Collection $products) use (&$count): void {
                $products->each(function (Product $product) use (&$count): void {
                    $this->syncProduct($product);
                    $count++;
                });
            });

        return $count;
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    private function browse(Store $store, array $filters, int $perPage, string $sort): LengthAwarePaginator
    {
        $paginator = $this->activeProductQuery($store, $filters)
            ->tap(fn (QueryBuilder $builder) => $this->applySort($builder, $sort, false))
            ->paginate($perPage, ['products.id'], 'page', Paginator::resolveCurrentPage());

        $paginator->setCollection($this->hydrateProducts(
            collect($paginator->items())->pluck('id')->map(fn (mixed $id): int => (int) $id)->all()
        ));

        return $paginator;
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    private function baseSearchQuery(Store $store, string $query, array $filters): QueryBuilder
    {
        $match = $this->toFtsQuery($store, $query);

        if ($match === '') {
            return $this->activeProductQuery($store, $filters);
        }

        return $this->activeProductQuery($store, $filters)
            ->join('products_fts', 'products_fts.product_id', '=', 'products.id')
            ->where('products_fts.store_id', $store->getKey())
            ->whereRaw('products_fts MATCH ?', [$match]);
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    private function activeProductQuery(Store $store, array $filters): QueryBuilder
    {
        $builder = DB::table('products')
            ->select('products.id')
            ->where('products.store_id', $store->getKey())
            ->where('products.status', ProductStatus::Active->value)
            ->whereNotNull('products.published_at');

        $this->applyFilters($builder, $filters);

        return $builder;
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    private function applyFilters(QueryBuilder $builder, array $filters): void
    {
        if (isset($filters['collection_id'])) {
            $builder->whereExists(function (QueryBuilder $query) use ($filters): void {
                $query
                    ->selectRaw('1')
                    ->from('collection_products')
                    ->whereColumn('collection_products.product_id', 'products.id')
                    ->where('collection_products.collection_id', (int) $filters['collection_id']);
            });
        }

        $vendors = collect(Arr::wrap($filters['vendor'] ?? []))
            ->filter(fn (mixed $vendor): bool => filled($vendor))
            ->map(fn (mixed $vendor): string => (string) $vendor)
            ->values()
            ->all();


sed -n '1,220p' app/Models/SearchSetting.php

SearchSetting.php

sed: app/Models/SearchSetting.php: No such file or directory


sed -n '1,260p' app/Livewire/Admin/Search/Settings.php

Settings.php

<?php

namespace App\Livewire\Admin\Search;

use App\Models\SearchSettings;
use App\Models\Store;
use App\Services\SearchService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Settings extends Component
{
    use AuthorizesRequests;

    #[Locked]
    public int $storeId;

    /**
     * @var array<int, string>
     */
    public array $synonymGroups = [];

    public string $stopWords = '';

    public ?string $lastIndexedAt = null;

    public bool $isReindexing = false;

    public ?int $reindexProgress = null;

    public function mount(): void
    {
        $store = $this->store();

        $this->authorize('update', $store);

        $this->storeId = $store->getKey();

        $settings = $this->settings();
        $this->synonymGroups = collect($settings->synonyms_json ?? [])
            ->map(fn (array $group): string => implode(', ', $group))
            ->values()
            ->all();
        $this->stopWords = implode(', ', $settings->stop_words_json ?? []);
        $this->lastIndexedAt = $settings->updated_at?->toDayDateTimeString();
    }

    public function addSynonymGroup(): void
    {
        $this->synonymGroups[] = '';
    }

    public function removeSynonymGroup(int $index): void
    {
        unset($this->synonymGroups[$index]);
        $this->synonymGroups = array_values($this->synonymGroups);
    }

    public function save(): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->validate([
            'synonymGroups' => ['array', 'max:50'],
            'synonymGroups.*' => ['nullable', 'string', 'max:500'],
            'stopWords' => ['nullable', 'string', 'max:2000'],
        ], [], [
            'synonymGroups.*' => 'synonym group',
            'stopWords' => 'stop words',
        ]);

        $settings = $this->settings();
        $settings->forceFill([
            'synonyms_json' => $this->parseSynonyms(),
            'stop_words_json' => $this->parseWordList($this->stopWords),
        ])->save();

        $this->lastIndexedAt = $settings->updated_at?->toDayDateTimeString();

        session()->flash('status', 'Search settings saved');
        $this->dispatch('toast', type: 'success', message: __('Search settings saved'));
    }

    public function triggerReindex(SearchService $search): void
    {
        $store = $this->scopedStore();

        $this->authorize('update', $store);

        $this->isReindexing = true;
        $this->reindexProgress = 10;

        $count = $search->reindex($store);

        $settings = $this->settings();
        $settings->touch();

        $this->lastIndexedAt = $settings->refresh()->updated_at?->toDayDateTimeString();
        $this->reindexProgress = 100;
        $this->isReindexing = false;

        session()->flash('status', __('Search index rebuilt for :count products', ['count' => $count]));
        $this->dispatch('toast', type: 'success', message: __('Search index rebuilt'));
    }

    public function pollReindexStatus(): void
    {
        $this->reindexProgress = $this->isReindexing ? $this->reindexProgress : null;
    }

    public function render(): mixed
    {
        return view('livewire.admin.search.settings')->layout('layouts.app', [
            'title' => __('Search settings'),
        ]);
    }

    /**
     * @return list<list<string>>
     */
    private function parseSynonyms(): array
    {
        return collect($this->synonymGroups)
            ->map(fn (string $group): array => $this->parseWordList($group))
            ->filter(fn (array $group): bool => count($group) >= 2)
            ->values()
            ->all();
    }

    /**
     * @return list<string>
     */
    private function parseWordList(string $words): array
    {
        return collect(explode(',', mb_strtolower($words)))
            ->map(fn (string $word): string => trim($word))
            ->filter()
            ->unique()
            ->values()
            ->all();
    }

    private function settings(): SearchSettings
    {
        return SearchSettings::withoutGlobalScopes()->firstOrCreate([
            'store_id' => $this->storeId ?? $this->store()->getKey(),
        ]);
    }

    private function store(): Store
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function scopedStore(): Store
    {
        return Store::query()->findOrFail($this->storeId);
    }
}


Output:
Chunk ID: fc0810
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1125
Output:
<?php

namespace App\Livewire\Admin\Search;

use App\Models\SearchSettings;
use App\Models\Store;
use App\Services\SearchService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Settings extends Component
{
    use AuthorizesRequests;

    #[Locked]
    public int $storeId;

    /**
     * @var array<int, string>
     */
    public array $synonymGroups = [];

    public string $stopWords = '';

    public ?string $lastIndexedAt = null;

    public bool $isReindexing = false;

    public ?int $reindexProgress = null;

    public function mount(): void
    {
        $store = $this->store();

        $this->authorize('update', $store);

        $this->storeId = $store->getKey();

        $settings = $this->settings();
        $this->synonymGroups = collect($settings->synonyms_json ?? [])
            ->map(fn (array $group): string => implode(', ', $group))
            ->values()
            ->all();
        $this->stopWords = implode(', ', $settings->stop_words_json ?? []);
        $this->lastIndexedAt = $settings->updated_at?->toDayDateTimeString();
    }

    public function addSynonymGroup(): void
    {
        $this->synonymGroups[] = '';
    }

    public function removeSynonymGroup(int $index): void
    {
        unset($this->synonymGroups[$index]);
        $this->synonymGroups = array_values($this->synonymGroups);
    }

    public function save(): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->validate([
            'synonymGroups' => ['array', 'max:50'],
            'synonymGroups.*' => ['nullable', 'string', 'max:500'],
            'stopWords' => ['nullable', 'string', 'max:2000'],
        ], [], [
            'synonymGroups.*' => 'synonym group',
            'stopWords' => 'stop words',
        ]);

        $settings = $this->settings();
        $settings->forceFill([
            'synonyms_json' => $this->parseSynonyms(),
            'stop_words_json' => $this->parseWordList($this->stopWords),
        ])->save();

        $this->lastIndexedAt = $settings->updated_at?->toDayDateTimeString();

        session()->flash('status', 'Search settings saved');
        $this->dispatch('toast', type: 'success', message: __('Search settings saved'));
    }

    public function triggerReindex(SearchService $search): void
    {
        $store = $this->scopedStore();

        $this->authorize('update', $store);

        $this->isReindexing = true;
        $this->reindexProgress = 10;

        $count = $search->reindex($store);

        $settings = $this->settings();
        $settings->touch();

        $this->lastIndexedAt = $settings->refresh()->updated_at?->toDayDateTimeString();
        $this->reindexProgress = 100;
        $this->isReindexing = false;

        session()->flash('status', __('Search index rebuilt for :count products', ['count' => $count]));
        $this->dispatch('toast', type: 'success', message: __('Search index rebuilt'));
    }

    public function pollReindexStatus(): void
    {
        $this->reindexProgress = $this->isReindexing ? $this->reindexProgress : null;
    }

    public function render(): mixed
    {
        return view('livewire.admin.search.settings')->layout('layouts.app', [
            'title' => __('Search settings'),
        ]);
    }

    /**
     * @return list<list<string>>
     */
    private function parseSynonyms(): array
    {
        return collect($this->synonymGroups)
            ->map(fn (string $group): array => $this->parseWordList($group))
            ->filter(fn (array $group): bool => count($group) >= 2)
            ->values()
            ->all();
    }

    /**
     * @return list<string>
     */
    private function parseWordList(string $words): array
    {
        return collect(explode(',', mb_strtolower($words)))
            ->map(fn (string $word): string => trim($word))
            ->filter()
            ->unique()
            ->values()
            ->all();
    }

    private function settings(): SearchSettings
    {
        return SearchSettings::withoutGlobalScopes()->firstOrCreate([
            'store_id' => $this->storeId ?? $this->store()->getKey(),
        ]);
    }

    private function store(): Store
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function scopedStore(): Store
    {
        return Store::query()->findOrFail($this->storeId);
    }
}


Output:
Chunk ID: 2f5e7b
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 16
Output:
sed: app/Models/SearchSetting.php: No such file or directory


sed -n '1,220p' database/migrations/*search*

*search*

sed -n '1,220p' 'database/migrations/*search*'

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('search_settings', function (Blueprint $table) {
            $table->foreignId('store_id')->primary()->constrained()->cascadeOnDelete();
            $table->text('synonyms_json')->default('[]');
            $table->text('stop_words_json')->default('[]');
            $table->timestamp('updated_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('search_settings');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('search_queries', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->text('query');
            $table->text('filters_json')->nullable();
            $table->unsignedInteger('results_count')->default(0);
            $table->timestamp('created_at')->nullable();

            $table->index('store_id', 'idx_search_queries_store_id');
            $table->index(['store_id', 'created_at'], 'idx_search_queries_store_created');
            $table->index(['store_id', 'query'], 'idx_search_queries_store_query');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('search_queries');
    }
};