Overview
Search is split into a handful of different parts behind the scenes.
- An
Index
class will exist for each configured index. It will know how to send data to and from the underlying driver. -
Searchable
classes are the things that can be indexed and searched. (e.g. anEntry
) -
ProvidesSearchables
classes (or just “Providers”) are classes that define all the applicable searchable items. (e.g. anEntries
provider givesEntry
instances.) -
Result
classes are wrappers that allow Statamic to use the searchable objects in a consistent way. (e.g. each result should have an identifier, type, Control Panel URL, etc)
Custom Searchables
In order to allow searching of custom items, you must create a provider and make your object implement Searchable.
In the following examples, we’ll assume you are wanting to store products as Eloquent models.
Implement Searchable
The object you want to make searchable should implement both the Statamic\Contracts\Data\Augmentable
and Statamic\Contracts\Search\Searchable
interfaces. To make things easier, you can import the HasAugmentedData
and Searchable
traits which will handle most of the heavy lifting for you.
use Illuminate\Database\Eloquent\Model;use Statamic\Contracts\Data\Augmentable;use Statamic\Contracts\Search\Result as ResultContract;use Statamic\Contracts\Search\Searchable as SearchableContract;use Statamic\Data\HasAugmentedData;use Statamic\Search\Result;use Statamic\Search\Searchable; class Product extends Model implements Augmentable, ContainsQueryableValues, SearchableContract{ use HasAugmentedData, Searchable; /** * The identifier that will be used in search indexes. */ public function getSearchReference(): string { return 'product::'.$this->id; } /** * The indexed value for a given field. */ public function getSearchValue(string $field) { return $this->$field; } /** * Convert to a search result class. * You can use the Result class, or feel free to create your own. */ public function toSearchResult(): ResultContract { return new Result($this, 'product'); } /** * Returns an array of the model's attributes to be used in augmentation. */ public function augmentedArrayData() { return $this->attributesToArray(); }}
When you’re making a searchable from an Eloquent model, you’ll need to override some offset
/__call
methods to prevent conflicts between Eloquent and Statamic’s implementations of those methods:
/** * These methods exist on both Eloquent's Model class and in Statamic's HasAugmentedInstance trait. * To prevent conflicts, we need to override these methods and force them to call Eloquent's method. */ public function offsetGet($offset): mixed{ return parent::offsetGet($offset);} public function offsetSet($offset, $value): void{ parent::offsetSet($offset, $value);} public function offsetUnset($offset): void{ parent::offsetUnset($offset);} public function offsetExists($offset): bool{ return parent::offsetExists($offset);} public function __call($method, $parameters){ return parent::__call($method, $parameters);}
Create Provider
You should have a class that implements ProvidesSearchables
, however it’s even easier for you to extend Provider
.
use Statamic\Search\Searchables\Provider; class ProductProvider extends Provider{ /** * The handle used within search configs. * * e.g. For 'searchables' => ['collection:blog', 'products:hats', 'users'] * they'd be 'collection', 'products', and 'users'. */ protected static $handle = 'products'; /** * The prefix used in each Searchable's reference. * * e.g. For 'entry::123', it would be 'entry'. */ protected static $referencePrefix = 'product'; /** * Convert an array of keys to the actual objects. * The keys will be searchable references with the prefix removed. */ public function find(array $keys): Collection { return Product::find($keys); } /** * Get a collection of all searchables. */ public function provide(): Collection { return Product::all(); // If you wanted to allow subsets of products, you could specify them in your // config then retrieve them appropriately here using keys. // e.g. 'searchables' => ['products:hats', 'products:shoes'], // $this->keys would be ['keys', 'hats']. return Product::whereIn('type', $this->keys)->get(); } /** * Check if a given object belongs to this provider. */ public function contains($searchable): bool { return $searchable instanceof Product; }}
Register the Provider
In order to use the provider, first register it within a service provider, and then it will be available to your search config by its handle.
use Statamic\Facades\Search; public function boot(){ ProductProvider::register();}
// config/statamic/search.php 'indexes' => [ 'products_and_blog_posts' => [ 'driver' => 'local', 'searchables' => [ 'products', 'collections:blog' ], 'fields' => ['title'] ]]
Event Listeners
You will want to update the indexes when you create, edit, or delete your searchable items.
Continuing with the Eloquent example, we can hook into model events in your service provider.
use Illuminate\Support\Facades\Event;use Statamic\Facades\Search; Event::listen('eloquent.saved: App\Models\Product', function ($model) { Search::updateWithinIndexes($model);}); Event::listen('eloquent.deleted: App\Models\Product', function ($model) { Search::deleteFromIndexes($model);});
The updateWithinIndexes
method will update the record in all appropriate indexes. If a filter determines that the record should be removed (e.g. if it changed into a draft), it’ll remove it.
The deleteFromIndexes
method will remove it from all appropriate indexes.
Custom Index Drivers
Statamic comes with two native search index drivers: Comb and Algolia. Comb is our “local” driver, where indexes are stored as json files. Algolia integrates with the service using their API.
For this example, we’ll integrate with a fictional service called FastSearch.
Create Index
You should have a class that extends Index
.
use Statamic\Search\Index; class FastSearchIndex extends Index{ private $client; public function __construct(FastSearchClient $client, $name, array $config, string $locale = null) { // In this example, we'll accept a fictional client class that will perform API requests. // If you have a constructor, don't forget to construct the parent class too. $this->client = $client; parent::__construct($name, $config, $locale); } /** * Return a query builder that will perform the search. */ public function search($query) { return (new FastSearchQuery($this))->query($query); } /** * Check whether the index actually exists. * i.e. Does it exist in the service, or as a json file, etc. */ public function exists() { $this->client->indexExists($this->name); } /** * Insert items into the index. */ protected function insertDocuments(Documents $documents) { $this->client->insertObjects($documents->all()); } /** * Delete an item from the index. */ public function delete($document) { $this->client->deleteObject($document); } /** * Delete the entire index. */ protected function deleteIndex() { $this->client->deleteIndex($this->name); }}
Register Index
public function boot(){ Search::extend('fast', function ($app, $config, $name) { $client = new FastSearchApiClient('api-key'); return new FastSearchIndex($client, $name, $config); });}
Create Query Builder
In the index class, the search
method wanted a query builder. You can create a class that extends our own, which only requires you to define a single method.
<?php namespace App\Search; use Statamic\Search\QueryBuilder;use Statamic\Support\Str; class CustomSearchQuery extends QueryBuilder{ /** * Get search results as an array. * e.g. [ * ['title' => 'One', 'search_score' => 500], * ['title' => 'Two', 'search_score' => 400], * ] */ public function getSearchResults() { $results = $this->index->searchUsingApi($query); // Statamic will expects a search_score to be in each result. // Some services like Algolia don't have scores and will just return them in order. // This is a trick to set the scores in sequential order, highest first. return $results->map(function ($result, $i) use ($results) { $result['search_score'] = $results->count() - $i; return $result; }); }}
This getSearchResults
method is used in the parent class in order to allow basic filtering and other query methods. Of course, you are free to build as much of your own query builder as you like.