Golang使用elastic庫來實現(xiàn)前后端模糊搜索功能
介紹
使用go的elastic庫來實現(xiàn)前后端模糊搜索功能的示例
工具
后端
- connectrpc.com/connect:與前端通信
- connectrpc.com/cors:解決瀏覽器跨域
- google.golang.org/protobuf: API定義
- buf:生成go的api
- sqlc:與Postgres數(shù)據(jù)庫交互
- github.com/elastic/go-elasticsearch/v9:go的es交互工具
前端
- react-ts
- vite
- @connectrpc/connect: 與后端通信
- @connectrpc/connect-web: 與后端通信
快速入門
先決條件
- go >=1.25.1
- node.js >=22
- docker >= 18
- postgres >= 12
- elastic-search >= 8
- elastic-kibana >= 8
- pgsync
- buf
- redis
實現(xiàn)思路
設(shè)計數(shù)據(jù)結(jié)構(gòu), 以一個經(jīng)典的電商商品舉例:
CREATE DATABASE ecommerce;
CREATE SCHEMA product;
SET search_path to product;
CREATE TYPE product_status AS ENUM (
'draft',-- 草稿
'pending_review', -- 待審核
'active', -- 上架
'inactive', -- 下架
'deleted' -- 刪除,進(jìn)入回收站,無法搜索,30天后物理刪除
);
-- 商品表
CREATE TABLE product.products
(
id bigserial primary key,
name varchar(255) not null,
description text, -- 商品描述
price decimal(10, 2) not null,
status product_status not null default 'draft',
merchant_id uuid not null,
category_id int not null,
category_name VARCHAR(100) not null,
cover_image text not null,
attributes jsonb not null default '{}',
sales_count int not null default 0,
rating_score decimal(2, 1) not null default 0.0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
deleted_at timestamptz not null default now()
);
-- 商品圖片表
CREATE TABLE product.images
(
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES product.products (id) ON DELETE CASCADE,
url VARCHAR(500) NOT NULL,
type VARCHAR(20) NOT NULL DEFAULT 'detail', -- cover/detail
sort_order INTEGER DEFAULT 0,
alt_text TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
-- 約束和索引
UNIQUE (product_id, sort_order),
CHECK (type IN ('cover', 'detail'))
);proto數(shù)據(jù)結(jié)構(gòu):
syntax = "proto3";
package api.product.v1;
option go_package = "github.com/sunmery/elastic-example/api/product/v1;productv1";
import "google/protobuf/timestamp.proto";
service ProductService {
rpc GetProduct(GetProductRequest) returns(GetProductResponse){}
}
message GetProductRequest {
string index = 1;
string name = 2;
}
message GetProductResponse{
repeated Product products = 1;
}
// 定義 ProductImage 消息 (對應(yīng) Go 結(jié)構(gòu)體中的 ProductImage)
message ProductImage {
// url 字段,對應(yīng) Go 結(jié)構(gòu)體中的 url 字段
string url = 1;
// type 字段,例如 "cover" (封面) 或 "detail" (詳情頁)
string type = 2;
// sort_order 字段,用于排序
int32 sort_order = 3;
// alt_text 字段,用于圖片替代文本
string alt_text = 4;
}
// 定義 ProductAttribute 消息 (對應(yīng) Go 結(jié)構(gòu)體中的 ProductAttribute)
message ProductAttribute {
// key 字段,屬性名 (例如 "顏色", "尺寸")
string key = 1;
// value 字段,屬性值 (例如 "紅色", "L")
string value = 2;
}
// 定義 Product 消息 (對應(yīng) Go 結(jié)構(gòu)體中的 Product)
message Product {
// 基礎(chǔ)信息
string id = 1;
string name = 2;
// repeated 對應(yīng) Go 中的切片 ([]string)
// repeated string name_suggest = 3;
string name_suggest = 3;
string description = 4;
// 價格和狀態(tài)
double price = 5;
string status = 6;
// 分類和商家信息
string merchant_id = 7;
int32 category_id = 8; // Go 的 int 對應(yīng) Protobuf 的 int32
string category_name = 9;
// 圖片和屬性 (嵌套消息)
// repeated 對應(yīng) Go 中的結(jié)構(gòu)體切片 ([]ProductImage)
repeated ProductImage images = 10;
string cover_image = 11;
// repeated 對應(yīng) Go 中的結(jié)構(gòu)體切片 ([]ProductAttribute)
map<string,string> attributes = 12;
// 統(tǒng)計信息
int32 sales_count = 13;
double rating_score = 14;
// 時間戳 (使用標(biāo)準(zhǔn) Protobuf 類型)
google.protobuf.Timestamp created_at = 15;
google.protobuf.Timestamp updated_at = 16;
}
go的數(shù)據(jù)結(jié)構(gòu):
package biz
import "time"
// ProductImage 商品圖片結(jié)構(gòu)
type ProductImage struct {
URL string `json:"url"`
Type string `json:"type"` // cover/detail
SortOrder int `json:"sort_order"`
AltText string `json:"alt_text,omitempty"`
}
// ProductAttribute 商品屬性結(jié)構(gòu)
type ProductAttribute struct {
Key string `json:"key"`
Value string `json:"value"`
}
// Product 商品主結(jié)構(gòu)
type Product struct {
ProductId int64 `json:"product_id"`
ProductName string `json:"product_name"`
NameSuggest string `json:"name_suggest,omitempty"` // 用于搜索建議
Description string `json:"description,omitempty"`
Price float64 `json:"price"`
Status string `json:"status"` // 上架/下架
MerchantID string `json:"merchant_id"`
CategoryID int `json:"category_id"`
CategoryName string `json:"category_name"`
Images []ProductImage `json:"images,omitempty"`
CoverImage string `json:"cover_image,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
SalesCount int `json:"sales_count"`
RatingScore float64 `json:"rating_score"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
構(gòu)建一個Web服務(wù)來給前端提供路由
func main() {
es := InitES()
Producter := NewProductServer(es)
mux := http.NewServeMux()
path, handler := productv1connect.NewProductServiceHandler(
Producter,
// Validation via Protovalidate is almost always recommended
connect.WithInterceptors(validate.NewInterceptor()),
)
mux.Handle(path, handler)
// CORS 配置
corsHandler := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: connectcors.AllowedMethods(),
AllowedHeaders: connectcors.AllowedHeaders(),
ExposedHeaders: connectcors.ExposedHeaders(),
MaxAge: 7200,
AllowCredentials: false,
})
// 創(chuàng)建處理器鏈:監(jiān)控中間件 -> CORS -> HTTP/2
handlerChain := corsHandler.Handler(mux)
p := new(http.Protocols)
p.SetHTTP1(true)
// Use h2c so we can serve HTTP/2 without TLS.
p.SetUnencryptedHTTP2(true)
s := http.Server{
Addr: "localhost:8080",
Handler: h2c.NewHandler(handlerChain, &http2.Server{}),
Protocols: p,
}
s.ListenAndServe()
}提供搜索功能:為了能夠讓用戶在搜索時提供更好的范圍,就需要從多個字段提供值來擴(kuò)大匹配的范圍。例如用戶想搜索一個商品名為"iPhone 18"時,數(shù)據(jù)庫存儲了一個name值,如果只從name字段去匹配,如果不引入es,可以寫一個sql為匹配前綴,那么可以取到,那么當(dāng)用戶搜索"手機(jī)"時,數(shù)據(jù)庫并不會返回該條目,因為它的name不包含手機(jī),而它也許只出現(xiàn)在description商品的介紹或者該商品的分類手機(jī)類目里,那么就可以很方便的使用es提供的功能來實現(xiàn):
func (p ProductServer) GetProduct(ctx context.Context, c *connect.Request[v1.GetProductRequest]) (*connect.Response[v1.GetProductResponse], error) {
searchFidles := []string{
"product_name",
"categoryName",
"description",
"attributes.*",
}
res, err := p.es.Search().Index(c.Msg.Index).Request(&search.Request{
Query: &types.Query{
MultiMatch: &types.MultiMatchQuery{
Query: c.Msg.Name,
Fields: searchFidles,
},
},
}).Do(ctx)
if err != nil {
return nil, err
}
var v1Products []*v1.Product // 存放 Protobuf 格式的商品列表
for _, hit := range res.Hits.Hits {
var bizProduct biz.Product
if err := json.Unmarshal(hit.Source_, &bizProduct); err != nil {
log.Printf("解析文檔失敗:%v", err)
continue
}
v1Product := bizToV1Product(&bizProduct)
v1Products = append(v1Products, v1Product)
fmt.Printf("文檔ID: %d, 評分: %f\n", hit.Id_, *hit.Score_)
fmt.Printf("商品名稱: %s, 價格: %.2f\n", bizProduct.ProductName, bizProduct.Price)
}
fmt.Printf("成功解析 %d 個商品\n", len(v1Products))
return connect.NewResponse(&v1.GetProductResponse{Products: v1Products}), nil
}
前端負(fù)責(zé)展示從后端接收到的數(shù)據(jù)并進(jìn)行排版展示
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { ProductService, type Product } from "./api/product_pb";
import { useState } from "react";
const transport = createConnectTransport({
baseUrl: "http://localhost:8080",
});
const client = createClient(ProductService, transport);
function App() {
const [index, setIndex] = useState<string>('products');
const [name, setName] = useState<string>('蘋果iPhone 15 Pro Max 256GB 原色鈦金屬');
const [products, setProducts] = useState<Product[]>([]);
const getDoc = async (index: string, name: string) => {
try {
const response = await client.getProduct({
index,
name
});
console.log("API Response:", response);
// 檢查響應(yīng)結(jié)構(gòu)并設(shè)置 products
if (response.products && Array.isArray(response.products)) {
setProducts(response.products);
} else {
console.warn("響應(yīng)中沒有 products 數(shù)組:", response);
setProducts([]);
}
} catch (error) {
console.error("獲取產(chǎn)品失敗:", error);
setProducts([]);
}
};
return (
<>
<div style={{ padding: '20px' }}>
<label style={{ display: 'block', marginBottom: '10px' }}>
索引名稱:
<input
type="text"
value={index}
onChange={(e) => setIndex(e.target.value)}
style={{ marginLeft: '10px', padding: '5px' }}
/>
</label>
<label style={{ display: 'block', marginBottom: '10px' }}>
搜索關(guān)鍵詞:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
style={{ marginLeft: '10px', padding: '5px' }}
/>
</label>
<button
onClick={() => getDoc(index, name)}
style={{
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
搜索產(chǎn)品
</button>
{/* 產(chǎn)品列表渲染 */}
<div style={{ marginTop: '20px' }}>
<h3>搜索結(jié)果 ({products.length} 個產(chǎn)品):</h3>
{products.length === 0 ? (
<p>沒有找到產(chǎn)品</p>
) : (
<ol>
{products.map((item:Product) => (
<li key={item.id} style={{ marginBottom: '15px', padding: '10px', border: '1px solid #ddd' }}>
<p>產(chǎn)品名稱:{item.name}</p>
<p>價格:{item.price}</p>
<p>描述:{item.description}</p>
<p>狀態(tài):{item.status}</p>
<p>分類:{item.categoryName}</p>
</li>
))}
</ol>
)}
</div>
</div>
</>
);
}
export default App;
運行
基礎(chǔ)設(shè)施
將example.com替換為你的地址
將password替換為更安全的密碼
elastic
docker compose -f infrastructure/elastic.yaml up -d
postgres
docker compose -f infrastructure/postgres/compose.yaml up -d
redis
docker compose -f infrastructure/redis/compose.yaml up -d
pgsync
docker compose -f infrastructure/pgsync/compose.yaml up -d
后端
將example.com替換為你的地址
go mod tidy go run .
前端
pnpm i pnpm dev
測試
將example.com替換為你的地址
elastic search:
export ELASTICSEARCH_URL="http://example.com:9200"
curl -X GET $ELASTICSEARCH_URL/products/_search -H 'Content-Type: application/json' -d'
{
"query": {
"match": {
"product_name": "蘋果"
}
},
"size": 10
}
'
后端:
curl -v -X POST http://localhost:8080/api.product.v1.ProductService/GetProduct -H 'Content-Type: application/json' -d'
{
"name": "手機(jī)",
"index": "products"
}
'
前端:

注意事項
同步問題:pgsync設(shè)定了每20s從postgres數(shù)據(jù)庫,redis緩存獲取數(shù)據(jù)并同步到es,并非實時同步,請根據(jù)實際需求來設(shè)定合理時間。
es插件:pgync不支持ik等第三方分詞插件,所以在schema.json即使定義了也不會起作用,pgsync不會不識別
到此這篇關(guān)于Golang使用elastic庫來實現(xiàn)前后端模糊搜索功能的文章就介紹到這了,更多相關(guān)Golang模糊搜索內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言標(biāo)準(zhǔn)輸入輸出庫的基本使用教程
輸入輸出在任何一門語言中都必須提供的一個功能,下面這篇文章主要給大家介紹了關(guān)于Go語言標(biāo)準(zhǔn)輸入輸出庫的基本使用,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-02-02
Golang基于Vault實現(xiàn)敏感數(shù)據(jù)加解密
數(shù)據(jù)加密是主要的數(shù)據(jù)安全防護(hù)技術(shù)之一,敏感數(shù)據(jù)應(yīng)該加密存儲在數(shù)據(jù)庫中,降低泄露風(fēng)險,本文將介紹一下利用Vault實現(xiàn)敏感數(shù)據(jù)加解密的方法,需要的可以參考一下2023-07-07

