|
| 1 | +# Search Performance Optimization |
| 2 | + |
| 3 | +Laravel Restify includes an optional search performance optimization that replaces inefficient subqueries with JOIN-based searches for related fields. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +By default, when searching through BelongsTo relationship fields, Laravel Restify uses subqueries which can be slow for large datasets: |
| 8 | + |
| 9 | +```sql |
| 10 | +-- Default behavior (subqueries) |
| 11 | +SELECT * FROM `invoices` WHERE ( |
| 12 | + UPPER(`invoices`.`gross_amount`) LIKE '%CSC%' |
| 13 | + OR (SELECT `vendors`.`code` FROM `vendors` WHERE `vendors`.`id` = `invoices`.`vendor_id` LIMIT 1) LIKE '%csc%' |
| 14 | + OR (SELECT `vendors`.`name` FROM `vendors` WHERE `vendors`.`id` = `invoices`.`vendor_id` LIMIT 1) LIKE '%csc%' |
| 15 | +) |
| 16 | +``` |
| 17 | + |
| 18 | +With JOIN optimization enabled, these become efficient JOIN-based queries: |
| 19 | + |
| 20 | +```sql |
| 21 | +-- Optimized behavior (JOINs) |
| 22 | +SELECT invoices.* FROM `invoices` |
| 23 | +LEFT JOIN `vendors` AS vendors_for_vendor ON invoices.vendor_id = vendors_for_vendor.id |
| 24 | +WHERE ( |
| 25 | + UPPER(invoices.gross_amount) LIKE '%CSC%' |
| 26 | + OR UPPER(vendors_for_vendor.code) LIKE '%CSC%' |
| 27 | + OR UPPER(vendors_for_vendor.name) LIKE '%CSC%' |
| 28 | +) |
| 29 | +``` |
| 30 | + |
| 31 | +## Configuration |
| 32 | + |
| 33 | +### Enabling JOIN Optimization |
| 34 | + |
| 35 | +Add the following to your `.env` file: |
| 36 | + |
| 37 | +```env |
| 38 | +RESTIFY_SEARCH_USE_JOINS=true |
| 39 | +``` |
| 40 | + |
| 41 | +Or configure it directly in `config/restify.php`: |
| 42 | + |
| 43 | +```php |
| 44 | +'search' => [ |
| 45 | + 'case_sensitive' => true, |
| 46 | + 'use_joins' => true, // Enable JOIN optimization |
| 47 | +], |
| 48 | +``` |
| 49 | + |
| 50 | +### Default Behavior |
| 51 | + |
| 52 | +**By default, JOIN optimization is disabled** to maintain backward compatibility. The system will continue using subqueries until explicitly enabled. |
| 53 | + |
| 54 | +## When JOINs Are Used |
| 55 | + |
| 56 | +JOIN optimization is **only applied when**: |
| 57 | + |
| 58 | +1. ✅ The `restify.search.use_joins` config is set to `true` |
| 59 | +2. ✅ You're searching through a **BelongsTo relationship** field |
| 60 | +3. ✅ The relationship field is marked as **searchable** |
| 61 | + |
| 62 | +JOIN optimization is **NOT applied for**: |
| 63 | + |
| 64 | +- ❌ Direct field searches on the main model |
| 65 | +- ❌ HasMany or other relationship types |
| 66 | +- ❌ When the config flag is disabled (default) |
| 67 | + |
| 68 | +## Repository Configuration |
| 69 | + |
| 70 | +No changes are needed to your existing repository configuration. The optimization works with your current setup: |
| 71 | + |
| 72 | +```php |
| 73 | +class InvoiceRepository extends Repository |
| 74 | +{ |
| 75 | + public static function searchables(): array |
| 76 | + { |
| 77 | + return [ |
| 78 | + 'gross_amount', // Direct field - no JOIN needed |
| 79 | + 'number', // Direct field - no JOIN needed |
| 80 | + ]; |
| 81 | + } |
| 82 | + |
| 83 | + public static function related(): array |
| 84 | + { |
| 85 | + return [ |
| 86 | + 'vendor' => BelongsTo::make('vendor', VendorRepository::class)->searchable([ |
| 87 | + 'vendors.code', // Will use JOIN when optimization enabled |
| 88 | + 'vendors.name', // Will use JOIN when optimization enabled |
| 89 | + ]), |
| 90 | + ]; |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +## Performance Benefits |
| 96 | + |
| 97 | +### Query Efficiency |
| 98 | +- **Eliminates N+1 subquery problems** - Each related field search no longer creates separate subqueries |
| 99 | +- **Improved query performance** - JOINs are typically much faster than correlated subqueries |
| 100 | +- **Reduced database load** - Single query instead of multiple subqueries per search term |
| 101 | + |
| 102 | +### Eager Loading Optimization |
| 103 | +When JOIN optimization is enabled, the system automatically: |
| 104 | +- **Detects already-joined relationships** |
| 105 | +- **Excludes them from eager loading** to prevent duplicate queries |
| 106 | +- **Maintains data integrity** while improving performance |
| 107 | + |
| 108 | +### Example Performance Impact |
| 109 | + |
| 110 | +**Before Optimization (3 separate subqueries)**: |
| 111 | +```sql |
| 112 | +-- Main query + 2 subqueries for each search term |
| 113 | +SELECT * FROM invoices WHERE ( |
| 114 | + gross_amount LIKE '%term%' OR |
| 115 | + (SELECT code FROM vendors WHERE...) LIKE '%term%' OR |
| 116 | + (SELECT name FROM vendors WHERE...) LIKE '%term%' |
| 117 | +) |
| 118 | +``` |
| 119 | + |
| 120 | +**After Optimization (1 JOIN query)**: |
| 121 | +```sql |
| 122 | +-- Single query with JOIN |
| 123 | +SELECT invoices.* FROM invoices |
| 124 | +LEFT JOIN vendors AS vendors_for_vendor ON invoices.vendor_id = vendors_for_vendor.id |
| 125 | +WHERE ( |
| 126 | + gross_amount LIKE '%term%' OR |
| 127 | + vendors_for_vendor.code LIKE '%term%' OR |
| 128 | + vendors_for_vendor.name LIKE '%term%' |
| 129 | +) |
| 130 | +``` |
| 131 | + |
| 132 | +## Backward Compatibility |
| 133 | + |
| 134 | +This feature is **100% backward compatible**: |
| 135 | + |
| 136 | +- **Default behavior unchanged** - Existing applications continue working without any changes |
| 137 | +- **Opt-in optimization** - You must explicitly enable JOIN optimization |
| 138 | +- **No breaking changes** - All existing repository configurations work as before |
| 139 | +- **Gradual adoption** - Enable per environment (dev/staging first, then production) |
| 140 | + |
| 141 | +## Best Practices |
| 142 | + |
| 143 | +### When to Enable |
| 144 | +- ✅ **Large datasets** with frequent relationship searches |
| 145 | +- ✅ **Performance-critical applications** where search speed matters |
| 146 | +- ✅ **Well-tested environments** where you can validate the behavior |
| 147 | + |
| 148 | +### When to Keep Disabled |
| 149 | +- ❌ **Small datasets** where performance difference is negligible |
| 150 | +- ❌ **Legacy systems** where you want to maintain exact query behavior |
| 151 | +- ❌ **Complex relationship setups** that need specific subquery behavior |
| 152 | + |
| 153 | +### Testing Strategy |
| 154 | +1. **Enable in development/staging first** |
| 155 | +2. **Run your existing test suite** to ensure no regressions |
| 156 | +3. **Monitor query performance** before and after |
| 157 | +4. **Gradually roll out to production** |
| 158 | + |
| 159 | +## Troubleshooting |
| 160 | + |
| 161 | +### Common Issues |
| 162 | + |
| 163 | +**Q: JOINs aren't being used even though the config is enabled** |
| 164 | +A: Verify that: |
| 165 | +- The searchable fields are on BelongsTo relationships (not direct model fields) |
| 166 | +- The relationship is properly configured with `->searchable([...])` |
| 167 | +- Cache has been cleared after config changes |
| 168 | + |
| 169 | +**Q: Getting different search results after enabling JOINs** |
| 170 | +A: This could indicate: |
| 171 | +- Multi-tenant constraints that were handled differently in subqueries |
| 172 | +- Complex relationship setups that need adjustment |
| 173 | +- Consider disabling the optimization for that specific use case |
| 174 | + |
| 175 | +**Q: Performance didn't improve as expected** |
| 176 | +A: Check that: |
| 177 | +- You have proper database indexes on JOIN columns |
| 178 | +- The dataset is large enough to see meaningful performance differences |
| 179 | +- Other query bottlenecks aren't masking the improvement |
| 180 | + |
| 181 | +### Debugging |
| 182 | + |
| 183 | +Enable query logging to see the generated SQL: |
| 184 | + |
| 185 | +```php |
| 186 | +DB::enableQueryLog(); |
| 187 | +// Perform your search |
| 188 | +$queries = DB::getQueryLog(); |
| 189 | +dd($queries); |
| 190 | +``` |
| 191 | + |
| 192 | +Look for `LEFT JOIN` statements in the search queries when optimization is enabled. |
| 193 | + |
| 194 | +## Migration Guide |
| 195 | + |
| 196 | +### Step 1: Test in Development |
| 197 | +```env |
| 198 | +# In .env |
| 199 | +RESTIFY_SEARCH_USE_JOINS=true |
| 200 | +``` |
| 201 | + |
| 202 | +### Step 2: Validate Query Behavior |
| 203 | +Run your test suite and verify search results remain consistent. |
| 204 | + |
| 205 | +### Step 3: Performance Testing |
| 206 | +Measure query performance before and after enabling the optimization. |
| 207 | + |
| 208 | +### Step 4: Production Rollout |
| 209 | +Enable in production after thorough testing in staging environments. |
| 210 | + |
| 211 | +### Step 5: Monitor |
| 212 | +Watch for any performance regressions or unexpected query behavior. |
0 commit comments