---
title: "提升電商網站搜尋體驗，使用Elasticsearch進行高效全文檢索 - Part's 1"
description: "說明如何導入 Elasticsearch 倒排索引技術，提升電商網站全文搜尋品質，並整合 Next.js API 與 Kibana 實現高效關鍵字查詢。"
canonical_url: "https://blog.markkulab.net/post/enhancing-the-search-experience"
author: "Mark Ku"
author_url: "https://blog.markkulab.net/author/mark-ku"
site: "Mark Ku's Blog"
date_published: "2024-01-21 01:01:35 +0800"
category: "Database"
tags: ["elasticsearch", "full text search", "ecommerce", "nextjs", "kibana", "search", "docker", "indexing"]
language: "zh-TW"
license: "CC BY 4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
attribution: "轉載或引用請註明作者並附上原文連結"
---

# 提升電商網站搜尋體驗，使用Elasticsearch進行高效全文檢索 - Part's 1

## 時空背景
經客服經理反饋，發現我們網站的搜尋功能體驗非常的差，用戶常常反應搜尋不到的規格，然後他說希望能像 Google 一樣搜尋體驗，而且可以快速依據產品規格，找到相關的產品，因此我花了些時間評估後，發現全文檢索的技術，應該是能解決我們的問題。

## 為何需要使用 - 全文檢索
首先，我們得先了解什麼是全文檢索工具（Full-Text Search Tools），它是一種軟體工具，主要功能在有效地搜尋和檢索大量的文字數據，並返回相關的搜尋結果，可以在數秒內檢索大規模文字數據集，提供多種搜尋選項，例如關鍵字搜尋、短語搜尋、模糊搜尋等，使用戶能夠以多種方式查找所需資訊。

