diff --git a/server/rest.go b/server/rest.go index ab0f2ad8b..2d8b0adb0 100644 --- a/server/rest.go +++ b/server/rest.go @@ -539,7 +539,8 @@ func (s *RestServer) CreateWebService() { Param(ws.QueryParameter("write-back-delay", "Timestamp delay of write back feedback (format 0h0m0s)").DataType("string")). Param(ws.QueryParameter("n", "Number of returned items").DataType("integer")). Param(ws.QueryParameter("offset", "Offset of returned items").DataType("integer")). - Returns(http.StatusOK, "OK", []string{}). + Param(ws.QueryParameter("return-items", "Include full item data in response").DataType("boolean")). + Returns(http.StatusOK, "OK", []data.Item{}). Writes([]string{})) ws.Route(ws.GET("/recommend/{user-id}/{category}").To(s.getRecommend). Deprecate().Doc("Get recommendation for user. Set X-API-Version: 2 to return scores."). @@ -552,7 +553,8 @@ func (s *RestServer) CreateWebService() { Param(ws.QueryParameter("write-back-delay", "Timestamp delay of write back feedback (format 0h0m0s)").DataType("string")). Param(ws.QueryParameter("n", "Number of returned items").DataType("integer")). Param(ws.QueryParameter("offset", "Offset of returned items").DataType("integer")). - Returns(http.StatusOK, "OK", []string{}). + Param(ws.QueryParameter("return-items", "Include full item data in response").DataType("boolean")). + Returns(http.StatusOK, "OK", []data.Item{}). Writes([]string{})) ws.Route(ws.POST("/session/recommend").To(s.sessionRecommend). Doc("Get recommendation for session."). @@ -883,13 +885,14 @@ func (s *RestServer) getRecommend(request *restful.Request, response *restful.Re } else { scores = []cache.Score{} } - results := lo.Map(scores, func(item cache.Score, index int) string { - return item.Id + itemIds := lo.Map(scores, func(score cache.Score, _ int) string { + return score.Id }) + includeItems := request.QueryParameter("return-items") == "true" // write back if writeBackFeedback != "" { startTime := time.Now() - for _, itemId := range results { + for _, itemId := range itemIds { // insert to data store feedback := data.Feedback{ FeedbackKey: data.FeedbackKey{ @@ -906,12 +909,48 @@ func (s *RestServer) getRecommend(request *restful.Request, response *restful.Re } } } + // Fetch full item data only when requested + var itemMap map[string]data.Item + if includeItems { + fetchedItems, err := s.DataClient.BatchGetItems(ctx, itemIds) + if err != nil { + InternalServerError(response, err) + return + } + itemMap = make(map[string]data.Item, len(fetchedItems)) + for _, item := range fetchedItems { + itemMap[item.ItemId] = item + } + } // Send result if apiVersion == "2" { - Ok(response, scores) + if includeItems { + scoredItems := make([]ScoredItem, 0, len(scores)) + for _, s := range scores { + si := ScoredItem{Id: s.Id, Score: s.Score} + if item, ok := itemMap[s.Id]; ok { + si.Item = &item + } + scoredItems = append(scoredItems, si) + } + Ok(response, scoredItems) + } else { + Ok(response, scores) + } return } - Ok(response, results) + // Send response: include full item data only when requested + if includeItems { + items := make([]data.Item, 0) + for _, id := range itemIds { + if item, ok := itemMap[id]; ok { + items = append(items, item) + } + } + Ok(response, items) + } else { + Ok(response, itemIds) + } } func (s *RestServer) sessionRecommend(request *restful.Request, response *restful.Response) { @@ -1011,6 +1050,13 @@ func (s *RestServer) sessionRecommend(request *restful.Request, response *restfu Ok(response, result) } +// ScoredItem is a scored item with optional full item data for X-Api-Version: 2. +type ScoredItem struct { + Id string `json:"Id"` + Score float64 `json:"Score"` + Item *data.Item `json:"Item,omitempty"` +} + // Success is the returned data structure for data insert operations. type Success struct { RowAffected int diff --git a/server/rest_test.go b/server/rest_test.go index b4a9cd1a5..db7237c8d 100644 --- a/server/rest_test.go +++ b/server/rest_test.go @@ -88,6 +88,25 @@ func (suite *ServerTestSuite) marshal(v interface{}) string { return string(s) } +// marshalScoredItems builds the expected JSON for the recommend endpoint. +// It fetches full item data from the test DataClient and orders them to match itemIds. +func (suite *ServerTestSuite) marshalScoredItems(itemIds []string) string { + ctx := suite.T().Context() + fetched, err := suite.DataClient.BatchGetItems(ctx, itemIds) + suite.NoError(err) + itemMap := make(map[string]data.Item, len(fetched)) + for _, item := range fetched { + itemMap[item.ItemId] = item + } + var items []data.Item + for _, id := range itemIds { + if item, ok := itemMap[id]; ok { + items = append(items, item) + } + } + return suite.marshal(items) +} + func (suite *ServerTestSuite) TestUsers() { t := suite.T() users := []data.User{ @@ -1083,6 +1102,19 @@ func (suite *ServerTestSuite) TestGetRecommends() { Status(http.StatusOK). Body(suite.marshal([]string{"6", "7", "8"})). End() + // Test return-items=true returns full item data + apitest.New(). + Handler(suite.handler). + Get("/api/recommend/0"). + Header("X-API-Key", apiKey). + QueryParams(map[string]string{ + "n": "3", + "return-items": "true", + }). + Expect(suite.T()). + Status(http.StatusOK). + Body(suite.marshalScoredItems([]string{"6", "7", "8"})). + End() } func (suite *ServerTestSuite) TestGetRecommendsMultiCategories() {