利用Redis如何實現自動補全功能
忘了redis從哪個版本開啟,能夠根據輸入的部分命令字首給出提示,即自動補全。接下來筆者介紹基於redis實現這個很酷的功能。
about sorted set
假設結果中有mara,marabel,marcela。現在我們輸入mar,就能得到這三個名字,並且輸出結果按照字典排序。在實現這個需求之間,我們先簡單介紹sorted set。
大家都知道sorted set是按照score排序的:
127.0.0.1:6380> zadd test 85 sida 127.0.0.1:6380> zadd test 80 xiaolang 127.0.0.1:6380> zadd test 60 afei 127.0.0.1:6380> zadd test 90 chenssy 127.0.0.1:6380> zadd test 98 yunaiv 127.0.0.1:6380> zrange test 0 -1 1) "afei" 2) "xiaolang" 3) "sida" 4) "chenssy" 5) "yunaiv"
但是如果score都一樣,sorted set是按照什麼排序的呢?是按照字典排序的:
127.0.0.1:6380> zadd exam 0 sida 127.0.0.1:6380> zadd exam 0 xiaolang 127.0.0.1:6380> zadd exam 0 chenssy 127.0.0.1:6380> zadd exam 0 yunaiv 127.0.0.1:6380> zadd exam 0 afei 127.0.0.1:6380> zrange exam 0 -1 1) "afei" 2) "chenssy" 3) "sida" 4) "xiaolang" 5) "yunaiv"
這是sorted set一個非常重要的特性,也是我們自動補全需求的一個要點。但是這還不夠,離我們的最終需求還有一段路要走。幸運的是sorted set還有另一個很酷的命令:ZRANK。這個命令能知道你要查詢的key在sorted set中的位置:
127.0.0.1:6380> zrank exam sida (integer) 2 127.0.0.1:6380> zrank exam yunaiv (integer) 4 127.0.0.1:6380> zrange exam 2 4 1) "sida" 2) "xiaolang" 3) "yunaiv"
到這裡感覺離我們實現自動補全的第一個版本非常接近了,我們能得到sorted set中按照字典排序後任意一個member及其後面N個member。
簡單實現
為了實現最終的自動補全,我們需要付出一些代價:空間。
意思是,對於某個準備新增到sorted set中的member,例如afei,我們不僅要把完整的詞(afei)新增到sorted set中,而且還要新增所有可能的字首(a,af,afe,afei)。這裡為了解決某個詞是真正的member還是某個member的字首(例如bar既是一個完整的詞,也是某個member例如bark的可能字首),我們加了一個小把戲,即在真正member的後面增加一個特殊字元,例如"$",那麼afei這個member就會新增下列這些詞:
a,afei,afei$
現在假設我們需要新增三個詞:foo,bar,foobar 來進行一些測試, 那麼sorted set中就會有如下這些詞:
127.0.0.1:6380> zrange autoc 0 -1 1) "b" 2) "ba" 3) "bar" 4) "bar$" 5) "f" 6) "fo" 7) "foo" 8) "foo$" 9) "foob" 10) "fooba" 11) "foobar" 12) "foobar$"
離我們最終的目標又要近了很多。現在假設使用者輸入"fo",那麼為了實現自動補全,我們需要執行如下命令,仔細檢視結果,foo$和foobar$就是我們需要的結果,只需要將特殊字尾$去掉即可(其他沒有以$結尾的詞全部忽略):
127.0.0.1:6380> zrank autoc fo (integer) 5 127.0.0.1:6380> zrange autoc 5 -1 1) "fo" 2) "foo" 3) "foo$" 4) "foob" 5) "fooba" 6) "foobar" 7) "foobar$"
更多詞的測試
網址http://antirez.com/misc/female-names.txt 提供了近5000個女性名字。按照前面說的規則,將所有名字的所有可能字首全部新增到sorted set中。假定使用者輸入member,那麼只需要通過如下兩個命令,得到字典排序後用戶輸入的member後面的50個詞,然後只取$結尾的詞:
127.0.0.1:6380> zrank autoc member (integer) 8 127.0.0.1:6380> zrange autoc 8 50
時間&空間複雜度
根據官方文件可知,zrank和zrange的事件複雜度都是O(log(N))。因此,即使資料量比較大,這種方案也是可行的。
ZRANK key member
起始版本:2.0.0
時間複雜度:O(log(N))ZRANGE key start stop [WITHSCORES]
起始版本:1.2.0
時間複雜度:O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements returned.
那麼需要多少記憶體呢?
假設在最糟糕的情況下,一個長度為M的詞需要新增M+1個詞到sorted set中。那麼如果有N個詞,總計需要新增N*(Ma+1)個詞到sorted set中,Ma是這N個詞的平均長度。
幸運的是,實際情況遠比這個要好得多,以近5000個女性名字為例,只需要新增大約15000個詞到sorted set中,因為這些名詞存在大量重複的字首。以3個名字為例:marci,marcia,marcile。如果按照最糟糕的情況,需要新增3*(6+1)=21個詞到sorted set中,然而實際情況呢,只需要新增11個詞到sorted set中即可:
m,ma,mar,marc,marci,marci$,marcia,marcia$,marcil,marcile,marcile$。
而且,隨著樣本越來越大,重複的字首會越大越多。
所以,需要的記憶體也還OK。是不是覺得美滋滋?哈
查詢預測
我們這次的自動補全是按照字典排序,很多時候,這是我們需要的。但是也有一些情況,我們希望按照相似度來排序。例如google搜尋那樣,搜尋結果按照頻率和熱度等維度進行排序。例如,當用戶輸入ne,使用者應該希望看到Netflix,news,new york times等這些熱門關鍵詞。
這樣做起來就不那麼容易了,如果要能達到根據頻率排序,我們需要一個特別的sorted set能以非阻塞的方式實時更新每個字首的頻率:
當用戶輸入一個例如"foo"這種查詢,由於它的所有字首為:"f","fo","foo"。那麼對每個字首都執行命令:ZINCRBY <prefix> 1 foo ;如果使用者輸入"foobar",那麼對每個字首都執行:ZINCRBY <prefix> 1 foobar;
這種方法還有一個問題,許多自動補全系統只需要展示TOP N個關鍵詞,假設N為10。但是我們這種方法,如果要計算TOP 10,我們需要取得關鍵詞遠不止10個,理論上我們要這個字首作為key的sorted set中所有member都取出來。
幸運的是,從統計學上來講,每個對於sorted set中有300個member的字首,就能得到TOP 5關鍵詞。如果查詢越頻繁,它的得分越高,它最終被選中的概率也就越高。因此,我們要做的就是對搜尋字串的每個字首:
- 如果字首作為key的sorted set中member數量還沒有達到300,那麼只需要簡單的對其zincrby即可;
- 如果字首作為key的sorted set中member數量已經達到了300,我們將那些得分比較低的member刪除,增加新的member進來,從而達到關鍵詞頻率不斷迭代的效果。
這個方法一下子可能理解不過來,沒關係,舉個栗子。假設現在使用者輸入了next,其字首n為key的sorted set中已經有netflix(100),news(120),new york(80),near(23),nequ(1)。由於這個字首對應的sorted set中的member數量還沒有300,所以,執行:zincrby n 1 next。其中n是字首,next是使用者輸入的關鍵詞。假設現在使用者輸入了next,其字首n為key的sorted set中已經有netflix(100),省略295個score大於1的關鍵詞,nequ(1)。由於這個字首對應的sorted set中的member數量達到了300,所以,先刪除得分比較低的nequ,再執行:zincrby n 1 next。
這個方法跟使用者輸入關鍵詞分佈有很大的關聯性,如果使用者輸入的所有關鍵詞頻率比較接近,那麼這個方法得到的資料並不是很可靠。但是我們知道這不是問題,因為搜尋就是絕大部分人在搜尋那一小部分關鍵詞集合。
清理階段
由於上面提到的搜尋長尾效應,我們可以講那些得分為1的member清理掉,因為使用者對這些關鍵詞幾乎沒有任何關注度。清理操作還能夠減少使用記憶體,從而讓redis儲存更多更有用的資料。
知識總結
- sorted set資料結構中,如果member的score一樣,那麼按照字典排序。
- zrank命令能得到指定member在結果中的位置,並可以取排在它後面N個member。
- zincrby能給指定的sorted set中的member加分。
參考:http://oldblog.antirez.com/post/autocomplete-with-redis.html
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對我們的支援。