Laravel المتقدم

البحث النصي الكامل مع Scout

18 دقيقة الدرس 13 من 40

البحث النصي الكامل مع Laravel Scout

يوفر Laravel Scout حلاً بسيطاً يعتمد على برامج التشغيل لإضافة البحث النصي الكامل إلى نماذج Eloquent الخاصة بك. يدعم Scout محركات بحث متعددة بما في ذلك Algolia و Meilisearch ومحركات مخصصة. في هذا الدرس، سنستكشف كيفية تنفيذ وظائف بحث قوية في تطبيقات Laravel الخاصة بك.

تثبيت وتكوين Scout

أولاً، قم بتثبيت Scout واختر برنامج تشغيل محرك البحث المفضل لديك.

# تثبيت Scout composer require laravel/scout # نشر التكوين php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider" # تثبيت برنامج تشغيل Algolia composer require algolia/algoliasearch-client-php # أو تثبيت برنامج تشغيل Meilisearch composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle # التكوين في .env SCOUT_DRIVER=algolia ALGOLIA_APP_ID=your-app-id ALGOLIA_SECRET=your-secret-key # أو لـ Meilisearch SCOUT_DRIVER=meilisearch MEILISEARCH_HOST=http://127.0.0.1:7700 MEILISEARCH_KEY=your-master-key
ملاحظة: Meilisearch هو بديل ممتاز مفتوح المصدر لـ Algolia. يمكنك تشغيله محلياً عبر Docker: docker run -d -p 7700:7700 getmeili/meilisearch:latest

جعل النماذج قابلة للبحث

أضف سمة Searchable إلى أي نموذج تريد جعله قابلاً للبحث.

namespace App\Models; use Illuminate\Database\Eloquent\Model; use Laravel\Scout\Searchable; class Post extends Model { use Searchable; /** * احصل على مصفوفة البيانات القابلة للفهرسة للنموذج. */ public function toSearchableArray() { return [ 'id' => $this->id, 'title' => $this->title, 'content' => $this->content, 'excerpt' => $this->excerpt, 'author' => $this->author->name, 'category' => $this->category->name, 'tags' => $this->tags->pluck('name')->toArray(), 'published_at' => $this->published_at->timestamp, ]; } /** * احصل على اسم الفهرس للنموذج. */ public function searchableAs() { return 'posts_index'; } /** * حدد ما إذا كان يجب أن يكون النموذج قابلاً للبحث. */ public function shouldBeSearchable() { return $this->is_published; } } // نموذج المنتج مع فهرسة مخصصة class Product extends Model { use Searchable; public function toSearchableArray() { return [ 'id' => $this->id, 'name' => $this->name, 'description' => $this->description, 'price' => $this->price, 'stock' => $this->stock, 'category' => $this->category->name, 'brand' => $this->brand, 'sku' => $this->sku, 'rating' => $this->averageRating(), 'is_available' => $this->stock > 0, ]; } public function shouldBeSearchable() { return $this->status === 'active' && $this->stock > 0; } }

فهرسة البيانات

يقوم Scout تلقائياً بفهرسة النماذج عند إنشائها أو تحديثها. يمكنك أيضاً التحكم في الفهرسة يدوياً.

# استيراد جميع السجلات الموجودة إلى فهرس البحث php artisan scout:import "App\Models\Post" # مسح جميع السجلات من الفهرس php artisan scout:flush "App\Models\Post" # حذف وإعادة استيراد (مفيد لتغييرات المخطط) php artisan scout:flush "App\Models\Post" php artisan scout:import "App\Models\Post" // فهرسة نموذج يدوياً $post = Post::find(1); $post->searchable(); // فهرسة نماذج متعددة $posts = Post::where('is_published', true)->get(); $posts->searchable(); // الإزالة من الفهرس $post->unsearchable(); // إيقاف الفهرسة مؤقتاً أثناء العمليات الدفعية Post::withoutSyncingToSearch(function () { Post::where('spam', true)->delete(); }); // إيقاف المزامنة بشكل مشروط if ($this->app->environment('testing')) { Model::disableSearchSyncing(); }
نصيحة: استخدم withoutSyncingToSearch عند إجراء عمليات مجمعة لتجنب إجراء مئات من استدعاءات API إلى خدمة البحث الخاصة بك. أعد الاستيراد بعد اكتمال العمليات المجمعة.