## 全文檢索原理 
接著得了解全文檢索技術和資料庫最大的差別就是，就是當資料量越大時，要對全表掃描時，對資料庫要耗費相當長的時間，但全文檢索技術採用[倒排索引](https://www.zhihu.com/question/23202010)的檢索技術，以依據各種分詞器將資料以 key 及 value 的形式儲存，查詢時就會變得簡單及快速。

### 以資料庫存儲的商品為例

| SkuID ( 庫存量單位ID ) | Name (庫存量單位名稱)| 
| --------               | --------             | 
| 1                      | Kingstom 16GB Ram    | 
| 2                      | Transcend 16GB Ram   | 
| ...                    | ...                  | 
P.S. Sku ( Stock Keeping Unit )為庫存量單位的ID

### 全文檢索採用的『倒排索引』的檢索方式，透過事前分詞及記錄順序，當使用者查詢時，才有辦法簡單快速及精準。

| Term (分詞) | SkuID (庫存量單位ID)| 
| --------    | --------            | 
| kingstom    | 1                   | 
| 16gb        | 1,2                 | 
| ram         | 1,2                 | 
| transcend   | 1,2                 | 

## 內建分詞器
Elasticsearch 本身就內建的分詞器，有標準分詞器、空格分詞器、簡單分詞器等等，它們的主要工作就是把文字拆成一個個詞彙，還有做一些處理，像是轉成小寫、詞幹提取、過濾停用詞等等，讓系統可以更好地建立索引和進行全文檢索，你可以根據需要，選擇適合的分詞器和分析器，或者自己客製化一個，這樣就能有更好的搜尋效果了。

### 測試分詞器
可以先看過，知道可以透過語法測試分詞，等到後面 ElasticSearch 及 Kabana 架好後，可以透過Dev Tools執行下面語法測試分詞。
```
POST _analyze
{
  "analyzer": "standard",
  "text": "Transcend 16GB Ram"
}
```
![分詞拆解的結果](https://blog.markkulab.net/content/markku/posts/enhancing-the-search-experience/images/2.png)
P.S. Elasticsearch 並沒有內建中文的分詞器，但可以另外安裝常用的[中文分詞器](https://blog.csdn.net/qq_26803795/article/details/106522611)，EX:IK 分词器、Smartcn 分词器和 Jieba 。

## 其實ElasticSearch 與 RDBMS 的概念類似，這邊整理了名詞的對照表

| ElasticSearch | RDBMS   　  | 
| --------  | --------   　   | 
| INDEX (索引)    | 表   　   | 
| DOCUMENT (文件) | 行   　   | 
| FIELD   (欄位)  | 欄位   　 | 
| MAPPING (結構)  | 表結構　　| 


## 建立及啟動容器
### Docker Compose  (docker-compose.yml )
```
# 注意version要和docker-compose的版本對應 docker-compose --version
version: '3.8'
services:
  elasticsearch:
    image: elasticsearch:7.17.3
    ports:
      - "9200:9200"
      - "9300:9300"
    environment:
      - discovery.type=single-node
    container_name: elasticsearch

  kibana:
    image: kibana:7.17.3
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    container_name: kibana
    depends_on:
      - elasticsearch
```

### 運行及啟動 Docker Compose 容器
```
docker-compose -p elasticsearch-group up
```

### 用 Powershell 新增防火牆規則
```
New-NetFirewallRule -DisplayName "elasticsearch -Port 5000" -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow

New-NetFirewallRule -DisplayName "elasticsearch -Port 9200" -Direction Inbound -Protocol TCP -LocalPort 9200 -Action Allow

New-NetFirewallRule -DisplayName "kibana -Port 9200" -Direction Inbound -Protocol TCP -LocalPort 5601 -Action Allow
```

### 首先，先訪問 ElasticSearch http://localhost:9200/_cat/indices 查詢所有的索引，ElasticSearch 其實很單純，他本身就內建了rest api ，透過訪問URL 就能拿到我們要的結果
![ElasticSearch查詢所有的索引](https://blog.markkulab.net/content/markku/posts/enhancing-the-search-experience/images/3.png)

### 接著，再訪問 Kabana http://localhost:5601/ ，點開導覽列，找到DevTools，就可以透過 UI  去操作先前在Docker 指定的ElasticSearch 。

```
GET /_cat/indices 
```
![Kabana查詢所有的索引](https://blog.markkulab.net/content/markku/posts/enhancing-the-search-experience/images/4.png)


### 資料庫則需要先定表結構，才能新增資料，但ElasticSearch 相當強大，先新增資料，會依據資料自動推斷表結構的欄位型態。
#### 單筆新增資料
```
POST product/_doc/1004
{
    "product_id": "1004",
    "product_name": "Super Tablet",
    "brand": "Techie",
    "price": 599.99,
    "processor": "ARM Cortex-A76",
    "video_card": "Integrated Mali-G76",
    "ram": "4GB",
    "storage": "128GB SSD",
    "special_features": ["Touchscreen", "Lightweight"],
    "stock": 25
}

```
P.S _doc => 為預設文檔的類型，新版本中不建議修改文檔的類型，此功能逐步會被淘汰。

#### 批量新增資料
```
POST _bulk
{"index": {"_index": "product", "_id": "1001"}}
{"product_id": "1001", "product_name": "UltraBook Pro", "brand": "Techie", "price": 999.99, "processor": "Intel Core i7", "video_card": "NVIDIA GeForce RTX 3060", "ram": "16GB", "storage": "512GB SSD", "special_features": ["Wifi", "Special Promotion"], "stock": 50}
{"index": {"_index": "product", "_id": "1002"}}
{"product_id": "1002", "product_name": "Gamer PowerHouse", "brand": "Xtreme", "price": 1299.99, "processor": "AMD Ryzen 7", "video_card": "AMD Radeon RX 6800 XT", "ram": "32GB", "storage": "1TB SSD", "special_features": ["Custom Liquid Cooling", "Special Promotion"], "stock": 30}
{"index": {"_index": "product", "_id": "1003"}}
{"product_id": "1003", "product_name": "PortaLight", "brand": "SleekTech", "price": 799.99, "processor": "Intel Core i5", "video_card": "Integrated Intel Iris Xe Graphics", "ram": "8GB", "storage": "256GB SSD", "special_features": ["Wifi"], "stock": 70}
```

### 查詢 product 的結構
```
GET /product/_mapping
```
P.S 資料庫則需要先定表結構，才能新增資料，但ElasticSearch 相當強大，先新增資料，會依據資料自動推斷表結構的欄位型態。
###  查詢該筆_id=1001的資料
```
GET /product/_doc/1001
```
#### 查詢回來的結果
```
{
  "_index" : "ecommerce",
  "_type" : "_doc",
  "_id" : "1001",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "product_id" : "1001",
    "product_name" : "UltraBook Pro",
    "brand" : "Techie",
    "price" : 999.99,
    "processor" : "Intel Core i7",
    "video_card" : "NVIDIA GeForce RTX 3060",
    "ram" : "16GB",
    "storage" : "512GB SSD",
    "special_features" : [
      "Wifi",
      "Special Promotion"
    ],
    "stock" : 50
  }
}

```

#### 依據查詢回來的結果，整理了一些對照

| 欄位             | 說明                                                         |
| ---------------- | ------------------------------------------------------------ |
| _index           | document 所屬的 index 名稱                                   |
| _type            | document 類型                                                |
| _id              | document ID 編號                                             |
| _version         | 版本訊息，每進行一次更新、刪除，都會增加 version 的值        |
| _source          | 此 document 的原始 json 資料                                 |

### 獲取所有的資料
```
GET /product/_search
```
### 查詢單筆資料
```
GET /product/_search
{
  "query": {
    "match": {
      "product_name": "pro"
    }
  }
}
```
### 整理一些常見的查詢條件
| 查詢條件類型           | 描述                                                                                          | 範例                        |
|--------------------|-----------------------------------------------------------------------------------------------|-----------------------------|
| **`match`**     | 用於全文檢索，對搜尋詞進行分詞，然後搜尋每個分詞，支援文字字段的分析，適用於搜尋文字字段。 | 搜尋 "快速的電腦"          |
| **`match_phrase`** | 也用於全文檢索，但要求匹配整個詞組，考慮詞序，適用於精確詞組匹配的情況。               | 搜尋 "快速的電腦"，但要求匹配整個詞組|
| **`multi_match`** | 允許在多個字段中進行 `match` 查詢，適用於在多個字段中搜尋相同關鍵字的情況。           | 在標題和描述中搜尋 "蘋果"     |
| **`term`**       | 用於精確值匹配，不進行分詞，適用於非分析字段，通常用於數字、日期或未分析的文字字段。   | 搜尋具有特定 ID 的文件         |
| **`fuzzy`**      | 用於處理拼寫錯誤和近似匹配，基於 Levenshtein 編輯距離算法，適用於容忍拼寫錯誤的情況。 | 搜尋 "aple" 可以匹配 "蘋果"   |
| **`wildcard`**   | 允許使用通配符 `*` 和 `?` 進行模糊匹配。適用於需要匹配特定模式的情況。              | 搜尋 "appl*e" 可以匹配 "測試"  |

### 糊模查詢
```
GET /product/_search
{
  "query": {
    "wildcard": {
      "product_name": "pro*"
    }
  }
}
```
###  模糊查詢，允許錯字
```
GET /product/_search
{
  "query": {
    "fuzzy": {
      "product_name": {
        "value": "pro",
        "fuzziness": "AUTO"
      }
    }
  }
}
```

### 多欄位搜尋
```
GET /product/_search
{
  "query": {
    "multi_match": {
      "query": "您的關鍵字",
      "fields": ["price", "processor", "video_card", "ram", "storage", "special_features"]
    }
  }
}
```
### 查出索引文件資訊 - ( 筆數 )

```
POST /product/_doc
{
  "query": {
    "bool": {
      "must": [
        { "match": { "product_name": "4060" } }
      ]
    }
  }
}
```

### 依據ID刪除該筆資料

```
DELETE /product/_doc/1001
```
### 刪除索引含資料
```
DELETE /product
```
### 刪除所有資料
```
POST /product/_delete_by_query
{
  "query": {
    "match_all": {}
  }
}
```


## NextJS 整合Elasticsearch
### 安裝 @elastic/elasticsearch 套件
```
npm install @elastic/elasticsearch
```
### 接著，建立 elasticsearch Client
```
// lib/elasticsearch.ts
import { Client } from '@elastic/elasticsearch';

const client = new Client({
    node: 'http://localhost:9200', // 更改為您的 Elasticsearch 節點地址
});

export default client;

```
### 在 Next.js 中建立 查詢 Elasticsearch 的 Api 

```
// pages/api/search.ts
import client from '../../lib/elasticsearch';

import client from '@utils/elasticsearch';
import { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    try {
        // 從請求體或查詢參數中獲取搜尋關鍵字
        const keyword = req.body.keyword || req.query.keyword;

        // 建立多字段 Elasticsearch 查詢
        const query = {
            multi_match: {
                query: keyword,
                fields: ['price', 'processor', 'video_card', 'ram', 'storage', 'special_features'],
            },
        };

        // 執行 Elasticsearch 查詢
        const body = await client.search({
            index: 'product',
            body: { query },
        });

        // 返回查詢結果
        res.status(200).json(body.hits.hits);
    } catch (error: any) {
        res.status(500).json({ message: error.message });
    }
}

```
### 最後在前端建立搜尋框及搜尋按鈕

```
// app/elasticsearch/page.tsx
'use client';
import { useState } from 'react';

export default function Search() {
    const [keyword, setKeyword] = useState<string>('3060');
    const [searchResults, setSearchResults] = useState<any[]>([]); // 存儲搜尋結果的狀態

    async function searchProducts(keyword: string): Promise<any[]> {
        const response = await fetch('/api/search', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ keyword }),
        });
        const data = await response.json();
        debugger;
        return data;
    }

    const handleSearch = async () => {
        const results = await searchProducts(keyword);
        debugger;
        // 更新搜尋結果的狀態
        setSearchResults(results);

        // 處理搜尋結果
    };

    return (
        <div className="p-4">
            <input
                type="text"
                value={keyword}
                onChange={(e) => setKeyword(e.target.value)}
                className="mr-2 rounded-md border border-gray-300 p-2"
            />
            <button onClick={handleSearch} className="rounded-md bg-blue-500 p-2 text-white">
                Search
            </button>

            {/* 渲染搜尋結果 */}
            <div className="mt-4">{JSON.stringify(searchResults)}</div>
        </div>
    );
}
```

### 系統整合完成，是不是很簡單呢!
![網頁搜尋畫面](https://blog.markkulab.net/content/markku/posts/enhancing-the-search-experience/images/5.png)

## 參考資料
* [Elasticsearch全文检索入门这一篇就够了](https://zhuanlan.zhihu.com/p/94181307)  
* [elasticsearch模仿淘宝、京东、百度、谷歌搜尋，自动补全、自动完成](https://blog.csdn.net/successCodeMan/article/details/115181760?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170576110716800197022855%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fvipall.%2522%257D&request_id=170576110716800197022855&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~vipall~first_rank_ecpm_v1~rank_v31_ecpm-8-115181760-null-null&utm_term=ElasticSearch%20%E9%9B%BB%E5%95%86&spm=1018.2226.3001.4187)
* [搜尋功能全流程解析](https://www.woshipm.com/pd/5762531.html)
* [訂單搜尋優化](https://blog.csdn.net/CrossLimit/article/details/124908251)
* [oramasearch 開源全文檢索工具](https://cloud.oramasearch.com/indexes)

---

## 關於本文與作者

本文出自 [Mark Ku's Blog](https://blog.markkulab.net/post/enhancing-the-search-experience)

授權條款： [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — 轉載或引用請註明作者並附上原文連結

### 關於作者

**[Mark Ku](https://blog.markkulab.net/author/mark-ku)** — Software Solution Provider

- 10+ 年資深軟體工程師，現為 AI 應用 Builder
- 專注大型平台架構設計，從北美電商到AI SaaS訂閱收費系統
- 結合 AI Agent 與自動化，打造高效可演進的產品技術基礎

### 作者開發的免費工具

以下工具皆可免費使用：

- [免費 PDF 簽名工具](https://blog.markkulab.net/tools/pdf-sign): 線上 PDF 簽名工具，瀏覽器內完成手繪、打字、上傳簽名，可拖曳放置、縮放、下載。所有處理都在你的裝置完成，檔案不會上傳。
- [VS Code Refactory](https://blog.markkulab.net/tools/refactory): Refactory 是一款 VS Code 重構擴充套件：34 個重構動作、37 條 code smell 檢查、Code Health 儀表板、18 種語言、534 支測試。懂你的專案慣例：介面放哪、DI 註冊寫在哪、'use client' 該不該加；還會用 git 修改頻率 × 複雜度排出「該先修哪個檔案」，並一鍵把壞味道交給你自己電腦上的 Claude Code 修。免費使用，原始碼不離開你的機器。
- [DB-Kit 資料庫管理工具](https://blog.markkulab.net/tools/db-kit): DB-Kit 是一個用 Tauri + Rust + React 打造的輕量跨平台資料庫管理工具，用單一一致的介面同時管理 MySQL、MariaDB、PostgreSQL、SQL Server、Oracle、SQLite、MongoDB、Redis、Kafka、Elasticsearch 與 RabbitMQ 十一種資料來源：連線密碼以 OS keychain 加密、SSH Tunnel、完整 CRUD、視覺化查詢建構器、多結果集同時顯示、跨連線資料傳輸與比對同步、Excel / CSV 匯入匯出、執行計畫視覺化、ER 圖、排程備份、SQL 壓力測試（p50～p99 延遲百分位）、15 條規則的 SQL 審查、Kafka 訊息瀏覽與監控告警；繁中 / 英文雙語介面，內建 AI 助手（自然語言生成 SQL、AI 審查與調校建議）與命令列工具 dbk。免費開源（MIT），提供 Windows / macOS / Linux 安裝檔。
- [VS Code Super Mermaid](https://blog.markkulab.net/tools/super-mermaid): Super Mermaid 是一款 VS Code 擴充套件：開箱即用的漂亮 Mermaid 圖表，自動上色、即時預覽、滑鼠平移縮放、PNG / SVG 高解析匯出，內建 21 種範本與多種主題。免費開源（MIT）。
- [React Super Mermaid](https://blog.markkulab.net/tools/react-super-mermaid): react-super-mermaid 是一個開源 React 元件庫：一行 <MermaidViewer> 即可渲染漂亮的 Mermaid 圖表，內建 colorful / sketch 主題、平移縮放、圖內搜尋、SVG / PNG 高解析匯出。輕量、SSR 安全、完整 TypeScript 型別。免費開源（MIT）。
- [Jira / Confluence Super Mermaid](https://blog.markkulab.net/tools/jira-super-mermaid): Atlassian Forge app：在 Jira issue 與 Confluence 內文直接寫 Mermaid 語法，畫流程圖、時序圖、狀態機與甘特圖。11 種圖表、SVG / PNG 匯出、明暗主題、完整中日韓文字支援。取得 Runs on Atlassian 資格：圖表存在你自己的站台，app 不呼叫任何第三方服務。免費，即將上架 Atlassian Marketplace。
- [Mermaid 線上預覽](https://blog.markkulab.net/tools/mermaid-preview): 在瀏覽器裡寫 Mermaid、即時看圖，整張圖表壓進網址就能分享。免註冊、不上傳伺服器，相容 mermaid.live 的分享連結。
- [React Intl Phone Number](https://blog.markkulab.net/tools/react-intl-phone-number): react-intl-phone-number 是一個開源 React 元件：framework-agnostic、不依賴 antd，提供 E.164 進出、可搜尋國旗 / 國碼下拉、可配置驗證等級（strict / mobile-strict / loose）、可主題化 CSS 與 i18n，電話邏輯由 google-libphonenumber 驅動。輕量、完整 TypeScript 型別。免費開源（MIT）。
- [Uptime Kuma Cluster](https://blog.markkulab.net/tools/uptime-kuma-cluster): 把單機版 Uptime Kuma 改造成高可用叢集：OpenResty + Lua 智慧負載平衡、MariaDB 共享狀態、健康檢查與自動 Failover，附叢集管理 REST API，一行 Docker Compose 啟動。免費開源（MIT）。
- [特教專案](https://blog.markkulab.net/education): 為特殊教育學生製作的學習教材

### 每日 Podcast

- [科技新鮮事](https://blog.markkulab.net/category/tech-news): 每日精選 AI 與科技趨勢，透過語音摘要快速掌握最新技術動態，涵蓋 AI 應用、軟體架構、DevOps 與工程實戰。 — RSS: https://blog.markkulab.net/feed.xml
- [AI股市蝦聊](https://blog.markkulab.net/category/ai-stock-chat): 每個交易日用 AI 分析台股盤勢，以雙人對話聊當天的盤中觀察與隔日預測。 — RSS: https://blog.markkulab.net/ai-stock-chat/feed.xml

### 電子報

[訂閱電子報](https://blog.markkulab.net/subscribe) — 第一時間收到新文章通知，無垃圾信、隨時可取消訂閱。
