This commit introduces a Product service to Lunar. This product service calls Meilisearch to fetch an indexed product. The indexer has been updated to also include the collection and the price of the product. A product search service has also been created to be used by the frontend's search
82 lines
3.6 KiB
Markdown
82 lines
3.6 KiB
Markdown
# Product Search
|
|
|
|
`Modules\Core\Search\ProductSearchService` provides locale-aware full-text product search on
|
|
top of Laravel Scout + Meilisearch.
|
|
|
|
---
|
|
|
|
## Why locale-aware search isn't a filter
|
|
|
|
Lunar's Meilisearch indexer (`Lunar\Search\ScoutIndexer::mapSearchableAttributes()`) flattens
|
|
every translated attribute into **locale-suffixed fields on a single document** — a product with
|
|
a translated `name` produces `name_en`, `name_el`, etc. as separate top-level fields, not
|
|
separate documents per locale and not a filterable `locale` field.
|
|
|
|
That means "search in Greek" isn't a `->filter('locale = el')` — Meilisearch has no such field to
|
|
filter on. It's a choice of **which fields the query targets**: `name_el`/`description_el`
|
|
instead of `name_en`/`description_en`. This is what Meilisearch's `attributesToSearchOn` search
|
|
parameter controls, exposed through Scout via `Builder::options()`, which passes straight through
|
|
to the underlying Meilisearch client call (`Laravel\Scout\Engines\MeilisearchEngine::performSearch()`
|
|
merges `$builder->options` directly into the search request).
|
|
|
|
---
|
|
|
|
## Usage
|
|
|
|
```php
|
|
use Modules\Core\Search\ProductSearchService;
|
|
|
|
$results = app(ProductSearchService::class)->search('running shoes');
|
|
// or an explicit locale, bypassing App::getLocale():
|
|
$results = app(ProductSearchService::class)->search('running shoes', 'el');
|
|
```
|
|
|
|
Returns an `Illuminate\Database\Eloquent\Collection` of `Lunar\Models\Product` — Scout's
|
|
`->get()` hydrates real models from the database after the Meilisearch query, so relations
|
|
(`variants`, `brand`, `media`, etc.) are available on the results as normal.
|
|
|
|
`$locale` defaults to `App::getLocale()` — already set correctly on every storefront request by
|
|
`Modules\Core\Localization\LocaleMiddleware` (see `localization.md`), so callers in controllers
|
|
don't need to pass it explicitly.
|
|
|
|
---
|
|
|
|
## Missing-translation fallback
|
|
|
|
If a product was only ever given an English name, `name_el` doesn't exist on that document at
|
|
all (Lunar's indexer only writes a `{handle}_{locale}` field for locales actually present in the
|
|
attribute's stored data — see `ScoutIndexer::mapSearchableAttributes()`). Searching strictly
|
|
against `name_el` would make that product invisible to Greek-locale search, even though it's a
|
|
real catalog item.
|
|
|
|
To avoid silently hiding incompletely-translated products, `ProductSearchService` targets **both**
|
|
the resolved locale's fields **and** the default language's fields
|
|
(`Lunar\Models\Language::getDefault()->code`) — e.g. searching in `el` targets `name_el`,
|
|
`name_en`, `description_el`, `description_en` together (assuming `en` is the default language).
|
|
A product missing an `el` translation still matches via its `en` fields.
|
|
|
|
---
|
|
|
|
## Field list is dynamic, not hardcoded
|
|
|
|
The set of attribute handles searched (`name`, `description`, or whatever else) comes from
|
|
`Lunar\Facades\AttributeManifest::getSearchableAttributes(Product::morphName())` — the same
|
|
source `ScoutIndexer` itself uses to decide what gets indexed. If an admin marks a new attribute
|
|
searchable in the panel, `ProductSearchService` picks it up automatically; nothing in this class
|
|
needs to change.
|
|
|
|
---
|
|
|
|
## Re-syncing after indexer changes
|
|
|
|
Changing which attributes are searchable, or `ProductIndexer`'s filterable/sortable fields,
|
|
requires re-syncing Meilisearch's index settings and re-indexing existing documents:
|
|
|
|
```bash
|
|
php artisan lunar:meilisearch:setup
|
|
php artisan lunar:search:index "Lunar\Models\Product" --refresh
|
|
```
|
|
|
|
`ProductSearchService` itself needs no re-sync when locales change — `attributesToSearchOn` is
|
|
computed per-query from the live language list, not baked into index settings.
|