Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 6 additions & 11 deletions src/boost/docs/scout.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,17 @@ After installing Scout, you should publish the Scout configuration file using th
php artisan vendor:publish --tag=scout-config
```

Finally, add the `Hypervel\Scout\Searchable` trait and the `Hypervel\Scout\Contracts\SearchableInterface` contract to the model you would like to make searchable. The trait will register a model observer that will automatically keep the model in sync with your search driver:
Finally, add the `Hypervel\Scout\Searchable` trait to the model you would like to make searchable. The trait will register a model observer that will automatically keep the model in sync with your search driver:

```php
<?php

namespace App\Models;

use Hypervel\Database\Eloquent\Model;
use Hypervel\Scout\Contracts\SearchableInterface;
use Hypervel\Scout\Searchable;

class Post extends Model implements SearchableInterface
class Post extends Model
{
use Searchable;
}
Expand Down Expand Up @@ -222,10 +221,9 @@ By default, the entire `toArray` form of a given model will be persisted to its
namespace App\Models;

use Hypervel\Database\Eloquent\Model;
use Hypervel\Scout\Contracts\SearchableInterface;
use Hypervel\Scout\Searchable;

class Post extends Model implements SearchableInterface
class Post extends Model
{
use Searchable;

Expand Down Expand Up @@ -256,12 +254,11 @@ When searching, Scout will typically use the default search engine specified in
namespace App\Models;

use Hypervel\Database\Eloquent\Model;
use Hypervel\Scout\Contracts\SearchableInterface;
use Hypervel\Scout\Engines\Engine;
use Hypervel\Scout\Scout;
use Hypervel\Scout\Searchable;

class User extends Model implements SearchableInterface
class User extends Model
{
use Searchable;

Expand Down Expand Up @@ -421,10 +418,9 @@ When using a third-party engine, each Eloquent model is synced with a given sear
namespace App\Models;

use Hypervel\Database\Eloquent\Model;
use Hypervel\Scout\Contracts\SearchableInterface;
use Hypervel\Scout\Searchable;

class Post extends Model implements SearchableInterface
class Post extends Model
{
use Searchable;

Expand Down Expand Up @@ -461,10 +457,9 @@ By default, Scout will use the primary key of the model as the model's unique ID
namespace App\Models;

use Hypervel\Database\Eloquent\Model;
use Hypervel\Scout\Contracts\SearchableInterface;
use Hypervel\Scout\Searchable;

class User extends Model implements SearchableInterface
class User extends Model
{
use Searchable;

Expand Down
5 changes: 2 additions & 3 deletions src/boost/docs/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ The search techniques described above are all query builder methods that you cal
<a name="database-engine"></a>
### Database Engine

Scout's built-in database engine performs full-text and `LIKE` searches against your existing database — no external service or extra infrastructure required. Simply add the `Searchable` trait to your model, implement the `SearchableInterface` contract, and define a `toSearchableArray` method that returns the columns you want to be searchable.
Scout's built-in database engine performs full-text and `LIKE` searches against your existing database — no external service or extra infrastructure required. Simply add the `Searchable` trait to your model and define a `toSearchableArray` method that returns the columns you want to be searchable.

You may use PHP attributes to control the search strategy for each column. `SearchUsingFullText` will use your database's full-text index, `SearchUsingPrefix` will only match from the beginning of the string (`example%`), and any columns without an attribute use a default `LIKE` strategy with wildcards on both sides (`%example%`):

Expand All @@ -168,10 +168,9 @@ namespace App\Models;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Scout\Attributes\SearchUsingFullText;
use Hypervel\Scout\Attributes\SearchUsingPrefix;
use Hypervel\Scout\Contracts\SearchableInterface;
use Hypervel\Scout\Searchable;

class Article extends Model implements SearchableInterface
class Article extends Model
{
use Searchable;

Expand Down
5 changes: 3 additions & 2 deletions src/scout/src/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
/**
* Fluent search query builder for searchable models.
*
* @template TModel of Model&SearchableInterface
* @template TModel of Model
*/
class Builder
{
Expand All @@ -38,7 +38,7 @@ class Builder
/**
* The model instance.
*
* @var TModel
* @var SearchableInterface&TModel
*/
public Model $model;

Expand Down Expand Up @@ -118,6 +118,7 @@ public function __construct(
?Closure $callback = null,
bool $softDelete = false
) {
/** @var SearchableInterface&TModel $model */
$this->model = $model;
$this->query = $query;
$this->callback = $callback;
Expand Down
2 changes: 1 addition & 1 deletion src/scout/src/Console/IndexCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function handle(EngineManager $manager, Repository $config): int

if ($model !== null
&& $config->boolean('scout.soft_delete', false)
&& in_array(SoftDeletes::class, class_uses_recursive($model))) {
&& in_array(SoftDeletes::class, class_uses_recursive($model), true)) {
$settings = $engine->configureSoftDeleteFilter($settings);
}

Expand Down
9 changes: 6 additions & 3 deletions src/scout/src/Console/QueueImportCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Hypervel\Config\Repository;
use Hypervel\Console\Command;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Scout\Console\Traits\ResolvesScoutModelClass;
use Hypervel\Scout\Contracts\SearchableInterface;
use Hypervel\Scout\Exceptions\ScoutException;
Expand Down Expand Up @@ -45,7 +46,7 @@ public function handle(Repository $config): int
{
$class = $this->resolveModelClass((string) $this->argument('model'));

/** @var SearchableInterface $model */
/** @var Model&SearchableInterface $model */
$model = new $class;

$chunk = max(1, (int) ($this->option('chunk') ?? $config->integer('scout.chunk.searchable', 500)));
Expand All @@ -67,8 +68,9 @@ public function handle(Repository $config): int
/**
* Dispatch range jobs for an integer-keyed model using min/max arithmetic.
*/
protected function dispatchIntegerRange(string $class, SearchableInterface $model, int $chunk, string $order, ?string $queueName, ?string $connection): int
protected function dispatchIntegerRange(string $class, Model $model, int $chunk, string $order, ?string $queueName, ?string $connection): int
{
/** @var Model&SearchableInterface $model */
$query = $class::makeAllSearchableQuery();
$keyName = $model->getScoutKeyName();
$qualified = $query->qualifyColumn($keyName);
Expand Down Expand Up @@ -142,8 +144,9 @@ protected function dispatchIntegerRange(string $class, SearchableInterface $mode
* dispatches one MakeRangeSearchable per chunk with the first/last keys
* in that chunk. Workers re-query their range via whereBetween.
*/
protected function dispatchStringRange(string $class, SearchableInterface $model, int $chunk, string $order, ?string $queueName, ?string $connection): int
protected function dispatchStringRange(string $class, Model $model, int $chunk, string $order, ?string $queueName, ?string $connection): int
{
/** @var Model&SearchableInterface $model */
$query = $class::makeAllSearchableQuery();
$keyName = $model->getScoutKeyName();
$qualified = $query->qualifyColumn($keyName);
Expand Down
2 changes: 1 addition & 1 deletion src/scout/src/Console/SyncIndexSettingsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function handle(EngineManager $manager, Repository $config): int

if ($model !== null
&& $config->boolean('scout.soft_delete', false)
&& in_array(SoftDeletes::class, class_uses_recursive($model))) {
&& in_array(SoftDeletes::class, class_uses_recursive($model), true)) {
$settings = $engine->configureSoftDeleteFilter($settings);
}

Expand Down
21 changes: 17 additions & 4 deletions src/scout/src/Contracts/SearchableInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,49 @@
use Closure;
use Hypervel\Database\Eloquent\Builder as EloquentBuilder;
use Hypervel\Database\Eloquent\Collection;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Scout\Builder;
use Hypervel\Scout\Engines\Engine;

/**
* Contract for models that can be indexed and searched.
* Internal shape for models that can be indexed and searched.
*
* This interface defines the public API that searchable models must implement.
* The Searchable trait provides a default implementation of these methods.
* Application models gain this capability through the Searchable trait and do
* not need to implement this interface directly. Keep this shape compatible
* with the trait while widening collection boundaries to Model for engines.
*
* @phpstan-require-extends \Hypervel\Database\Eloquent\Model
* @phpstan-require-extends Model
*/
interface SearchableInterface
{
/**
* Perform a search against the model's indexed data.
*
* @return Builder<Model&static>
*/
public static function search(string $query = '', ?Closure $callback = null): Builder;

/**
* Get the requested models from an array of object IDs.
*
* @param array<int|string> $ids
* @return Collection<int, Model&static>
*/
public function getScoutModelsByIds(Builder $builder, array $ids): Collection;

/**
* Get a query builder for retrieving the requested models from an array of object IDs.
*
* @param array<int|string> $ids
* @return EloquentBuilder<Model&static>
*/
public function queryScoutModelsByIds(Builder $builder, array $ids): EloquentBuilder;

/**
* Modify the collection of models being made searchable.
*
* @param Collection<int, Model> $models
* @return Collection<int, Model>
*/
public function makeSearchableUsing(Collection $models): Collection;

Expand Down
31 changes: 12 additions & 19 deletions src/scout/src/Engines/AlgoliaEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public function __construct(
/**
* Update the given models in the search index.
*
* @param EloquentCollection<int, Model&SearchableInterface> $models
* @param EloquentCollection<int, Model> $models
* @throws AlgoliaException
*/
public function update(EloquentCollection $models): void
Expand All @@ -64,7 +64,7 @@ public function update(EloquentCollection $models): void
return;
}

/** @var Model&SearchableInterface $firstModel */
/** @var EloquentCollection<int, Model&SearchableInterface> $models */
$firstModel = $models->first();
$index = $firstModel->indexableAs();

Expand All @@ -73,7 +73,6 @@ public function update(EloquentCollection $models): void
}

$objects = $models->map(function (Model $model) {
/** @var Model&SearchableInterface $model */
$searchableData = $model->toSearchableArray();

if (empty($searchableData)) {
Expand All @@ -100,20 +99,20 @@ public function update(EloquentCollection $models): void
/**
* Remove the given models from the search index.
*
* @param EloquentCollection<int, Model&SearchableInterface> $models
* @param EloquentCollection<int, Model> $models
*/
public function delete(EloquentCollection $models): void
{
if ($models->isEmpty()) {
return;
}

/** @var Model&SearchableInterface $firstModel */
/** @var EloquentCollection<int, Model&SearchableInterface> $models */
$firstModel = $models->first();

$keys = $models instanceof RemoveableScoutCollection
? $models->pluck($firstModel->getScoutKeyName())
: $models->map(fn (SearchableInterface $model) => $model->getScoutKey());
: $models->map(fn (Model $model) => $model->getScoutKey());

$this->algolia->deleteObjects($firstModel->indexableAs(), $keys->all());
}
Expand Down Expand Up @@ -342,11 +341,10 @@ public function mapIds(mixed $results): Collection

/**
* Map the given results to instances of the given model.
*
* @param Model&SearchableInterface $model
*/
public function map(Builder $builder, mixed $results, Model $model): EloquentCollection
{
/** @var Model&SearchableInterface $model */
if (count($results['hits']) === 0) {
return $model->newCollection();
}
Expand All @@ -356,13 +354,12 @@ public function map(Builder $builder, mixed $results, Model $model): EloquentCol
/** @var array<int|string> $objectIds */
$objectIdPositions = array_flip($objectIds);

/** @var EloquentCollection<int, Model&SearchableInterface> $scoutModels */
$scoutModels = $model->getScoutModelsByIds($builder, $objectIds);

// Search engines serialize numeric Scout keys as strings.
$mapped = $scoutModels
->filter(fn ($m) => in_array($m->getScoutKey(), $objectIds))
->filter(fn ($m) => in_array($m->getScoutKey(), $objectIds, false))
->map(function ($m) use ($results, $objectIdPositions) {
/** @var Model&SearchableInterface $m */
$result = $results['hits'][$objectIdPositions[$m->getScoutKey()]] ?? [];

foreach ($result as $key => $value) {
Expand All @@ -381,11 +378,10 @@ public function map(Builder $builder, mixed $results, Model $model): EloquentCol

/**
* Map the given results to instances of the given model via a lazy collection.
*
* @param Model&SearchableInterface $model
*/
public function lazyMap(Builder $builder, mixed $results, Model $model): LazyCollection
{
/** @var Model&SearchableInterface $model */
if (count($results['hits']) === 0) {
return LazyCollection::empty();
}
Expand All @@ -395,13 +391,11 @@ public function lazyMap(Builder $builder, mixed $results, Model $model): LazyCol
/** @var array<int|string> $objectIds */
$objectIdPositions = array_flip($objectIds);

/** @var LazyCollection<int, Model&SearchableInterface> $cursor */
$cursor = $model->queryScoutModelsByIds($builder, $objectIds)->cursor();

return $cursor
->filter(fn ($m) => in_array($m->getScoutKey(), $objectIds))
->filter(fn ($m) => in_array($m->getScoutKey(), $objectIds, false))
->map(function ($m) use ($results, $objectIdPositions) {
/** @var Model&SearchableInterface $m */
$result = $results['hits'][$objectIdPositions[$m->getScoutKey()]] ?? [];

foreach ($result as $key => $value) {
Expand All @@ -426,11 +420,10 @@ public function getTotalCount(mixed $results): int

/**
* Flush all of the model's records from the engine.
*
* @param Model&SearchableInterface $model
*/
public function flush(Model $model): void
{
/** @var Model&SearchableInterface $model */
$this->algolia->clearObjects($model->indexableAs());
}

Expand Down Expand Up @@ -539,7 +532,7 @@ public function configureSoftDeleteFilter(array $settings = []): array
*/
protected function usesSoftDelete(Model $model): bool
{
return in_array(SoftDeletes::class, class_uses_recursive($model));
return in_array(SoftDeletes::class, class_uses_recursive($model), true);
}

/**
Expand Down
Loading