diff --git a/README.md b/README.md index 574baea..e6f916c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ You only need to run "First Time Setup", the first time. After that you can use - Start the development environment `./vendor/bin/sail up -d` - Stop the development environment `./vendor/bin/sail down` -- Refresh the database structure `./vendor/bin/sail migrate:fresh --force` +- Refresh the database structure `./vendor/bin/sail artisan migrate:fresh --force` +- Updating Dependencies `./vendor/bin/sail composer update` ### 👇 Importing Data diff --git a/app/Console/Commands/PerformanceTestCommand.php b/app/Console/Commands/PerformanceTestCommand.php new file mode 100644 index 0000000..93f2df4 --- /dev/null +++ b/app/Console/Commands/PerformanceTestCommand.php @@ -0,0 +1,157 @@ + 'san', + 'by_name' => 'dog', + 'by_state' => 'California', + 'by_postal' => '92124', + 'by_country' => 'United+States', // Use URL-encoded value + ]; + + /** + * Execute the console command. + */ + public function handle(): int + { + $iterations = (int) $this->option('iterations'); + + $this->runIndividualTests($iterations); + $this->runCumulativeTests($iterations); + + return 0; + } + + /** + * Run tests for each filter individually. + */ + private function runIndividualTests(int $iterations): void + { + $this->info("\n--- 🚀 Running Individual Filter Tests ({$iterations} iterations each) ---"); + $results = []; + + foreach ($this->scenarios as $filter => $value) { + $this->output->write("Testing filter '{$filter}'..."); + $times = $this->runTest("{$filter}={$value}", $iterations); + + if (empty($times)) { + $this->output->writeln(' FAIL'); + + continue; + } + $this->output->writeln(' OK'); + + $results[] = [ + 'filter' => $filter, + 'avg' => number_format(array_sum($times) / count($times), 2), + 'min' => number_format(min($times), 2), + 'max' => number_format(max($times), 2), + ]; + } + + $this->table( + ['Filter', 'Avg (ms)', 'Min (ms)', 'Max (ms)'], + $results + ); + } + + /** + * Run tests with filters being added cumulatively. + */ + private function runCumulativeTests(int $iterations): void + { + $this->info("\n--- 🚀 Running Cumulative Filter Tests ({$iterations} iterations each) ---"); + $results = []; + $queryParams = []; + + foreach ($this->scenarios as $filter => $value) { + $queryParams[$filter] = $value; + $queryString = http_build_query($queryParams); + $activeFilters = implode(', ', array_keys($queryParams)); + + $this->output->write("Testing filters: {$activeFilters}..."); + $times = $this->runTest($queryString, $iterations); + + if (empty($times)) { + $this->output->writeln(' FAIL'); + + continue; + } + $this->output->writeln(' OK'); + + $results[] = [ + 'filters' => $activeFilters, + 'avg' => number_format(array_sum($times) / count($times), 2), + 'min' => number_format(min($times), 2), + 'max' => number_format(max($times), 2), + ]; + } + + $this->table( + ['Active Filters', 'Avg (ms)', 'Min (ms)', 'Max (ms)'], + $results + ); + } + + /** + * Run the actual HTTP requests and measure response times. + */ + private function runTest(string $queryString, int $iterations): array + { + $times = []; + + // IMPORTANT: This is the correct URL for the API endpoint + $baseUrl = config('app.url').'/v1/breweries/meta'; + $url = $baseUrl.'?'.$queryString; + + // Add debugging output for the URL + if ($this->getOutput()->isVerbose()) { + $this->line(" -> Testing URL: {$url}"); + } + + for ($i = 0; $i < $iterations; $i++) { + $startTime = microtime(true); + try { + $response = Http::get($url); + if (! $response->successful()) { + Log::error('Perf Test Failed Response', ['url' => $url, 'status' => $response->status(), 'body' => $response->body()]); + + return []; // Stop this test on first failure + } + } catch (\Exception $e) { + Log::error('Perf Test Exception', ['url' => $url, 'message' => $e->getMessage()]); + + return []; // Stop this test on first failure + } + + $endTime = microtime(true); + $times[] = ($endTime - $startTime) * 1000; // in milliseconds + } + + return $times; + } +} diff --git a/app/Models/Traits/V1/BreweryFilters.php b/app/Models/Traits/V1/BreweryFilters.php index 6b893b7..f2ade60 100644 --- a/app/Models/Traits/V1/BreweryFilters.php +++ b/app/Models/Traits/V1/BreweryFilters.php @@ -15,47 +15,39 @@ public function scopeApplyFilters(Builder $query, Request $request): Builder return $query ->when($request->has('by_city'), function (Builder $query) use ($request) { $pattern = urldecode($request->input('by_city')); - - $query->whereLike('city', "%{$pattern}%"); + $query->whereLike('city', "{$pattern}%"); }) ->when($request->has('by_country'), function (Builder $query) use ($request) { $pattern = urldecode($request->input('by_country')); - - $query->whereLike('country', "%{$pattern}%"); + $query->whereLike('country', "{$pattern}%"); }) // ->when($request->has('by_dist'), function (Builder $query) use ($request) { // [$latitude, $longitude] = explode(',', $request->input('by_dist')); - // $query->orderByDistance($latitude, $longitude); // }) ->when($request->has('by_ids'), function (Builder $query) use ($request) { $values = array_map('trim', explode(',', $request->input('by_ids'))); - $query->whereIn('id', $values); }) ->when($request->has('by_name'), function (Builder $query) use ($request) { $pattern = urldecode($request->input('by_name')); - + // NOTE: Keeping by_name as-is since it's harder to get exact matches $query->whereLike('name', "%{$pattern}%"); }) ->when($request->has('by_postal'), function (Builder $query) use ($request) { $pattern = urldecode($request->input('by_postal')); - - $query->whereLike('postal_code', "%{$pattern}%"); + $query->whereLike('postal_code', "{$pattern}%"); }) ->when($request->has('by_state'), function (Builder $query) use ($request) { $pattern = urldecode($request->input('by_state')); - - $query->whereLike('state_province', "%{$pattern}%"); + $query->whereLike('state_province', "{$pattern}%"); }) ->when($request->has('by_type'), function (Builder $query) use ($request) { $types = array_map('trim', explode(',', $request->input('by_type'))); - $query->whereIn('brewery_type', $types); }) ->when($request->has('exclude_types'), function (Builder $query) use ($request) { $types = array_map('trim', explode(',', $request->input('exclude_types'))); - $query->whereNotIn('brewery_type', $types); }); } @@ -68,12 +60,10 @@ public function scopeApplySorts(Builder $query, Request $request): Builder return $query ->when($request->has('by_dist'), function (Builder $query) use ($request) { [$latitude, $longitude] = array_map('trim', explode(',', $request->input('by_dist'))); - $query->orderByDistance($latitude, $longitude); }) ->when($request->has('sort'), function (Builder $query) use ($request) { $values = explode(',', $request->input('sort')); - $values = collect($values) ->map(function ($value) { return array_map('trim', explode(':', $value)); diff --git a/database/migrations/2025_08_20_235803_add_indexes_to_breweries_table.php b/database/migrations/2025_08_20_235803_add_indexes_to_breweries_table.php new file mode 100644 index 0000000..8237fb3 --- /dev/null +++ b/database/migrations/2025_08_20_235803_add_indexes_to_breweries_table.php @@ -0,0 +1,36 @@ +index('name'); + $table->index('city'); + $table->index('state_province'); + $table->index('country'); + $table->index('postal_code'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('breweries', function (Blueprint $table) { + $table->dropIndex(['name']); + $table->dropIndex(['city']); + $table->dropIndex(['state_province']); + $table->dropIndex(['country']); + $table->dropIndex(['postal_code']); + }); + } +};