From b90a23589994115194c5d173278460c58b43a2f1 Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Wed, 15 Jan 2025 14:13:27 +0000 Subject: [PATCH 1/9] Init search --- packages/search/README.md | 42 ++++ packages/search/composer.json | 47 ++++ packages/search/config/search.php | 15 ++ .../src/Contracts/InstantSearchContract.php | 5 + .../src/Contracts/SearchManagerContract.php | 5 + .../search/src/Data/Builder/SearchQuery.php | 19 ++ packages/search/src/Data/SearchFacet.php | 18 ++ .../src/Data/SearchFacet/FacetValue.php | 18 ++ packages/search/src/Data/SearchHit.php | 16 ++ .../search/src/Data/SearchHit/Highlight.php | 15 ++ packages/search/src/Data/SearchResults.php | 27 +++ .../search/src/Engines/AbstractEngine.php | 203 ++++++++++++++++ .../search/src/Engines/DatabaseEngine.php | 49 ++++ .../search/src/Engines/MeilisearchEngine.php | 154 ++++++++++++ .../search/src/Engines/TypesenseEngine.php | 227 ++++++++++++++++++ packages/search/src/Facades/Search.php | 14 ++ packages/search/src/SearchManager.php | 61 +++++ packages/search/src/SearchServiceProvider.php | 19 ++ packages/search/src/helpers.php | 3 + 19 files changed, 957 insertions(+) create mode 100644 packages/search/README.md create mode 100644 packages/search/composer.json create mode 100644 packages/search/config/search.php create mode 100644 packages/search/src/Contracts/InstantSearchContract.php create mode 100644 packages/search/src/Contracts/SearchManagerContract.php create mode 100644 packages/search/src/Data/Builder/SearchQuery.php create mode 100644 packages/search/src/Data/SearchFacet.php create mode 100644 packages/search/src/Data/SearchFacet/FacetValue.php create mode 100644 packages/search/src/Data/SearchHit.php create mode 100644 packages/search/src/Data/SearchHit/Highlight.php create mode 100644 packages/search/src/Data/SearchResults.php create mode 100644 packages/search/src/Engines/AbstractEngine.php create mode 100644 packages/search/src/Engines/DatabaseEngine.php create mode 100644 packages/search/src/Engines/MeilisearchEngine.php create mode 100644 packages/search/src/Engines/TypesenseEngine.php create mode 100644 packages/search/src/Facades/Search.php create mode 100644 packages/search/src/SearchManager.php create mode 100644 packages/search/src/SearchServiceProvider.php create mode 100644 packages/search/src/helpers.php diff --git a/packages/search/README.md b/packages/search/README.md new file mode 100644 index 000000000..0e1c4640d --- /dev/null +++ b/packages/search/README.md @@ -0,0 +1,42 @@ + +## Lunar Search + +This packages brings E-Commerce search to Lunar. +--- + +## Requirements +- Lunar >= 1.x + +## License + +Lunar is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). + +## Installation + +### Require the composer package + +```sh +composer require lunarphp/search +``` + +## Usage + +### Basic Search + +At a basic level, you can search models using the provided facade. + +```php +use Lunar\Search\Facades\Search; + +// Search on a specific model +$results = Search::on(\Lunar\Models\Collection::class)->query('Hoodies')->get(); + +// Search on Lunar\Models\Product by default. +$results = Search::query('Hoodies')->get(); +``` + +Under the hood this will detect what Scout driver is mapped under `lunar.search.engine_map` and +then perform a search using that given driver. To increase performance the results will not be +hydrated from the database, but instead will be the raw results from the search provider. + + diff --git a/packages/search/composer.json b/packages/search/composer.json new file mode 100644 index 000000000..09409b648 --- /dev/null +++ b/packages/search/composer.json @@ -0,0 +1,47 @@ +{ + "name": "lunarphp/search", + "type": "project", + "description": "Ecommerce search for LunarPHP", + "keywords": ["lunarphp", "laravel", "ecommerce", "e-commerce", "headless", "store", "shop", "search"], + "license": "MIT", + "authors": [ + { + "name": "Lunar", + "homepage": "https://lunarphp.io/" + } + ], + "require": { + "php": "^8.2", + "lunarphp/core": "^1.0.0-beta", + "spatie/laravel-data": "^4.9.0", + "typesense/typesense-php": "^4.9", + "meilisearch/meilisearch-php": "^1.10", + "http-interop/http-factory-guzzle": "^1.2" + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Lunar\\Search\\": "src/" + } + }, + "extra": { + "lunar": { + "name": "Search" + }, + "laravel": { + "providers": [ + "Lunar\\Search\\SearchServiceProvider" + ] + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true, + "php-http/discovery": true + } + } +} diff --git a/packages/search/config/search.php b/packages/search/config/search.php new file mode 100644 index 000000000..7b1dfd4ca --- /dev/null +++ b/packages/search/config/search.php @@ -0,0 +1,15 @@ + [ + \Lunar\Models\Product::class => [ + 'brand' => [], + // 'size' => [], + // 'colour' => [ + // 'Red' => [ + // 'hex_value' => '#FF0000', + // ], + // ], + ], + ], +]; diff --git a/packages/search/src/Contracts/InstantSearchContract.php b/packages/search/src/Contracts/InstantSearchContract.php new file mode 100644 index 000000000..dea6485d8 --- /dev/null +++ b/packages/search/src/Contracts/InstantSearchContract.php @@ -0,0 +1,5 @@ + */ + public array $matches, + public ?string $snippet, + ) {} +} diff --git a/packages/search/src/Data/SearchResults.php b/packages/search/src/Data/SearchResults.php new file mode 100644 index 000000000..03190e5c6 --- /dev/null +++ b/packages/search/src/Data/SearchResults.php @@ -0,0 +1,27 @@ +queryExtenders[] = $callable; + + return $this; + } + + public function filter(array $filters): self + { + foreach ($filters as $key => $value) { + $this->addFilter($key, $value); + } + + return $this; + } + + public function addFilter($key, $value): self + { + $this->filters[$key] = $value; + + return $this; + } + + public function getFilters(): array + { + return $this->filters; + } + + public function perPage(int $perPage): self + { + $this->perPage = $perPage; + + return $this; + } + + public function getFacets(): array + { + return $this->facets; + } + + public function setFacets(array $facets): self + { + $this->facets = $facets; + + return $this; + } + + public function removeFacet(string $field, mixed $value = null): self + { + if (empty($this->facets[$field])) { + return $this; + } + + if (! $value) { + unset($this->facets[$field]); + + return $this; + } + + $this->facets[$field] = collect($this->facets[$field])->reject( + fn ($faceValue) => $faceValue == $value + )->toArray(); + + return $this; + } + + public function sort(string $sort): self + { + $this->sort = $sort; + + return $this; + } + + public function sortRaw(string $sort): self + { + $this->sortRaw = $sort; + + return $this; + } + + public function query(string $query): AbstractEngine + { + $this->query = $query; + + return $this; + } + + public function getQuery(): ?string + { + return $this->query; + } + + protected function getRawResults(\Closure $builder): LengthAwarePaginator + { + return $this->modelType::search($this->query, $builder)->paginateRaw(perPage: $this->perPage); + } + + protected function getFacetConfig(?string $field = null): ?array + { + if (! $field) { + return config('lunar.search.facets.'.$this->modelType); + } + + return config('lunar.search.facets.'.$this->modelType, [])[$field] ?? []; + } + + protected function getSearchQueries(): Collection + { + $facets = $this->getFacetConfig(); + + $queries = [ + SearchQuery::from([ + 'query' => $this->query, + 'facets' => array_keys($facets), + 'facet_filters' => $this->facets, + ]) + ]; + + foreach ($this->facets as $facetField => $facetFilterValues) { + $queries[] = SearchQuery::from([ + 'query' => $this->query, + 'facets' => [$facetField], + 'facet_filters' => collect($this->facets)->reject( + fn ($value, $field) => $field === $facetField + )->toArray() + ]); + } + + foreach ($this->queryExtenders as $extender) { + $params = call_user_func($extender, $this, $queries); + } + + return collect($queries); + } + + protected function sortByIsValid(): bool + { + $sort = $this->sort; + + if (! $sort) { + return true; + } + + $parts = explode(':', $sort); + + if (! isset($parts[1])) { + return false; + } + + if (! in_array($parts[1], ['asc', 'desc'])) { + return false; + } + + $config = $this->getFieldConfig(); + + if (empty($config)) { + return false; + } + + $field = collect($config)->first( + fn ($field) => $field['name'] == $parts[0] + ); + + return $field && ($field['sort'] ?? false); + } + + public function deleteByIds(Collection $ids): array + { + return []; + } + + abstract public function get(): mixed; + + abstract protected function getFieldConfig(): array; +} diff --git a/packages/search/src/Engines/DatabaseEngine.php b/packages/search/src/Engines/DatabaseEngine.php new file mode 100644 index 000000000..cfa7510c8 --- /dev/null +++ b/packages/search/src/Engines/DatabaseEngine.php @@ -0,0 +1,49 @@ +searchBuilder = function (Documents $documents, string $query, array $options) { + return $documents->search([ + 'q' => $query, + ...$options, + 'facet_by' => 'colour,size', + 'per_page' => 2, + // 'page' => , + ]); + }; + } + + public function get(): mixed + { + $results = get_search_builder($this->modelType, $this->query, forceQuery: true) + ->paginate(); + + $documents = collect($results->items())->map(fn ($hit) => SearchHit::from([ + 'highlights' => collect(), + 'document' => $hit->toSearchableArray(), + ])); + + return SearchResults::from([ + 'query' => $this->query, + 'total_pages' => $results->lastPage(), + 'page' => $results->currentPage(), + 'count' => $results->total(), + 'per_page' => $results->perPage(), + 'hits' => $documents, + 'facets' => collect(), + ]); + } + + protected function getFieldConfig(): array + { + return []; + } +} diff --git a/packages/search/src/Engines/MeilisearchEngine.php b/packages/search/src/Engines/MeilisearchEngine.php new file mode 100644 index 000000000..f4757c33b --- /dev/null +++ b/packages/search/src/Engines/MeilisearchEngine.php @@ -0,0 +1,154 @@ +getRawResults(function (Indexes $indexes, string $query, array $options) { + $engine = app(EngineManager::class)->engine('meilisearch'); + + $queries = $this->buildSearch( + $options, + $indexes + ); + + $response = $engine->multiSearch($queries); + + $completeResults = $response['results'][0]; + + unset($response['results'][0]); + $otherResults = $response['results']; + + $facets = collect($completeResults['facetDistribution'] ?? []); + + foreach ($otherResults as $result) { + foreach ($result['facetDistribution'] ?? [] as $field => $facet) { + $facets->put($field, $facet); + } + } + + return [ + ...$completeResults, + 'facetDistribution' => $facets + ]; + }); + + $results = $paginator->items(); + + return SearchResults::from([ + 'query' => $results['query'], + 'total_pages' => $paginator->lastPage(), + 'page' => $paginator->currentPage(), + 'count' => $paginator->total(), + 'per_page' => $paginator->perPage(), + 'hits' => collect($results['hits'])->map(fn ($hit) => SearchHit::from([ + 'highlights' => collect(), + 'document' => $hit, + ])), + 'facets' => $this->mapFacets($results), + 'links' => (clone $paginator)->setCollection( + collect($results['hits']) + )->appends([ + 'facets' => http_build_query($this->facets), + ])->links(), + ]); + } + + protected function buildSearch(array $options, Indexes $indexes): array + { + $searchQueries = $this->getSearchQueries(); + + $requests = []; + + $facets = $this->getFacetConfig(); + + foreach ($searchQueries as $searchQuery) { + $filters = collect(); + + $msQuery = new SearchQuery; + $msQuery->setIndexUid($indexes->getUid()); + $msQuery->setQuery($searchQuery->query); + $msQuery->setFacets(array_keys($facets)); + $msQuery->setHitsPerPage($options['hitsPerPage']); + $msQuery->setPage($options['page']); + + if ($this->sort) { + $msQuery->setSort([$this->sort]); + } + + foreach ($this->filters as $field => $values) { + $filter = $this->mapFilter($field, $values); + $filters->push($filter); + } + + foreach ($searchQuery->facetFilters as $field => $values) { + $filters->push($this->mapFilter($field, $values)); + } + + $msQuery->setFilter($filters->toArray()); + $requests[] = $msQuery; + } + + return $requests; + } + + public function mapFacets(array $results): Collection + { + $facets = collect($results['facetDistribution'])->map( + fn ($values, $field) => SearchFacet::from([ + 'label' => $this->getFacetConfig($field)['label'] ?? $field, + 'field' => $field, + 'values' => collect($values)->map( + fn ($count, $value) => SearchFacet\FacetValue::from([ + 'label' => $value, + 'value' => $value, + 'count' => $count, + 'active' => in_array($value, $this->facets[$field] ?? []) + ]) + )->values(), + ]) + )->values(); + + foreach ($facets as $facet) { + $facetConfig = $this->getFacetConfig($facet->field); + foreach ($facet->values as $facetValue) { + if (empty($facetConfig[$facetValue->value])) { + continue; + } + $facetValue->additional($facetConfig[$facetValue->value]); + } + } + + return $facets; + } + + protected function mapFilter(string $field, mixed $value): string + { + $values = collect($value); + + if ($values->count() > 1) { + $values = $values->map( + fn ($value) => "{$field} = \"{$value}\"" + ); + + return '('.$values->join(' OR ').')'; + } + + return $field.' = "'.$values->first().'"'; + } + + protected function getFieldConfig(): array + { + return []; + } +} diff --git a/packages/search/src/Engines/TypesenseEngine.php b/packages/search/src/Engines/TypesenseEngine.php new file mode 100644 index 000000000..8481841e2 --- /dev/null +++ b/packages/search/src/Engines/TypesenseEngine.php @@ -0,0 +1,227 @@ +getRawResults(function (Documents $documents, string $query, array $options) { + $engine = app(EngineManager::class)->engine('typesense'); + + $request = [ + 'searches' => $this->buildSearch( + $options + ) + ]; + + $response = $engine->getMultiSearch()->perform($request, [ + 'collection' => (new $this->modelType)->searchableAs(), + ]); + + $completeResults = $response['results'][0]; + + unset( $response['results'][0]); + $otherResults = $response['results']; + + $facets = collect($completeResults['facet_counts'])->mapWithKeys( + fn ($facets) => [$facets['field_name'] => $facets] + ); + + foreach ($otherResults as $result) { + foreach ($result['facet_counts'] as $facet) { + $facets->put($facet['field_name'], $facet); + } + } + + return [ + ...$completeResults, + 'facet_counts' => $facets->toArray() + ]; + }); + + + } catch (\GuzzleHttp\Exception\ConnectException|ServiceUnavailable $e) { + Log::error($e->getMessage()); + $paginator = new LengthAwarePaginator( + items: [ + 'hits' => [], + 'facet_counts' => [], + ], + total: 0, + perPage: $this->perPage, + currentPage: 1, + ); + } + + $results = $paginator->items(); + + $documents = collect($results['hits'])->map(fn ($hit) => SearchHit::from([ + 'highlights' => collect($hit['highlights'] ?? [])->map( + fn ($highlight) => SearchHit\Highlight::from([ + 'field' => $highlight['field'], + 'matches' => $highlight['matched_tokens'], + 'snippet' => $highlight['snippet'], + ]) + ), + 'document' => $hit['document'], + ])); + + $facets = collect($results['facet_counts'] ?? [])->map( + fn ($facet) => SearchFacet::from([ + 'label' => $this->getFacetConfig($facet['field_name'])['label'] ?? '', + 'field' => $facet['field_name'], + 'values' => collect($facet['counts'])->map( + fn ($value) => SearchFacet\FacetValue::from([ + 'label' => $value['value'], + 'value' => $value['value'], + 'count' => $value['count'], + ]) + ), + ]) + ); + + foreach ($facets as $facet) { + $facetConfig = $this->getFacetConfig($facet->field); + + foreach ($facet->values as $facetValue) { + $valueConfig = $facetConfig['values'][$facetValue->value] ?? null; + + if (! $valueConfig) { + continue; + } + + $facetValue->label = $valueConfig['label'] ?? $facetValue->value; + unset($valueConfig['label']); + + $facetValue->additional($valueConfig); + } + } + + $newPaginator = clone $paginator; + + return SearchResults::from([ + 'query' => $this->query, + 'total_pages' => $paginator->lastPage(), + 'page' => $paginator->currentPage(), + 'count' => $paginator->total(), + 'per_page' => $paginator->perPage(), + 'hits' => $documents, + 'facets' => $facets, + 'links' => $newPaginator->setCollection( + collect($results['hits']) + )->appends([ + 'facets' => http_build_query($this->facets), + ])->links(), + ]); + } + + + protected function buildSearch(array $options): array + { + $searchQueries = $this->getSearchQueries(); + + $requests = []; + + $facets = $this->getFacetConfig(); + + foreach ($searchQueries as $searchQuery) { + + $filters = collect($options['filter_by']); + + foreach ($this->filters as $key => $value) { + $filters->push($key.':'.collect($value)->join(',')); + } + + $facetQuery = collect(); + + $facetConfig = collect($facets)->filter( + fn ($facet, $field) => in_array($field, $searchQuery->facets) + ); + + foreach ($facetConfig as $facetConfigValue) { + if (empty($facetConfigValue['facet_query'])) { + continue; + } + $facetQuery->push($facetConfigValue['facet_query']); + } + + $facetQuery = $facetQuery->join(','); + + foreach ($searchQuery->facetFilters as $field => $values) { + $values = collect($values)->map(function ($value) { + if ($value == 'false' || $value == 'true') { + return $value; + } + return '`'.$value.'`'; + }); + + + if ($values->count() > 1) { + $filters->push($field.':['.collect($values)->join(',').']'); + + continue; + } + + $filters->push($field.':='.collect($values)->join(',')); + } + + $queryBy = $options['query_by']; + + if (!$this->query) { + $queryBy = str_replace('embedding,', '', $queryBy); + } + + $params = [ + ...$options, + 'query_by' => $queryBy, + 'q' => $searchQuery->query, + 'facet_query' => $facetQuery, + 'prefix' => false, + 'exlude_fields' => 'embedding', + 'max_facet_values' => 50, + 'sort_by' => $this->sortRaw ?: ($this->sortByIsValid() ? $this->sort : '_text_match:desc'), + 'facet_by' => implode(',', $searchQuery->facets), + ]; + + if ($this->query) { + $params['vector_query'] = "embedding:([], k: 200)"; + } + + if ($filters->count()) { + $params['filter_by'] = $filters->join(' && '); + } + + $requests[] = $params; + } + + return $requests; + } + + public function deleteByIds(Collection $ids): array + { + $typesense = app(EngineManager::class)->engine('typesense'); + $index = (new Product)->searchableAs(); + return $typesense->getCollections()[$index]->documents->delete([ + 'filter_by' => 'id: ['.$ids->join(',').']', + ]); + } + + + protected function getFieldConfig(): array + { + return config('scout.typesense.model-settings.'.$this->modelType.'.collection-schema.fields', []); + } +} diff --git a/packages/search/src/Facades/Search.php b/packages/search/src/Facades/Search.php new file mode 100644 index 000000000..563813464 --- /dev/null +++ b/packages/search/src/Facades/Search.php @@ -0,0 +1,14 @@ +buildProvider(DatabaseEngine::class); + } + + public function createMeilisearchDriver() + { + return $this->buildProvider(MeilisearchEngine::class); + } + + public function createTypesenseDriver() + { + return $this->buildProvider(TypesenseEngine::class); + } + + public function buildProvider($provider) + { + return $this->container->make($provider); + } + + public function model(string $model): self + { + $this->model = $model; + + return $this; + } + + public function driver($driver = null): AbstractEngine + { + if ($driver) { + return parent::driver($driver); + } + + $engineMap = config('lunar.search.engine_map'); + + $engine = $engineMap[$this->model] ?? $driver; + + return parent::driver($engine); + } + + public function getDefaultDriver() + { + return config('scout.driver', 'database'); + } +} diff --git a/packages/search/src/SearchServiceProvider.php b/packages/search/src/SearchServiceProvider.php new file mode 100644 index 000000000..d1cd1504e --- /dev/null +++ b/packages/search/src/SearchServiceProvider.php @@ -0,0 +1,19 @@ +app->singleton(SearchManagerContract::class, fn ($app) => $app->make(SearchManager::class)); + } + + public function boot() + { + $this->mergeConfigFrom(__DIR__.'/../config/search.php', 'lunar.search'); + } +} diff --git a/packages/search/src/helpers.php b/packages/search/src/helpers.php new file mode 100644 index 000000000..0bfd2f7bf --- /dev/null +++ b/packages/search/src/helpers.php @@ -0,0 +1,3 @@ + Date: Wed, 15 Jan 2025 14:18:40 +0000 Subject: [PATCH 2/9] Pint --- packages/search/composer.json | 2 +- .../search/src/Data/Builder/SearchQuery.php | 2 -- packages/search/src/Data/SearchResults.php | 1 - packages/search/src/Engines/AbstractEngine.php | 11 +++++------ .../search/src/Engines/MeilisearchEngine.php | 6 +++--- .../search/src/Engines/TypesenseEngine.php | 18 ++++++++---------- 6 files changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/search/composer.json b/packages/search/composer.json index 09409b648..a792b1e49 100644 --- a/packages/search/composer.json +++ b/packages/search/composer.json @@ -12,7 +12,7 @@ ], "require": { "php": "^8.2", - "lunarphp/core": "^1.0.0-beta", + "lunarphp/core": "self.version", "spatie/laravel-data": "^4.9.0", "typesense/typesense-php": "^4.9", "meilisearch/meilisearch-php": "^1.10", diff --git a/packages/search/src/Data/Builder/SearchQuery.php b/packages/search/src/Data/Builder/SearchQuery.php index 5985932b8..f7a0a70d8 100644 --- a/packages/search/src/Data/Builder/SearchQuery.php +++ b/packages/search/src/Data/Builder/SearchQuery.php @@ -2,8 +2,6 @@ namespace Lunar\Search\Data\Builder; -use Lunar\Search\Data\SearchFacet\FacetValue; -use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\MapName; use Spatie\LaravelData\Data; use Spatie\LaravelData\Mappers\SnakeCaseMapper; diff --git a/packages/search/src/Data/SearchResults.php b/packages/search/src/Data/SearchResults.php index 03190e5c6..0ffb08958 100644 --- a/packages/search/src/Data/SearchResults.php +++ b/packages/search/src/Data/SearchResults.php @@ -2,7 +2,6 @@ namespace Lunar\Search\Data; -use Illuminate\Contracts\Pagination\Paginator; use Illuminate\View\View; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\MapName; diff --git a/packages/search/src/Engines/AbstractEngine.php b/packages/search/src/Engines/AbstractEngine.php index 54005a1a7..2e4013ca0 100644 --- a/packages/search/src/Engines/AbstractEngine.php +++ b/packages/search/src/Engines/AbstractEngine.php @@ -25,7 +25,6 @@ abstract class AbstractEngine protected string $sortRaw = ''; - public function extendQuery(\Closure $callable): self { $this->queryExtenders[] = $callable; @@ -141,16 +140,16 @@ protected function getSearchQueries(): Collection 'query' => $this->query, 'facets' => array_keys($facets), 'facet_filters' => $this->facets, - ]) + ]), ]; foreach ($this->facets as $facetField => $facetFilterValues) { $queries[] = SearchQuery::from([ - 'query' => $this->query, - 'facets' => [$facetField], + 'query' => $this->query, + 'facets' => [$facetField], 'facet_filters' => collect($this->facets)->reject( fn ($value, $field) => $field === $facetField - )->toArray() + )->toArray(), ]); } @@ -196,7 +195,7 @@ public function deleteByIds(Collection $ids): array { return []; } - + abstract public function get(): mixed; abstract protected function getFieldConfig(): array; diff --git a/packages/search/src/Engines/MeilisearchEngine.php b/packages/search/src/Engines/MeilisearchEngine.php index f4757c33b..4b8fe6097 100644 --- a/packages/search/src/Engines/MeilisearchEngine.php +++ b/packages/search/src/Engines/MeilisearchEngine.php @@ -27,7 +27,7 @@ public function get(): SearchResults $completeResults = $response['results'][0]; unset($response['results'][0]); - $otherResults = $response['results']; + $otherResults = $response['results']; $facets = collect($completeResults['facetDistribution'] ?? []); @@ -39,7 +39,7 @@ public function get(): SearchResults return [ ...$completeResults, - 'facetDistribution' => $facets + 'facetDistribution' => $facets, ]; }); @@ -113,7 +113,7 @@ public function mapFacets(array $results): Collection 'label' => $value, 'value' => $value, 'count' => $count, - 'active' => in_array($value, $this->facets[$field] ?? []) + 'active' => in_array($value, $this->facets[$field] ?? []), ]) )->values(), ]) diff --git a/packages/search/src/Engines/TypesenseEngine.php b/packages/search/src/Engines/TypesenseEngine.php index 8481841e2..ccb04e466 100644 --- a/packages/search/src/Engines/TypesenseEngine.php +++ b/packages/search/src/Engines/TypesenseEngine.php @@ -24,7 +24,7 @@ public function get(): SearchResults $request = [ 'searches' => $this->buildSearch( $options - ) + ), ]; $response = $engine->getMultiSearch()->perform($request, [ @@ -33,8 +33,8 @@ public function get(): SearchResults $completeResults = $response['results'][0]; - unset( $response['results'][0]); - $otherResults = $response['results']; + unset($response['results'][0]); + $otherResults = $response['results']; $facets = collect($completeResults['facet_counts'])->mapWithKeys( fn ($facets) => [$facets['field_name'] => $facets] @@ -48,11 +48,10 @@ public function get(): SearchResults return [ ...$completeResults, - 'facet_counts' => $facets->toArray() + 'facet_counts' => $facets->toArray(), ]; }); - } catch (\GuzzleHttp\Exception\ConnectException|ServiceUnavailable $e) { Log::error($e->getMessage()); $paginator = new LengthAwarePaginator( @@ -128,7 +127,6 @@ public function get(): SearchResults ]); } - protected function buildSearch(array $options): array { $searchQueries = $this->getSearchQueries(); @@ -165,10 +163,10 @@ protected function buildSearch(array $options): array if ($value == 'false' || $value == 'true') { return $value; } + return '`'.$value.'`'; }); - if ($values->count() > 1) { $filters->push($field.':['.collect($values)->join(',').']'); @@ -180,7 +178,7 @@ protected function buildSearch(array $options): array $queryBy = $options['query_by']; - if (!$this->query) { + if (! $this->query) { $queryBy = str_replace('embedding,', '', $queryBy); } @@ -197,7 +195,7 @@ protected function buildSearch(array $options): array ]; if ($this->query) { - $params['vector_query'] = "embedding:([], k: 200)"; + $params['vector_query'] = 'embedding:([], k: 200)'; } if ($filters->count()) { @@ -214,12 +212,12 @@ public function deleteByIds(Collection $ids): array { $typesense = app(EngineManager::class)->engine('typesense'); $index = (new Product)->searchableAs(); + return $typesense->getCollections()[$index]->documents->delete([ 'filter_by' => 'id: ['.$ids->join(',').']', ]); } - protected function getFieldConfig(): array { return config('scout.typesense.model-settings.'.$this->modelType.'.collection-schema.fields', []); From aeeff06415345a6c2d98c06b140912cba8052323 Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Wed, 15 Jan 2025 14:23:43 +0000 Subject: [PATCH 3/9] PHPStan fix --- packages/search/src/Engines/DatabaseEngine.php | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/search/src/Engines/DatabaseEngine.php b/packages/search/src/Engines/DatabaseEngine.php index cfa7510c8..638878faa 100644 --- a/packages/search/src/Engines/DatabaseEngine.php +++ b/packages/search/src/Engines/DatabaseEngine.php @@ -8,22 +8,9 @@ class DatabaseEngine extends AbstractEngine { - public function __construct() - { - $this->searchBuilder = function (Documents $documents, string $query, array $options) { - return $documents->search([ - 'q' => $query, - ...$options, - 'facet_by' => 'colour,size', - 'per_page' => 2, - // 'page' => , - ]); - }; - } - public function get(): mixed { - $results = get_search_builder($this->modelType, $this->query, forceQuery: true) + $results = get_search_builder($this->modelType, $this->query) ->paginate(); $documents = collect($results->items())->map(fn ($hit) => SearchHit::from([ From ec8f9b9268303d5c641052fc3bae5607c0626e1d Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Wed, 15 Jan 2025 14:47:54 +0000 Subject: [PATCH 4/9] wip --- composer.json | 31 ++++++---- packages/meilisearch/composer.json | 2 +- phpunit.xml | 3 + tests/search/TestCase.php | 60 +++++++++++++++++++ .../Unit/Engines/AbstractEngineTest.php | 15 +++++ 5 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 tests/search/TestCase.php create mode 100644 tests/search/Unit/Engines/AbstractEngineTest.php diff --git a/composer.json b/composer.json index 3df56addb..54f378ff2 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "filament/filament": "^3.2.25", "filament/spatie-laravel-media-library-plugin": "^3.2", "guzzlehttp/guzzle": "^7.3", + "http-interop/http-factory-guzzle": "^1.2", "kalnoy/nestedset": "^v6.x-dev", "laravel/framework": "^10.0|^11.0", "laravel/scout": "^10.0", @@ -28,15 +29,17 @@ "livewire/livewire": "^3.0", "lukascivil/treewalker": "0.9.1", "marvinosswald/filament-input-select-affix": "^0.2.0", - "meilisearch/meilisearch-php": "^v1.6.0", + "meilisearch/meilisearch-php": "^1.10", "php": "^8.2", "spatie/laravel-activitylog": "^4.4", "spatie/laravel-blink": "^1.7", + "spatie/laravel-data": "^4.9.0", "spatie/laravel-medialibrary": "^11.0.0", "spatie/laravel-permission": "^6.4", "spatie/php-structure-discoverer": "^2.0", "stripe/stripe-php": "^14.4", - "technikermathe/blade-lucide-icons": "^v3.0" + "technikermathe/blade-lucide-icons": "^v3.0", + "typesense/typesense-php": "^4.9" }, "require-dev": { "larastan/larastan": "^2.9", @@ -50,7 +53,8 @@ "autoload": { "files": [ "packages/admin/src/helpers.php", - "packages/core/src/helpers.php" + "packages/core/src/helpers.php", + "packages/search/src/helpers.php" ], "psr-4": { "Lunar\\": "packages/core/src", @@ -63,6 +67,7 @@ "Lunar\\Meilisearch\\": "packages/meilisearch/src/", "Lunar\\Opayo\\": "packages/opayo/src/", "Lunar\\Paypal\\": "packages/paypal/src/", + "Lunar\\Search\\": "packages/search/src/", "Lunar\\Shipping\\": "packages/table-rate-shipping/src", "Lunar\\Shipping\\Database\\Factories\\": "packages/table-rate-shipping/database/factories", "Lunar\\Stripe\\": "packages/stripe/src/" @@ -76,28 +81,31 @@ "Lunar\\Tests\\Paypal\\": "tests/paypal", "Lunar\\Tests\\Shipping\\": "tests/shipping", "Lunar\\Shipping\\Tests\\": "packages/table-rate-shipping/tests", - "Lunar\\Tests\\Stripe\\": "tests/stripe" + "Lunar\\Tests\\Stripe\\": "tests/stripe", + "Lunar\\Tests\\Search\\": "tests/search" } }, "extra": { "lunar": { "name": [ - "Meilisearch", + "Table Rate Shipping", "Opayo Payments", + "Search", + "Meilisearch", "Paypal Payments", - "Stripe Payments", - "Table Rate Shipping" + "Stripe Payments" ] }, "laravel": { "providers": [ - "Lunar\\Shipping\\ShippingServiceProvider", "Lunar\\Stripe\\StripePaymentsServiceProvider", "Lunar\\Paypal\\PaypalServiceProvider", - "Lunar\\Opayo\\OpayoServiceProvider", "Lunar\\Meilisearch\\MeilisearchServiceProvider", - "Lunar\\LunarServiceProvider", - "Lunar\\Admin\\LunarPanelProvider" + "Lunar\\Search\\SearchServiceProvider", + "Lunar\\Admin\\LunarPanelProvider", + "Lunar\\Opayo\\OpayoServiceProvider", + "Lunar\\Shipping\\ShippingServiceProvider", + "Lunar\\LunarServiceProvider" ] } }, @@ -107,6 +115,7 @@ "lunarphp/meilisearch": "self.version", "lunarphp/opayo": "self.version", "lunarphp/paypal": "self.version", + "lunarphp/search": "self.version", "lunarphp/stripe": "self.version", "lunarphp/table-rate-shipping": "self.version" }, diff --git a/packages/meilisearch/composer.json b/packages/meilisearch/composer.json index 968439ba5..9b8557dfb 100644 --- a/packages/meilisearch/composer.json +++ b/packages/meilisearch/composer.json @@ -10,7 +10,7 @@ } ], "require": { - "meilisearch/meilisearch-php": "^v1.6.0" + "meilisearch/meilisearch-php": "^1.10" }, "autoload": { "psr-4": { diff --git a/phpunit.xml b/phpunit.xml index 3688e295e..17d03b347 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,6 +18,9 @@ tests/opayo/Unit tests/opayo/Feature + + tests/search/Unit + tests/stripe/Unit diff --git a/tests/search/TestCase.php b/tests/search/TestCase.php new file mode 100644 index 000000000..9fa392854 --- /dev/null +++ b/tests/search/TestCase.php @@ -0,0 +1,60 @@ +disableLogging(); + + Stripe::fake(); + } + + protected function getPackageProviders($app) + { + return [ + LunarServiceProvider::class, + BlinkServiceProvider::class, + StripePaymentsServiceProvider::class, + LivewireServiceProvider::class, + MediaLibraryServiceProvider::class, + ActivitylogServiceProvider::class, + ConverterServiceProvider::class, + NestedSetServiceProvider::class, + ]; + } + + protected function getEnvironmentSetUp($app) + { + // perform environment setup + } + + /** + * Define database migrations. + * + * @return void + */ + protected function defineDatabaseMigrations() + { + $this->loadLaravelMigrations(); + } +} diff --git a/tests/search/Unit/Engines/AbstractEngineTest.php b/tests/search/Unit/Engines/AbstractEngineTest.php new file mode 100644 index 000000000..e487aeadd --- /dev/null +++ b/tests/search/Unit/Engines/AbstractEngineTest.php @@ -0,0 +1,15 @@ +group('search'); + +it('can capture an order', function () { + +}); From 4b617f842191224ac15415da84ece45ab88de96d Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Thu, 16 Jan 2025 08:53:51 +0000 Subject: [PATCH 5/9] tests --- .../search/src/Engines/AbstractEngine.php | 9 +- phpunit.xml | 3 + tests/search/TestCase.php | 9 +- .../Unit/Engines/AbstractEngineTest.php | 86 +++++++++++++++++-- 4 files changed, 91 insertions(+), 16 deletions(-) diff --git a/packages/search/src/Engines/AbstractEngine.php b/packages/search/src/Engines/AbstractEngine.php index 2e4013ca0..08a32d97d 100644 --- a/packages/search/src/Engines/AbstractEngine.php +++ b/packages/search/src/Engines/AbstractEngine.php @@ -98,6 +98,11 @@ public function sort(string $sort): self return $this; } + public function getSort(): ?string + { + return $this->sort; + } + public function sortRaw(string $sort): self { $this->sortRaw = $sort; @@ -125,13 +130,13 @@ protected function getRawResults(\Closure $builder): LengthAwarePaginator protected function getFacetConfig(?string $field = null): ?array { if (! $field) { - return config('lunar.search.facets.'.$this->modelType); + return config('lunar.search.facets.'.$this->modelType, []); } return config('lunar.search.facets.'.$this->modelType, [])[$field] ?? []; } - protected function getSearchQueries(): Collection + public function getSearchQueries(): Collection { $facets = $this->getFacetConfig(); diff --git a/phpunit.xml b/phpunit.xml index a7f806c25..202639ff1 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -14,6 +14,9 @@ tests/core/Feature tests/core/Unit + + tests/search/Unit + tests/stripe/Unit diff --git a/tests/search/TestCase.php b/tests/search/TestCase.php index 9fa392854..03010a903 100644 --- a/tests/search/TestCase.php +++ b/tests/search/TestCase.php @@ -12,6 +12,7 @@ use Lunar\Tests\Stubs\User; use Spatie\Activitylog\ActivitylogServiceProvider; use Spatie\LaravelBlink\BlinkServiceProvider; +use Spatie\LaravelData\LaravelDataServiceProvider; use Spatie\MediaLibrary\MediaLibraryServiceProvider; class TestCase extends \Orchestra\Testbench\TestCase @@ -33,13 +34,9 @@ protected function getPackageProviders($app) { return [ LunarServiceProvider::class, - BlinkServiceProvider::class, - StripePaymentsServiceProvider::class, - LivewireServiceProvider::class, - MediaLibraryServiceProvider::class, - ActivitylogServiceProvider::class, ConverterServiceProvider::class, - NestedSetServiceProvider::class, + ActivitylogServiceProvider::class, + LaravelDataServiceProvider::class ]; } diff --git a/tests/search/Unit/Engines/AbstractEngineTest.php b/tests/search/Unit/Engines/AbstractEngineTest.php index e487aeadd..4cdb5fa5e 100644 --- a/tests/search/Unit/Engines/AbstractEngineTest.php +++ b/tests/search/Unit/Engines/AbstractEngineTest.php @@ -1,15 +1,85 @@ group('search'); -it('can capture an order', function () { +it('can set engine properties', function () { + $engine = new class extends AbstractEngine + { + public function get(): mixed + { + return null; + } + + protected function getFieldConfig(): array + { + return []; + } + }; + + $engine->filter(['foo' => 'bar']); + + expect($engine->getFilters()) + ->toHaveKey('foo') + ->and(expect($engine->getFilters()['foo'])) + ->toBe('bar'); + + $engine->addFilter('brand', 'Nike'); + + expect($engine->getFilters()) + ->toHaveKey('brand') + ->and(expect($engine->getFilters()['brand'])) + ->toBe('Nike'); + + $engine->setFacets([ + 'colour' => ['red', 'blue'], + ]); + + expect($engine->getFacets()) + ->toHaveKey('colour') + ->and(expect($engine->getFacets()['colour'])) + ->toBe(['red', 'blue']); + + $engine->sort('price:asc'); + + expect($engine->getSort()) + ->toBe('price:asc'); + + $engine->query('Potato'); + + expect($engine->getQuery()) + ->toBe('Potato'); +}); + +it('can set up multiple search queries correctly', function () { + $engine = new class extends AbstractEngine + { + public function get(): mixed + { + return null; + } + + protected function getFieldConfig(): array + { + return []; + } + }; + + $facets = [ + 'colour' => ['red', 'blue'], + 'size' => ['small', 'large'], + ]; + + $engine->setFacets($facets); + + $queries = $engine->getSearchQueries(); + expect($queries)->toHaveCount(3) + ->and($queries[0]->facetFilters)->toHaveCount(2) + ->and($queries[0]->facetFilters)->toBe($facets) + ->and($queries[1]->facetFilters)->toHaveCount(1) + ->and($queries[1]->facetFilters)->toBe(['size' => ['small', 'large']]) + ->and($queries[2]->facetFilters)->toHaveCount(1) + ->and($queries[2]->facetFilters)->toBe(['colour' => ['red', 'blue']]); }); From cc74677d55332ecb206027ca9c84bc7db8850a0f Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Thu, 16 Jan 2025 12:36:18 +0000 Subject: [PATCH 6/9] Tests and fixes --- .../search/src/Engines/AbstractEngine.php | 2 +- .../search/src/Engines/MeilisearchEngine.php | 2 +- packages/search/src/Facades/Search.php | 14 +++ phpunit.xml | 1 + .../Feature/Engines/MeilisearchEngineTest.php | 85 +++++++++++++++++++ tests/search/TestCase.php | 11 ++- 6 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 tests/search/Feature/Engines/MeilisearchEngineTest.php diff --git a/packages/search/src/Engines/AbstractEngine.php b/packages/search/src/Engines/AbstractEngine.php index 08a32d97d..79e8dae22 100644 --- a/packages/search/src/Engines/AbstractEngine.php +++ b/packages/search/src/Engines/AbstractEngine.php @@ -13,7 +13,7 @@ abstract class AbstractEngine protected array $queryExtenders = []; - protected ?string $query = null; + protected string $query = ''; protected array $filters = []; diff --git a/packages/search/src/Engines/MeilisearchEngine.php b/packages/search/src/Engines/MeilisearchEngine.php index 4b8fe6097..5a71b024c 100644 --- a/packages/search/src/Engines/MeilisearchEngine.php +++ b/packages/search/src/Engines/MeilisearchEngine.php @@ -104,7 +104,7 @@ protected function buildSearch(array $options, Indexes $indexes): array public function mapFacets(array $results): Collection { - $facets = collect($results['facetDistribution'])->map( + $facets = collect($results['facetDistribution'] ?? [])->map( fn ($values, $field) => SearchFacet::from([ 'label' => $this->getFacetConfig($field)['label'] ?? $field, 'field' => $field, diff --git a/packages/search/src/Facades/Search.php b/packages/search/src/Facades/Search.php index 563813464..5961e6d46 100644 --- a/packages/search/src/Facades/Search.php +++ b/packages/search/src/Facades/Search.php @@ -4,7 +4,21 @@ use Illuminate\Support\Facades\Facade; use Lunar\Search\Contracts\SearchManagerContract; +use Lunar\Search\Engines\AbstractEngine; +use Lunar\Search\Engines\DatabaseEngine; +use Lunar\Search\Engines\MeilisearchEngine; +use Lunar\Search\Engines\TypesenseEngine; +use Lunar\Search\SearchManager; +/** + * @method static DatabaseEngine createDatabaseDriver() + * @method static MeilisearchEngine createMeilisearchDriver() + * @method static TypesenseEngine createTypesenseDriver() + * @method static mixed buildProvider() + * @method static SearchManager model() + * @method static AbstractEngine driver() + * @method static string getDefaultDriver() + */ class Search extends Facade { protected static function getFacadeAccessor(): string diff --git a/phpunit.xml b/phpunit.xml index 202639ff1..cde3d17e9 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -15,6 +15,7 @@ tests/core/Unit + tests/search/Feature tests/search/Unit diff --git a/tests/search/Feature/Engines/MeilisearchEngineTest.php b/tests/search/Feature/Engines/MeilisearchEngineTest.php new file mode 100644 index 000000000..b79e0701e --- /dev/null +++ b/tests/search/Feature/Engines/MeilisearchEngineTest.php @@ -0,0 +1,85 @@ +group('search'); + +function mockWithResponse(array $response) +{ + $engine = \Pest\Laravel\partialMock(Lunar\Search\Engines\MeilisearchEngine::class, function (\Mockery\MockInterface $mock) use ($response) { + $mock->shouldAllowMockingProtectedMethods() + ->shouldReceive('getRawResults') + ->andReturn( + new \Illuminate\Pagination\LengthAwarePaginator( + items: $response, + total: 100, + perPage: 50, + currentPage: 1 + ) + ); + $mock->shouldReceive('setFacets')->andReturnSelf(); + $mock->shouldReceive('perPage')->andReturnSelf(); + $mock->shouldReceive('sort')->andReturnSelf(); + $mock->shouldReceive('extendQuery')->andReturnSelf(); + }); + + \Lunar\Search\Facades\Search::extend('meilisearch', fn () => $engine); +} + +beforeEach(function () { + \Illuminate\Support\Facades\Config::set('scout.driver', 'meilisearch'); + \Illuminate\Support\Facades\Config::set('lunar.search.engine_map.Lunar\Models\Product', 'meilisearch'); +}); + +it('can fetch empty results', function () { + mockWithResponse([ + 'hits' => [], + 'offset' => 0, + 'limit' => 0, + 'estimatedTotalHits' => 0, + 'processingTimeMs' => 0, + 'query' => '', + ]); + + $results = \Lunar\Search\Facades\Search::model(\Lunar\Models\Product::class)->get(); + + expect($results)->toBeInstanceOf(\Lunar\Search\Data\SearchResults::class); +}); + +it('can search complete results', function () { + mockWithResponse([ + 'hits' => [ + [ + 'id' => '123', + 'name' => 'Foo Bar', + ] + ], + 'facetDistribution' => [ + 'brand' => [ + 'Nike' => 100, + 'Adidas' => 100, + 'Puma' => 100, + ], + 'size' => [ + '10' => 100, + '12' => 50, + ], + ], + 'offset' => 0, + 'limit' => 0, + 'estimatedTotalHits' => 0, + 'processingTimeMs' => 0, + 'query' => '', + ]); + + $results = \Lunar\Search\Facades\Search::model(\Lunar\Models\Product::class)->get(); + + expect($results->hits) + ->toHaveCount(1) + ->and($results->facets) + ->toHaveCount(2) + ->and($results->facets[0]->label) + ->toBe('brand') + ->and($results->facets[0]->values) + ->toHaveCount(3) + ->and($results->facets[0]->values[0]->label) + ->toBe('Nike'); +}); diff --git a/tests/search/TestCase.php b/tests/search/TestCase.php index 03010a903..286254606 100644 --- a/tests/search/TestCase.php +++ b/tests/search/TestCase.php @@ -4,16 +4,13 @@ use Cartalyst\Converter\Laravel\ConverterServiceProvider; use Illuminate\Support\Facades\Config; -use Kalnoy\Nestedset\NestedSetServiceProvider; -use Livewire\LivewireServiceProvider; +use Laravel\Scout\ScoutServiceProvider; use Lunar\LunarServiceProvider; +use Lunar\Search\SearchServiceProvider; use Lunar\Stripe\Facades\Stripe; -use Lunar\Stripe\StripePaymentsServiceProvider; use Lunar\Tests\Stubs\User; use Spatie\Activitylog\ActivitylogServiceProvider; -use Spatie\LaravelBlink\BlinkServiceProvider; use Spatie\LaravelData\LaravelDataServiceProvider; -use Spatie\MediaLibrary\MediaLibraryServiceProvider; class TestCase extends \Orchestra\Testbench\TestCase { @@ -36,7 +33,9 @@ protected function getPackageProviders($app) LunarServiceProvider::class, ConverterServiceProvider::class, ActivitylogServiceProvider::class, - LaravelDataServiceProvider::class + LaravelDataServiceProvider::class, + SearchServiceProvider::class, + ScoutServiceProvider::class, ]; } From d50ec5fb57ba03e578d958994d7ca9664ec5d873 Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Thu, 16 Jan 2025 12:37:21 +0000 Subject: [PATCH 7/9] Add actions --- .github/workflows/split_packages.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/split_packages.yml b/.github/workflows/split_packages.yml index 7b86f845b..4bd58029d 100644 --- a/.github/workflows/split_packages.yml +++ b/.github/workflows/split_packages.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - package: ["admin", "core", "opayo", "paypal", "table-rate-shipping", "stripe", "meilisearch"] + package: ["admin", "core", "opayo", "paypal", "table-rate-shipping", "stripe", "meilisearch", "search"] steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5d352f866..1f3b67202 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: php: [8.3, 8.2] laravel: [11.*, 10.*] dependency-version: [prefer-stable] - testsuite: [core, admin, shipping, stripe] + testsuite: [core, admin, shipping, stripe, search] include: - laravel: 11.* testbench: 9.* From 9d108c8584d184211f5dc4c954622ed8ef311ec2 Mon Sep 17 00:00:00 2001 From: Author Date: Thu, 16 Jan 2025 12:40:27 +0000 Subject: [PATCH 8/9] chore: fix code style --- packages/search/src/Engines/DatabaseEngine.php | 1 - tests/search/Feature/Engines/MeilisearchEngineTest.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/search/src/Engines/DatabaseEngine.php b/packages/search/src/Engines/DatabaseEngine.php index 638878faa..012fdd96c 100644 --- a/packages/search/src/Engines/DatabaseEngine.php +++ b/packages/search/src/Engines/DatabaseEngine.php @@ -4,7 +4,6 @@ use Lunar\Search\Data\SearchHit; use Lunar\Search\Data\SearchResults; -use Typesense\Documents; class DatabaseEngine extends AbstractEngine { diff --git a/tests/search/Feature/Engines/MeilisearchEngineTest.php b/tests/search/Feature/Engines/MeilisearchEngineTest.php index b79e0701e..84b15de09 100644 --- a/tests/search/Feature/Engines/MeilisearchEngineTest.php +++ b/tests/search/Feature/Engines/MeilisearchEngineTest.php @@ -50,7 +50,7 @@ function mockWithResponse(array $response) [ 'id' => '123', 'name' => 'Foo Bar', - ] + ], ], 'facetDistribution' => [ 'brand' => [ From 52ec3f803246a5b4441978634843dab66bd46927 Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Thu, 16 Jan 2025 12:52:03 +0000 Subject: [PATCH 9/9] Update README --- packages/search/README.md | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/search/README.md b/packages/search/README.md index 0e1c4640d..ac55c5493 100644 --- a/packages/search/README.md +++ b/packages/search/README.md @@ -21,6 +21,29 @@ composer require lunarphp/search ## Usage +### Configuration + +Most configuration is done via `config/lunar/search.php`. Here you can specify which facets should be used and how they are displayed. + +```php +'facets' => [ + \Lunar\Models\Product::class => [ + 'brand' => [ + 'label' => 'Brand', + ], + 'colour' => [ + 'label' => 'Colour', + ], + 'size' => [ + 'label' => 'Size', + ], + 'shoe-size' => [ + 'label' => 'Shoe Size', + ] + ] +], +``` + ### Basic Search At a basic level, you can search models using the provided facade. @@ -40,3 +63,37 @@ then perform a search using that given driver. To increase performance the resul hydrated from the database, but instead will be the raw results from the search provider. +### Handling the response + +Searching returns a `Lunar\Data\SearchResult` DTO which you can use in your templates: + +```php +use Lunar\Search\Facades\Search; +$results = Search::query('Hoodies')->get(); +``` + +```bladehtml + +@foreach($results->hits as $hit) + {{ $hit->document['name'] }} +@endforeach + + +@foreach($results->facets as $facet) + + {{ $facet->label }} + @foreach($facet->values as $facetValue) + + $facetValue->active, + ]) + > + {{ $facetValue->label }} + + {{ $facetValue->count }} + @endforeach + +@endforeach +``` +