diff --git a/.env.example b/.env.example index 67718a4..9dba2da 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,12 @@ QUEUE_CONNECTION=database CACHE_STORE=database # CACHE_PREFIX= +SCOUT_DRIVER=meilisearch +SCOUT_QUEUE=false +MEILISEARCH_HOST=http://meilisearch:7700 +MEILISEARCH_KEY=masterKey +MEILISEARCH_PORT=7700 + MEMCACHED_HOST=127.0.0.1 REDIS_CLIENT=phpredis diff --git a/app/Http/Controllers/HuntsController.php b/app/Http/Controllers/HuntsController.php index 0023017..7202570 100644 --- a/app/Http/Controllers/HuntsController.php +++ b/app/Http/Controllers/HuntsController.php @@ -2,8 +2,13 @@ namespace App\Http\Controllers; +use App\Http\Requests\StoreHuntRequest; +use App\Http\Requests\UpdateHuntRequest; use App\Models\Hunts; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Log; class HuntsController extends Controller @@ -11,75 +16,31 @@ class HuntsController extends Controller /** * Display a listing of the resource. */ - public function index(Request $request) + public function index(Request $request): JsonResponse { try { - $query = Hunts::with('participants:id,avatar_uri'); + $searchTerm = $this->resolveSearchTerm($request); - if ($request->has('participating')) { - $participating = filter_var( - $request->query('participating'), - FILTER_VALIDATE_BOOLEAN, - FILTER_NULL_ON_FAILURE - ); + if ($searchTerm !== null) { + $hunts = Hunts::search($searchTerm) + ->query(fn (Builder $query) => $this->applyHuntFilters( + $query->with('participants:id,avatar_uri'), + $request + )) + ->paginate(10); - $userId = auth('sanctum')->id(); - - Log::info('Participating filter resolved', [ - 'participating' => $participating, - 'user_id' => $userId, - ]); - - if (! $userId) { - // Non connecté → aucune hunt "participating" - if ($participating === true) { - $query->whereRaw('1 = 0'); - } - } else { - if ($participating === true) { - // Hunts où l'utilisateur participe - $query->whereHas('participants', function ($q) use ($userId) { - $q->where('user_id', $userId); - }); - } else { - // Hunts où l'utilisateur NE participe PAS - $query->whereDoesntHave('participants', function ($q) use ($userId) { - $q->where('user_id', $userId); - }); - } - } + return response()->json($hunts); } - // Filtre active/inactive uniquement si le param est présent - if ($request->has('active')) { - $now = \Illuminate\Support\Carbon::now(); - $active = filter_var($request->query('active'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); - - if ($active === true) { - // Actives: start_at <= now AND (end_at is null OR end_at >= now) - $query->where('start_at', '<=', $now) - ->where(function ($q) use ($now) { - $q->whereNull('end_at') - ->orWhere('end_at', '>=', $now); - }); - } else { - // Inactives: NOT(active) - $query->where(function ($q) use ($now) { - $q->whereNull('start_at') - ->orWhere('start_at', '>', $now) - ->orWhere(function ($q2) use ($now) { - $q2->whereNotNull('end_at') - ->where('end_at', '<', $now); - }); - }); - } - } + $query = $this->applyHuntFilters( + Hunts::with('participants:id,avatar_uri'), + $request + ); $hunts = $query->paginate(10); return response()->json($hunts); - - } catch (\Exception $e) { + } catch (\Throwable $e) { Log::error('Error fetching hunts: '.$e->getMessage()); return response()->json([ @@ -89,10 +50,81 @@ class HuntsController extends Controller } } + private function applyHuntFilters(Builder $query, Request $request): Builder + { + if ($request->has('participating')) { + $participating = filter_var( + $request->query('participating'), + FILTER_VALIDATE_BOOLEAN, + FILTER_NULL_ON_FAILURE + ); + + $userId = auth('sanctum')->id(); + + Log::info('Participating filter resolved', [ + 'participating' => $participating, + 'user_id' => $userId, + ]); + + if (! $userId) { + if ($participating === true) { + $query->whereRaw('1 = 0'); + } + } else { + if ($participating === true) { + $query->whereHas('participants', function ($q) use ($userId) { + $q->where('user_id', $userId); + }); + } else { + $query->whereDoesntHave('participants', function ($q) use ($userId) { + $q->where('user_id', $userId); + }); + } + } + } + + if ($request->has('active')) { + $now = Carbon::now(); + $active = filter_var($request->query('active'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + + if ($active === true) { + $query->where('start_at', '<=', $now) + ->where(function ($q) use ($now) { + $q->whereNull('end_at') + ->orWhere('end_at', '>=', $now); + }); + } else { + $query->where(function ($q) use ($now) { + $q->whereNull('start_at') + ->orWhere('start_at', '>', $now) + ->orWhere(function ($q2) use ($now) { + $q2->whereNotNull('end_at') + ->where('end_at', '<', $now); + }); + }); + } + } + + return $query; + } + + private function resolveSearchTerm(Request $request): ?string + { + $searchTerm = $request->query('search') ?? $request->query('q'); + + if (! is_string($searchTerm)) { + return null; + } + + $searchTerm = trim($searchTerm); + + return $searchTerm !== '' ? $searchTerm : null; + } + /** * Store a newly created resource in storage. */ - public function store(\App\Http\Requests\StoreHuntRequest $request) + public function store(StoreHuntRequest $request): JsonResponse { $validated = $request->validated(); @@ -109,7 +141,7 @@ class HuntsController extends Controller /** * Display the specified resource. */ - public function show(Request $request, $id) + public function show(Request $request, int $id): JsonResponse { $hunt = Hunts::with(['steps', 'participants:id,avatar_uri'])->find($id); @@ -160,7 +192,7 @@ class HuntsController extends Controller return response()->json($hunt); } - public function participate(Request $request, $id) + public function participate(Request $request, int $id): JsonResponse { $hunt = Hunts::findOrFail($id); $user = $request->user(); @@ -174,7 +206,7 @@ class HuntsController extends Controller return response()->json(['message' => 'Participation started'], 201); } - public function unparticipate(Request $request, $id) + public function unparticipate(Request $request, int $id): JsonResponse { $hunt = Hunts::findOrFail($id); $user = $request->user(); @@ -191,7 +223,7 @@ class HuntsController extends Controller /** * Update the specified resource in storage. */ - public function update(\App\Http\Requests\UpdateHuntRequest $request, $id) + public function update(UpdateHuntRequest $request, int $id): JsonResponse { $hunt = Hunts::find($id); @@ -209,7 +241,7 @@ class HuntsController extends Controller /** * Remove the specified resource from storage. */ - public function destroy($id) + public function destroy(int $id): JsonResponse { $hunt = Hunts::find($id); diff --git a/app/Models/Hunts.php b/app/Models/Hunts.php index 3daa6e8..ac130b6 100644 --- a/app/Models/Hunts.php +++ b/app/Models/Hunts.php @@ -6,10 +6,14 @@ use App\Enums\HuntDifficulty; use App\Enums\HuntStatus; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Laravel\Scout\Searchable; class Hunts extends Model { use HasFactory; + use Searchable; protected $table = 'hunts'; @@ -42,15 +46,48 @@ class Hunts extends Model 'difficulty' => HuntDifficulty::class, ]; - public function steps() + public function steps(): HasMany { return $this->hasMany(HuntSteps::class, 'hunt_id'); } - public function participants() + public function participants(): BelongsToMany { return $this->belongsToMany(User::class, 'hunt_participants', 'hunt_id', 'user_id') ->withPivot(['current_step_number', 'status']) ->withTimestamps(); } + + /** + * @return array{ + * id: int, + * title: string, + * slug: string, + * description: string|null, + * city: string, + * status: string, + * difficulty: string, + * is_public: bool, + * start_at: string|null, + * end_at: string|null + * } + */ + public function toSearchableArray(): array + { + $status = $this->status instanceof HuntStatus ? $this->status->value : $this->status; + $difficulty = $this->difficulty instanceof HuntDifficulty ? $this->difficulty->value : $this->difficulty; + + return [ + 'id' => $this->id, + 'title' => $this->title, + 'slug' => $this->slug, + 'description' => $this->description, + 'city' => $this->city, + 'status' => $status, + 'difficulty' => $difficulty, + 'is_public' => $this->is_public, + 'start_at' => $this->start_at?->toAtomString(), + 'end_at' => $this->end_at?->toAtomString(), + ]; + } } diff --git a/compose.yaml b/compose.yaml index df63b31..a001bd7 100644 --- a/compose.yaml +++ b/compose.yaml @@ -23,6 +23,7 @@ services: - sail depends_on: - mysql + - meilisearch mysql: image: "mysql/mysql-server:8.0" ports: @@ -59,6 +60,17 @@ services: - sail depends_on: - mysql + meilisearch: + image: "getmeili/meilisearch:v1.7" + ports: + - "${MEILISEARCH_PORT:-7700}:7700" + environment: + MEILI_MASTER_KEY: "${MEILISEARCH_KEY:-masterKey}" + MEILI_ENV: "development" + volumes: + - "sail-meilisearch:/meili_data" + networks: + - sail networks: sail: @@ -66,3 +78,5 @@ networks: volumes: sail-mysql: driver: local + sail-meilisearch: + driver: local diff --git a/composer.json b/composer.json index c253759..7dc9b18 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,12 @@ "dedoc/scramble": "^0.13.5", "fakerphp/faker": "^1.23", "filament/filament": "^4.0", + "http-interop/http-factory-guzzle": "*", "laravel/framework": "^12.0", "laravel/sanctum": "^4.0", - "laravel/tinker": "^2.10.1" + "laravel/scout": "*", + "laravel/tinker": "^2.10.1", + "meilisearch/meilisearch-php": "*" }, "require-dev": { "laravel/boost": "^1.8", diff --git a/composer.lock b/composer.lock index 0f89724..6cda413 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "047de0487d6a961282c968d1142d9844", + "content-hash": "1b380d582f2a52b93929fa0d443b0384", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -2162,6 +2162,64 @@ ], "time": "2025-08-22T14:27:06+00:00" }, + { + "name": "http-interop/http-factory-guzzle", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/http-interop/http-factory-guzzle.git", + "reference": "c2c859ceb05c3f42e710b60555f4c35b6a4a3995" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/c2c859ceb05c3f42e710b60555f4c35b6a4a3995", + "reference": "c2c859ceb05c3f42e710b60555f4c35b6a4a3995", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.7||^2.0", + "php": ">=7.3", + "psr/http-factory": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^9.5" + }, + "suggest": { + "guzzlehttp/psr7": "Includes an HTTP factory starting in version 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Factory\\Guzzle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "An HTTP Factory using Guzzle PSR7", + "keywords": [ + "factory", + "http", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/http-interop/http-factory-guzzle/issues", + "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.1" + }, + "time": "2025-12-15T11:28:16+00:00" + }, { "name": "kirschbaum-development/eloquent-power-joins", "version": "4.2.11", @@ -2567,6 +2625,86 @@ }, "time": "2025-07-09T19:45:24+00:00" }, + { + "name": "laravel/scout", + "version": "v10.23.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/scout.git", + "reference": "fb6d94cfc5708e4202dc00d46e61af0b9f35b03c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/scout/zipball/fb6d94cfc5708e4202dc00d46e61af0b9f35b03c", + "reference": "fb6d94cfc5708e4202dc00d46e61af0b9f35b03c", + "shasum": "" + }, + "require": { + "illuminate/bus": "^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0", + "illuminate/database": "^9.0|^10.0|^11.0|^12.0", + "illuminate/http": "^9.0|^10.0|^11.0|^12.0", + "illuminate/pagination": "^9.0|^10.0|^11.0|^12.0", + "illuminate/queue": "^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^9.0|^10.0|^11.0|^12.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0" + }, + "conflict": { + "algolia/algoliasearch-client-php": "<3.2.0|>=5.0.0" + }, + "require-dev": { + "algolia/algoliasearch-client-php": "^3.2|^4.0", + "meilisearch/meilisearch-php": "^1.0", + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.31|^8.36|^9.15|^10.8", + "php-http/guzzle7-adapter": "^1.0", + "phpstan/phpstan": "^1.10", + "typesense/typesense-php": "^4.9.3" + }, + "suggest": { + "algolia/algoliasearch-client-php": "Required to use the Algolia engine (^3.2).", + "meilisearch/meilisearch-php": "Required to use the Meilisearch engine (^1.0).", + "typesense/typesense-php": "Required to use the Typesense engine (^4.9)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Scout\\ScoutServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Scout\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Scout provides a driver based solution to searching your Eloquent models.", + "keywords": [ + "algolia", + "laravel", + "search" + ], + "support": { + "issues": "https://github.com/laravel/scout/issues", + "source": "https://github.com/laravel/scout" + }, + "time": "2025-12-16T15:43:03+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.7", @@ -3572,6 +3710,86 @@ }, "time": "2025-07-25T09:04:22+00:00" }, + { + "name": "meilisearch/meilisearch-php", + "version": "v1.16.1", + "source": { + "type": "git", + "url": "https://github.com/meilisearch/meilisearch-php.git", + "reference": "f9f63e0e7d12ffaae54f7317fa8f4f4dfa8ae7b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/meilisearch/meilisearch-php/zipball/f9f63e0e7d12ffaae54f7317fa8f4f4dfa8ae7b6", + "reference": "f9f63e0e7d12ffaae54f7317fa8f4f4dfa8ae7b6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0", + "php-http/discovery": "^1.7", + "psr/http-client": "^1.0", + "symfony/polyfill-php81": "^1.33" + }, + "require-dev": { + "http-interop/http-factory-guzzle": "^1.2.0", + "php-cs-fixer/shim": "^3.59.3", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.5 || ^10.5", + "symfony/http-client": "^5.4|^6.0|^7.0" + }, + "suggest": { + "guzzlehttp/guzzle": "Use Guzzle ^7 as HTTP client", + "http-interop/http-factory-guzzle": "Factory for guzzlehttp/guzzle", + "symfony/http-client": "Use Symfony Http client" + }, + "type": "library", + "autoload": { + "psr-4": { + "MeiliSearch\\": "src/", + "Meilisearch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Clémentine Urquizar", + "email": "clementine@meilisearch.com" + }, + { + "name": "Bruno Casali", + "email": "bruno@meilisearch.com" + }, + { + "name": "Laurent Cazanove", + "email": "lau.cazanove@gmail.com" + }, + { + "name": "Tomas Norkūnas", + "email": "norkunas.tom@gmail.com" + } + ], + "description": "PHP wrapper for the Meilisearch API", + "keywords": [ + "api", + "client", + "instant", + "meilisearch", + "php", + "search" + ], + "support": { + "issues": "https://github.com/meilisearch/meilisearch-php/issues", + "source": "https://github.com/meilisearch/meilisearch-php/tree/v1.16.1" + }, + "time": "2025-09-18T10:15:45+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", @@ -4373,6 +4591,85 @@ }, "time": "2025-09-24T15:06:41+00:00" }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.4", @@ -7221,6 +7518,86 @@ ], "time": "2025-01-02T08:10:11+00:00" }, + { + "name": "symfony/polyfill-php81", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, { "name": "symfony/polyfill-php83", "version": "v1.33.0", diff --git a/config/scout.php b/config/scout.php new file mode 100644 index 0000000..d4e2c6f --- /dev/null +++ b/config/scout.php @@ -0,0 +1,210 @@ + env('SCOUT_DRIVER', 'meilisearch'), + + /* + |-------------------------------------------------------------------------- + | Index Prefix + |-------------------------------------------------------------------------- + | + | Here you may specify a prefix that will be applied to all search index + | names used by Scout. This prefix may be useful if you have multiple + | "tenants" or applications sharing the same search infrastructure. + | + */ + + 'prefix' => env('SCOUT_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Queue Data Syncing + |-------------------------------------------------------------------------- + | + | This option allows you to control if the operations that sync your data + | with your search engines are queued. When this is set to "true" then + | all automatic data syncing will get queued for better performance. + | + */ + + 'queue' => env('SCOUT_QUEUE', false), + + /* + |-------------------------------------------------------------------------- + | Database Transactions + |-------------------------------------------------------------------------- + | + | This configuration option determines if your data will only be synced + | with your search indexes after every open database transaction has + | been committed, thus preventing any discarded data from syncing. + | + */ + + 'after_commit' => false, + + /* + |-------------------------------------------------------------------------- + | Chunk Sizes + |-------------------------------------------------------------------------- + | + | These options allow you to control the maximum chunk size when you are + | mass importing data into the search engine. This allows you to fine + | tune each of these chunk sizes based on the power of the servers. + | + */ + + 'chunk' => [ + 'searchable' => 500, + 'unsearchable' => 500, + ], + + /* + |-------------------------------------------------------------------------- + | Soft Deletes + |-------------------------------------------------------------------------- + | + | This option allows to control whether to keep soft deleted records in + | the search indexes. Maintaining soft deleted records can be useful + | if your application still needs to search for the records later. + | + */ + + 'soft_delete' => false, + + /* + |-------------------------------------------------------------------------- + | Identify User + |-------------------------------------------------------------------------- + | + | This option allows you to control whether to notify the search engine + | of the user performing the search. This is sometimes useful if the + | engine supports any analytics based on this application's users. + | + | Supported engines: "algolia" + | + */ + + 'identify' => env('SCOUT_IDENTIFY', false), + + /* + |-------------------------------------------------------------------------- + | Algolia Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Algolia settings. Algolia is a cloud hosted + | search engine which works great with Scout out of the box. Just plug + | in your application ID and admin API key to get started searching. + | + */ + + 'algolia' => [ + 'id' => env('ALGOLIA_APP_ID', ''), + 'secret' => env('ALGOLIA_SECRET', ''), + 'index-settings' => [ + // 'users' => [ + // 'searchableAttributes' => ['id', 'name', 'email'], + // 'attributesForFaceting'=> ['filterOnly(email)'], + // ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Meilisearch Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Meilisearch settings. Meilisearch is an open + | source search engine with minimal configuration. Below, you can state + | the host and key information for your own Meilisearch installation. + | + | See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options + | + */ + + 'meilisearch' => [ + 'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'), + 'key' => env('MEILISEARCH_KEY'), + 'index-settings' => [ + // 'users' => [ + // 'filterableAttributes'=> ['id', 'name', 'email'], + // ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Typesense Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Typesense settings. Typesense is an open + | source search engine using minimal configuration. Below, you will + | state the host, key, and schema configuration for the instance. + | + */ + + 'typesense' => [ + 'client-settings' => [ + 'api_key' => env('TYPESENSE_API_KEY', 'xyz'), + 'nodes' => [ + [ + 'host' => env('TYPESENSE_HOST', 'localhost'), + 'port' => env('TYPESENSE_PORT', '8108'), + 'path' => env('TYPESENSE_PATH', ''), + 'protocol' => env('TYPESENSE_PROTOCOL', 'http'), + ], + ], + 'nearest_node' => [ + 'host' => env('TYPESENSE_HOST', 'localhost'), + 'port' => env('TYPESENSE_PORT', '8108'), + 'path' => env('TYPESENSE_PATH', ''), + 'protocol' => env('TYPESENSE_PROTOCOL', 'http'), + ], + 'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2), + 'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30), + 'num_retries' => env('TYPESENSE_NUM_RETRIES', 3), + 'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1), + ], + // 'max_total_results' => env('TYPESENSE_MAX_TOTAL_RESULTS', 1000), + 'model-settings' => [ + // User::class => [ + // 'collection-schema' => [ + // 'fields' => [ + // [ + // 'name' => 'id', + // 'type' => 'string', + // ], + // [ + // 'name' => 'name', + // 'type' => 'string', + // ], + // [ + // 'name' => 'created_at', + // 'type' => 'int64', + // ], + // ], + // 'default_sorting_field' => 'created_at', + // ], + // 'search-parameters' => [ + // 'query_by' => 'name' + // ], + // ], + ], + 'import_action' => env('TYPESENSE_IMPORT_ACTION', 'upsert'), + ], + +]; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 4bba9b6..a48aa8d 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -16,8 +16,8 @@ class DatabaseSeeder extends Seeder { $this->call([ - LevelSeeder::class, BordeauxHuntsSeeder::class, + LevelSeeder::class, ]); } } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 191a6df..aa3a626 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -13,6 +13,9 @@ services: APP_KEY: "${APP_KEY}" APP_URL: "${APP_URL}" APP_DEBUG: "false" + SCOUT_DRIVER: "${SCOUT_DRIVER}" + MEILISEARCH_HOST: "${MEILISEARCH_HOST}" + MEILISEARCH_KEY: "${MEILISEARCH_KEY}" DB_CONNECTION: "mysql" DB_HOST: "mysql" DB_PORT: 3306 @@ -29,6 +32,9 @@ services: APP_ENV: "production" APP_KEY: "${APP_KEY}" APP_URL: "${APP_URL}" + SCOUT_DRIVER: "${SCOUT_DRIVER}" + MEILISEARCH_HOST: "${MEILISEARCH_HOST}" + MEILISEARCH_KEY: "${MEILISEARCH_KEY}" DB_CONNECTION: "mysql" DB_HOST: "mysql" @@ -99,6 +105,17 @@ services: timeout: 5s retries: 10 start_period: 10s + meilisearch: + image: getmeili/meilisearch:v1.7 + restart: unless-stopped + environment: + MEILI_MASTER_KEY: "${MEILISEARCH_KEY}" + MEILI_ENV: "production" + volumes: + - meilisearch-data:/meili_data + ports: + - "7700:7700" volumes: mysql-data: + meilisearch-data: diff --git a/tests/Feature/HuntSearchTest.php b/tests/Feature/HuntSearchTest.php new file mode 100644 index 0000000..06ba86e --- /dev/null +++ b/tests/Feature/HuntSearchTest.php @@ -0,0 +1,34 @@ + 'collection']); + + $user = User::factory()->create(); + $matching = Hunts::factory()->create([ + 'title' => 'Chasse au tresor de Bordeaux', + 'description' => 'Une aventure urbaine', + ]); + $nonMatching = Hunts::factory()->create([ + 'title' => 'Parcours decouverte Lyon', + 'description' => 'Balade tranquille', + ]); + + $response = $this->actingAs($user, 'sanctum') + ->getJson("/api/hunts?{$param}=tresor"); + + $response->assertOk(); + $huntIds = collect($response->json('data'))->pluck('id')->all(); + + expect($huntIds) + ->toContain($matching->id) + ->not->toContain($nonMatching->id); +})->with([ + 'search' => 'search', + 'q' => 'q', +]);