├── LICENSE ├── README.md ├── algorithms.go ├── algorithms_poll.go ├── algorithms_rate.go ├── algorithms_test.go ├── collection.go ├── config.go ├── errors.go ├── example ├── cli.go └── http.go ├── gocommend.go ├── gocommend_test.go ├── input.go └── output.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 coseyo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gocommend 2 | =========== 3 | recommend system for golang. 4 | 5 | [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges) 6 | 7 | Developing... 8 | 9 | ## CLI example code below 10 | 11 | ```go 12 | 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | "gocommend" 18 | "log" 19 | "os" 20 | ) 21 | 22 | func main() { 23 | 24 | //gocommend.Redistest() 25 | 26 | argNum := len(os.Args) 27 | 28 | handle := os.Args[1] 29 | collection := os.Args[2] 30 | 31 | switch handle { 32 | case "importPoll": 33 | if argNum != 5 { 34 | fmt.Println("num of input params shuold be 5") 35 | return 36 | } 37 | userId := os.Args[3] 38 | itemId := os.Args[4] 39 | //rate, _ := strconv.Atoi(os.Args[5]) 40 | i := gocommend.Input{} 41 | i.Init(collection) 42 | i.ImportPoll(userId, itemId) 43 | 44 | case "updatePoll": 45 | userId := os.Args[3] 46 | //itemId := os.Args[4] 47 | i := gocommend.Input{} 48 | i.Init(collection) 49 | err := i.UpdatePoll(userId, "") 50 | if err != nil { 51 | log.Println(err) 52 | } 53 | 54 | case "recommendForUser": 55 | userId := os.Args[3] 56 | //itemId := os.Args[4] 57 | recNum := 10 58 | o := gocommend.Output{} 59 | o.Init(collection, recNum) 60 | rs, err := o.RecommendItemForUser(userId) 61 | if err != nil { 62 | log.Println(err) 63 | return 64 | } 65 | log.Println(rs) 66 | } 67 | 68 | case "recommendForItem": 69 | itemId := os.Args[3] 70 | recNum := 10 71 | o := gocommend.Output{} 72 | o.Init(collection, recNum) 73 | rs, err := o.RecommendItemForItem(itemId) 74 | if err != nil { 75 | log.Println(err) 76 | return 77 | } 78 | log.Println(rs) 79 | } 80 | 81 | 82 | 83 | ```` 84 | 85 | ## HTTP example code below 86 | 87 | ```go 88 | 89 | package main 90 | 91 | import ( 92 | "encoding/json" 93 | "gocommend" 94 | "log" 95 | "net/http" 96 | "strconv" 97 | ) 98 | 99 | type commendServer struct { 100 | w http.ResponseWriter 101 | req *http.Request 102 | postData map[string][]string 103 | } 104 | 105 | func (this *commendServer) init(w http.ResponseWriter, req *http.Request) (err string) { 106 | this.w = w 107 | this.req = req 108 | this.req.ParseForm() 109 | this.postData = this.req.PostForm 110 | if len(this.postData) == 0 { 111 | err = "No post data" 112 | } 113 | return 114 | } 115 | 116 | func (this *commendServer) responseJson(result string, data interface{}, msg string) { 117 | this.w.Header().Set("content-type", "application/json") 118 | jsonData := map[string]interface{}{ 119 | "result": result, 120 | "data": data, 121 | "msg": msg, 122 | } 123 | rs, _ := json.Marshal(jsonData) 124 | this.w.Write(rs) 125 | } 126 | 127 | func (this *commendServer) getParam(key string, allowNull bool) (value string, err string) { 128 | valueArray, exist := this.postData[key] 129 | if allowNull == true { 130 | if exist == false { 131 | return "", "" 132 | } 133 | err = "" 134 | } else { 135 | if exist == false { 136 | err = " No key " + key 137 | return 138 | } 139 | if valueArray[0] == "" { 140 | err = " empty value " + key 141 | } 142 | } 143 | value = valueArray[0] 144 | return 145 | } 146 | 147 | func importPollHandler(w http.ResponseWriter, req *http.Request) { 148 | s := commendServer{} 149 | if err := s.init(w, req); err != "" { 150 | s.responseJson("error", "", err) 151 | return 152 | } 153 | 154 | collection, err1 := s.getParam("collection", false) 155 | userId, err2 := s.getParam("userId", false) 156 | itemId, err3 := s.getParam("itemId", false) 157 | if err1 != "" || err2 != "" || err3 != "" { 158 | s.responseJson("error", "", err1+err2+err3) 159 | return 160 | } 161 | 162 | i := gocommend.Input{} 163 | i.Init(collection) 164 | if err := i.ImportPoll(userId, itemId); err != nil { 165 | s.responseJson("error", "", err.Error()) 166 | return 167 | } 168 | s.responseJson("ok", "", "") 169 | } 170 | 171 | func updatePollHandler(w http.ResponseWriter, req *http.Request) { 172 | s := commendServer{} 173 | if err := s.init(w, req); err != "" { 174 | s.responseJson("error", "", err) 175 | return 176 | } 177 | 178 | collection, err1 := s.getParam("collection", false) 179 | userId, err2 := s.getParam("userId", false) 180 | itemId, err3 := s.getParam("itemId", true) 181 | if err1 != "" || err2 != "" || err3 != "" { 182 | s.responseJson("error", "", err1+err2+err3) 183 | return 184 | } 185 | 186 | i := gocommend.Input{} 187 | i.Init(collection) 188 | if err := i.UpdatePoll(userId, itemId); err != nil { 189 | s.responseJson("error", "", err.Error()) 190 | return 191 | } 192 | s.responseJson("ok", "", "") 193 | } 194 | 195 | func updateAllPollHandler(w http.ResponseWriter, req *http.Request) { 196 | s := commendServer{} 197 | if err := s.init(w, req); err != "" { 198 | s.responseJson("error", "", err) 199 | return 200 | } 201 | 202 | collection, err1 := s.getParam("collection", false) 203 | if err1 != "" { 204 | s.responseJson("error", "", err1) 205 | return 206 | } 207 | 208 | i := gocommend.Input{} 209 | i.Init(collection) 210 | if err := i.UpdateAllPoll(); err != nil { 211 | s.responseJson("error", "", err.Error()) 212 | return 213 | } 214 | s.responseJson("ok", "", "") 215 | } 216 | 217 | func recommendItemForUserHandler(w http.ResponseWriter, req *http.Request) { 218 | s := commendServer{} 219 | if err := s.init(w, req); err != "" { 220 | s.responseJson("error", "", err) 221 | return 222 | } 223 | 224 | collection, err1 := s.getParam("collection", false) 225 | userId, err2 := s.getParam("userId", false) 226 | num, err3 := s.getParam("num", true) 227 | if err1 != "" || err2 != "" || err3 != "" { 228 | s.responseJson("error", "", err1+err2+err3) 229 | return 230 | } 231 | 232 | recNum := 10 233 | if num != "" { 234 | recNum, _ = strconv.Atoi(num) 235 | } 236 | o := gocommend.Output{} 237 | o.Init(collection, recNum) 238 | rs, err := o.RecommendItemForUser(userId) 239 | log.Println(rs) 240 | if err != nil { 241 | s.responseJson("error", "", err.Error()) 242 | return 243 | } 244 | s.responseJson("ok", rs, "") 245 | } 246 | 247 | func recommendItemForItemHandler(w http.ResponseWriter, req *http.Request) { 248 | s := commendServer{} 249 | if err := s.init(w, req); err != "" { 250 | s.responseJson("error", "", err) 251 | return 252 | } 253 | 254 | collection, err1 := s.getParam("collection", false) 255 | itemId, err2 := s.getParam("itemId", false) 256 | num, err3 := s.getParam("num", true) 257 | if err1 != "" || err2 != "" || err3 != "" { 258 | s.responseJson("error", "", err1+err2+err3) 259 | return 260 | } 261 | 262 | recNum := 10 263 | if num != "" { 264 | recNum, _ = strconv.Atoi(num) 265 | } 266 | 267 | o := gocommend.Output{} 268 | o.Init(collection, recNum) 269 | rs, err := o.RecommendItemForItem(itemId) 270 | if err != nil { 271 | s.responseJson("error", "", err.Error()) 272 | return 273 | } 274 | s.responseJson("ok", rs, "") 275 | } 276 | 277 | func main() { 278 | 279 | http.HandleFunc("/importPoll", importPollHandler) 280 | http.HandleFunc("/updatePoll", updatePollHandler) 281 | http.HandleFunc("/updateAllPoll", updateAllPollHandler) 282 | http.HandleFunc("/recommendItemForUser", recommendItemForUserHandler) 283 | http.HandleFunc("/recommendItemForItem", recommendItemForItemHandler) 284 | 285 | http.ListenAndServe(":8888", nil) 286 | } 287 | 288 | ```` 289 | -------------------------------------------------------------------------------- /algorithms.go: -------------------------------------------------------------------------------- 1 | package gocommend 2 | 3 | import "github.com/garyburd/redigo/redis" 4 | 5 | // algorithm type's parent 6 | type algorithms struct { 7 | cSet collectionSet 8 | } 9 | 10 | func (this *algorithms) TrimRecommendItem(userId string) error { 11 | count, err := redis.Int(redisClient.Do("ZCARD", this.cSet.recommendedItem(userId))) 12 | if err != nil { 13 | return err 14 | } 15 | if count > MAX_RECOMMEND_ITEM { 16 | redisClient.Do("ZREMRANGEBYRANK", this.cSet.recommendedItem(userId), 0, (count - MAX_RECOMMEND_ITEM)) 17 | } 18 | return nil 19 | } 20 | 21 | func (this *algorithms) TrimUserSimilarity(userId string) error { 22 | count, err := redis.Int(redisClient.Do("ZCARD", this.cSet.userSimilarity(userId))) 23 | if err != nil { 24 | return err 25 | } 26 | if count > MAX_SIMILARITY_USER { 27 | redisClient.Do("ZREMRANGEBYRANK", this.cSet.userSimilarity(userId), 0, (count - MAX_SIMILARITY_USER)) 28 | } 29 | return nil 30 | } 31 | 32 | func (this *algorithms) TrimItemSimilarity(itemId string) error { 33 | count, err := redis.Int(redisClient.Do("ZCARD", this.cSet.itemSimilarity(itemId))) 34 | if err != nil { 35 | return err 36 | } 37 | if count > MAX_SIMILARITY_ITEM { 38 | redisClient.Do("ZREMRANGEBYRANK", this.cSet.itemSimilarity(itemId), 0, (count - MAX_SIMILARITY_ITEM)) 39 | } 40 | return nil 41 | } 42 | 43 | // 2 set's similarity 44 | func (this *algorithms) similaritySum(simSet string, compSet string) float64 { 45 | var similarSum float64 = 0.0 46 | userIds, err := redis.Values(redisClient.Do("SMEMBERS", compSet)) 47 | for _, rs := range userIds { 48 | userId, _ := redis.String(rs, err) 49 | score, _ := redis.Float64(redisClient.Do("ZSCORE", simSet, userId)) 50 | similarSum += score 51 | } 52 | return similarSum 53 | } 54 | -------------------------------------------------------------------------------- /algorithms_poll.go: -------------------------------------------------------------------------------- 1 | package gocommend 2 | 3 | import "github.com/garyburd/redigo/redis" 4 | 5 | // poll type 6 | // we use this type when we don't collect users's dislike data. 7 | type algorithmsPoll struct { 8 | algorithms 9 | } 10 | 11 | // CF calculate, u1 poll i1 i2 i3, u2 poll i2 i3, so get the similarity by jaccardCoefficient and update it 12 | func (this *algorithmsPoll) updateUserSimilarity(userId string) error { 13 | ratedItemSet, err := redis.Values(redisClient.Do("SMEMBERS", this.cSet.userLiked(userId))) 14 | 15 | if err != nil { 16 | return err 17 | } 18 | 19 | if len(ratedItemSet) == 0 { 20 | return nil 21 | } 22 | 23 | itemKeys := []string{} 24 | for _, rs := range ratedItemSet { 25 | itemId, _ := redis.String(rs, err) 26 | itemKeys = append(itemKeys, this.cSet.itemLiked(itemId)) 27 | } 28 | 29 | otherUserIdsWhoRated, err := redis.Values(redisClient.Do("SUNION", redis.Args{}.AddFlat(itemKeys)...)) 30 | 31 | if err != nil { 32 | return err 33 | } 34 | 35 | for _, rs := range otherUserIdsWhoRated { 36 | otherUserId, _ := redis.String(rs, err) 37 | if len(otherUserIdsWhoRated) == 1 || userId == otherUserId { 38 | continue 39 | } 40 | 41 | score := this.jaccardCoefficient(this.cSet.userLiked(userId), this.cSet.userLiked(otherUserId)) 42 | redisClient.Do("ZADD", this.cSet.userSimilarity(userId), score, otherUserId) 43 | } 44 | 45 | return err 46 | } 47 | 48 | // CF calculate, i1 polled by u1 u2 u3, i2 polled by u2 u3, so get the similarity by jaccardCoefficient and update it 49 | func (this *algorithmsPoll) updateItemSimilarity(itemId string) error { 50 | ratedUserSet, err := redis.Values(redisClient.Do("SMEMBERS", this.cSet.itemLiked(itemId))) 51 | 52 | if err != nil { 53 | return err 54 | } 55 | 56 | if len(ratedUserSet) == 0 { 57 | return nil 58 | } 59 | 60 | userKeys := []string{} 61 | for _, rs := range ratedUserSet { 62 | userId, _ := redis.String(rs, err) 63 | userKeys = append(userKeys, this.cSet.userLiked(userId)) 64 | } 65 | 66 | otherItemIdsBeingRated, err := redis.Values(redisClient.Do("SUNION", redis.Args{}.AddFlat(userKeys)...)) 67 | 68 | if err != nil { 69 | return err 70 | } 71 | if len(otherItemIdsBeingRated) == 1 { 72 | return nil 73 | } 74 | 75 | for _, rs := range otherItemIdsBeingRated { 76 | otherItemId, _ := redis.String(rs, err) 77 | if itemId == otherItemId { 78 | continue 79 | } 80 | 81 | score := this.jaccardCoefficient(this.cSet.itemLiked(itemId), this.cSet.itemLiked(otherItemId)) 82 | redisClient.Do("ZADD", this.cSet.itemSimilarity(itemId), score, otherItemId) 83 | } 84 | 85 | return err 86 | } 87 | 88 | // calculate 2 sets's similarity 89 | func (this *algorithmsPoll) jaccardCoefficient(set1 string, set2 string) float64 { 90 | var ( 91 | interset int = 0 92 | unionset int = 0 93 | ) 94 | 95 | resultInter, _ := redis.Values(redisClient.Do("SINTER", set1, set2)) 96 | len1 := len(resultInter) 97 | 98 | len2, _ := redis.Int(redisClient.Do("SCARD", set1)) 99 | len3, _ := redis.Int(redisClient.Do("SCARD", set2)) 100 | 101 | interset = len1 102 | unionset = len2 + len3 - len1 103 | return float64(interset) / float64(unionset) 104 | } 105 | 106 | // pick out the recommend items from the most similar users's rated items exclude having been rated ones, and update it. 107 | func (this *algorithmsPoll) updateRecommendationFor(userId string) error { 108 | 109 | mostSimilarUserIds, err := redis.Values(redisClient.Do("ZREVRANGE", this.cSet.userSimilarity(userId), 0, MAX_NEIGHBORS-1)) 110 | 111 | if len(mostSimilarUserIds) == 0 { 112 | return err 113 | } 114 | tempSet := this.cSet.userTemp(userId) 115 | recommendedSet := this.cSet.recommendedItem(userId) 116 | 117 | for _, rs := range mostSimilarUserIds { 118 | similarUserId, _ := redis.String(rs, err) 119 | redisClient.Do("SUNIONSTORE", tempSet, this.cSet.userLiked(similarUserId)) 120 | } 121 | diffItemIds, err := redis.Values(redisClient.Do("SDIFF", tempSet, this.cSet.userLiked(userId))) 122 | 123 | for _, rs := range diffItemIds { 124 | diffItemId, _ := redis.String(rs, err) 125 | score := this.predictFor(userId, diffItemId) 126 | redisClient.Do("ZADD", recommendedSet, score, diffItemId) 127 | } 128 | 129 | redisClient.Do("DEL", this.cSet.userTemp(userId)) 130 | return err 131 | } 132 | 133 | // get item's predict score for user 134 | func (this *algorithmsPoll) predictFor(userId string, itemId string) float64 { 135 | 136 | result1 := this.similaritySum(this.cSet.userSimilarity(userId), this.cSet.itemLiked(itemId)) 137 | 138 | itemLikedCount, _ := redis.Int(redisClient.Do("SCARD", this.cSet.itemLiked(itemId))) 139 | 140 | return float64(result1) / float64(itemLikedCount) 141 | } 142 | 143 | func (this *algorithmsPoll) updateAllData() error { 144 | userIds, err := redis.Values(redisClient.Do("SMEMBERS", this.cSet.allUser)) 145 | for _, rs := range userIds { 146 | userId, _ := redis.String(rs, err) 147 | err = this.updateData(userId, "") 148 | if err != nil { 149 | break 150 | } 151 | } 152 | return err 153 | } 154 | 155 | func (this *algorithmsPoll) updateData(userId string, itemId string) error { 156 | 157 | if err := this.updateUserSimilarity(userId); err != nil { 158 | return err 159 | } 160 | if err := this.updateRecommendationFor(userId); err != nil { 161 | return err 162 | } 163 | 164 | if itemId == "" { 165 | ratedItemSet, err := redis.Values(redisClient.Do("SMEMBERS", this.cSet.userLiked(userId))) 166 | for _, rs := range ratedItemSet { 167 | ratedItemId, _ := redis.String(rs, err) 168 | this.updateItemSimilarity(ratedItemId) 169 | } 170 | } else { 171 | if err := this.updateItemSimilarity(itemId); err != nil { 172 | return err 173 | } 174 | } 175 | return nil 176 | } 177 | -------------------------------------------------------------------------------- /algorithms_rate.go: -------------------------------------------------------------------------------- 1 | package gocommend 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/garyburd/redigo/redis" 7 | ) 8 | 9 | // rate type 10 | // Use this type when we collet both like and dislike data, 11 | type algorithmsRate struct { 12 | algorithms 13 | } 14 | 15 | func (this *algorithmsRate) updateSimilarityFor(userId string) error { 16 | ratedItemSet, err := redis.Values(redisClient.Do("SUNION", this.cSet.userLiked(userId), this.cSet.userDisliked(userId))) 17 | 18 | if err != nil { 19 | return err 20 | } 21 | 22 | if len(ratedItemSet) == 0 { 23 | return nil 24 | } 25 | 26 | itemLikeDislikeKeys := []string{} 27 | for _, rs := range ratedItemSet { 28 | itemId, _ := redis.String(rs, err) 29 | itemLikeDislikeKeys = append(itemLikeDislikeKeys, this.cSet.itemLiked(itemId)) 30 | itemLikeDislikeKeys = append(itemLikeDislikeKeys, this.cSet.itemDisliked(itemId)) 31 | } 32 | 33 | otherUserIdsWhoRated, err := redis.Values(redisClient.Do("SUNION", redis.Args{}.AddFlat(itemLikeDislikeKeys)...)) 34 | 35 | if err != nil { 36 | return err 37 | } 38 | if len(otherUserIdsWhoRated) == 1 { 39 | return nil 40 | } 41 | 42 | for _, rs := range otherUserIdsWhoRated { 43 | otherUserId, _ := redis.String(rs, err) 44 | if userId == otherUserId { 45 | continue 46 | } 47 | 48 | score := this.jaccardCoefficient(userId, otherUserId) 49 | redisClient.Do("ZADD", this.cSet.userSimilarity(userId), score, otherUserId) 50 | } 51 | 52 | return err 53 | } 54 | 55 | func (this *algorithmsRate) jaccardCoefficient(userId1 string, userId2 string) float64 { 56 | var ( 57 | similarity int = 0 58 | rateInCommon int = 0 59 | ) 60 | 61 | resultBothLike, _ := redis.Values(redisClient.Do("SINTER", this.cSet.userLiked(userId1), this.cSet.userLiked(userId2))) 62 | resultBothDislike, _ := redis.Values(redisClient.Do("SINTER", this.cSet.userDisliked(userId1), this.cSet.userDisliked(userId2))) 63 | resultUser1LikeUser2Dislike, _ := redis.Values(redisClient.Do("SINTER", this.cSet.userLiked(userId1), this.cSet.userDisliked(userId2))) 64 | resultUser1DislikeUser2Like, _ := redis.Values(redisClient.Do("SINTER", this.cSet.userDisliked(userId1), this.cSet.userLiked(userId2))) 65 | 66 | len1 := len(resultBothLike) 67 | len2 := len(resultBothDislike) 68 | len3 := len(resultUser1LikeUser2Dislike) 69 | len4 := len(resultUser1DislikeUser2Like) 70 | 71 | similarity = len1 + len2 - len3 - len4 72 | rateInCommon = len1 + len2 + len3 + len4 73 | return float64(similarity) / float64(rateInCommon) 74 | } 75 | 76 | func (this *algorithmsRate) updateRecommendationFor(userId string) error { 77 | 78 | mostSimilarUserIds, err := redis.Values(redisClient.Do("ZREVRANGE", this.cSet.userSimilarity(userId), 0, MAX_NEIGHBORS-1)) 79 | 80 | if len(mostSimilarUserIds) == 0 { 81 | return err 82 | } 83 | 84 | for _, rs := range mostSimilarUserIds { 85 | similarUserId, _ := redis.String(rs, err) 86 | redisClient.Do("SUNIONSTORE", this.cSet.userTemp(userId), this.cSet.userLiked(similarUserId)) 87 | } 88 | 89 | diffItemIds, err := redis.Values(redisClient.Do("SDIFF", this.cSet.userTemp(userId), this.cSet.userLiked(userId), this.cSet.userDisliked(userId))) 90 | for _, rs := range diffItemIds { 91 | diffItemId, _ := redis.String(rs, err) 92 | score := this.predictFor(userId, diffItemId) 93 | redisClient.Do("ZADD", this.cSet.recommendedItem(userId), score, diffItemId) 94 | } 95 | 96 | redisClient.Do("DEL", this.cSet.userTemp(userId)) 97 | return err 98 | } 99 | 100 | func (this *algorithmsRate) predictFor(userId string, itemId string) float64 { 101 | 102 | result1 := this.similaritySum(this.cSet.userSimilarity(userId), this.cSet.itemLiked(itemId)) 103 | 104 | result2 := this.similaritySum(this.cSet.userSimilarity(userId), this.cSet.itemDisliked(itemId)) 105 | 106 | sum := result1 - result2 107 | 108 | itemLikedCount, _ := redis.Int(redisClient.Do("SCARD", this.cSet.itemLiked(itemId))) 109 | 110 | itemDislikedCount, _ := redis.Int(redisClient.Do("SCARD", this.cSet.itemLiked(itemId))) 111 | 112 | return float64(sum) / float64(itemLikedCount+itemDislikedCount) 113 | } 114 | 115 | // update socre 116 | func (this *algorithms) updateWilsonScore(itemId string) error { 117 | var ( 118 | total int 119 | pOS float64 120 | score float64 = 0.0 121 | ) 122 | 123 | resultLike, _ := redis.Int(redisClient.Do("SCARD", this.cSet.itemLiked(itemId))) 124 | resultDislike, _ := redis.Int(redisClient.Do("SCARD", this.cSet.itemDisliked(itemId))) 125 | 126 | total = resultLike + resultDislike 127 | if total > 0 { 128 | pOS = float64(resultLike) / float64(total) 129 | score = this.willsonScore(total, pOS) 130 | } 131 | 132 | _, err := redisClient.Do("ZADD", this.cSet.scoreRank, score, itemId) 133 | 134 | return err 135 | } 136 | 137 | // willson score 138 | func (this *algorithms) willsonScore(total int, pOS float64) float64 { 139 | 140 | // 95% 141 | var z float64 = 1.96 142 | 143 | n := float64(total) 144 | 145 | return math.Abs((pOS + z*z/(2*n) - z*math.Sqrt(pOS*(1-pOS)+z*z/(4*n))) / (1 + z*z/n)) 146 | } 147 | 148 | func (this *algorithmsRate) updateAllData() error { 149 | userIds, err := redis.Values(redisClient.Do("SMEMBERS", this.cSet.allUser)) 150 | for _, rs := range userIds { 151 | userId, _ := redis.String(rs, err) 152 | err = this.updateData(userId, "") 153 | if err != nil { 154 | break 155 | } 156 | } 157 | return err 158 | } 159 | 160 | func (this *algorithmsRate) updateData(userId string, itemId string) error { 161 | 162 | if err := this.updateSimilarityFor(userId); err != nil { 163 | return err 164 | } 165 | if err := this.updateRecommendationFor(userId); err != nil { 166 | return err 167 | } 168 | 169 | if itemId == "" { 170 | ratedItemSet, err := redis.Values(redisClient.Do("SMEMBERS", this.cSet.userLiked(userId))) 171 | for _, rs := range ratedItemSet { 172 | ratedItemId, _ := redis.String(rs, err) 173 | this.updateWilsonScore(ratedItemId) 174 | } 175 | } else { 176 | if err := this.updateWilsonScore(itemId); err != nil { 177 | return err 178 | } 179 | } 180 | return nil 181 | } 182 | -------------------------------------------------------------------------------- /algorithms_test.go: -------------------------------------------------------------------------------- 1 | package gocommend -------------------------------------------------------------------------------- /collection.go: -------------------------------------------------------------------------------- 1 | package gocommend 2 | 3 | // redis key set 4 | type collectionSet struct { 5 | collectionPrefix string 6 | scoreRank string 7 | mostLiked string 8 | mostDisliked string 9 | allItem string 10 | allUser string 11 | } 12 | 13 | func (this *collectionSet) init(collection string) { 14 | this.collectionPrefix = DB_PREFIX + ":" + collection 15 | this.scoreRank = this.collectionPrefix + ":scoreRank" 16 | this.mostLiked = this.collectionPrefix + ":mostLiked" 17 | this.mostDisliked = this.collectionPrefix + ":mostDisliked" 18 | this.allItem = this.collectionPrefix + ":allItem" 19 | this.allUser = this.collectionPrefix + ":allUser" 20 | } 21 | 22 | func (this *collectionSet) userLiked(userId string) string { 23 | return this.collectionPrefix + ":" + "userLiked" + ":" + userId 24 | } 25 | 26 | func (this *collectionSet) itemLiked(itemId string) string { 27 | return this.collectionPrefix + ":" + "itemLiked" + ":" + itemId 28 | } 29 | 30 | func (this *collectionSet) userDisliked(userId string) string { 31 | return this.collectionPrefix + ":" + "userDisliked" + ":" + userId 32 | } 33 | 34 | func (this *collectionSet) itemDisliked(itemId string) string { 35 | return this.collectionPrefix + ":" + "itemDisliked" + ":" + itemId 36 | } 37 | 38 | func (this *collectionSet) userSimilarity(userId string) string { 39 | return this.collectionPrefix + ":" + "userSimilarity" + ":" + userId 40 | } 41 | 42 | func (this *collectionSet) itemSimilarity(itemId string) string { 43 | return this.collectionPrefix + ":" + "itemSimilarity" + ":" + itemId 44 | } 45 | 46 | func (this *collectionSet) userTemp(userId string) string { 47 | return this.collectionPrefix + ":" + "userTemp" + ":" + userId 48 | } 49 | 50 | func (this *collectionSet) itemTemp(itemId string) string { 51 | return this.collectionPrefix + ":" + "itemTemp" + ":" + itemId 52 | } 53 | 54 | func (this *collectionSet) userTempDiff(userId string) string { 55 | return this.collectionPrefix + ":" + "userTempDiff" + ":" + userId 56 | } 57 | 58 | func (this *collectionSet) itemTempDiff(userId string) string { 59 | return this.collectionPrefix + ":" + "itemTempDiff" + ":" + userId 60 | } 61 | 62 | func (this *collectionSet) recommendedItem(userId string) string { 63 | return this.collectionPrefix + ":" + "recommendedItem" + ":" + userId 64 | } 65 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package gocommend 2 | 3 | const ( 4 | // the num of most similar users to get for calculation the recommend item 5 | MAX_NEIGHBORS = 10 6 | 7 | // max recommend item num to store 8 | MAX_RECOMMEND_ITEM = 30 9 | 10 | // redis key prefix 11 | DB_PREFIX = "gocommend" 12 | 13 | MAX_SIMILARITY_ITEM = 100 14 | 15 | MAX_SIMILARITY_USER = 100 16 | 17 | LOCAL_REDIS_HOST = "192.168.1.7" 18 | 19 | LOCAL_REDIS_PORT = "6379" 20 | 21 | REMOTE_REDIS_HOST = "10.20.187.251" 22 | 23 | REMOTE_REDIS_PORT = "11311" 24 | 25 | LOCAL_STARTUP = false 26 | ) 27 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package gocommend 2 | 3 | const ( 4 | emptyCollection = "Empty collection." 5 | missingHeader = "Missing required header: %q" 6 | ) 7 | 8 | type gocommendError struct { 9 | Message string 10 | } 11 | 12 | func (e gocommendError) Error() string { 13 | return e.Message 14 | } 15 | -------------------------------------------------------------------------------- /example/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "gocommend" 6 | "log" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | 12 | //gocommend.Redistest() 13 | 14 | argNum := len(os.Args) 15 | 16 | handle := os.Args[1] 17 | collection := os.Args[2] 18 | 19 | switch handle { 20 | case "importPoll": 21 | if argNum != 5 { 22 | fmt.Println("num of input params shuold be 5") 23 | return 24 | } 25 | userId := os.Args[3] 26 | itemId := os.Args[4] 27 | //rate, _ := strconv.Atoi(os.Args[5]) 28 | i := gocommend.Input{} 29 | i.Init(collection) 30 | i.ImportPoll(userId, itemId) 31 | 32 | case "updatePoll": 33 | userId := os.Args[3] 34 | //itemId := os.Args[4] 35 | i := gocommend.Input{} 36 | i.Init(collection) 37 | err := i.UpdatePoll(userId, "") 38 | if err != nil { 39 | log.Println(err) 40 | } 41 | 42 | case "recommendForUser": 43 | userId := os.Args[3] 44 | //itemId := os.Args[4] 45 | recNum := 10 46 | o := gocommend.Output{} 47 | o.Init(collection, recNum) 48 | rs, err := o.RecommendItemForUser(userId) 49 | if err != nil { 50 | log.Println(err) 51 | return 52 | } 53 | log.Println(rs) 54 | 55 | case "recommendForItem": 56 | itemId := os.Args[3] 57 | recNum := 10 58 | o := gocommend.Output{} 59 | o.Init(collection, recNum) 60 | rs, err := o.RecommendItemForItem(itemId) 61 | if err != nil { 62 | log.Println(err) 63 | return 64 | } 65 | log.Println(rs) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example/http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "gocommend" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | type commendServer struct { 12 | w http.ResponseWriter 13 | req *http.Request 14 | postData map[string][]string 15 | } 16 | 17 | func (this *commendServer) init(w http.ResponseWriter, req *http.Request) (err string) { 18 | this.w = w 19 | this.req = req 20 | this.req.ParseForm() 21 | this.postData = this.req.PostForm 22 | if len(this.postData) == 0 { 23 | err = "No post data" 24 | } 25 | return 26 | } 27 | 28 | func (this *commendServer) responseJson(result string, data interface{}, msg string) { 29 | this.w.Header().Set("content-type", "application/json") 30 | jsonData := map[string]interface{}{ 31 | "result": result, 32 | "data": data, 33 | "msg": msg, 34 | } 35 | rs, _ := json.Marshal(jsonData) 36 | this.w.Write(rs) 37 | } 38 | 39 | func (this *commendServer) getParam(key string, allowNull bool) (value string, err string) { 40 | valueArray, exist := this.postData[key] 41 | if allowNull == true { 42 | if exist == false { 43 | return "", "" 44 | } 45 | err = "" 46 | } else { 47 | if exist == false { 48 | err = " No key " + key 49 | return 50 | } 51 | if valueArray[0] == "" { 52 | err = " empty value " + key 53 | } 54 | } 55 | value = valueArray[0] 56 | return 57 | } 58 | 59 | func importPollHandler(w http.ResponseWriter, req *http.Request) { 60 | s := commendServer{} 61 | if err := s.init(w, req); err != "" { 62 | s.responseJson("error", "", err) 63 | return 64 | } 65 | 66 | collection, err1 := s.getParam("collection", false) 67 | userId, err2 := s.getParam("userId", false) 68 | itemId, err3 := s.getParam("itemId", false) 69 | if err1 != "" || err2 != "" || err3 != "" { 70 | s.responseJson("error", "", err1+err2+err3) 71 | return 72 | } 73 | 74 | i := gocommend.Input{} 75 | i.Init(collection) 76 | if err := i.ImportPoll(userId, itemId); err != nil { 77 | s.responseJson("error", "", err.Error()) 78 | return 79 | } 80 | s.responseJson("ok", "", "") 81 | } 82 | 83 | func updatePollHandler(w http.ResponseWriter, req *http.Request) { 84 | s := commendServer{} 85 | if err := s.init(w, req); err != "" { 86 | s.responseJson("error", "", err) 87 | return 88 | } 89 | 90 | collection, err1 := s.getParam("collection", false) 91 | userId, err2 := s.getParam("userId", false) 92 | itemId, err3 := s.getParam("itemId", true) 93 | if err1 != "" || err2 != "" || err3 != "" { 94 | s.responseJson("error", "", err1+err2+err3) 95 | return 96 | } 97 | 98 | i := gocommend.Input{} 99 | i.Init(collection) 100 | if err := i.UpdatePoll(userId, itemId); err != nil { 101 | s.responseJson("error", "", err.Error()) 102 | return 103 | } 104 | s.responseJson("ok", "", "") 105 | } 106 | 107 | func updateAllPollHandler(w http.ResponseWriter, req *http.Request) { 108 | s := commendServer{} 109 | if err := s.init(w, req); err != "" { 110 | s.responseJson("error", "", err) 111 | return 112 | } 113 | 114 | collection, err1 := s.getParam("collection", false) 115 | if err1 != "" { 116 | s.responseJson("error", "", err1) 117 | return 118 | } 119 | 120 | i := gocommend.Input{} 121 | i.Init(collection) 122 | if err := i.UpdateAllPoll(); err != nil { 123 | s.responseJson("error", "", err.Error()) 124 | return 125 | } 126 | s.responseJson("ok", "", "") 127 | } 128 | 129 | func recommendItemForUserHandler(w http.ResponseWriter, req *http.Request) { 130 | s := commendServer{} 131 | if err := s.init(w, req); err != "" { 132 | s.responseJson("error", "", err) 133 | return 134 | } 135 | 136 | collection, err1 := s.getParam("collection", false) 137 | userId, err2 := s.getParam("userId", false) 138 | num, err3 := s.getParam("num", true) 139 | if err1 != "" || err2 != "" || err3 != "" { 140 | s.responseJson("error", "", err1+err2+err3) 141 | return 142 | } 143 | 144 | recNum := 10 145 | if num != "" { 146 | recNum, _ = strconv.Atoi(num) 147 | } 148 | o := gocommend.Output{} 149 | o.Init(collection, recNum) 150 | rs, err := o.RecommendItemForUser(userId) 151 | log.Println(rs) 152 | if err != nil { 153 | s.responseJson("error", "", err.Error()) 154 | return 155 | } 156 | s.responseJson("ok", rs, "") 157 | } 158 | 159 | func recommendItemForItemHandler(w http.ResponseWriter, req *http.Request) { 160 | s := commendServer{} 161 | if err := s.init(w, req); err != "" { 162 | s.responseJson("error", "", err) 163 | return 164 | } 165 | 166 | collection, err1 := s.getParam("collection", false) 167 | itemId, err2 := s.getParam("itemId", false) 168 | num, err3 := s.getParam("num", true) 169 | if err1 != "" || err2 != "" || err3 != "" { 170 | s.responseJson("error", "", err1+err2+err3) 171 | return 172 | } 173 | 174 | recNum := 10 175 | if num != "" { 176 | recNum, _ = strconv.Atoi(num) 177 | } 178 | 179 | o := gocommend.Output{} 180 | o.Init(collection, recNum) 181 | rs, err := o.RecommendItemForItem(itemId) 182 | if err != nil { 183 | s.responseJson("error", "", err.Error()) 184 | return 185 | } 186 | s.responseJson("ok", rs, "") 187 | } 188 | 189 | func main() { 190 | 191 | http.HandleFunc("/importPoll", importPollHandler) 192 | http.HandleFunc("/updatePoll", updatePollHandler) 193 | http.HandleFunc("/updateAllPoll", updateAllPollHandler) 194 | http.HandleFunc("/recommendItemForUser", recommendItemForUserHandler) 195 | http.HandleFunc("/recommendItemForItem", recommendItemForItemHandler) 196 | 197 | http.ListenAndServe(":8888", nil) 198 | } 199 | -------------------------------------------------------------------------------- /gocommend.go: -------------------------------------------------------------------------------- 1 | package gocommend 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/garyburd/redigo/redis" 7 | ) 8 | 9 | var ( 10 | redisClient redis.Conn 11 | err error 12 | ) 13 | 14 | func init() { 15 | if LOCAL_STARTUP == true { 16 | redisClient, err = redis.Dial("tcp", LOCAL_REDIS_HOST+":"+LOCAL_REDIS_PORT) 17 | } else { 18 | redisClient, err = redis.Dial("tcp", REMOTE_REDIS_HOST+":"+REMOTE_REDIS_PORT) 19 | } 20 | if err != nil { 21 | log.Println(err.Error()) 22 | return 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gocommend_test.go: -------------------------------------------------------------------------------- 1 | package gocommend 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/garyburd/redigo/redis" 8 | ) 9 | 10 | func expect(t *testing.T, a interface{}, b interface{}) { 11 | if a != b { 12 | t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) 13 | } 14 | } 15 | 16 | func refute(t *testing.T, a interface{}, b interface{}) { 17 | if a == b { 18 | t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) 19 | } 20 | } 21 | 22 | func Test_redis(t *testing.T) { 23 | redisClient.Do("SET", "aaa", 123) 24 | a, err := redis.Int(redisClient.Do("GET", "aaa")) 25 | expect(t, err, nil) 26 | expect(t, a, 123) 27 | } 28 | 29 | func Test_importPoll(t *testing.T) { 30 | collection := "rec_test2" 31 | i := Input{} 32 | i.Init(collection) 33 | err := i.ImportPoll("u1", "i1") 34 | expect(t, err, nil) 35 | i.ImportPoll("u1", "i2") 36 | i.ImportPoll("u1", "i3") 37 | i.ImportPoll("u2", "i1") 38 | i.ImportPoll("u2", "i2") 39 | } 40 | 41 | func Test_updatePoll(t *testing.T) { 42 | collection := "rec_test2" 43 | i := Input{} 44 | i.Init(collection) 45 | err := i.UpdatePoll("u1", "") 46 | expect(t, err, nil) 47 | i.UpdatePoll("u2", "") 48 | } 49 | 50 | func Test_updateAllPoll(t *testing.T) { 51 | collection := "rec_test2" 52 | i := Input{} 53 | i.Init(collection) 54 | err := i.UpdateAllPoll() 55 | expect(t, err, nil) 56 | } 57 | 58 | func Test_RecommendItem(t *testing.T) { 59 | collection := "rec_test2" 60 | recNum := 10 61 | o := Output{} 62 | o.Init(collection, recNum) 63 | items, err := o.RecommendItemForUser("u2") 64 | expect(t, err, nil) 65 | expect(t, items[0], "i3") 66 | } 67 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package gocommend 2 | 3 | // input, now support two type of algo 4 | type Input struct { 5 | cSet collectionSet 6 | } 7 | 8 | // init cSet 9 | func (this *Input) Init(collection string) error { 10 | if collection == "" { 11 | return gocommendError{emptyCollection} 12 | } 13 | this.cSet = collectionSet{} 14 | this.cSet.init(collection) 15 | return nil 16 | } 17 | 18 | // import rate type data 19 | func (this *Input) ImportRate(userId string, itemId string, rate int) error { 20 | if rate > 0 { 21 | if err := like(&this.cSet, userId, itemId); err != nil { 22 | return err 23 | } 24 | } else { 25 | if err := dislike(&this.cSet, userId, itemId); err != nil { 26 | return err 27 | } 28 | } 29 | if err := this.UpdateRate(userId, itemId); err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // import poll type data 37 | func (this *Input) ImportPoll(userId string, itemId string) error { 38 | 39 | if err := like(&this.cSet, userId, itemId); err != nil { 40 | return err 41 | } 42 | if err := this.UpdatePoll(userId, itemId); err != nil { 43 | return err 44 | } 45 | return nil 46 | } 47 | 48 | // update rate data 49 | func (this *Input) UpdateRate(userId string, itemId string) error { 50 | algo := algorithmsRate{} 51 | algo.cSet = this.cSet 52 | return algo.updateData(userId, itemId) 53 | } 54 | 55 | func (this *Input) UpdateAllRate() error { 56 | algo := algorithmsRate{} 57 | algo.cSet = this.cSet 58 | return algo.updateAllData() 59 | } 60 | 61 | // update poll data 62 | func (this *Input) UpdatePoll(userId string, itemId string) error { 63 | algo := algorithmsPoll{} 64 | algo.cSet = this.cSet 65 | return algo.updateData(userId, itemId) 66 | } 67 | 68 | func (this *Input) UpdateAllPoll() error { 69 | algo := algorithmsPoll{} 70 | algo.cSet = this.cSet 71 | return algo.updateAllData() 72 | } 73 | 74 | // import original data 75 | func like(cSet *collectionSet, userId string, itemId string) error { 76 | var ( 77 | rs interface{} 78 | err error 79 | ) 80 | if rs, err = redisClient.Do("SISMEMBER", cSet.itemLiked(itemId), userId); err != nil { 81 | return err 82 | } 83 | if sis, _ := rs.(int); sis == 0 { 84 | redisClient.Do("ZINCRBY", cSet.mostLiked, 1, itemId) 85 | } 86 | if _, err = redisClient.Do("SADD", cSet.allUser, userId); err != nil { 87 | return err 88 | } 89 | if _, err = redisClient.Do("SADD", cSet.userLiked(userId), itemId); err != nil { 90 | return err 91 | } 92 | if _, err = redisClient.Do("SADD", cSet.itemLiked(itemId), userId); err != nil { 93 | return err 94 | } 95 | if _, err = redisClient.Do("ZREM", cSet.recommendedItem(userId), itemId); err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | 102 | // import original data 103 | func dislike(cSet *collectionSet, userId string, itemId string) error { 104 | var ( 105 | rs interface{} 106 | err error 107 | ) 108 | if rs, err = redisClient.Do("SISMEMBER", cSet.itemDisliked(itemId), userId); err != nil { 109 | return err 110 | } 111 | if sis, _ := rs.(int); sis == 0 { 112 | redisClient.Do("ZINCRBY", cSet.mostDisliked, 1, itemId) 113 | } 114 | if _, err = redisClient.Do("SADD", cSet.allUser, userId); err != nil { 115 | return err 116 | } 117 | if _, err = redisClient.Do("SADD", cSet.allUser, itemId); err != nil { 118 | return err 119 | } 120 | if _, err = redisClient.Do("SADD", cSet.userDisliked(userId), itemId); err != nil { 121 | return err 122 | } 123 | if _, err = redisClient.Do("SADD", cSet.itemDisliked(itemId), userId); err != nil { 124 | return err 125 | } 126 | if _, err = redisClient.Do("ZREM", cSet.recommendedItem(userId), itemId); err != nil { 127 | return err 128 | } 129 | 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | package gocommend 2 | 3 | import "github.com/garyburd/redigo/redis" 4 | 5 | // output, get data what you want 6 | type Output struct { 7 | recNum int 8 | cSet collectionSet 9 | } 10 | 11 | // init the params and set the cSet 12 | func (this *Output) Init(collection string, recNum int) error { 13 | if collection == "" { 14 | return gocommendError{emptyCollection} 15 | } 16 | this.recNum = recNum - 1 17 | this.cSet = collectionSet{} 18 | this.cSet.init(collection) 19 | return nil 20 | } 21 | 22 | // convert interface slice to string slice 23 | func (this *Output) toStrings(arrayInterface []interface{}) (strings []string) { 24 | for _, rs := range arrayInterface { 25 | s, _ := redis.String(rs, nil) 26 | strings = append(strings, s) 27 | } 28 | return 29 | } 30 | 31 | // get recommend items for user 32 | func (this *Output) RecommendItemForUser(userId string) ([]string, error) { 33 | arrayInterface, err := redis.Values(redisClient.Do("ZREVRANGE", this.cSet.recommendedItem(userId), 0, this.recNum)) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return this.toStrings(arrayInterface), err 38 | } 39 | 40 | // get recommend items by item similarty 41 | func (this *Output) RecommendItemForItem(itemId string) ([]string, error) { 42 | arrayInterface, err := redis.Values(redisClient.Do("ZREVRANGE", this.cSet.itemSimilarity(itemId), 0, this.recNum)) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return this.toStrings(arrayInterface), err 47 | } 48 | 49 | // get the best rated items 50 | func (this *Output) BestRated() ([]string, error) { 51 | arrayInterface, err := redis.Values(redisClient.Do("ZREVRANGE", this.cSet.scoreRank, 0, this.recNum)) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return this.toStrings(arrayInterface), err 56 | } 57 | 58 | func (this *Output) MostLiked() ([]string, error) { 59 | arrayInterface, err := redis.Values(redisClient.Do("ZREVRANGE", this.cSet.mostLiked, 0, this.recNum)) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return this.toStrings(arrayInterface), err 64 | } 65 | 66 | func (this *Output) MostSimilarUsers(userId string) ([]string, error) { 67 | arrayInterface, err := redis.Values(redisClient.Do("ZREVRANGE", this.cSet.userSimilarity(userId), 0, this.recNum)) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return this.toStrings(arrayInterface), err 72 | } 73 | --------------------------------------------------------------------------------