前段時間寫需求寫到搜尋模組,伴著條件越來越多,各種條件組合的也越來越複雜,後期不好維護,所以改進了一下以前的搜尋寫法,業餘時間寫了一個小案例,和大家一起探討探討。
完整案例
基於 es7.14 開發,使用 ik_smart 分詞
使用 olivere/elastic go-es擴充套件
使用到
gin
路由批次生成 測試資料
批次匯入資料到es case
v1改進前程式碼&v2 改進後程式碼
請求結構
SearchRequest 搜尋請求結構
// SearchRequest 搜尋請求結構
type SearchRequest struct {
Keyword string `form:"keyword"` // 關鍵詞
CategoryId uint8 `form:"category_id"` // 分類
Sort uint8 `form:"sort" binding:"omitempty,oneof=1 2 3"` // 排序 1=瀏覽量;2=收藏;3=點贊;
IsSolve uint8 `form:"is_solve" binding:"omitempty,oneof=1 2"` // 是否解決
Page int `form:"page,default=1"` // 頁數
PageSize int `form:"page_size,default=10"` // 每頁數量
}
改進前
Search() 文章搜尋和 Recommend() 文章推薦的程式碼幾乎一樣,只是條件有所不同,重複程式碼太多,也不好維護。
例:Search() 處理搜尋請求
func (a ArticleV1) Search(c *gin.Context) {
req := new(model.SearchRequest)
if err := c.ShouldBind(req); err != nil {
c.JSON(400, err.Error())
return
}
// 構建搜尋
builder := es.Client.Search().Index(model.ArticleEsAlias)
bq := elastic.NewBoolQuery()
// 標題
if req.Keyword != "" {
builder.Query(bq.Must(elastic.NewMatchQuery("title", req.Keyword)))
}
// 分類
if req.CategoryId != 0 {
builder.Query(bq.Filter(elastic.NewTermQuery("category_id", req.CategoryId)))
}
// 是否解決
if req.IsSolve != 0 {
builder.Query(bq.Filter(elastic.NewTermQuery("is_solve", req.IsSolve)))
}
// 排序
switch req.Sort {
case SortBrowseDesc:
builder.Sort("brows_num", false)
case SortUpvoteDesc:
builder.Sort("upvote_num", false)
case SortCollectDesc:
builder.Sort("collect_num", false)
default:
builder.Sort("created_at", false)
}
// 分頁
from := (req.Page - 1) * req.PageSize
// 指定查詢欄位
include := []string{"id", "category_id", "title", "brows_num", "collect_num", "upvote_num", "is_recommend", "is_solve", "created_at"}
builder.
FetchSourceContext(
elastic.NewFetchSourceContext(true).Include(include...),
).
From(from).
Size(req.PageSize)
// 執行查詢
do, err := builder.Do(context.Background())
if err != nil {
c.JSON(500, err.Error())
return
}
// 獲取匹配到的數量
total := do.TotalHits()
// 序列化資料
list := make([]model.SearchResponse, len(do.Hits.Hits))
for i, raw := range do.Hits.Hits {
tmpArticle := model.SearchResponse{}
if err := json.Unmarshal(raw.Source, &tmpArticle); err != nil {
log.Println(err)
}
list[i] = tmpArticle
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": gin.H{
"total": total,
"list": list,
},
})
return
}
請求測試一下
curl GET '127.0.0.1:8080/article/v1/search?keyword=茴香豆&page=1&page_size=2&sort=1'
點這裡檢視返回結果
{
"code": 200,
"data": {
"list": [
{
"id": 8912,
"category_id": 238,
"title": "茴香豆上賬;又好笑,",
"brows_num": 253,
"collect_num": 34,
"upvote_num": 203,
"is_recommend": 2,
"is_solve": 1,
"created_at": "2021-02-01 15:19:36"
}
...
],
"total": 157
}
}
Recommend() 處理推薦請求
func (a ArticleV1) Recommend(c *gin.Context) {}
// Recommend 文章推薦
func (a ArticleV1) Recommend(c *gin.Context) {
// 構建搜尋
builder := es.Client.Search().Index(model.ArticleEsAlias)
bq := elastic.NewBoolQuery()
builder.Query(bq.Filter(
// 推薦文章
elastic.NewTermQuery("category_id", model.ArticleIsRecommendYes),
// 已解決
elastic.NewTermQuery("is_solve", model.ArticleIsSolveYes),
))
// 瀏覽量排序
builder.Sort("brows_num", false)
do, err := builder.From(0).Size(10).Do(context.Background())
if err != nil {
return
}
// 序列化資料
...
改進後
先看結果
把所有查詢的條件都拆分開來,像查詢資料庫一樣查詢 es,多方便呢,只需要組合需要的條件就可以得到想要的結果
// Search 文章搜尋
func (a ArticleV2) Search(c *gin.Context) {
req := new(model.SearchRequest)
if err := c.ShouldBind(req); err != nil {
c.JSON(400, err.Error())
return
}
// 像查資料庫一樣方便的新增條件即可查詢
list, total, err := service.NewArticle().
WhereKeyword(req.Keyword).
WhereCategoryId(req.CategoryId).
WhereIsSolve(req.IsSolve).
Sort(req.Sort).
Paginate(req.Page, req.PageSize).
DecodeSearch()
if err != nil {
c.JSON(400, err.Error())
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": gin.H{
"total": total,
"list": list,
},
})
return
}
// Recommend 文章推薦
func (a ArticleV2) Recommend(c *gin.Context) {
// 像查資料庫一樣方便的新增條件即可查詢
list, _, err := service.NewArticle().
WhereCategoryId(model.ArticleIsRecommendYes).
WhereIsSolve(model.ArticleIsSolveYes).
OrderByDesc("brows_num").
PageSize(5).
DecodeRecommend()
if err != nil {
c.JSON(400, err.Error())
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": gin.H{
"total": len(list),
"list": list,
},
})
return
}
怎麼做到的呢
既然只是條件不同,那用組合的方式,把需要的條件組裝起來再執行查詢
internal/service/article.go
本案例中涉及到了 es
的 must
filter
sort
條件,所以我們要先構建一個 struct
儲存要組合的條件
type article struct {
must []elastic.Query
filter []elastic.Query
sort []elastic.Sorter
from int
size int
}
建構函式
func NewArticle() *article {
return &article{
must: make([]elastic.Query, 0),
filter: make([]elastic.Query, 0),
sort: make([]elastic.Sorter, 0),
from: 0,
size: 10,
}
}
新增組合的條件的方法
剛開始用過
AddKeyword()
WithKeyword
方法名,但都感覺不太好,後來想到Laravel
中有快捷的查詢條件的方法,如whereId(1)
會生成 sql :xxx where id = 1
所以就仿照了Laravel
的方式命名了
// WhereKeyword 關鍵詞
func (a article) WhereKeyword(keyword string) article {
if keyword != "" {
a.must = append(a.must, elastic.NewMatchQuery("title", keyword))
}
return a
}
// WhereCategoryId 分類
func (a article) WhereCategoryId(categoryId uint8) article {
if categoryId != 0 {
a.filter = append(a.filter, elastic.NewTermQuery("category_id", categoryId))
}
return a
}
// WhereIsSolve 是否已解決
func (a article) WhereIsSolve(isSolve uint8) article {
if isSolve != 0 {
a.filter = append(a.filter, elastic.NewTermQuery("is_solve", isSolve))
}
return a
}
// Sort 排序
func (a article) Sort(sort uint8) article {
switch sort {
case SortBrowseDesc:
return a.OrderByDesc("brows_num")
case SortUpvoteDesc:
return a.OrderByDesc("upvote_num")
case SortCollectDesc:
return a.OrderByDesc("collect_num")
}
return a
}
// OrderByDesc 透過欄位倒序排序
func (a article) OrderByDesc(field string) article {
a.sort = append(a.sort, elastic.SortInfo{Field: field, Ascending: false})
return a
}
// OrderByAsc 透過欄位正序排序
func (a article) OrderByAsc(field string) article {
a.sort = append(a.sort, elastic.SortInfo{Field: field, Ascending: true})
return a
}
// Paginate 分頁
// page 當前頁碼
// pageSize 每頁數量
func (a article) Paginate(page, pageSize int) article {
a.from = (page - 1) * pageSize
a.size = pageSize
return a
}
到這裡已經把需要的全部條件已經構建好了,現在條件有了,需要執行最後的搜尋了
// Searcher 執行查詢
func (a article) Searcher(include ...interface{}) ([]json.RawMessage, int64, error) {
builder := es.Client.Search().Index(model.ArticleEsAlias)
// 查詢的欄位
includeKeys := make([]string, 0)
if len(include) > 0 {
includeKeys = structer.Keys(include[0])
}
// 構建查詢
builder.Query(
// 構建 bool query 條件
elastic.NewBoolQuery().Must(a.must...).Filter(a.filter...),
)
// 執行查詢
do, err := builder.
FetchSourceContext(elastic.NewFetchSourceContext(true).Include(includeKeys...)).
From(a.from).
Size(a.size).
SortBy(a.sort...).
Do(context.Background())
if err != nil {
return nil, 0, err
}
total := do.TotalHits()
list := make([]json.RawMessage, len(do.Hits.Hits))
for i, hit := range do.Hits.Hits {
list[i] = hit.Source
}
return list, total, nil
}
到這裡就可以使用上面構建好的組合條件的模式針對不同的介面條件組合不同的條件獲取結果
list, _, err := service.NewArticle().
WhereCategoryId(model.ArticleIsRecommendYes).
WhereIsSolve(model.ArticleIsSolveYes).
OrderByDesc("brows_num").
PageSize(5).
Searcher()
但這個時候還有個問題,這裡的 list
返回的是一個 []json.RawMessage
的陣列,每一條紀錄都是一條json
字串,有時候我們需要對查詢出來的資料進行進一步的處理,這個時候就需要把內容序列化成對應的 struct
了,我們可以再寫一個DecodeXxxx()
的方法,來針對不同的用途序列化成對應的 struct
推薦結果的結構體,這樣返回一個
[]model.RecommendResponse
可用的結構體,呼叫時就不直接呼叫Searcher()
呼叫DecodeRecommend()
就好了。
func (a article) DecodeRecommend() ([]model.RecommendResponse, int64, error) {
rawList, total, err := a.Searcher(new(model.RecommendResponse))
if err != nil {
return nil, total, err
}
list := make([]model.RecommendResponse, len(rawList))
for i, raw := range rawList {
tmp := model.RecommendResponse{}
if err := json.Unmarshal(raw, &tmp); err != nil {
log.Println(err)
continue
}
list[i] = tmp
}
return list, total, nil
}
總結
到此已經完成了搜尋的簡單封裝,完整的案例可以去 github.com/zxr615/go-escase 交流交流
本作品採用《CC 協議》,轉載必須註明作者和本文連結