
經客服經理反饋,發現我們網站的搜尋功能體驗非常的差,用戶常常反應搜尋不到的規格,然後他說希望能像 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>
);
}