laravel / scout

Laravel Scout provides a driver based solution to searching your Eloquent models.
https://laravel.com/docs/scout
MIT License
1.54k stars 327 forks source link

use scopeSearch to allow searching on relationships #803

Closed sjcobb2022 closed 7 months ago

sjcobb2022 commented 7 months ago

Hi there,

I have been using scout for a while and I think it is a great piece of software. Initially I was playing around with my own seach trait, which did a little bit of trickery revolving around using a scopeSearch.

Because using a scoped function has some backend trickery, we can allow our functions to be "understood" by it's relationships.

For example:

// app/Traits/MySearch.php
// modified from https://www.twilio.com/en-us/blog/build-live-search-box-laravel-livewire-mysql

namespace App\Traits;

use Illuminate\Database\Eloquent\Builder;

trait MySearch
{
    private function buildWildCards(string $term): string
    {
        if ($term == "") {
            return $term;
        }

        // Strip MySQL reserved symbols
        $reservedSymbols = ['-', '+', '<', '>', '@', '(', ')', '~', '"'];
        $term = str_replace($reservedSymbols, '', $term);

        $words = explode(' ', $term);

        foreach($words as $idx => $word) {
            // Add operators so we can leverage the boolean mode of
            // fulltext indices.
            $words[$idx] = "+" . $word . "*";
        }

        $term = implode(' ', $words);

        error_log($term);

        return $term;
    }

    public function scopeSearch(Builder $query, string $term): Builder
    {
        $columns = implode(',', $this->searchable);

        // Boolean mode allows us to match john* for words starting with john
        // (https://dev.mysql.com/doc/refman/5.6/en/fulltext-boolean.html)
        $query->whereRaw(
            "MATCH ({$columns}) AGAINST (? IN BOOLEAN MODE)",
            $this->buildWildCards($term)
        );

        return $query;
    }
}
// app/Models/User.php

class User extends Authenticatable
{
    use HasApiTokens;
    use HasFactory;
    use Notifiable;
    use MySearch;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    public function todos(): HasMany
    {
        return $this->hasMany(Todo::class);
    }

    /**
     * Get the indexable data array for the model.
     *
     * @return array<string, mixed>
     */
    public function toSearchableArray(): array
    {
        return [
            'name' => $this->name,
            'email' => $this->email
        ];
    }
}

class Todo extends Model
{
    use HasFactory;
    use MySearch;

    protected $fillable = [
        "user_id",
        "title",
        "completed",
    ];

    protected $casts = [
        'completed' => 'boolean',
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    #[SearchUsingFullText(['title'])]
    public function toSearchableArray(): array
    {
        return [
            'title' => $this->title,
        ];
    }
 }

This allows for some code to be written like this for example:

User::first()->todos()->search('some_query')

I found this to be a really powerful feature, that allows for much more flexible use of the search functionality.

I believe that this function could very easily be ported over to Laravel Scout. It should be as simple as adding a scopedSearch() to the main trait to implement this. One would not even need remove the original search function.

If there are no objections to this (for any technical reason/limitation), I would be glad to get a PR up.

driesvints commented 7 months ago

Hi there. If you could send in a PR, we can have a look at the code involved and see if it can be accepted. Thanks.