Laravel Scout and TNTSearch search withTrashed

1.8k views Asked by At

I've configured Laravel Scout and can use ::search() on my models. The same models also use SoftDeletes. How can I combine the ::search() with withTrashed()?

The code below does not work.

MyModel::search($request->input('search'))->withTrashed()->paginate(10);

The below does work but does not include the trashed items.

MyModel::search($request->input('search'))->paginate(10);

Update 1 I found in the scout/ModelObserver that deleted items are made unsearchable. Which is a bummer; I wanted my users to be able to search through their trash.

Update 2 I tried using ::withoutSyncingToSearch, as suggested by @camelCase, which I had high hopes for, but this also didn't work.

$model = MyModel::withTrashed()->where('slug', $slug)->firstOrFail();

if ($model->deleted_at) {
    $model->forceDelete();
} else {
    MyModel::withoutSyncingToSearch(function () use ($model) {
        $model->delete();
    });
}

This caused an undefined offset when searching for a deleted item. By the way, I'm using the TNTSearch driver for Laravel Scout. I don't know if this is an error with TNTSearch or with Laravel Scout.

2

There are 2 answers

8
camelCase On BEST ANSWER

I've worked out a solution to your issue. I'll be submitting a pull request for Scout to hopefully get it merged with the official package.

This approach allows you to include soft deleted models in your search:

App\User::search('search query string')->withTrashed()->get();

To only show soft deleted models in your search:

App\User::search('search query string')->onlyTrashed()->get();

You need to modify 3 files:

Builder.php

In laravel\scout\src\Builder.php add the following:

/**
 * Whether the search should include soft deleted models.
 *
 * @var boolean
 */
public $withTrashed = false;

/**
 * Whether the search should only include soft deleted models.
 *
 * @var boolean
 */
public $onlyTrashed = false;

/**
 * Specify the search should include soft deletes
 * 
 * @return $this
 */
public function withTrashed()
{
    $this->withTrashed = true;

    return $this;
}

/**
 * Specify the search should only include soft deletes
 *
 * @return $this
 */
public function onlyTrashed()
{
    $this->onlyTrashed = true;

    return $this;
}

/**
 * Paginate the given query into a simple paginator.
 *
 * @param  int  $perPage
 * @param  string  $pageName
 * @param  int|null  $page
 * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
 */
public function paginate($perPage = null, $pageName = 'page', $page = null)
{
    $engine = $this->engine();

    $page = $page ?: Paginator::resolveCurrentPage($pageName);

    $perPage = $perPage ?: $this->model->getPerPage();

    $results = Collection::make($engine->map(
        $rawResults = $engine->paginate($this, $perPage, $page), $this->model, $this->withTrashed, $this->onlyTrashed
    )); // $this->withTrashed, $this->onlyTrashed is new

    $paginator = (new LengthAwarePaginator($results, $engine->getTotalCount($rawResults), $perPage, $page, [
        'path' => Paginator::resolveCurrentPath(),
        'pageName' => $pageName,
    ]));

    return $paginator->appends('query', $this->query);
}

Engine.php

In laravel\scout\src\Engines\Engine.php modify the following:

/**
 * Map the given results to instances of the given model.
 *
 * @param  mixed  $results
 * @param  \Illuminate\Database\Eloquent\Model  $model
 * @param  boolean  $withTrashed // New
 * @return \Illuminate\Database\Eloquent\Collection
 */
abstract public function map($results, $model, $withTrashed, $onlyTrashed); // $withTrashed, $onlyTrashed is new

/**
 * Get the results of the given query mapped onto models.
 *
 * @param  \Laravel\Scout\Builder  $builder
 * @return \Illuminate\Database\Eloquent\Collection
 */
public function get(Builder $builder)
{
    return Collection::make($this->map(
        $this->search($builder), $builder->model, $builder->withTrashed, $builder->onlyTrashed // $builder->withTrashed, $builder->onlyTrashed is new
    ));
}

And finally, you just need to modify your relative search engine. I'm using Algolia, but the map method appears to be the same for TNTSearch.

AlgoliaEngine.php

In laravel\scout\src\Engines\AlgoliaEngine.php modify the map method to match the abstract class we modified above:

/**
 * Map the given results to instances of the given model.
 *
 * @param  mixed  $results
 * @param  \Illuminate\Database\Eloquent\Model  $model
 * @param  boolean  $withTrashed // New
 * @return \Illuminate\Database\Eloquent\Collection
 */
public function map($results, $model, $withTrashed, $onlyTrashed) // $withTrashed, $onlyTrashed is new
{
    if (count($results['hits']) === 0) {
        return Collection::make();
    }

    $keys = collect($results['hits'])
                    ->pluck('objectID')->values()->all();

    $modelQuery = $model->whereIn(
        $model->getQualifiedKeyName(), $keys
    );

    if ($withTrashed) $modelQuery->withTrashed(); // This is where the query will include deleted items, if specified
    if ($onlyTrashed) $modelQuery->onlyTrashed(); // This is where the query will only include deleted items, if specified

    $models = $modelQuery->get()->keyBy($model->getKeyName());

    return Collection::make($results['hits'])->map(function ($hit) use ($model, $models) {
        $key = $hit['objectID'];

        if (isset($models[$key])) {
            return $models[$key];
        }
    })->filter();
}

TNTSearchEngine.php

/**
 * Map the given results to instances of the given model.
 *
 * @param mixed                               $results
 * @param \Illuminate\Database\Eloquent\Model $model
 *
 * @return Collection
 */
public function map($results, $model, $withTrashed, $onlyTrashed)
{
    if (count($results['ids']) === 0) {
        return Collection::make();
    }

    $keys = collect($results['ids'])->values()->all();

    $model_query = $model->whereIn(
        $model->getQualifiedKeyName(), $keys
    );

    if ($withTrashed) $model_query->withTrashed();
    if ($onlyTrashed) $model_query->onlyTrashed();

    $models = $model_query->get()->keyBy($model->getKeyName());

    return collect($results['ids'])->map(function ($hit) use ($models) {
        if (isset($models[$hit])) {
            return $models[$hit];
        }
    })->filter();
}

Let me know how it works.

NOTE: This approach still requires you to manually pause syncing using the withoutSyncingToSearch method while deleting a model; otherwise the search criteria will be updated with unsearchable().

0
rslhdyt On

this is my solution.

// will return matched ids of my model instance
$searchResultIds = MyModel::search($request->input('search'))->raw()['ids'];

MyModel::whereId('id', $searchResultIds)->onlyTrashed()->get();