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']);
+ });
+ }
+};