diff --git a/config/config.go b/config/config.go index 2d2ad2059..ac9704e52 100644 --- a/config/config.go +++ b/config/config.go @@ -25,6 +25,7 @@ import ( "sync" "time" + "github.com/antonmedv/expr" "github.com/go-playground/locales/en" ut "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" @@ -109,6 +110,7 @@ type RecommendConfig struct { Replacement ReplacementConfig `mapstructure:"replacement"` Offline OfflineConfig `mapstructure:"offline"` Online OnlineConfig `mapstructure:"online"` + Candidate CandidateConfig `mapstructure:"candidate"` } type DataSourceConfig struct { @@ -164,6 +166,20 @@ type OnlineConfig struct { NumFeedbackFallbackItemBased int `mapstructure:"num_feedback_fallback_item_based" validate:"gt=0"` } +type CandidateConfig struct { + Top []TopConfig `mapstructure:"top"` +} + +type TopConfig struct { + Name string `mapstructure:"name" validate:"required"` + Score string `mapstructure:"score" validate:"required"` +} + +func (top *TopConfig) Validate() error { + _, err := expr.Compile(top.Score) + return err +} + type TracingConfig struct { EnableTracing bool `mapstructure:"enable_tracing"` Exporter string `mapstructure:"exporter" validate:"oneof=jaeger zipkin otlp otlphttp"` diff --git a/config/config_test.go b/config/config_test.go index 43f4647cb..131199093 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -450,3 +450,10 @@ func TestConfig_OfflineRecommendDigest(t *testing.T) { cfg2.Recommend.Replacement.PositiveReplacementDecay = 0.2 assert.Equal(t, cfg1.OfflineRecommendDigest(), cfg2.OfflineRecommendDigest()) } + +func TestTopConfig(t *testing.T) { + top := TopConfig{Score: "a+b"} + assert.NoError(t, top.Validate()) + top = TopConfig{Score: "%%"} + assert.Error(t, top.Validate()) +} diff --git a/go.mod b/go.mod index ae0abaf86..f5d457671 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/ReneKroon/ttlcache/v2 v2.11.0 github.com/XSAM/otelsql v0.17.0 + github.com/antonmedv/expr v1.12.5 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/benhoyt/goawk v1.20.0 github.com/bits-and-blooms/bitset v1.2.1 diff --git a/go.sum b/go.sum index da2aab9b9..6b17f7d78 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antonmedv/expr v1.12.5 h1:Fq4okale9swwL3OeLLs9WD9H6GbgBLJyN/NUHRv+n0E= +github.com/antonmedv/expr v1.12.5/go.mod h1:FPC8iWArxls7axbVLsW+kpg1mz29A1b2M6jt+hZfDkU= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= diff --git a/server/rest.go b/server/rest.go index 3260c0cfa..50a741c8a 100644 --- a/server/rest.go +++ b/server/rest.go @@ -418,6 +418,26 @@ func (s *RestServer) CreateWebService() { Returns(http.StatusOK, "OK", []cache.Document{}). Writes([]cache.Document{})) + // Get top items + ws.Route(ws.GET("/top/{name}").To(s.getTopItem). + Doc("Get top items."). + Metadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}). + Param(ws.HeaderParameter("X-API-Key", "API key").DataType("string")). + Param(ws.PathParameter("name", "Name of the top item recommender.").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", []cache.Document{}). + Writes([]cache.Document{})) + ws.Route(ws.GET("/top/{name}/{category}").To(s.getTopItem). + Doc("Get top items in category."). + Metadata(restfulspec.KeyOpenAPITags, []string{RecommendationAPITag}). + Param(ws.HeaderParameter("X-API-Key", "API key").DataType("string")). + Param(ws.PathParameter("name", "Name of the top item recommender.").DataType("string")). + Param(ws.PathParameter("category", "Category of returned items.").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", []cache.Document{}). + Writes([]cache.Document{})) // Get popular items ws.Route(ws.GET("/popular").To(s.getPopular). Doc("Get popular items."). @@ -614,6 +634,13 @@ func (s *RestServer) searchDocuments(collection, subset, category string, isItem Ok(response, items) } +func (s *RestServer) getTopItem(request *restful.Request, response *restful.Response) { + name := request.PathParameter("name") + category := request.PathParameter("category") + log.ResponseLogger(response).Debug("get top items", zap.String("name", name), zap.String("category", category)) + s.searchDocuments(cache.TopItems, name, category, true, request, response) +} + func (s *RestServer) getPopular(request *restful.Request, response *restful.Response) { category := request.PathParameter("category") log.ResponseLogger(response).Debug("get category popular items in category", zap.String("category", category)) diff --git a/storage/cache/database.go b/storage/cache/database.go index c4dbc5dde..14a046afc 100644 --- a/storage/cache/database.go +++ b/storage/cache/database.go @@ -85,6 +85,8 @@ const ( // Categorized the latest items - latest_items/{category} LatestItems = "latest_items" + TopItems = "top_items" + // ItemCategories is the set of item categories. The format of key: // Global item categories - item_categories ItemCategories = "item_categories"