經客服經理反饋,發現我們網站的搜尋功能體驗非常的差,用戶常常反應搜尋不到的規格,然後他說希望能像 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" }
P.S. Elasticsearch 並沒有內建中文的分詞器,但可以另外安裝常用的中文分詞器,EX:IK 分词器、Smartcn 分词器和 Jieba 。
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> ); }