文章出處

山坡網的用戶抱怨“為什么搜索‘二鬼子李富貴’找不到‘二鬼子漢奸李富貴’?我用百度搜都能找到。”

當時我就滴汗了,用戶說的有道理,應該要能搜索到。

之前的方案很簡單,用戶輸入的字串會在數據庫里做正則表達式匹配,以便用“二鬼子”能搜到“二鬼子漢奸李富貴”。事實證明,我想當然了,即便是這么簡單的一個書名搜索,也不能馬虎。

那就來分析一下怎么做吧,即便不是專業做搜索的,思路上也可以先YY一下。按照本能,先把問題大而化小。

1. 先把搜索字符串進行中文分詞

2. 用詞組在數據庫里做 or 包含匹配。

3. 搜索出來的結果按與搜索條件相關度排序。

看起來也不難(玩笑話,每一條水都很深),一條一條來解決。

1. 中文分詞。

我找了一下,免費的不多,選擇了盤古分詞Go語言Port。從Github看到,代碼是四個月以前的了,字典文件有些老。所以我在盤古網站上下載了最新的字典,測試了一下Go的代碼,運行結果良好。

這個Port可能蠻久沒有更新過,代碼的結構不能直接go get,需要自己下載src里面的segment文件夾出來使用。

我把新的字典文件全都放到了revel app的conf文件夾里,如下圖所示。

image

然后在Controller.Init方法里加上初始化代碼。

revel.OnAppStart(func() {
   segHandler = segment.NewSegment()
   err := segHandler.Init(path.Join(revel.ConfPaths[0], "dicts"))
   if err != nil {
     glog.Fatalln("Failed to init segment handler", err)
   }

然后在需要的地方就可以開始用它分詞了。

//對搜索的字符串進行分詞
searchKey := "(" + key + "|"

segs := segHandler.DoSegment(key)
for cur := segs.Front(); cur != nil; cur = cur.Next() {
  word := cur.Value.(*dict.WordInfo)
  if word.Word != "的" {
    searchKey += word.Word + "|"
  }
}

searchKey = strings.TrimRight(searchKey, "|") + ")"

searchResults, pageSum, err := d.findBookBy(M{"$or": []M{
  M{"title": M{"$regex": searchKey}},
  M{"author": M{"$regex": key}},
  M{"category": M{"$regex": key}}}}, "-score", pageNum, numPerPage)
if err != nil {
  return nil, pageSum, err
}

思路是把搜索條件分成詞組,再組合成正則表達式,比如“二鬼子李富貴”變成“(二|鬼子|李富貴)”,然后使用mongodb的正則查詢。

這里我把“的”字去掉了,因為中文里面“的”字用的太多了,基本沒有查詢價值。

2. 搜索出來的結果按與搜索條件相關度排序。

搜索引擎里,這部分是技術含量最大的。我這邊只是牛刀小試,所以方案簡單很多,把搜索出來的書籍標題與搜索條件比對相似度。

正好,字符串比對相似度的庫我之前Port過一個,叫做simhash(當時為什么port我都忘了,哈,工具箱里東西多還是有好處的!)。算法具體就不多說了,免得跑題。看用法吧。

needle := "Reading bytes into structs using reflection"
hayStack := "Golang - mapping an variable length array to a struct"

likeness := GetLikenessValue(needle, hayStack)
fmt.Println("Likeness:", likeness)
就一個函數,輸入兩個字符串,輸出一個從0到1的浮點數,代表相似百分比。
為了方便計算,我在SearchResult結構中加入了一個新的字段,OriginalQueryString,存儲原始搜索條件,之后實現一下Sort接口。

type SearchResult struct {
  Id                bson.ObjectId "_id"
  Title             string
  OriginQueryString string //原始的搜索條件,用于排序
}

type SearchResults []SearchResult

func (srs SearchResults) Len() int {
  return len(srs)
}

func (srs SearchResults) Less(i, j int) bool {
  likenessI := simhash.GetLikenessValue(srs[i].Title, srs[i].OriginQueryString)
  likenessJ := simhash.GetLikenessValue(srs[j].Title, srs[j].OriginQueryString)

  return likenessI < likenessJ
}

func (srs SearchResults) Swap(i, j int) {
  srs[i], srs[j] = srs[j], srs[i]
}

就可以在搜索出來之后按照相關性排序了。

//為searchResult的OriginQueryString賦值,以便按照搜索相關性排序
for i, _ := range searchResults {
  searchResults[i].OriginQueryString = key
}

sort.Sort(sort.Reverse(SearchResults(searchResults)))

我的實現到這里就完成了。

但其實有一部分很重要的東西我取巧了。由于使用模糊搜索,結果集的大小是無法預料的,全部取的話隨時可能把內存用完。分批的話怎么保證相關性排序的準確性呢?好問題,這里是非常關鍵又很難做的部分,我取巧的方式是把書籍按評分排序,然后取前20個出來,僅僅在這20本書中做相似度排序。這并不是完美的方案,僅僅只是夠用。

后期如果有時間,可以用mongodb的游標做一個即省內存又靠譜的實現。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()