From 84a72d284c30ccdadfc7fb46a3d52ef02b7ffa82 Mon Sep 17 00:00:00 2001 From: Phan An Date: Wed, 23 Dec 2020 11:53:00 +0100 Subject: [PATCH] feat: add Laravel Scount & TNTSearch --- app/Models/Album.php | 31 ++++- app/Models/Artist.php | 25 +++- app/Models/Playlist.php | 23 +++- app/Models/Song.php | 21 +++ composer.json | 3 +- composer.lock | 219 +++++++++++++++++++++++++++++- config/app.php | 3 + config/scout.php | 117 ++++++++++++++++ storage/search-indexes/.gitignore | 2 + 9 files changed, 422 insertions(+), 22 deletions(-) create mode 100644 config/scout.php create mode 100644 storage/search-indexes/.gitignore diff --git a/app/Models/Album.php b/app/Models/Album.php index d5b60ca4..aba85bdf 100644 --- a/app/Models/Album.php +++ b/app/Models/Album.php @@ -9,22 +9,23 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Laravel\Scout\Searchable; /** - * @property string $cover The album cover's file name - * @property string|null $cover_path The absolute path to the cover file - * @property bool $has_cover If the album has a non-default cover image + * @property string $cover The album cover's file name + * @property string|null $cover_path The absolute path to the cover file + * @property bool $has_cover If the album has a non-default cover image * @property int $id - * @property string $name Name of the album + * @property string $name Name of the album * @property bool $is_compilation If the album is a compilation from multiple artists - * @property Artist $artist The album's artist + * @property Artist $artist The album's artist * @property int $artist_id * @property Collection $songs - * @property bool $is_unknown If the album is the Unknown Album + * @property bool $is_unknown If the album is the Unknown Album * @property string|null $thumbnail_name The file name of the album's thumbnail * @property string|null $thumbnail_path The full path to the thumbnail. * Notice that this doesn't guarantee the thumbnail exists. - * @property string|null $thumbnail The public URL to the album's thumbnail + * @property string|null $thumbnail The public URL to the album's thumbnail * * @method static self firstOrCreate(array $where, array $params = []) * @method static self|null find(int $id) @@ -35,6 +36,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; class Album extends Model { use HasFactory; + use Searchable; use SupportsDeleteWhereIDsNotIn; public const UNKNOWN_ID = 1; @@ -144,4 +146,19 @@ class Album extends Model { return $this->thumbnail_name ? album_cover_url($this->thumbnail_name) : null; } + + /** @return array */ + public function toSearchableArray(): array + { + $array = [ + 'id' => $this->id, + 'name' => $this->name, + ]; + + if (!$this->artist->is_unknown && !$this->artist->is_various) { + $array['artist'] = $this->artist->name; + } + + return $array; + } } diff --git a/app/Models/Artist.php b/app/Models/Artist.php index f4556a3d..aeac616a 100644 --- a/app/Models/Artist.php +++ b/app/Models/Artist.php @@ -10,15 +10,16 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Laravel\Scout\Searchable; /** - * @property int $id - * @property string $name - * @property string|null $image Public URL to the artist's image - * @property bool $is_unknown If the artist is Unknown Artist - * @property bool $is_various If the artist is Various Artist - * @property Collection $songs - * @property bool $has_image If the artist has a (non-default) image + * @property int $id + * @property string $name + * @property string|null $image Public URL to the artist's image + * @property bool $is_unknown If the artist is Unknown Artist + * @property bool $is_various If the artist is Various Artist + * @property Collection $songs + * @property bool $has_image If the artist has a (non-default) image * @property string|null $image_path Absolute path to the artist's image * * @method static self find(int $id) @@ -31,6 +32,7 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough; class Artist extends Model { use HasFactory; + use Searchable; use SupportsDeleteWhereIDsNotIn; public const UNKNOWN_ID = 1; @@ -122,4 +124,13 @@ class Artist extends Model return file_exists(artist_image_path($image)); } + + /** @return array */ + public function toSearchableArray(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + ]; + } } diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php index 35289dce..f7ad85f4 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -9,15 +9,16 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Laravel\Scout\Searchable; /** - * @property int $user_id + * @property int $user_id * @property Collection $songs - * @property int $id - * @property array $rules - * @property bool $is_smart - * @property string $name - * @property user $user + * @property int $id + * @property array $rules + * @property bool $is_smart + * @property string $name + * @property user $user * * @method static Builder orderBy(string $field, string $order = 'asc') */ @@ -25,6 +26,7 @@ class Playlist extends Model { use CanFilterByUser; use HasFactory; + use Searchable; protected $hidden = ['user_id', 'created_at', 'updated_at']; protected $guarded = ['id']; @@ -48,4 +50,13 @@ class Playlist extends Model { return (bool) $this->rules; } + + /** @return array */ + public function toSearchableArray(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + ]; + } } diff --git a/app/Models/Song.php b/app/Models/Song.php index 38027c45..8181c348 100644 --- a/app/Models/Song.php +++ b/app/Models/Song.php @@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Collection; +use Laravel\Scout\Searchable; /** * @property string $path @@ -41,6 +42,7 @@ use Illuminate\Support\Collection; class Song extends Model { use HasFactory; + use Searchable; use SupportsDeleteWhereIDsNotIn; protected $guarded = []; @@ -241,6 +243,25 @@ class Song extends Model return compact('bucket', 'key'); } + /** @return array */ + public function toSearchableArray(): array + { + $array = [ + 'id' => $this->id, + 'title' => $this->title, + ]; + + if (!$this->artist->is_unknown && !$this->artist->is_various) { + $array['artist'] = $this->artist->name; + } + + if (!$this->album->is_unknown) { + $array['album'] = $this->album->name; + } + + return $array; + } + public function __toString(): string { return $this->id; diff --git a/composer.json b/composer.json index 2bc743c5..b8a15c3c 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,8 @@ "intervention/image": "^2.5", "laravel/sanctum": "^2.6", "doctrine/dbal": "^2.10", - "lstrojny/functional-php": "^1.14" + "lstrojny/functional-php": "^1.14", + "teamtnt/laravel-scout-tntsearch-driver": "^11.1" }, "require-dev": { "mockery/mockery": "~1.0", diff --git a/composer.lock b/composer.lock index 3ca136c2..b3d32f47 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "becead0effd39fcac75e3efa67c6f3af", + "content-hash": "e6435a1c6c0bb1c2e691c892936a2366", "packages": [ { "name": "aws/aws-sdk-php", @@ -1664,6 +1664,75 @@ }, "time": "2020-11-24T17:31:19+00:00" }, + { + "name": "laravel/scout", + "version": "v8.5.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/scout.git", + "reference": "b8b8a35cc3ac82cbc07dfa83955492d2ef6e237b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/scout/zipball/b8b8a35cc3ac82cbc07dfa83955492d2ef6e237b", + "reference": "b8b8a35cc3ac82cbc07dfa83955492d2ef6e237b", + "shasum": "" + }, + "require": { + "illuminate/bus": "^6.0|^7.0|^8.0", + "illuminate/contracts": "^6.0|^7.0|^8.0", + "illuminate/database": "^6.0|^7.0|^8.0", + "illuminate/http": "^6.0|^7.0|^8.0", + "illuminate/pagination": "^6.0|^7.0|^8.0", + "illuminate/queue": "^6.0|^7.0|^8.0", + "illuminate/support": "^6.0|^7.0|^8.0", + "php": "^7.2|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^8.0|^9.3" + }, + "suggest": { + "algolia/algoliasearch-client-php": "Required to use the Algolia engine (^2.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Scout\\ScoutServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Scout\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Scout provides a driver based solution to searching your Eloquent models.", + "keywords": [ + "algolia", + "laravel", + "search" + ], + "support": { + "issues": "https://github.com/laravel/scout/issues", + "source": "https://github.com/laravel/scout" + }, + "time": "2020-12-22T17:29:20+00:00" + }, { "name": "league/commonmark", "version": "1.5.7", @@ -5482,6 +5551,154 @@ ], "time": "2020-12-16T17:02:19+00:00" }, + { + "name": "teamtnt/laravel-scout-tntsearch-driver", + "version": "v11.1.0", + "source": { + "type": "git", + "url": "https://github.com/teamtnt/laravel-scout-tntsearch-driver.git", + "reference": "a9c27a68dc2bd74fb354165633520de95708215d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/teamtnt/laravel-scout-tntsearch-driver/zipball/a9c27a68dc2bd74fb354165633520de95708215d", + "reference": "a9c27a68dc2bd74fb354165633520de95708215d", + "shasum": "" + }, + "require": { + "illuminate/bus": "~5.4|^6.0|^7.0|^8.0", + "illuminate/contracts": "~5.4|^6.0|^7.0|^8.0", + "illuminate/database": "~5.4|^6.0|^7.0|^8.0", + "illuminate/pagination": "~5.4|^6.0|^7.0|^8.0", + "illuminate/queue": "~5.4|^6.0|^7.0|^8.0", + "illuminate/support": "~5.4|^6.0|^7.0|^8.0", + "laravel/scout": "7.*|^8.0|^8.3", + "php": ">=7.1", + "teamtnt/tntsearch": "2.*" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^8.0|^9.3" + }, + "suggest": { + "teamtnt/tntsearch": "Required to use the TNTSearch engine." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + }, + "laravel": { + "providers": [ + "TeamTNT\\Scout\\TNTSearchScoutServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "TeamTNT\\Scout\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "TNT Studio", + "email": "info@tntstudio.hr" + } + ], + "description": "Driver for Laravel Scout search package based on https://github.com/teamtnt/tntsearch", + "keywords": [ + "laravel", + "scout", + "search", + "tntsearch" + ], + "support": { + "issues": "https://github.com/teamtnt/laravel-scout-tntsearch-driver/issues", + "source": "https://github.com/teamtnt/laravel-scout-tntsearch-driver/tree/v11.1.0" + }, + "time": "2020-11-11T11:17:48+00:00" + }, + { + "name": "teamtnt/tntsearch", + "version": "v2.6.0", + "source": { + "type": "git", + "url": "https://github.com/teamtnt/tntsearch.git", + "reference": "d9b2d764491c87f03ec214ed8dbc27336cf0c0e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/teamtnt/tntsearch/zipball/d9b2d764491c87f03ec214ed8dbc27336cf0c0e4", + "reference": "d9b2d764491c87f03ec214ed8dbc27336cf0c0e4", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-pdo_sqlite": "*", + "ext-sqlite3": "*", + "php": "~7.1|^8" + }, + "require-dev": { + "phpunit/phpunit": "7.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "TeamTNT\\TNTSearch\\": "src" + }, + "files": [ + "helper/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nenad Tičarić", + "email": "nticaric@gmail.com", + "homepage": "http://www.tntstudio.us", + "role": "Developer" + } + ], + "description": "A fully featured full text search engine written in PHP", + "homepage": "https://github.com/teamtnt/tntsearch", + "keywords": [ + "Fuzzy search", + "bm25", + "fulltext", + "geosearch", + "search", + "stemming", + "teamtnt", + "text classification", + "tntsearch" + ], + "support": { + "issues": "https://github.com/teamtnt/tntsearch/issues", + "source": "https://github.com/teamtnt/tntsearch/tree/v2.6.0" + }, + "funding": [ + { + "url": "https://ko-fi.com/nticaric", + "type": "ko_fi" + }, + { + "url": "https://opencollective.com/tntsearch", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/nticaric", + "type": "patreon" + } + ], + "time": "2020-12-21T09:11:54+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "2.2.3", diff --git a/config/app.php b/config/app.php index a57f3e54..4e564b1a 100644 --- a/config/app.php +++ b/config/app.php @@ -127,6 +127,9 @@ return [ Jackiedo\DotenvEditor\DotenvEditorServiceProvider::class, Intervention\Image\ImageServiceProvider::class, + Laravel\Scout\ScoutServiceProvider::class, + TeamTNT\Scout\TNTSearchScoutServiceProvider::class, + /* * Application Service Providers... */ diff --git a/config/scout.php b/config/scout.php new file mode 100644 index 00000000..ea2561e5 --- /dev/null +++ b/config/scout.php @@ -0,0 +1,117 @@ + env('SCOUT_DRIVER', 'tntsearch'), + + /* + |-------------------------------------------------------------------------- + | Index Prefix + |-------------------------------------------------------------------------- + | + | Here you may specify a prefix that will be applied to all search index + | names used by Scout. This prefix may be useful if you have multiple + | "tenants" or applications sharing the same search infrastructure. + | + */ + + 'prefix' => env('SCOUT_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Queue Data Syncing + |-------------------------------------------------------------------------- + | + | This option allows you to control if the operations that sync your data + | with your search engines are queued. When this is set to "true" then + | all automatic data syncing will get queued for better performance. + | + */ + + 'queue' => env('SCOUT_QUEUE', false), + + /* + |-------------------------------------------------------------------------- + | Chunk Sizes + |-------------------------------------------------------------------------- + | + | These options allow you to control the maximum chunk size when you are + | mass importing data into the search engine. This allows you to fine + | tune each of these chunk sizes based on the power of the servers. + | + */ + + 'chunk' => [ + 'searchable' => 500, + 'unsearchable' => 500, + ], + + /* + |-------------------------------------------------------------------------- + | Soft Deletes + |-------------------------------------------------------------------------- + | + | This option allows to control whether to keep soft deleted records in + | the search indexes. Maintaining soft deleted records can be useful + | if your application still needs to search for the records later. + | + */ + + 'soft_delete' => false, + + /* + |-------------------------------------------------------------------------- + | Identify User + |-------------------------------------------------------------------------- + | + | This option allows you to control whether to notify the search engine + | of the user performing the search. This is sometimes useful if the + | engine supports any analytics based on this application's users. + | + | Supported engines: "algolia" + | + */ + + 'identify' => env('SCOUT_IDENTIFY', false), + + /* + |-------------------------------------------------------------------------- + | Algolia Configuration + |-------------------------------------------------------------------------- + | + | Here you may configure your Algolia settings. Algolia is a cloud hosted + | search engine which works great with Scout out of the box. Just plug + | in your application ID and admin API key to get started searching. + | + */ + + 'algolia' => [ + 'id' => env('ALGOLIA_APP_ID', ''), + 'secret' => env('ALGOLIA_SECRET', ''), + ], + + 'tntsearch' => [ + 'storage' => storage_path('search-indexes'), + 'fuzziness' => env('TNTSEARCH_FUZZINESS', true), + 'fuzzy' => [ + 'prefix_length' => 2, + 'max_expansions' => 50, + 'distance' => 2, + ], + 'asYouType' => false, + 'searchBoolean' => env('TNTSEARCH_BOOLEAN', false), + ], +]; diff --git a/storage/search-indexes/.gitignore b/storage/search-indexes/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/storage/search-indexes/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore