iT邦幫忙

0

當客戶說「你們網站有沒有 AI」:網頁設計師從零實作 AI 搜尋功能

  • 分享至 

  • xImage
  •  

客戶開始問「你們網站有沒有 AI」了。

不是問你有沒有用 AI 設計網站——那個問題早在 2024 年就問完了。現在他們問的是:「網站本身有沒有 AI 功能?」彷彿網站是一個需要內建智慧的存在,而不是一張會呼吸的電子名片。

身為網頁設計師,我的第一個念頭是:我又不是 AI 工程師,這種需求要怎麼接?

但實際研究了一圈之後,我發現這件事沒有那麼難。2026 年的工具鏈已經足夠成熟,一個具備基本技術能力的網頁設計師,完全可以在一週內為客戶的網站加入「AI 語義搜尋」功能,而且不需要昂貴的 API 費用。

這篇文章是我的實戰紀錄,從需求拆解到代碼實作,全部是真的。


為什麼是「搜尋」而不是「聊天機器人」

多數客戶說「想要 AI」,心裡想的是「想要一個聰明的搜尋框」。

這個期待是合理的。相比一個需要訓練和維護的聊天機器人,語義搜尋(Semantic Search)更容易落地:

  • 用戶已經知道怎麼用搜尋框——不需要教育使用者
  • 回饋是即時的——輸入關鍵字,立刻看到結果
  • 不需要額外的客服機器人設定——省去訓練資料和意圖識別的成本
  • 適用於任何內容驅動網站——官方網站、知識庫、產品目錄、部落格

更重要的是:對多數台灣中小企業來說,網站內的搜尋功能幾乎都是爛的。用 Google 自訂搜尋或 WordPress 預設搜尋,輸入「網站」找不到「官方網站」,輸入「費用」找不到「收費方式」。

與其說是加入 AI,不如說是把原本就該做好的搜尋功能做好


技術架構:為什麼選擇 Next.js + Supabase pgvector

在評估了幾種方案之後,我最終選擇了這個組合:

  • 前端框架:Next.js(App Router)
  • 向量資料庫:Supabase pgvector(PostgreSQL 擴充)
  • Embedding 模型:OpenAI text-embedding-3-small(或本地模型)

選擇的理由很樸實:

第一,Supabase 有免費額度。 每月 500MB 的資料庫空間,對多數小型網站來說綽綽有餘。pgvector 直接內建在 PostgreSQL 裡,不需要另外架設向量資料庫服務。

第二,Next.js 的 Server Actions 讓 API 路由變得極簡。 不需要寫獨立的 API route 檔案,直接在 Server Component 裡面執行資料庫查詢。

第三,Vercel 部署足夠簡單。 客戶的網站最終要部署,Vercel + Supabase 的組合在台灣的網路環境下延遲可接受。

當然,如果你對資料主權有更嚴格的要求,也可以把 Supabase 換成 self-hosted 的 PostgreSQL + pgvector,部署在自己的 VPS 上。


實作步驟一:資料的 Embedding 準備

AI 搜尋的核心流程是這樣的:

內容 → 分塊(Chunking)→ 向量化(Embedding)→ 存入向量資料庫
使用者查詢 → 向量化 → 語義相似度搜尋 → 回傳結果

第一步是讓網站內容轉成向量。這段通常在網站後台管理介面處理,假設你已經有一個 CMS,流程如下:

// 假設這是你從 CMS 取出的文章資料
const documents = [
  {
    id: '1',
    title: '網站設計服務方案',
    content: '我們提供從零到上線的網站設計服務,包含 UI/UX 設計、前端開發、後端系統。',
    url: '/services/web-design'
  },
  {
    id: '2',
    title: 'SEO 優化服務',
    content: '協助客戶分析網站技術 SEO 問題,提供關鍵字研究與內容策略建議。',
    url: '/services/seo'
  }
];

// 使用 OpenAI 產生 Embedding
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
});

async function embedDocument(text) {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text
  });
  return response.data[0].embedding;
}

實務上,內容不會只有一兩篇——如果是知識庫可能有上百篇。這時候你需要一個任務佇列(job queue)來處理批次 embedding,避免 API rate limit。


實作步驟二:存入 Supabase pgvector

Supabase 的 pgvector 擴充讓你可以在 PostgreSQL 裡直接做向量相似度搜尋。假設你的文章表格長這樣:

-- migrations/001_create_articles_table.sql

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE articles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  url TEXT NOT NULL,
  embedding VECTOR(1536),  -- text-embedding-3-small 的維度是 1536
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 建立索引,加速相似度搜尋
CREATE INDEX ON articles USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

注意到 embedding 欄位的型別是 VECTOR(1536) 嗎?這就是 pgvector 的魔法——它允許你在一個資料庫欄位裡儲存 1536 維的向量,然後用 SQL 直接做餘弦相似度(cosine similarity)搜尋。

// 將文件及其 embedding 存入資料庫
async function storeArticle(doc) {
  const embedding = await embedDocument(doc.title + ' ' + doc.content);

  await supabase.from('articles').insert({
    title: doc.title,
    content: doc.content,
    url: doc.url,
    embedding: embedding  // pg 會自動處理 JS array → vector 轉換
  });
}

實作步驟三:語義搜尋查詢

最關鍵的部分來了——當使用者輸入一個查詢,我們要:

  1. 將查詢文字向量化
  2. 在資料庫裡找出與查詢向量最相似的文章
// app/api/search/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

export async function POST(req: NextRequest) {
  const { query } = await req.json();

  // 將使用者查詢向量化
  const queryEmbedding = await embedDocument(query);

  // 在 pgvector 裡做語義相似度搜尋
  const { data: results, error } = await supabase.rpc(
    'match_articles',
    {
      query_embedding: queryEmbedding,
      match_threshold: 0.7,   // 相似度閾值
      match_count: 5          // 最多回傳 5 筆結果
    }
  );

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }

  return NextResponse.json({ results });
}

match_articles 是一個 Supabase RPC 函數,底層是這樣的 SQL:

CREATE OR REPLACE FUNCTION match_articles(
  query_embedding VECTOR(1536),
  match_threshold FLOAT,
  match_count INT
)
RETURNS TABLE (
  id UUID,
  title TEXT,
  content TEXT,
  url TEXT,
  similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
  RETURN QUERY
  SELECT
    articles.id,
    articles.title,
    articles.content,
    articles.url,
    1 - (articles.embedding <=> query_embedding) AS similarity
  FROM articles
  WHERE 1 - (articles.embedding <=> query_embedding) > match_threshold
  ORDER BY articles.embedding <=> query_embedding
  LIMIT match_count;
END;
$$;

<=> 是 pgvector 的「向量距離」運算子,$1 - (embedding <=> query_embedding) 就是把距離轉換成相似度分數(0 到 1)。


實作步驟四:前端搜尋介面

有了 API 之後,前端就是一個普通的 React 元件:

// components/AISearch.tsx
'use client';

import { useState } from 'react';

export default function AISearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  const handleSearch = async (e) => {
    e.preventDefault();
    if (!query.trim()) return;

    setLoading(true);
    const res = await fetch('/api/search', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query })
    });

    const data = await res.json();
    setResults(data.results || []);
    setLoading(false);
  };

  return (
    <div>
      <form onSubmit={handleSearch}>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="輸入你想找的內容..."
          style={{ width: '100%', padding: '12px', fontSize: '16px' }}
        />
        <button type="submit" disabled={loading}>
          {loading ? '搜尋中...' : '搜尋'}
        </button>
      </form>

      {results.length > 0 && (
        <ul>
          {results.map((r) => (
            <li key={r.id}>
              <a href={r.url}>{r.title}</a>
              <p>{r.content.substring(0, 120)}...</p>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

這段代碼沒有任何華麗的 UI——重點是讓你看到核心邏輯。實際專案裡,你可以加上 Highlight 關鍵字、相似度分數顯示,或是「找不到結果時推薦相關頁面」的 fallback 邏輯。


一週能完成嗎?

老實說,可以。

假設你對 Next.js 有基本了解,整個流程可以這樣安排:

  • 第一天:申請 Supabase 專案 + 建立資料表 + 跑一次手動 embedding 測試
  • 第二天:實作 API 路由 + RPC 函數 + 確認向量搜尋能 work
  • 第三天:前端搜尋介面 + UI 優化
  • 第四天:內容遷移(把現有 CMS 文章全部重新 embedding)
  • 第五天:部署 + 測試 + 修正 bug

當然,這是理想狀況。真實世界會有各種意外:pgvector 版本不相容、Embedding 模型費用比預期高、使用者輸入繁體中文時的向量空間表現不如預期……


繁體中文的向量空間陷阱

多數 Embedding 模型對繁體中文的支援不如簡體中文或英文。在 text-embedding-3-small 上測試過,你會發現「網站設計」和「網站開發」的相似度很高,但「台北」和「台中」的相似度反而比預期低——這是因為在模型的訓練資料裡,繁體中文的語料相對少,導致向量空間的解析度不夠細。

解決方案有幾種:

第一,使用針對中文優化的模型。 比如 thenlper/gte-large-zh 或阿里巴巴的 m3e-large,對繁體中文的語意理解會更細緻。

第二,使用多語言模型並加強特定領域的微調。 如果你的網站是法律或醫療相關,這點特別重要。

第三,用 hybrid search 混合關鍵字搜尋。 把 pgvector 的語義相似度與傳統 BM25 關鍵字匹配做混合,確保精確匹配時不被向量搜尋稀釋。

我在實作時最終選擇了第三種方案,額外加上了一個 full_text_search 欄位用於關鍵字匹配,在排序時把 BM25 分數和向量相似度做加權平均。效果顯著提升——使用者搜「費用」,真的能找到「收費方式」和「價格方案」,而不只是語義上「相關」的頁面。


結語:從客戶需求到技術落地,其實沒有那麼遠

「你們網站有沒有 AI」——這個問題最好的回答不是口頭上說「有」,而是讓客戶實際體驗到。

一個語義搜尋框,看起來不起眼,但它解決了一個真實的問題:網站內的資訊終於能被找到了。對多數中小型網站來說,這本身就是一個足夠有價值的 AI 功能。

剩下的,就只是把那張 MVP 做出來而已。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言