feat: meilisearch

This commit is contained in:
Leon 2026-01-13 16:02:34 +01:00
parent da3068f95e
commit df3c300155
10 changed files with 800 additions and 70 deletions

View File

@ -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

View File

@ -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);

View File

@ -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(),
];
}
}

View File

@ -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

View File

@ -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",

379
composer.lock generated
View File

@ -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",

210
config/scout.php Normal file
View File

@ -0,0 +1,210 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Search Engine
|--------------------------------------------------------------------------
|
| This option controls the default search connection that gets used while
| using Laravel Scout. This connection is used when syncing all models
| to the search service. You should adjust this based on your needs.
|
| Supported: "algolia", "meilisearch", "typesense",
| "database", "collection", "null"
|
*/
'driver' => 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'),
],
];

View File

@ -16,8 +16,8 @@ class DatabaseSeeder extends Seeder
{
$this->call([
LevelSeeder::class,
BordeauxHuntsSeeder::class,
LevelSeeder::class,
]);
}
}

View File

@ -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:

View File

@ -0,0 +1,34 @@
<?php
use App\Models\Hunts;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('filters hunts using the search parameter', function (string $param) {
config(['scout.driver' => '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',
]);