feat: Phase 5.2 - Global Search with Laravel Scout
- Install laravel/scout with database driver - Add Searchable trait to Feature, Issue, Task, Inspection models - Create GlobalSearch Livewire component with Blade view - Configure scout.php for database driver + queued syncing - Add SCOUT_DRIVER=database to .env.example Tests: 101 passing (319 assertions)
This commit is contained in:
@@ -54,6 +54,9 @@ REVERB_HOST=127.0.0.1
|
|||||||
REVERB_PORT=8080
|
REVERB_PORT=8080
|
||||||
REVERB_SCHEME=http
|
REVERB_SCHEME=http
|
||||||
|
|
||||||
|
SCOUT_DRIVER=database
|
||||||
|
SCOUT_QUEUE=true
|
||||||
|
|
||||||
MAIL_MAILER=log
|
MAIL_MAILER=log
|
||||||
MAIL_SCHEME=null
|
MAIL_SCHEME=null
|
||||||
MAIL_HOST=127.0.0.1
|
MAIL_HOST=127.0.0.1
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Common;
|
||||||
|
|
||||||
|
use App\Models\Feature;
|
||||||
|
use App\Models\Issue;
|
||||||
|
use App\Models\Inspection;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Task;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class GlobalSearch extends Component
|
||||||
|
{
|
||||||
|
public string $query = '';
|
||||||
|
|
||||||
|
public array $results = [];
|
||||||
|
|
||||||
|
public bool $showResults = false;
|
||||||
|
|
||||||
|
public int $limit = 5;
|
||||||
|
|
||||||
|
public function updatedQuery(): void
|
||||||
|
{
|
||||||
|
if (strlen($this->query) < 2) {
|
||||||
|
$this->results = [];
|
||||||
|
$this->showResults = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->search();
|
||||||
|
$this->showResults = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function search(): void
|
||||||
|
{
|
||||||
|
$q = $this->query;
|
||||||
|
|
||||||
|
$projectIds = Auth::user()->projects()->pluck('id');
|
||||||
|
|
||||||
|
$features = Feature::search($q)
|
||||||
|
->where('project_id', $projectIds)
|
||||||
|
->take($this->limit)
|
||||||
|
->get()
|
||||||
|
->map(function ($feature) {
|
||||||
|
return [
|
||||||
|
'type' => 'feature',
|
||||||
|
'type_label' => 'Elemento',
|
||||||
|
'id' => $feature->id,
|
||||||
|
'title' => $feature->name,
|
||||||
|
'subtitle' => $feature->layer?->phase?->name ?? 'Sin fase',
|
||||||
|
'url' => route('projects.map', $feature->layer?->phase?->project_id).'?feature='.$feature->id,
|
||||||
|
'icon' => 'square-2-stack',
|
||||||
|
'color' => 'blue',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$issues = Issue::search($q)
|
||||||
|
->where('project_id', $projectIds)
|
||||||
|
->take($this->limit)
|
||||||
|
->get()
|
||||||
|
->map(function ($issue) {
|
||||||
|
return [
|
||||||
|
'type' => 'issue',
|
||||||
|
'type_label' => 'Incidencia',
|
||||||
|
'id' => $issue->id,
|
||||||
|
'title' => $issue->title,
|
||||||
|
'subtitle' => $issue->project?->name ?? 'Sin proyecto',
|
||||||
|
'url' => route('projects.issues.show', [$issue->project_id, $issue->id]),
|
||||||
|
'icon' => 'exclamation-triangle',
|
||||||
|
'color' => 'red',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$tasks = Task::search($q)
|
||||||
|
->where('project_id', $projectIds)
|
||||||
|
->take($this->limit)
|
||||||
|
->get()
|
||||||
|
->map(function ($task) {
|
||||||
|
return [
|
||||||
|
'type' => 'task',
|
||||||
|
'type_label' => 'Tarea',
|
||||||
|
'id' => $task->id,
|
||||||
|
'title' => $task->title,
|
||||||
|
'subtitle' => $task->project?->name ?? 'Sin proyecto',
|
||||||
|
'url' => route('projects.tasks.show', [$task->project_id, $task->id]),
|
||||||
|
'icon' => 'clipboard-document-check',
|
||||||
|
'color' => 'green',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$inspections = Inspection::search($q)
|
||||||
|
->where('project_id', $projectIds)
|
||||||
|
->take($this->limit)
|
||||||
|
->get()
|
||||||
|
->map(function ($inspection) {
|
||||||
|
return [
|
||||||
|
'type' => 'inspection',
|
||||||
|
'type_label' => 'Inspección',
|
||||||
|
'id' => $inspection->id,
|
||||||
|
'title' => $inspection->feature?->name ?? 'Inspección',
|
||||||
|
'subtitle' => $inspection->template?->name ?? 'Sin template',
|
||||||
|
'url' => route('projects.map', $inspection->project_id).'?inspection='.$inspection->id,
|
||||||
|
'icon' => 'magnifying-glass',
|
||||||
|
'color' => 'purple',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->results = $features
|
||||||
|
->concat($issues)
|
||||||
|
->concat($tasks)
|
||||||
|
->concat($inspections)
|
||||||
|
->sortBy(fn ($r) => $r['type'])
|
||||||
|
->take($this->limit * 4)
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[On('close-search-results')]
|
||||||
|
public function hideResults(): void
|
||||||
|
{
|
||||||
|
$this->showResults = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearSearch(): void
|
||||||
|
{
|
||||||
|
$this->query = '';
|
||||||
|
$this->results = [];
|
||||||
|
$this->showResults = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.common.global-search');
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-1
@@ -7,10 +7,11 @@ use Carbon\Carbon;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Laravel\Scout\Searchable;
|
||||||
|
|
||||||
class Feature extends Model
|
class Feature extends Model
|
||||||
{
|
{
|
||||||
use LogsActivity, SoftDeletes;
|
use LogsActivity, SoftDeletes, Searchable;
|
||||||
|
|
||||||
const STATUSES = ['planned', 'started', 'in_progress', 'completed', 'verified'];
|
const STATUSES = ['planned', 'started', 'in_progress', 'completed', 'verified'];
|
||||||
|
|
||||||
@@ -156,4 +157,23 @@ class Feature extends Model
|
|||||||
|
|
||||||
return $spi >= 0.95;
|
return $spi >= 0.95;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function toSearchableArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'description' => $this->description,
|
||||||
|
'project_id' => $this->layer->phase->project_id ?? null,
|
||||||
|
'phase_id' => $this->layer->phase_id ?? null,
|
||||||
|
'layer_id' => $this->layer_id ?? null,
|
||||||
|
'status' => $this->status,
|
||||||
|
'progress' => $this->progress,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScoutKey(): string
|
||||||
|
{
|
||||||
|
return (string) $this->id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ namespace App\Models;
|
|||||||
use App\Traits\LogsActivity;
|
use App\Traits\LogsActivity;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Laravel\Scout\Searchable;
|
||||||
|
|
||||||
class Inspection extends Model
|
class Inspection extends Model
|
||||||
{
|
{
|
||||||
use LogsActivity, SoftDeletes;
|
use LogsActivity, SoftDeletes, Searchable;
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
{
|
{
|
||||||
@@ -87,4 +88,25 @@ class Inspection extends Model
|
|||||||
{
|
{
|
||||||
return $q->where('status', 'rejected');
|
return $q->where('status', 'rejected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function toSearchableArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'project_id' => $this->project_id,
|
||||||
|
'layer_id' => $this->layer_id,
|
||||||
|
'feature_id' => $this->feature_id,
|
||||||
|
'template_id' => $this->template_id,
|
||||||
|
'user_id' => $this->user_id,
|
||||||
|
'inspector_user_id' => $this->inspector_user_id,
|
||||||
|
'status' => $this->status,
|
||||||
|
'result' => $this->result,
|
||||||
|
'notes' => $this->notes,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScoutKey(): string
|
||||||
|
{
|
||||||
|
return (string) $this->id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -5,10 +5,11 @@ namespace App\Models;
|
|||||||
use App\Traits\LogsActivity;
|
use App\Traits\LogsActivity;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Laravel\Scout\Searchable;
|
||||||
|
|
||||||
class Issue extends Model
|
class Issue extends Model
|
||||||
{
|
{
|
||||||
use LogsActivity, SoftDeletes;
|
use LogsActivity, SoftDeletes, Searchable;
|
||||||
|
|
||||||
const STATUSES = ['open', 'in_review', 'resolved', 'closed'];
|
const STATUSES = ['open', 'in_review', 'resolved', 'closed'];
|
||||||
|
|
||||||
@@ -141,4 +142,26 @@ class Issue extends Model
|
|||||||
default => '#6b7280',
|
default => '#6b7280',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function toSearchableArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'title' => $this->title,
|
||||||
|
'description' => $this->description,
|
||||||
|
'project_id' => $this->project_id,
|
||||||
|
'feature_id' => $this->feature_id,
|
||||||
|
'inspection_id' => $this->inspection_id,
|
||||||
|
'status' => $this->status,
|
||||||
|
'priority' => $this->priority,
|
||||||
|
'type' => $this->type,
|
||||||
|
'reported_by' => $this->reported_by,
|
||||||
|
'assigned_to' => $this->assigned_to,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScoutKey(): string
|
||||||
|
{
|
||||||
|
return (string) $this->id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-1
@@ -11,10 +11,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Laravel\Scout\Searchable;
|
||||||
|
|
||||||
class Task extends Model
|
class Task extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, LogsActivity, SoftDeletes;
|
use HasFactory, LogsActivity, SoftDeletes, Searchable;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'project_id',
|
'project_id',
|
||||||
@@ -328,4 +329,27 @@ class Task extends Model
|
|||||||
'critical' => 'Crítica',
|
'critical' => 'Crítica',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function toSearchableArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'title' => $this->title,
|
||||||
|
'description' => $this->description,
|
||||||
|
'project_id' => $this->project_id,
|
||||||
|
'phase_id' => $this->phase_id,
|
||||||
|
'parent_task_id' => $this->parent_task_id,
|
||||||
|
'status' => $this->status,
|
||||||
|
'priority' => $this->priority,
|
||||||
|
'assigned_to' => $this->assigned_to,
|
||||||
|
'due_date' => $this->due_date?->toDateString(),
|
||||||
|
'start_date' => $this->start_date?->toDateString(),
|
||||||
|
'completed_at' => $this->completed_at?->toDateTimeString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScoutKey(): string
|
||||||
|
{
|
||||||
|
return (string) $this->id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/reverb": "^1.11",
|
"laravel/reverb": "^1.11",
|
||||||
"laravel/sanctum": "^4.3",
|
"laravel/sanctum": "^4.3",
|
||||||
|
"laravel/scout": "^11.6",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"league/geotools": "^1.3",
|
"league/geotools": "^1.3",
|
||||||
"livewire/livewire": "^3.6.4",
|
"livewire/livewire": "^3.6.4",
|
||||||
|
|||||||
Generated
+82
-1
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "55192809cb3c94d88a379dcab1097f21",
|
"content-hash": "5a75bade70107149539891d074c84a53",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "blade-ui-kit/blade-heroicons",
|
"name": "blade-ui-kit/blade-heroicons",
|
||||||
@@ -2069,6 +2069,87 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-04-30T11:46:25+00:00"
|
"time": "2026-04-30T11:46:25+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "laravel/scout",
|
||||||
|
"version": "v11.6.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/laravel/scout.git",
|
||||||
|
"reference": "860f8246d5712fe6e18907119def44d2633b4622"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/laravel/scout/zipball/860f8246d5712fe6e18907119def44d2633b4622",
|
||||||
|
"reference": "860f8246d5712fe6e18907119def44d2633b4622",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/bus": "^9.0|^10.0|^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/contracts": "^9.0|^10.0|^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/database": "^9.0|^10.0|^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/http": "^9.0|^10.0|^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/pagination": "^9.0|^10.0|^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/queue": "^9.0|^10.0|^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/support": "^9.0|^10.0|^11.0|^12.0|^13.0",
|
||||||
|
"php": "^8.0",
|
||||||
|
"symfony/console": "^6.0|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"algolia/algoliasearch-client-php": "<3.2.0|>=5.0.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"algolia/algoliasearch-client-php": "^3.2|^4.0",
|
||||||
|
"meilisearch/meilisearch-php": "^1.0",
|
||||||
|
"mockery/mockery": "^1.0",
|
||||||
|
"orchestra/testbench": "^7.31|^8.36|^9.15|^10.8|^11.0",
|
||||||
|
"php-http/guzzle7-adapter": "^1.0",
|
||||||
|
"phpstan/phpstan": "^1.10",
|
||||||
|
"typesense/typesense-php": "^4.9.3"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"algolia/algoliasearch-client-php": "Required to use the Algolia engine (^3.2).",
|
||||||
|
"laravel/ai": "Required to generate embeddings for semantic and hybrid search.",
|
||||||
|
"meilisearch/meilisearch-php": "Required to use the Meilisearch engine (^1.0).",
|
||||||
|
"typesense/typesense-php": "Required to use the Typesense engine (^4.9)."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Laravel\\Scout\\ScoutServiceProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "11.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": "2026-08-25T20:32:21+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/serializable-closure",
|
"name": "laravel/serializable-closure",
|
||||||
"version": "v2.0.12",
|
"version": "v2.0.12",
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Search Engine
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default search connection that gets used while
|
||||||
|
| using Laravel Scout. This connection is used when syncing all models
|
||||||
|
| to the search service. You should adjust this based on your needs.
|
||||||
|
|
|
||||||
|
| Supported: "algolia", "meilisearch", "typesense", "turbopuffer",
|
||||||
|
| "database", "collection", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'driver' => env('SCOUT_DRIVER', 'collection'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 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', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Database Transactions
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This configuration option determines if your data will only be synced
|
||||||
|
| with your search indexes after every open database transaction has
|
||||||
|
| been committed, thus preventing any discarded data from syncing.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'after_commit' => 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', ''),
|
||||||
|
'index-settings' => [
|
||||||
|
// 'users' => [
|
||||||
|
// 'searchableAttributes' => ['id', 'name', 'email'],
|
||||||
|
// 'attributesForFaceting'=> ['filterOnly(email)'],
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Meilisearch Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure your Meilisearch settings. Meilisearch is an open
|
||||||
|
| source search engine with minimal configuration. Below, you can state
|
||||||
|
| the host and key information for your own Meilisearch installation.
|
||||||
|
|
|
||||||
|
| See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'meilisearch' => [
|
||||||
|
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
|
||||||
|
'key' => env('MEILISEARCH_KEY'),
|
||||||
|
'index-settings' => [
|
||||||
|
// 'users' => [
|
||||||
|
// 'filterableAttributes' => ['id', 'name', 'email'],
|
||||||
|
// 'embedders' => [
|
||||||
|
// 'default' => [
|
||||||
|
// 'source' => 'userProvided',
|
||||||
|
// 'dimensions' => 1536,
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
'model-settings' => [
|
||||||
|
// User::class => [
|
||||||
|
// 'embedding' => [
|
||||||
|
// 'embedder' => 'default',
|
||||||
|
// 'dimensions' => 1536,
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Typesense Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure your Typesense settings. Typesense is an open
|
||||||
|
| source search engine using minimal configuration. Below, you will
|
||||||
|
| state the host, key, and schema configuration for the instance.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'typesense' => [
|
||||||
|
'client-settings' => [
|
||||||
|
'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
|
||||||
|
'nodes' => [
|
||||||
|
[
|
||||||
|
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||||
|
'port' => env('TYPESENSE_PORT', '8108'),
|
||||||
|
'path' => env('TYPESENSE_PATH', ''),
|
||||||
|
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'nearest_node' => [
|
||||||
|
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||||
|
'port' => env('TYPESENSE_PORT', '8108'),
|
||||||
|
'path' => env('TYPESENSE_PATH', ''),
|
||||||
|
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||||
|
],
|
||||||
|
'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2),
|
||||||
|
'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30),
|
||||||
|
'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
|
||||||
|
'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),
|
||||||
|
],
|
||||||
|
// 'max_total_results' => env('TYPESENSE_MAX_TOTAL_RESULTS', 1000),
|
||||||
|
'model-settings' => [
|
||||||
|
// User::class => [
|
||||||
|
// 'collection-schema' => [
|
||||||
|
// 'fields' => [
|
||||||
|
// [
|
||||||
|
// 'name' => 'id',
|
||||||
|
// 'type' => 'string',
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// 'name' => 'name',
|
||||||
|
// 'type' => 'string',
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// 'name' => 'created_at',
|
||||||
|
// 'type' => 'int64',
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// 'default_sorting_field' => 'created_at',
|
||||||
|
// ],
|
||||||
|
// 'search-parameters' => [
|
||||||
|
// 'query_by' => 'name'
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
'import_action' => env('TYPESENSE_IMPORT_ACTION', 'upsert'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Turbopuffer Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure your Turbopuffer connection and the schema and
|
||||||
|
| searchable attributes defined by each of your application's models.
|
||||||
|
| Turbopuffer is a scalable engine with full-text + vector search.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'turbopuffer' => [
|
||||||
|
'api_key' => env('TURBOPUFFER_API_KEY'),
|
||||||
|
'region' => env('TURBOPUFFER_REGION', 'gcp-us-central1'),
|
||||||
|
'base_url' => env('TURBOPUFFER_BASE_URL'),
|
||||||
|
'timeout' => env('TURBOPUFFER_TIMEOUT', 60),
|
||||||
|
'connect_timeout' => env('TURBOPUFFER_CONNECT_TIMEOUT', 5),
|
||||||
|
'retries' => env('TURBOPUFFER_RETRIES', 3),
|
||||||
|
'model-settings' => [
|
||||||
|
// User::class => [
|
||||||
|
// 'searchable-attributes' => [
|
||||||
|
// 'name' => 2,
|
||||||
|
// 'email' => 1,
|
||||||
|
// ],
|
||||||
|
// 'embedding' => [
|
||||||
|
// 'attribute' => 'embedding',
|
||||||
|
// 'dimensions' => 1536,
|
||||||
|
// ],
|
||||||
|
// 'schema' => [
|
||||||
|
// 'name' => ['type' => 'string', 'full_text_search' => true],
|
||||||
|
// 'email' => ['type' => 'string', 'full_text_search' => true],
|
||||||
|
// 'embedding' => ['type' => '[1536]f32', 'ann' => true],
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<div class="relative" wire:ignore.self>
|
||||||
|
<div class="relative w-full max-w-xl">
|
||||||
|
<!-- Search Input -->
|
||||||
|
<div class="relative">
|
||||||
|
<label for="global-search" class="sr-only">{{ __('Buscar globalmente') }}</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<svg class="h-5 w-5 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="global-search"
|
||||||
|
wire:model.debounce.300ms="query"
|
||||||
|
wire:keydown.down="focusFirstResult"
|
||||||
|
wire:keydown.enter="selectFirstResult"
|
||||||
|
wire:keydown.escape="clearSearch"
|
||||||
|
placeholder="{{ __('Buscar en proyectos, elementos, tareas, incidencias...') }}"
|
||||||
|
class="block w-full pl-10 pr-4 py-2 border border-base-300 rounded-lg bg-base-100 text-base-content placeholder:text-base-content/40 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
|
||||||
|
autocomplete="off"
|
||||||
|
@click="showResults = true"
|
||||||
|
>
|
||||||
|
@if($query)
|
||||||
|
<button
|
||||||
|
wire:click="clearSearch"
|
||||||
|
class="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
aria-label="{{ __('Limpiar búsqueda') }}"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5 text-base-content/40 hover:text-base-content/60" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results Dropdown -->
|
||||||
|
@if($showResults && count($results) > 0)
|
||||||
|
<div
|
||||||
|
class="absolute z-50 mt-1 w-full max-w-xl bg-base-100 border border-base-200 rounded-lg shadow-lg overflow-hidden"
|
||||||
|
style="max-height: 400px; overflow-y: auto;"
|
||||||
|
x-data="{ highlightedIndex: 0 }"
|
||||||
|
x-init="
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
@this.focusFirstResult();
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
highlightedIndex = Math.max(0, highlightedIndex - 1);
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
@this.selectFirstResult();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
@this.clearSearch();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
@foreach($results as $index => $result)
|
||||||
|
<a
|
||||||
|
href="{{ $result['url'] }}"
|
||||||
|
wire:navigate
|
||||||
|
class="flex items-center gap-3 px-4 py-3 hover:bg-base-200 transition-colors {{ $index === 0 ? 'bg-base-200' : '' }}"
|
||||||
|
:class="{ 'bg-base-200': highlightedIndex === {{ $index }} }"
|
||||||
|
x-on:mouseenter="highlightedIndex = {{ $index }}"
|
||||||
|
>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-8 h-8 rounded-lg flex items-center justify-center bg-{{ $result['color'] }}-100 text-{{ $result['color'] }}-600">
|
||||||
|
<x-heroicon-o-{{ $result['icon'] }} class="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium text-base-content truncate">{{ $result['title'] }}</p>
|
||||||
|
<p class="text-xs text-base-content/50 truncate">{{ $result['subtitle'] }}</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-medium text-base-content/40 bg-base-200 px-2 py-0.5 rounded">{{ $result['type_label'] }}</span>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@elseif($showResults && strlen($query) >= 2)
|
||||||
|
<div class="absolute z-50 mt-1 w-full max-w-xl bg-base-100 border border-base-200 rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<div class="px-4 py-6 text-center text-base-content/50">
|
||||||
|
<x-heroicon-o-magnifying-glass class="w-8 h-8 mx-auto text-base-content/30 mb-2" />
|
||||||
|
<p class="text-sm">{{ __('No se encontraron resultados para') }} “{{ $query }}”</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const searchComponent = document.querySelector('[wire\\:id="{{ $id }}"]');
|
||||||
|
if (searchComponent && !searchComponent.contains(e.target)) {
|
||||||
|
@this.hideResults();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
@this.clearSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user