استعلامات البحث الأساسية

قم بإجراء عمليات البحث باستخدام منشئ الاستعلامات البديهي لـ Scout.

// بحث بسيط $posts = Post::search('Laravel tutorial')->get(); // بحث بصفحات $posts = Post::search('Vue.js')->paginate(15); // بحث مع حجم صفحة مخصص $posts = Post::search('PHP')->paginate(20); // الحصول على صفحة محددة $posts = Post::search('JavaScript')->paginate(15, 'page', 2); // البحث والتصفية حسب السمات $posts = Post::search('Laravel') ->where('is_published', true) ->where('category_id', 5) ->get(); // البحث مع callback استعلام للقيود الإضافية $posts = Post::search('tutorial')->query(function ($builder) { $builder->where('views', '>', 1000); })->get(); // الحصول على إجمالي عدد النتائج $count = Post::search('Laravel')->count(); // البحث مع ترتيب مخصص $posts = Post::search('programming') ->orderBy('published_at', 'desc') ->get(); // البحث في المتحكم public function search(Request $request) { $query = $request->input('q'); $posts = Post::search($query) ->where('is_published', 1) ->query(fn($builder) => $builder->with(['author', 'category'])) ->paginate(15); return view('posts.search', [ 'posts' => $posts, 'query' => $query, ]); }

البحث المتقدم مع التصفية

نفذ سيناريوهات بحث معقدة مع مرشحات متعددة وبحث وجهي.

// البحث عن المنتجات مع مرشحات متعددة public function searchProducts(Request $request) { $search = Product::search($request->input('query', '')); // التصفية حسب الفئة if ($request->has('category')) { $search->where('category_id', $request->category); } // التصفية حسب نطاق السعر if ($request->has('min_price')) { $search->where('price', '>=', $request->min_price); } if ($request->has('max_price')) { $search->where('price', '<=', $request->max_price); } // التصفية حسب العلامة التجارية if ($request->has('brands')) { $search->whereIn('brand', $request->brands); } // التصفية حسب التوفر if ($request->boolean('in_stock')) { $search->where('is_available', true); } // خيارات الفرز $sortBy = $request->input('sort', 'relevance'); switch ($sortBy) { case 'price_low': $search->orderBy('price', 'asc'); break; case 'price_high': $search->orderBy('price', 'desc'); break; case 'rating': $search->orderBy('rating', 'desc'); break; // 'relevance' هو الترتيب الافتراضي لـ Scout } return $search->paginate(24); } // البحث المتقدم مع الأوجه (Algolia) use Algolia\AlgoliaSearch\SearchIndex; public function searchWithFacets($query) { $index = app(SearchIndex::class, ['indexName' => 'products_index']); $results = $index->search($query, [ 'facets' => ['category', 'brand', 'color'], 'filters' => 'price > 10 AND price < 1000', 'attributesToRetrieve' => ['name', 'price', 'image'], 'hitsPerPage' => 20, ]); return [ 'hits' => $results['hits'], 'facets' => $results['facets'], 'total' => $results['nbHits'], ]; }
تحذير: الطريقة where في Scout تدعم فقط فحوصات المساواة الأساسية. للتصفية المعقدة (استعلامات النطاق، شروط OR)، استخدم callback query أو API الأصلية لمحرك البحث.

محركات البحث المخصصة

أنشئ محركات Scout مخصصة للتكامل مع أي خدمة بحث أو لتنفيذ منطق مخصص.

