Merge branch 'main' of ssh://git.ma-al.com:8822/goc_daniel/b2b into test

This commit is contained in:
2026-03-24 12:15:07 +01:00
6 changed files with 188 additions and 44 deletions

View File

@@ -1,6 +1,8 @@
package restricted package restricted
import ( import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/service/meiliService" "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/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
@@ -23,8 +25,12 @@ func NewMeiliSearchHandler() *MeiliSearchHandler {
func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router { func MeiliSearchHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewMeiliSearchHandler() handler := NewMeiliSearchHandler()
r.Get("/test", handler.Test) // for superadmin only
r.Get("/create-index", handler.CreateIndex) r.Get("/create-index", handler.CreateIndex)
r.Get("/test", handler.Test)
// for all users
r.Get("/search", handler.Search)
return r 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))) 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)))
}

View File

@@ -34,11 +34,14 @@ type MeiliSearchProduct struct {
Price float64 `gorm:"column:price"` Price float64 `gorm:"column:price"`
Description string `gorm:"column:description"` Description string `gorm:"column:description"`
DescriptionShort string `gorm:"column:description_short"` DescriptionShort string `gorm:"column:description_short"`
Usage string `gorm:"column:usage"` Usage string `gorm:"column:used_for"`
EAN13 string `gorm:"column:ean13"` EAN13 string `gorm:"column:ean13"`
Reference string `gorm:"column:reference"` Reference string `gorm:"column:reference"`
Width float64 `gorm:"column:width"` Width float64 `gorm:"column:width"`
Height float64 `gorm:"column:height"` Height float64 `gorm:"column:height"`
Depth float64 `gorm:"column:depth"` Depth float64 `gorm:"column:depth"`
Weight float64 `gorm:"column:weight"` Weight float64 `gorm:"column:weight"`
CategoryID uint `gorm:"column:id_category"`
CategoryName string `gorm:"column:category_name"`
Variations uint `gorm:"column:variations"`
} }

View File

