Tucker-Eric / EloquentFilter

An Eloquent Way To Filter Laravel Models And Their Relationships
http://tucker-eric.github.io/EloquentFilter
MIT License
1.72k stars 120 forks source link

How should you handle Laravel Scout integration? #162

Closed francoism90 closed 3 years ago

francoism90 commented 3 years ago

I'm exploring the best way to integrate Laravel Scout using (custom) filters and it seems #37 ([https://github.com/Tucker-Eric/EloquentFilter/issues/37#issuecomment-321892146]) provides some information how one can integrate this nicely.

However that ticket is created a few years ago and I don't know if things should be done differently nowadays.

Could you please tell me if the same solution still is valid?

Many thanks!

Tucker-Eric commented 3 years ago

Yep! That same approach would still work!

francoism90 commented 3 years ago

@Tucker-Eric Thanks! That indeed seems to work.

However I'm thinking of this approach with Meilisearch:

public function handle()
    {
        parent::handle();

        $filters = $this->query->get()->map(function($model) {
            return 'id = '.$model->id;
        })->join(' OR ');

        // Overwrite QueryBuilder with ScoutBuilder
        $this->query = $this->getModel()->search(
            $this->input('query', ''),
            function ($engine, string $query, array $options) use ($filters) {
                $options['filters'] = $filters;

                return $engine->search($query, $options);
            }
        );

        return $this->query;
    }

This would mean I can use the filters and return a Laravel Scout (pagina) instance afterwards.

Any downsides of using this approach?

Thanks!

Tucker-Eric commented 3 years ago

That looks like it works. I don't see any downsides. Maybe some minor optimizations and use a whereIn instead of joining the results on OR

francoism90 commented 3 years ago

@Tucker-Eric Unfortunately it doesn't seem work with a large dataset.

The query simple selects all results (e.g. select * from books in handle) each time doing a request. The final query uses take/skip, which actually is correct because of adding the paginate method in the controller.

The only workaround I think would be to integrate the paginate method in handle (ModelFilter class) instead of adding it afterwards?

Ps. I need to use the direct filter with OR unfortunately, this is a limitation of the engine as it doesn't provide any whereIn.

Tucker-Eric commented 3 years ago

Are you referring to something like this?


public function handle()
{
    $filters = parent::handle()
        ->paginateFilter()
        ->getCollection()
        ->pluck('id')
        ->join(' OR ')

    // Overwrite QueryBuilder with ScoutBuilder
    return $this->query = $this->getModel()->search(
        $this->input('query', ''),
        function ($engine, string $query, array $options) use ($filters) {
            $options['filters'] = $filters;

            return $engine->search($query, $options);
        }
    );
}
francoism90 commented 3 years ago

@Tucker-Eric Thanks for helping me., really appreciate it :)

$filters = parent::handle()
            ->paginateFilter()
            ->getCollection()
            ->pluck('id')
            ->map(fn($id) => 'id = '.$id)
            ->join(' OR ');

        // Overwrite QueryBuilder with ScoutBuilder
        return $this->query = $this->getModel()->search(
            $this->input('query', ''),
            function ($engine, string $query, array $options) use ($filters) {
                $options['filters'] = $filters;

                return $engine->search($query, $options);
            }
        );

This works fine. :)

The first page is loaded correctly, trying to load the second page seems to reset the paginate collection.

This is my IndexController.php:

class IndexController extends Controller
{
    public function __invoke(Request $request): ResourceCollection
    {
        $books = Book::filter($request->all(), BookFilter::class)->simplePaginate();

        return BookResource::collection($books);
    }
}

Should I call something different here?

Tucker-Eric commented 3 years ago

I would almost suggest using scout search to get the id's instead of the filter and then filter and paginate in the filter. You could even throw that into a query method that will automatically get resolved when that input parameter is present.

public function query($query)
{
    $ids = $this->getModel()
        ->search($query)
        ->get()
        ->pluck('id');

    return $this->whereIn('id', $ids);
}

Then your controller can work the same or you can use the simplePaginateFilter in the controller:

class IndexController extends Controller
{
    public function __invoke(Request $request): ResourceCollection
    {
        $books = Book::filter($request->all(), BookFilter::class)->simplePaginateFilter();

        return BookResource::collection($books);
    }
}
francoism90 commented 3 years ago

@Tucker-Eric This seems indeed the best solution. Maybe Laravel Scout is too limited and should add some features to make it easier to integrate with the QueryBuilder.

Thanks for the help and your time. :)