namespace App\Scout\Engines; use Laravel\Scout\Engines\Engine; use Laravel\Scout\Builder; class CustomSearchEngine extends Engine { public function update($models) { // فهرسة النماذج المعطاة $models->each(function ($model) { // منطق الفهرسة المخصص الخاص بك $data = $model->toSearchableArray(); // إرسال إلى خدمة البحث الخاصة بك Http::post('https://your-search-api.com/index', [ 'id' => $model->getScoutKey(), 'data' => $data, ]); }); } public function delete($models) { // إزالة النماذج المعطاة من الفهرس $models->each(function ($model) { Http::delete("https://your-search-api.com/index/{$model->getScoutKey()}"); }); } public function search(Builder $builder) { return $this->performSearch($builder, [ 'filters' => $this->filters($builder), 'limit' => $builder->limit, ]); } public function paginate(Builder $builder, $perPage, $page) { return $this->performSearch($builder, [ 'filters' => $this->filters($builder), 'limit' => $perPage, 'offset' => ($page - 1) * $perPage, ]); } protected function performSearch(Builder $builder, array $options = []) { $response = Http::post('https://your-search-api.com/search', [ 'query' => $builder->query, 'index' => $builder->index ?: $builder->model->searchableAs(), 'options' => $options, ]); return $response->json(); } public function mapIds($results) { return collect($results['hits'])->pluck('id')->values(); } public function map(Builder $builder, $results, $model) { if (count($results['hits']) === 0) { return $model->newCollection(); } $objectIds = $this->mapIds($results); $models = $model->getScoutModelsByIds( $builder, $objectIds )->keyBy(function ($model) { return $model->getScoutKey(); }); return collect($results['hits'])->map(function ($hit) use ($models) { $key = $hit['id']; return $models[$key] ?? null; })->filter()->values(); } public function getTotalCount($results) { return $results['total'] ?? 0; } public function flush($model) { Http::delete("https://your-search-api.com/index/{$model->searchableAs()}"); } protected function filters(Builder $builder) { return collect($builder->wheres)->map(function ($value, $key) { return "{$key}={$value}"; })->values()->all(); } } // تسجيل المحرك في مزود الخدمة use App\Scout\Engines\CustomSearchEngine; public function boot() { resolve(EngineManager::class)->extend('custom', function () { return new CustomSearchEngine(); }); } // الاستخدام في config/scout.php 'driver' => env('SCOUT_DRIVER', 'custom'),

تسليط الضوء على نتائج البحث

نفذ تسليط الضوء على مصطلحات البحث لتحسين تجربة المستخدم.

// في نموذجك use Illuminate\Support\Str; public function getHighlightedTitleAttribute() { $query = request()->input('query', ''); if (empty($query)) { return $this->title; } return $this->highlightText($this->title, $query); } public function getHighlightedContentAttribute() { $query = request()->input('query', ''); if (empty($query)) { return Str::limit($this->content, 200); } $highlighted = $this->highlightText($this->content, $query); return Str::limit($highlighted, 200); } protected function highlightText($text, $query) { $terms = explode(' ', $query); foreach ($terms as $term) { if (strlen($term) < 3) continue; $pattern = '/(' . preg_quote($term, '/') . ')/i'; $text = preg_replace( $pattern, '<mark class="highlight">$1</mark>', $text ); } return $text; } // في العرض الخاص بك <div class="search-result"> <h3>{!! $post->highlighted_title !!}</h3> <p>{!! $post->highlighted_content !!}</p> </div> // CSS لتسليط الضوء <style> .highlight { background-color: yellow; font-weight: bold; padding: 2px 4px; } </style>
تمرين 1: أنشئ ميزة بحث متعددة النماذج تبحث عبر المنشورات والمنتجات والمستخدمين في وقت واحد. اعرض النتائج مجمعة حسب نوع النموذج مع واجهة علامات تبويب للتبديل بين أنواع النتائج.
تمرين 2: ابنِ بحثاً متقدماً عن المنتجات مع تصفية وجهية. قم بتضمين مرشحات للفئة ونطاق السعر والعلامة التجارية والتقييم والتوفر. اعرض عدد النتائج لكل خيار مرشح واسمح للمستخدمين بدمج مرشحات متعددة.
تمرين 3: نفذ نظام تحليلات البحث الذي يتتبع استعلامات البحث الشائعة، والاستعلامات بدون نتائج، ومعدلات نقر المستخدم على نتائج البحث. أنشئ لوحة تحكم إدارية لعرض هذه التحليلات وتحديد فجوات المحتوى.