@@ -19,9 +19,10 @@ func New() UICategoriesRepo {
func (repo *CategoriesRepo) GetAllCategories(id_lang uint) ([]model.ScannedCategory, error) { func (repo *CategoriesRepo) GetAllCategories(id_lang uint) ([]model.ScannedCategory, error) {
var allCategories []model.ScannedCategory var allCategories []model.ScannedCategory
err := db.DB.Raw(` err := db.DB.
SELECT Table("ps_category").
ps_category.id_category AS ID, Select(`
ps_category.id_category AS id,
ps_category_lang.name AS name, ps_category_lang.name AS name,
ps_category.active AS active, ps_category.active AS active,
ps_category_shop.position AS position, 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.is_root_category AS is_root_category,
ps_category_lang.link_rewrite AS link_rewrite, ps_category_lang.link_rewrite AS link_rewrite,
ps_lang.iso_code AS iso_code 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 = ? Joins(`
LEFT JOIN ps_category_shop ON ps_category_shop.id_category = ps_category.id_category AND ps_category_shop.id_shop = ? LEFT JOIN ps_category_lang
JOIN ps_lang ON ps_lang.id_lang = ps_category_lang.id_lang ON ps_category_lang.id_category = ps_category.id_category
`, AND ps_category_lang.id_shop = ?
constdata.SHOP_ID, id_lang, constdata.SHOP_ID). 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 Scan(&allCategories).Error
return allCategories, err return allCategories, err

View File

@@ -36,41 +36,43 @@ func (repo *ListProductsRepo) GetListing(id_lang uint, p find.Paging, filt *filt
// Limit(p.Limit()). // Limit(p.Limit()).
// Offset(p.Offset()) // Offset(p.Offset())
err := db.DB.Raw(` subQuery := db.DB.
SELECT Table("ps_image").
ps_product.id_product AS ID, 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_lang.name AS name,
ps_product.active AS active, ps_product.active AS active,
ps_product_lang.link_rewrite AS link_rewrite, ps_product_lang.link_rewrite AS link_rewrite,
COALESCE ( COALESCE(ps_image_shop.id_image, any_image.id_image) AS id_image
ps_image_shop.id_image, any_image.id_image `).
) AS id_image Joins(`
FROM ps_product
LEFT JOIN ps_product_lang LEFT JOIN ps_product_lang
ON ps_product_lang.id_product = ps_product.id_product ON ps_product_lang.id_product = ps_product.id_product
AND ps_product_lang.id_shop = ? AND ps_product_lang.id_shop = ?
AND ps_product_lang.id_lang = ? AND ps_product_lang.id_lang = ?
`, constdata.SHOP_ID, id_lang).
Joins(`
LEFT JOIN ps_image_shop LEFT JOIN ps_image_shop
ON ps_image_shop.id_product = ps_product.id_product ON ps_image_shop.id_product = ps_product.id_product
AND ps_image_shop.id_shop = ? AND ps_image_shop.id_shop = ?
AND ps_image_shop.cover = 1 AND ps_image_shop.cover = 1
LEFT JOIN ( `, constdata.SHOP_ID).
SELECT id_product, MIN(id_image) AS id_image Joins("LEFT JOIN (?) AS any_image ON ps_product.id_product = any_image.id_product", subQuery).
FROM ps_image Limit(p.Limit()).
GROUP BY id_product Offset(p.Offset()).
) 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()).
Scan(&listing).Error Scan(&listing).Error
if err != nil { if err != nil {
return find.Found[model.ProductInList]{}, err return find.Found[model.ProductInList]{}, err
} }
err = db.DB.Raw(` err = db.DB.
SELECT COUNT(*) Table("ps_product").
FROM ps_product`). Count(&total).Error
Scan(&total).Error
if err != nil { if err != nil {
return find.Found[model.ProductInList]{}, err return find.Found[model.ProductInList]{}, err
} }

View File

@@ -80,7 +80,6 @@ func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSe
var products []model.MeiliSearchProduct var products []model.MeiliSearchProduct
err := db.DB. err := db.DB.
Select("pl.`usage` AS `usage`").
Select(` Select(`
ps.id_product AS id_product, ps.id_product AS id_product,
pl.name AS name, pl.name AS name,
@@ -88,17 +87,24 @@ func (r *ProductDescriptionRepo) GetMeiliProducts(id_lang uint) ([]model.MeiliSe
ps.price AS price, ps.price AS price,
pl.description AS description, pl.description AS description,
pl.description_short AS description_short, pl.description_short AS description_short,
pl.usage AS used_for,
p.ean13 AS ean13, p.ean13 AS ean13,
p.reference AS reference, p.reference AS reference,
p.width AS width, p.width AS width,
p.height AS height, p.height AS height,
p.depth AS depth, 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"). 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_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_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). Where("ps.id_shop = ?", constdata.SHOP_ID).
Group("ps.id_product").
Scan(&products).Error Scan(&products).Error
if err != nil { if err != nil {
return products, fmt.Errorf("database error: %w", err) return products, fmt.Errorf("database error: %w", err)

View File

@@ -36,8 +36,9 @@ func New() *MeiliService {
// ==================================== FOR SUPERADMIN ONLY ==================================== // ==================================== FOR SUPERADMIN ONLY ====================================
func (s *MeiliService) CreateIndex(id_lang uint) error { 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++ { for i := 0; i < len(products); i++ {
products[i].Description, err = cleanHTML(products[i].Description) products[i].Description, err = cleanHTML(products[i].Description)
if err != nil { 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" primaryKey := "ProductID"
docOptions := &meilisearch.DocumentOptions{ docOptions := &meilisearch.DocumentOptions{
PrimaryKey: &primaryKey, PrimaryKey: &primaryKey,
SkipCreation: false, SkipCreation: false,
} }
task, err := s.meiliClient.Index(indexName).AddDocuments(products, docOptions) task, err := s.meiliClient.Index(indexName).AddDocuments(products, docOptions)
if err != nil { if err != nil {
return fmt.Errorf("meili AddDocuments error: %w", err) return fmt.Errorf("meili AddDocuments error: %w", err)
} }
finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond) finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
fmt.Printf("Task status: %s\n", finishedTask.Status) fmt.Printf("Task status: %s\n", finishedTask.Status)
fmt.Printf("Task error: %s\n", finishedTask.Error) 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 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) indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
searchReq := &meilisearch.SearchRequest{ searchReq := &meilisearch.SearchRequest{
Limit: 3, Limit: 4,
Facets: []string{
"CategoryID",
},
} }
// Perform search // Perform search
results, err := s.meiliClient.Index(indexName).Search("walek", searchReq) results, err := s.meiliClient.Index(indexName).Search("mat", searchReq)
if err != nil { if err != nil {
fmt.Printf("Meilisearch error: %v\n", err) fmt.Printf("Meilisearch error: %v\n", err)
return meilisearch.SearchResponse{}, 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 return *results, nil
} }
// Search performs a full-text search on the specified index // 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{ searchReq := &meilisearch.SearchRequest{
Limit: int64(limit), Limit: int64(limit),
Facets: []string{
"CategoryID",
},
Filter: filter_query,
} }
results, err := s.meiliClient.Index(indexName).Search(query, searchReq) results, err := s.meiliClient.Index(indexName).Search(query, searchReq)