MeiliSearch with Hugo

MeiliSearch with Hugo

Issue

說到搜尋的解決方案,最有名的大概就是 Algolia,可以方便的創建索引,也有很多寫好的前端網頁元件可以使用,唯一的缺點就是收費。雖然 Algolia 要收費,但其實對於我這個小網頁都索引量是完全不用錢的,但是就還是覺得自己架一個索引系統比較有感覺,然後我就在 GitHub 上發現了開源的 MeiliSearch,功能基本上跟 Algolia 很像,也支援中文,甚至有些前端元件可以直接使用 Algolia 的,缺點就是要自己架設 Server。

本文以搜尋本站為目的撰寫,所以後面的範例大多與 Hugo 相關,如果想要看完整的程式碼,可以直接到本站的 GitHub。

簡介 MeiliSearch Server

img

基本上 MeiliSearch 就是一個 RESTful API,目標是要提供現成的搜尋解決方案,完全開源,提供即時搜尋、拼寫錯誤、同義詞、模糊搜尋、自訂排名、排序和多語言搜尋的支援,連中文也不例外。根據官方文件的介紹,MeiliSearch 以易用性為首要目標,對於開發者,只需要少少幾行 code 就可以使用;對於使用者,提供直觀的即時輸入即時反應的搜尋結果。

官方也有很多的 SDK,.NET, Dart, Go, Java, JS, Python, Swift,而且上面這些還不是全部,所以在開發各語言的應用程式時,都可以考慮用 MeiliSearch 做為後端搜尋引擎。

MeiliSearch 也不是完全沒有缺點,缺點就是要自己有伺服器,像是本文今天的案例就是要幫 Static Site 做搜尋的功能,通常 Static Site 大家應該都是用 GitHub Pages 等 Statice Site Hosting 服務,所以網頁本身是沒有自己架 Server 的,為了加個搜尋功能會要多一個 Server 就是很大的缺點。

SearchIndex.json

在開始使用 MeiliSearch 之前,我們先來把要索引的資料準備好,MeiliSearch 會需要使用 json 當作索引,所以我們要產生一個 json 的範本。創建 /layouts/list.searchindex.json,內容如下:

[ {{- $i := 0 -}}
    {{- range where .Site.RegularPages "Section" "ne" "" -}}
       {{- if not .Params.noSearch -}}
          {{- if gt $i 0 }},{{ end -}}
          {"id":{{ $i }}, "date":"{{ .Date.Unix }}", "url":"{{ .Permalink }}", "title":{{ .Title | jsonify  }}, "summary":{{ with .Description}}{{ . | plainify | jsonify }}{{ else }}{{ .Summary | plainify | jsonify }}{{ end }}, "content":{{ .Content | plainify | jsonify }},"tags":[ {{- $t := 0 }}{{- range .Param "tags" -}}{{ if gt $t 0 }},{{ end }}{{ . | jsonify }}{{ $t = add $t 1 }}{{ end -}} ], "section": {{ .Section | jsonify -}} }
        {{- $i = add $i 1 -}}
    {{- end -}}
{{- end -}} ]

然後在 config.yaml 中加入下面的設定值:

outputFormats:
  SearchIndex:
    mediaType: "application/json"
    baseName: "searchindex"
    isPlainText: true
    notAlternative: true
outputs:
  home: ["HTML", "RSS", "SearchIndex"]

完成上面兩樣設定後,可以 hugo 一下試試看,有沒有在 /public/searchindex.json 中看到索引檔案,應該會類似如下:

[
  {
    "id": 0,
    "date": "1622891718",
    "url": "https://tonypepe.com/posts/others/hello-world/",
    "title": "Hello World",
    "summary": "Hello World. This is a test post.",
    "content": "This is my first post in hugo\ncontent",
    "tags": ["hugo", "test"],
    "section": "posts"
  }
]

架設 MeiliSearch Server

準備好索引檔案後,我們就來架設 MeiliSearch 的 Server,MeiliSearch 是用 Rust 編寫,所以也是跨平台通用。官方提供很多種安裝方式,還有一鍵啟動腳本,當然,為了避免弄髒環境,最推薦的當然還是 Docker,下面就先以 Docker 為範例:

docker run -it --rm \
    -p 7700:7700 \
    -v $(pwd)/data.ms:/data.ms \
    getmeili/meilisearch:latest

注意一下,上面掛載了一個 data.ms 的資料夾,所以在執行上面的指令前,記得先創建好。data.ms 這個資料夾是為了存放索引資料,避免因為 Docker 容器重啟就損失資料,所以很重要。

現在可以到 http://localhost:7700,會看到有一個簡易的 WEB UI。

img

如果不想用 Docker,還是可以參考官方文件的其他安裝方式安裝。

提交索引資料

