Google 自訂搜尋:設定簡單,但調性不符
我使用 Joomla! 系統建置的網站:華燈初上跟部落格都使用 Google 提供的自定義搜尋服務,將 Google 提供的嵌入碼加進佈景主題中,再透過「自訂 HTML」模組將搜尋表單顯示於佈景主題定義的模組位置,站內搜尋服務就完成了。
讓 Google 處理站內搜尋的好處,是將「搜尋網站內容」這件消耗伺服器效能的事委外處理,對於性能不高的共享型虛擬主機空間來說幫助很大,而且「搜尋及索引」就是 Google 的核心業務。
在學習 PHP 語法,製作 ffxitoolbox 第一版時也是使用 Google 自定義搜尋作為站內搜尋功能,不過沒過多久我就發現該服務不適合用在 ffxitoolbox 上,主要有兩點:
- Google 自定義搜尋的搜尋結果是 Google 索引過的內容,所以沒索引到的部分就找不到。加上搜尋功能的使用是輸入素材關鍵字尋找相關配方,搜尋結果是整頁的搜尋結果,點選後還要再找,不怎麼方便。
- 搜尋結果的呈現風格跟網站的設計差別很大,每次使用時很難跟網站畫面聯想在一起,雖然這不是 Google 的錯...
自建搜尋:畫面自行決定,佔用伺服器效能
2021 年初完成 ffxitoolbox 第一版製作,代表我從零開始學習 PHP 有了初步成就:我有能力使用 PHP 跟 MySQL 將 ffxi 的合成配方資料整理成網站內容提供閱覽。
在這之後我開始學習 Laravel 框架,設下的目標就是將 ffxitoolbox 以 Laravel 框架重寫:如果能夠完成代表我以經能用 Laravel 做出網站專案。就在作業接近收尾的階段,「站內搜尋」功能的建置浮出檯面,這次實在不想用 Google 自定義搜尋了...
用 Google 搜尋前人的智慧結晶時,我發現了〈How to add simple search to your Laravel blog/website?〉這篇文章,透過閱讀文章內容逐步操作,我讓 ffxitoolbox 的站內搜尋結果能以想要的方式呈現。
建立簡易站內搜尋的流程是:建立搜尋的 Controller,以 LIKE
語法搭配 %{search}%
搜尋資料庫(合成配方、分解及食物效果)中是否有符合的項目。之後將結果送到 Blade template 輸出,並且設置搜尋路由就可以了。以下是 ffxitoolbox 負責搜尋的 Controller 程式碼:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Discompose;
use App\Models\Foodresult;
use App\Models\Recipe;
class SearchController extends Controller
{
//搜尋功能
public function search(Request $request){
// 取得搜尋關鍵字
$search = $request->input('search');
// 查詢範圍
$recipes = Recipe::query()
->where('name', 'LIKE', "%{$search}%")
->orWhere('material1', 'LIKE', "%{$search}%")
->orWhere('material2', 'LIKE', "%{$search}%")
->orWhere('material3', 'LIKE', "%{$search}%")
->orWhere('material4', 'LIKE', "%{$search}%")
->orWhere('material5', 'LIKE', "%{$search}%")
->orWhere('material6', 'LIKE', "%{$search}%")
->orWhere('material7', 'LIKE', "%{$search}%")
->orWhere('material8', 'LIKE', "%{$search}%")
->get();
$discomposes = Discompose::query()
->where('material1', 'LIKE', "%{$search}%")
->orwhere('name', 'LIKE', "%{$search}%")
->orWhere('HQ1', 'LIKE', "%{$search}%")
->orWhere('HQ2', 'LIKE', "%{$search}%")
->orWhere('HQ3', 'LIKE', "%{$search}%")
->get();
$foodresults = Foodresult::query()
->where('Name', 'LIKE', "%{$search}%")
->get();
$binding = [
'search' => $search,
'discomposes' => $discomposes,
'foodresults' => $foodresults,
'recipes' => $recipes,
];
// 回傳搜尋結果,以搜尋頁面呈現
return view('frontend.search', $binding);
}
}
搜尋結果頁面是自己設計的,所以在搜尋結果中加入編輯功能也不是什麼難事。當配方資料獲得進一步確認時時我可以在前貒登入網站,利用搜尋功能找到編輯項目,選開啟編輯表單更新資料。
以一個全合成種類都有涉獵,常常會需要搜尋配方資料的人來說,當前提供的搜尋功能已經符合我的需求。雖然每次搜尋都會消耗伺服器效能,不過以使用者大概只有自己的情況下不擔心伺服器會被操掛(笑)。
外部資源加自訂顯示:Laravel Scout + Algolia
Google 提供的資源省掉運作負擔,但是頁面呈現不愛;自建站內搜尋結果符合預期,擔心使用的人多造成伺服器負載...有沒有集合兩者優點的第三種方案?有的,就是 Laravel Scout 與 Algolia 服務。
Algolia 以 SaaS 方式提供搜尋服務的,開發者可以將網站資料上傳至 Algolia 製作索引,然後透過自訂的視覺版型呈現搜尋結果,兼具效能與美觀。接下來將透過 Laravel 的 Scout 套件連接 Algolia,製作站內搜尋。
申請 Algolia 服務
前往 https://www.algolia.com/ 註冊帳號,接著為網站建立應用程式(Application)及索引(Index)名稱,之後點選畫面左下方「設定(Settings,齒輪圖示)」畫面中點選「API Keys」取得 Application ID 及 Admin API Key。
下載 Laravel Scout,設定 Algolia 通訊
在終端機畫面下載 Laravel Scout 套件
composer require laravel/scout
發佈套件,會建立 /config/scout.php
設定檔
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
在 .env
輸入與 Algolia 溝通所需的資料:Application ID
、Admin API Key
及索引名稱。
SCOUT_PREFIX=(索引名稱)
SCOUT_QUEUE=true #是否將索引作業排入隊列,建議為 true
ALGOLIA_APP_ID=(Application ID)
ALGOLIA_SECRET=(Admin API Key)
下載 Algolia 搜尋驅動
composer require algolia/algoliasearch-client-php
Model 加入索引
在要索引的 Model(本例是 Post
)引入命名空間
use Laravel\Scout\Searchable;
以及在類別中引入 Searchable
trait:
use Searchable;
新增 searchableAs
方法,指定存入索引名
public function searchableAs()
{
return config('scout.prefix'); //回傳 .env 中 SCOUT_PREFIX 參數值
// return 'blog'; //直接輸入回傳的索引名
}
新增 toSearchableArray
方法,指定哪些欄位不要納入索引:
public function toSearchableArray()
{
$array = $this->toArray();
unset($array['category_id']);
unset($array['cover_image']);
unset($array['user_id']);
unset($array['sort']);
unset($array['status']);
unset($array['featured']);
return $array;
}
將資料匯入 Algolia:
php artisan scout:import "App\Models\Post"
回到 Algolia 畫面,會看到索引下已經有記錄了。
負責搜尋的 Controller 及方法
建立負責網站運作的 Controller:SiteController
,在其中建立 search
方法負責處理搜尋資料:
php artisan make:controller SiteController
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class SiteController extends Controller
{
public function search(Request $request)
{
$keyword = $request->search;
$post_results = Post::search($keyword);
if($post_results == null)
{
$post_results = '';
}else{
$post_results = $post_results->orderBy('created_at', 'desc')->paginate(20);
}
return view('search', compact('keyword', 'post_results'));
}
}
增加路由
在 /routes/web.php
增加搜尋路由:
// 搜尋
Route::get('/search', 'App\Http\Controllers\SiteController@search')->name('search');
與搜尋有關的 Blade template
與搜尋有關的 Blade template 有兩個:提供搜尋輸入欄位的 /resources/views/widgets/search.blade.php
:
<h3 class="sidebar-title">站內搜尋</h3>
<div class="sidebar-item search-form">
<form action="{{ route('search') }}" method="GET">
<input type="text" name="search">
<button type="submit"><i class="bi bi-search"></i></button>
</form>
</div>
負責顯示搜尋結果的 /resources/views/search.blade.php
:
@extends('layouts.master')
@section('meta_description', '搜尋 '. $keyword . ' 的結果')
@section('title', '搜尋 '. $keyword . ' 的結果')
@section('cover_image', Voyager::image('articles/search.png'))
@section('url', url()->full())
@section('nav_home', 'active')
@section('content')
<!-- 網頁路徑 -->
<section class="breadcrumbs">
<div class="container">
<div class="d-flex justify-content-between align-items-center">
<h2>搜尋</h2>
<ol>
{{ Breadcrumbs::render('search') }} {!! $keyword !!} 的結果
</ol>
</div>
</div>
</section>
<!-- 網頁路徑 -->
<section id="blog" class="blog">
<div class="container">
<div class="row">
<div class="col-lg-12 entries">
<article class="entry entry-single">
<div class="entry-content">
@if($post_results !== '')
<h3>包含:<font color="blue">{{ $keyword }}</font> 的文章</h3>
@foreach($post_results as $post)
<h3><a href="/blog/{{ $post->slug }}">{{ $post->title }}</a></h3>
{{ $post->introtext }}
@endforeach
<div class="blog-pagination">
<ul class="justify-content-center">{{ $post_results->links() }}</ul>
</div>
@else
<h3>找不到與 {{ $keyword }} 有關的文章。</h3>
@endif
</div>
</article><!-- End blog entry -->
</div><!-- End blog entries list -->
</div><!-- End blog sidebar -->
</div>
</section><!-- End Blog Single Section -->
@stop
在前端搜尋欄位輸入關鍵字
在搜尋結果頁面觀看相關的文章
結語
站內搜尋是我最後一個實做的主要功能,除了尋求適合的方案之外,想等到網站內容累積到一定程度之後再實做,可以看到多筆資料結果也是原因之一。如果手邊實在沒有資料進行查詢,可以透過 Factory
跟 Faker
的協同運作產生假資料,這樣內容要有幾筆就有幾筆。
Algolia 另有推出 Scout Extended 套件擴展 Laravel Scout 功能,透過聚合器(aggregator)將多個 Model 內容集中在同一索引中,還有在搜尋列輸入時就能即時顯示的 live search 功能要等到實力有所精進後再實做出來。
規劃的專案功能皆以到位,該是把網站放上運作空間,讓 larablog 上線的時候了。
後記
在文章發表之後我繼續尋找有關 live search 的資料想弄清楚運作方式,進而瞭解到:live search 透過 AJAX 方式監聽搜尋欄位的輸入內容,返回找到的結果。
如果是 AJAX 的話那麼可以透過 livewire 做到嗎?參考網路上找到的資訊後有做出類似的成果,以下是過程記錄。
參考資料
- https://medium.com/@branick/search-with-laravel-livewire-cb6dcd4ad541
- https://www.twilio.com/blog/build-live-search-box-laravel-livewire-mysql
安裝 livewire,導入資源
安裝 livewire:
composer require livewire/livewire
在 Blade template 主版檔案嵌入 livewire 資源:在 CSS 引入段落加入:
@livewireStyles
JavaScript 引入段落加入:
@livewireScripts
建立 livewire 元件
php artisan make:livewire search
會產生兩個檔案:
- 類別檔案:/app/Http/Livewire/Search.php
- 視圖檔案:/resources/views/livewire/search.blade.php
編輯類別及視圖
在類別檔案加入搜尋事件程式碼:
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Post;
class Search extends Component
{
public $keyword = "";
public function render()
{
$posts = Post::search($this->keyword)->get();
return view('livewire.search', compact('posts'));
}
}
$keyword
作為接收輸入的變數,初始值是空字串。$posts
儲存搜尋結果(Algolia 索引化後的資料),將結果傳到 search 這個 Blade template。
編輯視圖檔案:定義搜尋欄位外觀,及搜尋結果呈現。
<div>
<input type="text" wire:model="keyword" placeholder="請輸入關鍵字"/>
@if($keyword == "")
@else
<div style="
display:block;
position:absolute;
z-index:+1;
width: 280px;
margin-top:10px;
padding: 10px 10px 0px 5px;
background-color: white;
border: 1px dotted;
border-radius: 10px;
">
@foreach($posts as $post)
<ul>
<li><a href="/blog/{{ $post->slug }}">{{ $post->title}}</a></li>
</ul>
@endforeach
</div>
@endif
</div>
這裡的重點在 wire:model="keyword"
,livewire 會監聽 $keyword
的變化,即時呈現搜尋結果。
發佈 livewire:
php artisan livewire:publish --config
在前端要呈現 live search 地方加上以下內容,顯示 live search 欄位:
@livewire('search')
尚須改進之處
在搜尋欄位輸入關鍵字後會在下方動態產生相關的文章標題,在呈現有符合 live search 的樣子,只是訊息變動的反應有點鈍。其次因為索引內容的範圍關係,顯示項目部分僅做到標題顯示,理想的情況是呈現的內容能再以出處的分類(如:文章/分類/標籤)分別顯示,這部分等到瞭解 Scout Extend 套件的運作細節後再做改良。