在 Laravel 中使用 Algolia 實作搜尋功能

Algolia 是一個專精於搜尋的網路服務
Elasticsearch 類似,只要將可供搜尋的資料匯入至 Algolia 的資料庫(Index)
就可以在前端使用 Call API 的方式進行搜尋並取得搜尋結果

有許多網站或是程式文件都是使用 Algolia 的搜尋服務(例如 Laravel 與 Tailwind CSS 的官方文件)
本文會介紹如何使用在 Laravel 中使用 Algolia 實現部落格中搜尋文章的功能
Here we go~

 

在 Algolia 中新建一個 Application 與 Index


想要使用 Algolia 的服務,那麼當然就要註冊一個 Algolia 的帳號,這邊就不多敘述帳號註冊的過程了
註冊好帳號之後,我們需要先新建一個 Application
因為是以部落格中搜尋文章為例,所以新建一個名為 Blog 的 Application

2021_07_14_19_38_34_60eeccba03b41.png
小專案選取免費方案即可

之後會請你選取 Data Center,因為 Japan 的延遲最低,所以建議選擇 Japan
Application 新建好之後,可以至 API Keys 中查看 Application ID 與 Admin API Key
ID 與 Key 會在接下來用到,可以先記起來
接下來在 Application 中新建一個 Index,用來存放搜尋的目標資料

2021_07_14_19_38_48_60eeccc88eaba.png
文章的話,取名 post 好像不錯

Index 新建好,就代表 Algolia 這邊已經告一個段落,接下來回到 Laravel 專案上

 

文章資料欄位設定


假設文章的資料表名稱為 posts
與之對應的 Model,命名為 Post.php
使用 migration 建立一個 posts 資料表,文章欄位設定如下

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title')->index();
            $table->mediumText('body');
            $table->integer('category_id')->unsigned()->index();
            $table->integer('reply_count')->unsigned()->default(0);
            $table->integer('view_count')->unsigned()->default(0);
            $table->integer('last_reply_user_id')->unsigned()->default(0);
            $table->integer('order')->unsigned()->default(0);
            $table->text('excerpt')->nullable();
            $table->string('slug')->nullable();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::drop('posts');
    }
}

使用 migrate 創建資料表之後,緊接著要在後端安裝套件開始使用 Algolia 

 

Laravel Scout 的…加強版本


Laravel 官方有推出一個用來整合 Algolia 的套件,Laravel Scout
但 Algolia 官方又以 Laravel Scout 為基礎推出一個加強版套件
既然是加強版,沒有理由不用 XD,因此我們先用 composer 安裝這個加強版套件

composer require algolia/scout-extended

輸入下方指令,會在 app/config 中生成一個設定文件 scout.php

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

scout.php 可以用來設定要將資料上傳至哪個 Algolia 的 Index、每次上傳資料的最大數目、開啟資料同步佇列

 

在文章的 Model,Post.php 中新增幾行程式碼,讓這個 Model 套用 Algolia 的搜尋功能
你也可以使用 searchableAs() 與 toSearchableArray() 這兩個方法來客製上傳到 Algolia 的文章資料

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
// 引入 Algolia Searchable
use Laravel\Scout\Searchable;

class Post extends Model
{
    // Trait Algolia Searchable
    use Searchable
    
    .
    .
    .

    // 設定上傳的 Algolia index 名稱
    public function searchableAs()
    {
        return config('scout.prefix');
    }

    // 調整匯入 Algolia 的 Model 資料
    public function toSearchableArray()
    {
        $array = $this->toArray();

        // Applies Scout Extended default transformations:
        $array = $this->transform($array);
        
        // 新增一個新的欄位儲存作者名稱,並上傳到 Algolia
        $array['author_name'] = $this->user->name;

        return $array;
    }
}

然後在 .env 設定檔案中設定剛剛取得的 ID 與 KEY

# Index 名稱
SCOUT_PREFIX=posts

# Application ID
ALGOLIA_APP_ID=秘密

# Admin API key
ALGOLIA_SECRET=秘密

設定好之後,我們可以將 posts 的資料上傳到 Algolia

php artisan scout:import

指令執行成功之後,在 Algolia 上應該就可以看到 index 中有資料了

2021_07_14_19_39_09_60eeccdd7eaf1.png
資料上傳成功

這時候我們需要設定有哪些欄位可以被搜尋
以文章來說,如果我們只想要搜尋文章標題與文章內容

有兩個方法

  1. 我們可以直接到 Algolia Index 的主控台進行設定
  2. 或是將 Algolia Index 主控台上的設定拷貝一份下來
    並在 app/config 底下生成一個設定文件 scout-{index 的名稱}.php

這裡使用第二個方法,輸入下方指令

php artisan scout:optimize

這個設定文件會根據 Index 的名稱生成一份 scout-{index 的名稱}.php 的設定檔案
我們可以設定其中的 searchableAttributes,決定要搜尋哪個欄位的資料

<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Searchable Attributes
    |--------------------------------------------------------------------------
    |
    | Limits the scope of a search to the attributes listed in this setting. Defining
    | specific attributes as searchable is critical for relevance because it gives
    | you direct control over what information the search engine should look at.
    |
    | Supported: Null, Array
    | Example: ["name", "email", "unordered(city)"]
    |
     */
    
    // 設定要搜尋哪個欄位,這裡設定文章標題與內容
    'searchableAttributes' => ['title', 'body'],
    .
    .
    .
]

設定完之後,我們需要將改寫的設定更新至 Algolia Index,輸入下方指令