前面有提到 MeiliSearch 是 RESTful API,所以要向他搜尋或是提供資料,都是用 HTTP Method。官方文件寫的都是 curl,我們還是使用 GUI 的方式介紹。HTTP 的 GUI 大家應該都會想到 Postman,但今天我們要使用他的開源替代 Hoppsotch,他是一個網頁工具,所以不需要安裝。

可以先對 /healthGET 試試看有沒有連上,如果沒有連上,可能要檢查一下有沒有成功啟動 MeiliSearch Server。

img

確定連上後,我們就來提交索引資料,如果還沒有 Hugo 產生的索引資料,可以先到本站的 GitHub,拿本站的 searchindex.json 做測試。

因為 MeiliSearch 也可以同時有多個 Indexes 做搜尋,我們下面的範例以 hugo_blog 作為範例名稱,大家可以根據需求自己決定使用的名稱。

img

可以看到上圖對 /indexes/<<IndexName>>/documentsPOST 方法,就可以把索引資料提交到 MeiliSearch Server。記得內容類型是 application/json,然後把索引資料放到 body。

現在回到 localhost:7700,應該就可以看到原本空空如也的 WEB UI 現在可以做搜尋了,可以先在這裡做一些簡單的搜尋,試試看自己的索引資料有沒有被成功的使用。

img

MeiliSearch in Production

MeiliSearch 預設的啟動方式其實是 development,這模式只是方便本地做測試使用的,如果要使用在生產環境,應該要以 production 模式啟動。在 Production 模式下,會有兩個不一樣的地方,第一項是 Web UI 會被停用,第二項是會必須要設定 Master Key,下面還是用 Docker 作為範例:

docker run -it --rm \
    -p 7700:7700 \
    -v $(pwd)/data.ms:/data.ms \
    -e MEILI_ENV=production \
    -e MEILI_MASTER_KEY=<Your Master Key> \
    getmeili/meilisearch:latest

上面設定的 Master Key,就是管理權限最大的 API Key,所以要記得妥善保存。

下面用剛剛的 Master Key 去對 /keysGET,然後在 Headers 中,加上剛剛的 Master Key,就可以拿到預設的搜尋 KEY,和預設的 ADMIN KEY。如果要再另外新增 API KEY,可以參考官方的文件,這裡就不多著墨。

img

為網頁加上搜尋功能

我們會用到 MeiliSearch 提供的 Instant MeiliSearch,這個 JS 庫就是使用 Algolia 的 Instant Search 改的,所以有些文件可以直接去看 Algolia 的官方文件。Instant MeiliSearch 最簡單的使用方式也是直接用 CDN 的方式,下面是一個最簡單的模板,看懂後用類似的邏輯加到自己的網頁中就好。

html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
  </head>

  <body>
    <div>
      <div id="searchbox"></div>
      <div id="hits"></div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/@meilisearch/instant-meilisearch/dist/instant-meilisearch.umd.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4"></script>
    <script src="./app.js"></script>
  </body>
</html>

下面是剛剛的 html 會用到的 app.js,用來作為 Search 的設定值,只要把下面的網址設定成自己的網址,和自己在前幾步驟拿到的搜尋 API KEY 就可以使用了:

const search = instantsearch({
  indexName: "steam-video-games",
  searchClient: instantMeiliSearch(
    "https://integration-demos.meilisearch.com",
    "q7QHwGiX841a509c8b05ef29e55f2d94c02c00635f729ccf097a734cbdf7961530f47c47",
  ),
});

search.addWidgets([
  instantsearch.widgets.searchBox({
    container: "#searchbox",
  }),
  instantsearch.widgets.hits({
    container: "#hits",
    templates: {
      item: `
        <div>
          <div class="hit-name">
            {{#helpers.highlight}}{ "attribute": "name" }{{/helpers.highlight}}
          </div>
        </div>
      `,
    },
  }),
]);

search.start();

如果覺得他預設提供的 UI 很醜,可以用 CSS 的方式去改他。

GitHub Actions 自動提交索引

不廢話,直接上本站的 yml:

name: MeiliSearch Index

on:
  push:
    branches:
      - master # Set a branch to deploy

jobs:
  deploy:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true # Fetch Hugo themes (true OR recursive)
          fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Node
        uses: actions/setup-node@v2
        with:
          node-version: "14"

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: "latest"
          extended: true

      - name: Build
        run: |
          npm i
          hugo --minify

      - name: Post Index
        run: |
          curl \
            -X POST "https://search.tonypepe.com/indexes/hugo_blog/documents" \
            -H 'Authorization: Bearer ${{ secrets.MEILISEARCH_KEY }}' \
            -H 'Content-Type: application/json' \
            --data-binary "@public/searchindex.json"

其實重點就是在最後一步,把索引檔案 POST 到 MeiliSearch 就可以了。

Referenc


TonyPepe
Made with Love in Taiwan
Copyright
2020 - 2023