
經客服經理反饋,發現我們網站的搜尋功能體驗非常的差,用戶常常反應搜尋不到的規格,然後他說希望能像 Google 一樣搜尋體驗,而且可以快速依據產品規格,找到相關的產品,因此我花了些時間評估後,發現全文檢索的技術,應該是能解決我們的問題。
首先,我們得先了解什麼是全文檢索工具(Full-Text Search Tools),它是一種軟體工具,主要功能在有效地搜尋和檢索大量的文字數據,並返回相關的搜尋結果,可以在數秒內檢索大規模文字數據集,提供多種搜尋選項,例如關鍵字搜尋、短語搜尋、模糊搜尋等,使用戶能夠以多種方式查找所需資訊。
接著得了解全文檢索技術和資料庫最大的差別就是,就是當資料量越大時,要對全表掃描時,對資料庫要耗費相當長的時間,但全文檢索技術採用倒排索引的檢索技術,以依據各種分詞器將資料以 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"
}
    
    
| ElasticSearch | RDBMS | 
|---|---|
| INDEX (索引) | 表 | 
| DOCUMENT (文件) | 行 | 
| FIELD (欄位) | 欄位 | 
| MAPPING (結構) | 表結構 | 
# 注意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 -p elasticsearch-group up
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
    
    
GET /_cat/indices
    
    
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}
GET /product/_mapping
P.S 資料庫則需要先定表結構,才能新增資料,但ElasticSearch 相當強大,先新增資料,會依據資料自動推斷表結構的欄位型態。
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" } }
      ]
    }
  }
}
DELETE /product/_doc/1001
DELETE /product
POST /product/_delete_by_query
{
  "query": {
    "match_all": {}
  }
}
npm install @elastic/elasticsearch
// lib/elasticsearch.ts
import { Client } from '@elastic/elasticsearch';
const client = new Client({
    node: 'http://localhost:9200', // 更改為您的 Elasticsearch 節點地址
});
export default client;
// 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>
    );
}