php artisan scout:sync

指令執行過程會詢問是否要將設定檔案更新到 Algolia Index 的設定,輸入 yes 就可以了

 

前端搜尋 UI


既然 Algolia 上有資料
接下來就是做一個前端搜尋 UI,使用 Call API 的方式取得搜尋結果並顯示
這邊我們會需要官方的安裝官方的 JavaScript API Client
首先在頁面上設定 algoliasearch 的 key

<script>
    const algoliaId = "{{ config('scout.algolia.id') }}";
    const algoliaSearchKey = "{{ Algolia\ScoutExtended\Facades\Algolia::searchKey(App\Models\Post::class) }}";
    const algoliaIndex = "{{ config('scout.prefix') }}";
</script>

編寫 algolia.js 檔案,設定 Search Box

import algoliasearch from 'algoliasearch';
import autocomplete from 'autocomplete.js';

const client = algoliasearch(algoliaId, algoliaSearchKey);
const posts = client.initIndex(algoliaIndex);

function newHitsSource(index, params) {
    return function doSearch(query, cb) {
        index
            .search(query, params)
            .then(function (res) {
                cb(res.hits, res);
            })
            .catch(function (err) {
                console.error(err);
                cb([]);
            });
    };
}

autocomplete(
    '#aa-search-input',
    {
        hint: false,
        templates: {
            dropdownMenu: '<div class="aa-dataset-post"></div>',
            footer: 'Search By Algolia'
        }
    },
    [
        {
            source: newHitsSource(posts, { hitsPerPage: 10 }),
            displayKey: 'title',
            templates: {
                header: '<div class="aa-suggestions-category">文章</div>',
                suggestion: function (suggestion) {
                    return `
                        <span class="w-100">
                            <a class="link-secondary text-decoration-none d-block w-100" href="${suggestion.url}">
                                ${suggestion._highlightResult.title.value}
                            </a>
                        </span>
                    `;
                },
                empty:
                    '<div class="d-flex justify-content-center align-items-center p-3">找不到符合搜尋字詞的文章</div>'
            }
        }
    ]
).on('autocomplete:selected', function (event, suggestion) {
    location.href = suggestion.url;
});

編寫 algolia.css,設定在 Search Box  的 CSS

.aa-input-container {
    display: inline-block;
    position: relative;
}
.aa-input-search {
    width: 200px;
    padding: 5px 28px 5px 5px;
    border: 2px solid #e4e4e4;
    border-radius: 4px;
    -webkit-transition: 0.2s;
    transition: 0.2s;
    box-shadow: 4px 4px 0 rgba(241, 241, 241, 0.35);
    font-size: 16px;
    box-sizing: border-box;
    color: #333;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
}
.aa-input-search::-webkit-search-decoration,
.aa-input-search::-webkit-search-cancel-button,
.aa-input-search::-webkit-search-results-button,
.aa-input-search::-webkit-search-results-decoration {
    display: none;
}
.aa-input-search:focus {
    outline: 0;
    border-color: #3a96cf;
    box-shadow: 4px 4px 0 rgba(58, 150, 207, 0.1);
}
.aa-hint {
    color: #e4e4e4;
}
.aa-dropdown-menu {
    background-color: #fff;
    border: 2px solid rgba(50, 50, 50, 0.6);
    border-top-width: 0;
    width: 500px;
    margin-top: 10px;
    box-shadow: 4px 4px 0 rgba(241, 241, 241, 0.35);
    font-size: 16px;
    border-radius: 4px;
    box-sizing: border-box;
}
.aa-suggestion {
    padding: 6px 12px;
    margin-bottom: 5px;
    cursor: pointer;
    -webkit-transition: 0.2s;
    transition: 0.2s;
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-pack: justify;
    -ms-flex-pack: justify;
    justify-content: space-between;
    -webkit-box-align: center;
    -ms-flex-align: center;
    align-items: center;
}
.aa-suggestion:hover,
.aa-suggestion.aa-cursor {
    background-color: rgba(200, 200, 200, 0.35);
}
.aa-suggestions-category {
    text-transform: uppercase;
    border-bottom: 2px solid rgba(50, 50, 50, 0.6);
    border-top: 2px solid rgba(50, 50, 50, 0.6);
    padding: 6px 12px;
    color: #333;
}
.aa-suggestion > span em {
    font-weight: 700;
    font-style: normal;
    background-color: rgba(58, 150, 207, 0.1);
    padding: 2px 0 2px 2px;
}

最後在頁面上載入

<link href="{{ asset('css/algolia.css') }}" rel="stylesheet">

...

{{-- Search Box 位置 --}}
<div class="aa-input-container me-auto" id="aa-input-container">
    <input type="search" id="aa-search-input" class="aa-input-search"
    placeholder="搜尋文章" name="search" autocomplete="off" />
</div>

...

<script>
    const algoliaId = "{{ config('scout.algolia.id') }}";
    const algoliaSearchKey = "{{ Algolia\ScoutExtended\Facades\Algolia::searchKey(App\Models\Post::class) }}";
    const algoliaIndex = "{{ config('scout.prefix') }}";
</script>

{{-- 載入剛剛編寫的 Search Box --}}
<script src="{{ asset('js/algolia.js') }}"></script>

大功告成,可以搜尋文章囉~

sharkHead
written by
sharkHead

後端工程師, PHP 基金會每月 5 鎂小額贊助人 稍微擅長 PHP、Python 與 Google Search,偶爾寫寫 TypeScript 對於逗號後面必須加空格有著絕對的堅持