如何在後台搜尋商品¶
概述¶
CYBERBIZ 後台商品搜尋功能提供強大的全文檢索與多維度篩選能力,透過 Elasticsearch 實現高效能的商品查詢。本文件詳細說明後台商品搜尋的技術架構、API 端點、搜尋參數及實作細節。
架構概覽¶
商品搜尋系統採用三層架構:
graph TD
A[Frontend - Admin UI] --> B[Rails Controller]
B --> C[Use Case Layer]
C --> D[Repository Layer]
D --> E[Elasticsearch Index]
D --> F[MySQL Database]
核心元件¶
| 元件 | 檔案路徑 | 功能說明 |
|---|---|---|
| Controller | app/controllers/admin/products_controller.rb |
處理 HTTP 請求與回應 |
| Use Case | app/features/admin_context/product/use_cases/elastic_search_products.rb |
商業邏輯處理 |
| Repository | app/features/admin_context/product/repositories/products.rb |
資料存取層 |
| Elastic Searcher | app/services/product_filters/elastic_searcher.rb |
Elasticsearch 查詢建構 |
API 端點¶
基本搜尋¶
搜尋參數¶
基本參數¶
| 參數名稱 | 類型 | 說明 | 範例 |
|---|---|---|---|
q |
String | 全文搜尋關鍵字 | q=iPhone |
page |
Integer | 頁碼 | page=1 |
per |
Integer | 每頁筆數(預設:10) | per=20 |
limit |
Integer | 結果數量限制 | limit=100 |
篩選參數¶
搜尋實作流程¶
1. Controller 層處理¶
app/controllers/admin/products_controller.rb:280-299
def search
result =
if params[:version] == 'v2'
if shop.has_plugin?('new_admin_product')
# 使用 V2 搜尋(Elasticsearch)
search_use_case = AdminContext::Product::UseCases::ElasticSearchProducts.new(
admin_product_repository, language
)
options = params.permit!.to_h.symbolize_keys
options[:creator_id] = current_user.id if restrict_product_scope_by_user?
search_result = search_use_case.execute(shop, current_user, options)
search_result[:products] = search_result.delete(:data)
search_result
else
{ error: t('.v2_permission_deny') }
end
else
# 使用傳統搜尋(資料庫)
get_products
end
render(json: result)
end
版本差異
- V1 搜尋:使用 MySQL 資料庫查詢,透過 Ransack gem 建構 SQL 查詢
- V2 搜尋:使用 Elasticsearch,支援全文檢索與更複雜的篩選條件
2. Use Case 層處理¶
app/features/admin_context/product/use_cases/elastic_search_products.rb
class ElasticSearchProducts
def execute(shop, current_user, query_params)
query = query_params[:q]
# 參數裝飾處理
query_params = ::Products::SearchParamsDecorator.new(query_params)
.execute(current_user)
# 設定預載入關聯
includes = %i[variants tags photos options pos_shop branch_store pim_infos]
add_shop_specific_includes!(shop, includes)
# 執行搜尋
@repository.search_products(
shop,
@language,
query,
query_params,
includes: includes,
methods: %i[photo]
)
end
end
3. Repository 層處理¶
app/features/admin_context/product/repositories/products.rb:115-132
def search_products(shop, language, query, query_params, options = {})
# 執行 Elasticsearch 查詢
search_result = get_search_products(shop, query, query_params)
product_ids = search_result.map(&:id)
# 從資料庫載入完整商品資料
methods = options.delete(:methods)
relations = options.delete(:includes)
relations.unshift(:dicts) if language.try(:not_default?)
@products_rmodel = shop.products.includes(relations).where(id: product_ids)
# 回傳結果
{
data: convert_products(@products_rmodel, language, relations),
total_pages: search_result.total_pages,
total_count: search_result.total_count,
current_page: search_result.current_page,
per: search_result.limit_value
}
end
Elasticsearch 索引結構¶
ProductVariantsIndex 欄位¶
# 主要索引欄位
{
shop_id: Integer, # 商店 ID
product_id: Integer, # 商品 ID
title: String, # 商品標題
sku: String, # SKU
price: Float, # 價格
inventory_quantity: Integer, # 庫存數量
tags_id: Array<Integer>, # 標籤 ID
option_text: Array<String>, # 選項文字
custom_collection_id: Array<Integer>, # 自訂分類 ID
smart_collection_id: Array<Integer>, # 智慧分類 ID
pos_shop_id: Integer, # POS 商店 ID
branch_store_id: Integer, # 分店 ID
warehouse_id: Integer, # 倉庫 ID
product_availability: String, # 商品可用性
selling_availability: Boolean, # 銷售可用性
inventory_availability: String, # 庫存可用性
bonus_availability: String, # 紅利可用性
searchable: Boolean # 是否可搜尋
}
篩選器實作¶
app/services/product_filters/elastic_searcher.rb
class ElasticSearcher < BaseElasticSearch
def search(terms = {}, options = {})
chain = filter(terms)
chain = paginate(chain, options) if options[:non_page].blank?
chain
end
private
def filter(terms)
chain = ProductVariantsIndex.filter(term: { shop_id: @shop.id })
# 套用各種篩選條件
chain = filter_by_inventory_availability(chain, terms[:inventory_availability])
chain = filter_by_product_availability(chain, terms[:product_availability])
chain = filter_by_selling_availability(chain, terms[:selling_availability])
chain = filter_by_price(chain, terms[:price])
chain = filter_by_option_texts(chain, terms[:option_text])
chain = filter_by_custom_collection_ids(chain, terms[:custom_collection_ids])
chain = filter_by_tags_ids(chain, terms[:tags_ids])
chain = filter_by_pos_shop_ids(chain, terms[:pos_shop_ids])
chain
end
end
進階功能¶
批次操作¶
商品搜尋結果支援批次操作:
權限控制¶
# 商品權限檢查
def restrict_product_scope_by_user?
return false unless shop.has_plugin?('product_permission')
!current_user.has_right?('products_view_all')
end
# 限制搜尋範圍
if restrict_product_scope_by_user?
conditions[:creator_id_eq] = current_user.id
end
權限注意事項
- POS 使用者只能搜尋所屬 POS 商店的商品
- 具有
product_permission插件的商店可限制使用者只能查看自己建立的商品 - 管理員需要
products_view_all權限才能查看所有商品
效能優化¶
1. N+1 查詢預防¶
# 預載入關聯以避免 N+1 查詢
products = products.includes(%i[
variants # 商品規格
options # 商品選項
tags # 標籤
photos # 圖片
pos_shop # POS 商店
])
2. 分頁載入¶
3. Elasticsearch 聚合¶
# 使用聚合取得統計資料
def aggregate(chain, aggregations)
aggs = {
min_price: { min: { field: :price } },
max_price: { max: { field: :price } },
product_count: { cardinality: { field: :product_id } }
}
chain.aggs(aggs).aggs
end
前端整合¶
TypeScript 型別定義¶
frontend/admin/src/features/products/domain/models/ProductSearchParams.ts
export type SearchProductV2Params = {
q: string; // 搜尋關鍵字
types: ProductType[]; // 商品類型
vendors: ProductVendor[]; // 供應商
tags: ProductTag[]; // 標籤
genres?: ProductGenreOption[]; // 類別
sell_status: ProductSellStatusOption[]; // 銷售狀態
publicity: ProductPublicityOption[]; // 發佈狀態
stores: BranchStore[]; // 商店
warehouses?: ProductWarehouseOption[]; // 倉庫
custom_collections: ProductCollection[]; // 自訂分類
smart_collections: ProductCollection[]; // 智慧分類
handles?: string[]; // 商品代碼
store_types?: ProductStoreTypeOption[]; // 商店類型
}
API 呼叫範例¶
// 搜尋商品
async searchProducts(params: SearchProductV2Params) {
const response = await fetch('/admin/products/search?version=v2', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
});
return response.json();
}
// 使用範例
const results = await searchProducts({
q: 'iPhone',
types: ['電子產品'],
tags: ['熱銷'],
sell_status: ['on_sale'],
page: 1,
per: 20
});
疑難排解¶
常見問題¶
搜尋結果為空
可能原因:
- Elasticsearch 索引未更新
- 權限不足
- 篩選條件過於嚴格
解決方案:
搜尋速度緩慢
可能原因:
- N+1 查詢問題
- 索引未優化
- 查詢條件複雜
解決方案:
- 檢查並優化預載入關聯
- 使用 explain 分析 Elasticsearch 查詢
- 考慮增加快取層
權限錯誤
錯誤訊息: v2_permission_deny
解決方案:
確認商店已啟用 new_admin_product 插件:
相關文件¶
附錄¶
完整參數對照表¶
| 參數 | 資料庫欄位 | Elasticsearch 欄位 | 說明 |
|---|---|---|---|
q |
title, sku, tags.name |
_all |
全文搜尋 |
product_types |
products.product_type |
product_type |
商品類型 |
vendors |
products.vendor |
vendor |
供應商 |
tags |
tags.id |
tags_id |
標籤 |
pos_shops |
products.pos_shop_id |
pos_shop_id |
POS 商店 |
warehouse_ids |
products.warehouse_id |
warehouse_id |
倉庫 |
sell_status |
計算欄位 | selling_availability |
銷售狀態 |
publicity |
products.published |
product_availability |
發佈狀態 |
程式碼位置索引¶
| 功能 | 檔案路徑 |
|---|---|
| 後端 | |
| Controller | app/controllers/admin/products_controller.rb:280-299 |
| Use Case | app/features/admin_context/product/use_cases/elastic_search_products.rb |
| Repository | app/features/admin_context/product/repositories/products.rb:115-132 |
| Elastic Searcher | app/services/product_filters/elastic_searcher.rb |
| 前端 | |
| Search Params | frontend/admin/src/features/products/domain/models/ProductSearchParams.ts |
| Products Contract | frontend/admin/src/features/products/index/ProductsContract.ts |
| Products Repository | frontend/admin/src/features/products/domain/source/ProductsRepository.ts |