山坡網的用戶抱怨“為什么搜索‘二鬼子李富貴’找不到‘二鬼子漢奸李富貴’?我用百度搜都能找到。”
當時我就滴汗了,用戶說的有道理,應該要能搜索到。
之前的方案很簡單,用戶輸入的字串會在數據庫里做正則表達式匹配,以便用“二鬼子”能搜到“二鬼子漢奸李富貴”。事實證明,我想當然了,即便是這么簡單的一個書名搜索,也不能馬虎。
那就來分析一下怎么做吧,即便不是專業做搜索的,思路上也可以先YY一下。按照本能,先把問題大而化小。
1. 先把搜索字符串進行中文分詞。
2. 用詞組在數據庫里做 or 包含匹配。
3. 搜索出來的結果按與搜索條件相關度排序。
看起來也不難(玩笑話,每一條水都很深),一條一條來解決。
1. 中文分詞。
我找了一下,免費的不多,選擇了盤古分詞的Go語言Port。從Github看到,代碼是四個月以前的了,字典文件有些老。所以我在盤古網站上下載了最新的字典,測試了一下Go的代碼,運行結果良好。
這個Port可能蠻久沒有更新過,代碼的結構不能直接go get,需要自己下載src里面的segment文件夾出來使用。
我把新的字典文件全都放到了revel app的conf文件夾里,如下圖所示。
然后在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的游標做一個即省內存又靠譜的實現。
文章列表