diff --git a/app/delivery/web/api/restricted/meiliSearch.go b/app/delivery/web/api/restricted/meiliSearch.go index 8a41381..c297d37 100644 --- a/app/delivery/web/api/restricted/meiliSearch.go +++ b/app/delivery/web/api/restricted/meiliSearch.go @@ -1,6 +1,8 @@ package restricted import ( + "strconv" + "git.ma-al.com/goc_daniel/b2b/app/service/meiliService" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" @@ -23,8 +25,12 @@ func NewMeiliSearchHandler() *MeiliSearchHandler { func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router { handler := NewMeiliSearchHandler() - r.Get("/test", handler.Test) + // for superadmin only r.Get("/create-index", handler.CreateIndex) + r.Get("/test", handler.Test) + + // for all users + r.Get("/search", handler.Search) return r } @@ -61,3 +67,49 @@ func (h *MeiliSearchHandler) Test(c fiber.Ctx) error { return c.JSON(response.Make(&test, 0, i18n.T_(c, response.Message_OK))) } + +func (h *MeiliSearchHandler) Search(c fiber.Ctx) error { + id_lang, ok := c.Locals("langID").(uint) + if !ok { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + query := c.Query("query") + + limit_attribute := c.Query("limit") + limit, err := strconv.Atoi(limit_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + id_category_attribute := c.Query("id_category") + id_category, err := strconv.Atoi(id_category_attribute) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + price_lower_bound_attribute := c.Query("price_lower_bound") + price_lower_bound, err := strconv.ParseFloat(price_lower_bound_attribute, 64) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + price_upper_bound_attribute := c.Query("price_upper_bound") + price_upper_bound, err := strconv.ParseFloat(price_upper_bound_attribute, 64) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + test, err := h.meiliService.Search(id_lang, query, uint(limit), uint(id_category), price_lower_bound, price_upper_bound) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(&test, 0, i18n.T_(c, response.Message_OK))) +} diff --git a/app/model/productDescription.go b/app/model/productDescription.go index d7c0e2e..7bfa078 100644 --- a/app/model/productDescription.go +++ b/app/model/productDescription.go @@ -34,11 +34,14 @@ type MeiliSearchProduct struct { Price float64 `gorm:"column:price"` Description string `gorm:"column:description"` DescriptionShort string `gorm:"column:description_short"` - Usage string `gorm:"column:usage"` + Usage string `gorm:"column:used_for"` EAN13 string `gorm:"column:ean13"` Reference string `gorm:"column:reference"` Width float64 `gorm:"column:width"` Height float64 `gorm:"column:height"` Depth float64 `gorm:"column:depth"` Weight float64 `gorm:"column:weight"` + CategoryID uint `gorm:"column:id_category"` + CategoryName string `gorm:"column:category_name"` + Variations uint `gorm:"column:variations"` } diff --git a/app/repos/categoriesRepo/categoriesRepo.go b/app/repos/categoriesRepo/categoriesRepo.go index 1556f59..fb703c4 100644 --- a/app/repos/categoriesRepo/categoriesRepo.go +++ b/app/repos/categoriesRepo/categoriesRepo.go @@ -19,9 +19,10 @@ func New() UICategoriesRepo { func (repo *CategoriesRepo) GetAllCategories(id_lang uint) ([]model.ScannedCategory, error) { var allCategories []model.ScannedCategory - err := db.DB.Raw(` - SELECT - ps_category.id_category AS ID, + err := db.DB. + Table("ps_category"). + Select(` + ps_category.id_category AS id, ps_category_lang.name AS name, ps_category.active AS active, ps_category_shop.position AS position, @@ -29,12 +30,22 @@ func (repo *CategoriesRepo) GetAllCategories(id_lang uint) ([]model.ScannedCateg ps_category.is_root_category AS is_root_category, ps_category_lang.link_rewrite AS link_rewrite, ps_lang.iso_code AS iso_code - FROM ps_category - LEFT JOIN ps_category_lang ON ps_category_lang.id_category = ps_category.id_category AND ps_category_lang.id_shop = ? AND ps_category_lang.id_lang = ? - LEFT JOIN ps_category_shop ON ps_category_shop.id_category = ps_category.id_category AND ps_category_shop.id_shop = ? - JOIN ps_lang ON ps_lang.id_lang = ps_category_lang.id_lang - `, - constdata.SHOP_ID, id_lang, constdata.SHOP_ID). + `). + Joins(` + LEFT JOIN ps_category_lang + ON ps_category_lang.id_category = ps_category.id_category + AND ps_category_lang.id_shop = ? + AND ps_category_lang.id_lang = ? + `, constdata.SHOP_ID, id_lang). + Joins(` + LEFT JOIN ps_category_shop + ON ps_category_shop.id_category = ps_category.id_category + AND ps_category_shop.id_shop = ? + `, constdata.SHOP_ID). + Joins(` + JOIN ps_lang + ON ps_lang.id_lang = ps_category_lang.id_lang + `). Scan(&allCategories).Error return allCategories, err diff --git a/app/repos/listProductsRepo/listProductsRepo.go b/app/repos/listProductsRepo/listProductsRepo.go index db82261..539b6fc 100644 --- a/app/repos/listProductsRepo/listProductsRepo.go +++ b/app/repos/listProductsRepo/listProductsRepo.go @@ -36,41 +36,43 @@ func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filt // Limit(p.Limit()). // Offset(p.Offset()) - err := db.DB.Raw(` - SELECT - ps_product.id_product AS ID, + subQuery := db.DB. + Table("ps_image"). + Select("id_product, MIN(id_image) AS id_image"). + Group("id_product") + + err := db.DB. + Table("ps_product"). + Select(` + ps_product.id_product AS id, ps_product_lang.name AS name, ps_product.active AS active, ps_product_lang.link_rewrite AS link_rewrite, - COALESCE ( - ps_image_shop.id_image, any_image.id_image - ) AS id_image - FROM ps_product + COALESCE(ps_image_shop.id_image, any_image.id_image) AS id_image + `). + Joins(` LEFT JOIN ps_product_lang - ON ps_product_lang.id_product = ps_product.id_product - AND ps_product_lang.id_shop = ? + ON ps_product_lang.id_product = ps_product.id_product + AND ps_product_lang.id_shop = ? AND ps_product_lang.id_lang = ? + `, constdata.SHOP_ID, id_lang). + Joins(` LEFT JOIN ps_image_shop - ON ps_image_shop.id_product = ps_product.id_product - AND ps_image_shop.id_shop = ? + ON ps_image_shop.id_product = ps_product.id_product + AND ps_image_shop.id_shop = ? AND ps_image_shop.cover = 1 - LEFT JOIN ( - SELECT id_product, MIN(id_image) AS id_image - FROM ps_image - GROUP BY id_product - ) any_image - ON ps_product.id_product = any_image.id_product - LIMIT ? OFFSET ?`, - constdata.SHOP_ID, id_lang, constdata.SHOP_ID, p.Limit(), p.Offset()). + `, constdata.SHOP_ID). + Joins("LEFT JOIN (?) AS any_image ON ps_product.id_product = any_image.id_product", subQuery). + Limit(p.Limit()). + Offset(p.Offset()). Scan(&listing).Error if err != nil { return find.Found[model.ProductInList]{}, err } - err = db.DB.Raw(` - SELECT COUNT(*) - FROM ps_product`). - Scan(&total).Error + err = db.DB. + Table("ps_product"). + Count(&total).Error if err != nil { return find.Found[model.ProductInList]{}, err } diff --git a/app/repos/productDescriptionRepo/productDescriptionRepo.go b/app/repos/productDescriptionRepo/productDescriptionRepo.go index fece6b8..d97020a 100644 --- a/app/repos/productDescriptionRepo/productDescriptionRepo.go +++ b/app/repos/productDescriptionRepo/productDescriptionRepo.go @@ -80,7 +80,6 @@ func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSe var products []model.MeiliSearchProduct err := db.DB. - Select("pl.`usage` AS `usage`"). Select(` ps.id_product AS id_product, pl.name AS name, @@ -88,17 +87,24 @@ func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSe ps.price AS price, pl.description AS description, pl.description_short AS description_short, + pl.usage AS used_for, p.ean13 AS ean13, p.reference AS reference, p.width AS width, p.height AS height, p.depth AS depth, - p.weight AS weight + p.weight AS weight, + ps.id_category_default AS id_category, + cl.name AS category_name, + COUNT(DISTINCT pas.id_product_attribute) AS variations `). Table("ps_product_shop AS ps"). Joins("LEFT JOIN ps_product_lang AS pl ON ps.id_product = pl.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang). Joins("LEFT JOIN ps_product AS p ON p.id_product = ps.id_product"). + Joins("LEFT JOIN ps_category_lang AS cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang). + Joins("LEFT JOIN ps_product_attribute_shop AS pas ON pas.id_product = ps.id_product AND pas.id_shop = ?", constdata.SHOP_ID). Where("ps.id_shop = ?", constdata.SHOP_ID). + Group("ps.id_product"). Scan(&products).Error if err != nil { return products, fmt.Errorf("database error: %w", err) diff --git a/app/service/meiliService/meiliService.go b/app/service/meiliService/meiliService.go index 196d38a..c58bfa1 100644 --- a/app/service/meiliService/meiliService.go +++ b/app/service/meiliService/meiliService.go @@ -36,8 +36,9 @@ func New() *MeiliService { // ==================================== FOR SUPERADMIN ONLY ==================================== func (s *MeiliService) CreateIndex(id_lang uint) error { - products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang) + indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) + products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang) for i := 0; i < len(products); i++ { products[i].Description, err = cleanHTML(products[i].Description) if err != nil { @@ -67,22 +68,65 @@ func (s *MeiliService) CreateIndex(id_lang uint) error { } } - indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) primaryKey := "ProductID" docOptions := &meilisearch.DocumentOptions{ PrimaryKey: &primaryKey, SkipCreation: false, } - task, err := s.meiliClient.Index(indexName).AddDocuments(products, docOptions) if err != nil { return fmt.Errorf("meili AddDocuments error: %w", err) } - finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) fmt.Printf("Task status: %s\n", finishedTask.Status) fmt.Printf("Task error: %s\n", finishedTask.Error) + filterableAttributes := []interface{}{ + "CategoryID", + "Price", + } + task, err = s.meiliClient.Index(indexName).UpdateFilterableAttributes(&filterableAttributes) + if err != nil { + return fmt.Errorf("meili AddDocuments error: %w", err) + } + finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) + fmt.Printf("Task status: %s\n", finishedTask.Status) + fmt.Printf("Task error: %s\n", finishedTask.Error) + + displayedAttributes := []string{ + "ProductID", + "Name", + "Active", + "Price", + "EAN13", + "Reference", + "Variations", + } + task, err = s.meiliClient.Index(indexName).UpdateDisplayedAttributes(&displayedAttributes) + if err != nil { + return fmt.Errorf("meili AddDocuments error: %w", err) + } + finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) + fmt.Printf("Task status: %s\n", finishedTask.Status) + fmt.Printf("Task error: %s\n", finishedTask.Error) + + searchableAttributes := []string{ + "Name", + "DescriptionShort", + "Reference", + "EAN13", + "CategoryName", + "Description", + "Usage", + } + task, err = s.meiliClient.Index(indexName).UpdateSearchableAttributes(&searchableAttributes) + if err != nil { + return fmt.Errorf("meili AddDocuments error: %w", err) + } + finishedTask, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) + fmt.Printf("Task status: %s\n", finishedTask.Status) + fmt.Printf("Task error: %s\n", finishedTask.Error) + return err } @@ -91,25 +135,51 @@ func (s *MeiliService) Test(id_lang uint) (meilisearch.SearchResponse, error) { indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) searchReq := &meilisearch.SearchRequest{ - Limit: 3, + Limit: 4, + Facets: []string{ + "CategoryID", + }, } // Perform search - results, err := s.meiliClient.Index(indexName).Search("walek", searchReq) + results, err := s.meiliClient.Index(indexName).Search("mat", searchReq) if err != nil { fmt.Printf("Meilisearch error: %v\n", err) return meilisearch.SearchResponse{}, err } - fmt.Printf("Search results for query 'walek' in %s: %d hits\n", indexName, len(results.Hits)) + fmt.Printf("Search results for query 'mat' in %s: %d hits\n", indexName, len(results.Hits)) return *results, nil } // Search performs a full-text search on the specified index -func (s *MeiliService) Search(indexName string, query string, limit int) (meilisearch.SearchResponse, error) { +func (s *MeiliService) Search(id_lang uint, query string, limit uint, id_category uint, price_lower_bound float64, price_upper_bound float64) (meilisearch.SearchResponse, error) { + indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10) + + filter_query := "" + if id_category != 0 { + filter_query = "CategoryID = " + strconv.FormatUint(uint64(id_category), 10) + } + if price_lower_bound > 0.0 { + if filter_query != "" { + filter_query += " AND " + } + filter_query += "Price >= " + strconv.FormatFloat(price_lower_bound, 'f', -1, 64) + } + if price_upper_bound > 0.0 { + if filter_query != "" { + filter_query += " AND " + } + filter_query += "Price <= " + strconv.FormatFloat(price_upper_bound, 'f', -1, 64) + } + searchReq := &meilisearch.SearchRequest{ Limit: int64(limit), + Facets: []string{ + "CategoryID", + }, + Filter: filter_query, } results, err := s.meiliClient.Index(indexName).Search(query, searchReq)