A+ A A-

larablog 建構日誌:站內搜尋

Google 自訂搜尋:設定簡單,但調性不符

  我使用 Joomla! 系統建置的網站:華燈初上跟部落格都使用 Google 提供的自定義搜尋服務,將 Google 提供的嵌入碼加進佈景主題中,再透過「自訂 HTML」模組將搜尋表單顯示於佈景主題定義的模組位置,站內搜尋服務就完成了。

華燈初上的 Google 自定義搜尋畫面

  讓 Google 處理站內搜尋的好處,是將「搜尋網站內容」這件消耗伺服器效能的事委外處理,對於性能不高的共享型虛擬主機空間來說幫助很大,而且「搜尋及索引」就是 Google 的核心業務。

  在學習 PHP 語法,製作 ffxitoolbox 第一版時也是使用 Google 自定義搜尋作為站內搜尋功能,不過沒過多久我就發現該服務不適合用在 ffxitoolbox 上,主要有兩點:

  1. Google 自定義搜尋的搜尋結果是 Google 索引過的內容,所以沒索引到的部分就找不到。加上搜尋功能的使用是輸入素材關鍵字尋找相關配方,搜尋結果是整頁的搜尋結果,點選後還要再找,不怎麼方便。
  2. 搜尋結果的呈現風格跟網站的設計差別很大,每次使用時很難跟網站畫面聯想在一起,雖然這不是 Google 的錯...

自建搜尋:畫面自行決定,佔用伺服器效能

  2021 年初完成 ffxitoolbox 第一版製作,代表我從零開始學習 PHP 有了初步成就:我有能力使用 PHP 跟 MySQL 將 ffxi 的合成配方資料整理成網站內容提供閱覽。

  在這之後我開始學習 Laravel 框架,設下的目標就是將 ffxitoolbox 以 Laravel 框架重寫:如果能夠完成代表我以經能用 Laravel 做出網站專案。就在作業接近收尾的階段,「站內搜尋」功能的建置浮出檯面,這次實在不想用 Google 自定義搜尋了...

  用 Google 搜尋前人的智慧結晶時,我發現了〈How to add simple search to your Laravel blog/website?〉這篇文章,透過閱讀文章內容逐步操作,我讓 ffxitoolbox 的站內搜尋結果能以想要的方式呈現。

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。

API Keys 畫面

下載 Laravel Scout,設定 Algolia 通訊

  在終端機畫面下載 Laravel Scout 套件

composer require laravel/scout

  發佈套件,會建立 /config/scout.php 設定檔

php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

  在 .env 輸入與 Algolia 溝通所需的資料:Application IDAdmin 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') }}&nbsp; {!! $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>&nbsp;的文章</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>找不到與 &nbsp;{{ $keyword }} 有關的文章。</h3>
                        @endif
                    </div>

                </article><!-- End blog entry -->
            </div><!-- End blog entries list -->
        </div><!-- End blog sidebar -->

    </div>
</section><!-- End Blog Single Section -->
@stop

  在前端搜尋欄位輸入關鍵字

搜尋欄位輸入關鍵字

  在搜尋結果頁面觀看相關的文章

關鍵字搜尋結果

結語

  站內搜尋是我最後一個實做的主要功能,除了尋求適合的方案之外,想等到網站內容累積到一定程度之後再實做,可以看到多筆資料結果也是原因之一。如果手邊實在沒有資料進行查詢,可以透過 FactoryFaker 的協同運作產生假資料,這樣內容要有幾筆就有幾筆。

  Algolia 另有推出 Scout Extended 套件擴展 Laravel Scout 功能,透過聚合器(aggregator)將多個 Model 內容集中在同一索引中,還有在搜尋列輸入時就能即時顯示的 live search 功能要等到實力有所精進後再實做出來。

  規劃的專案功能皆以到位,該是把網站放上運作空間,讓 larablog 上線的時候了。

後記

  在文章發表之後我繼續尋找有關 live search 的資料想弄清楚運作方式,進而瞭解到:live search 透過 AJAX 方式監聽搜尋欄位的輸入內容,返回找到的結果。

  如果是 AJAX 的話那麼可以透過 livewire 做到嗎?參考網路上找到的資訊後有做出類似的成果,以下是過程記錄。

參考資料

安裝 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 == "")
        &nbsp;
    @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 示意

尚須改進之處

  在搜尋欄位輸入關鍵字後會在下方動態產生相關的文章標題,在呈現有符合 live search 的樣子,只是訊息變動的反應有點鈍。其次因為索引內容的範圍關係,顯示項目部分僅做到標題顯示,理想的情況是呈現的內容能再以出處的分類(如:文章/分類/標籤)分別顯示,這部分等到瞭解 Scout Extend 套件的運作細節後再做改良。