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

This commit is contained in:
2026-03-30 15:17:53 +02:00
444 changed files with 10123 additions and 3830 deletions

View File

@@ -510,16 +510,31 @@ func (s *AuthService) generateAccessToken(user *model.Customer) (string, error)
func (s *AuthService) UpdateJWTToken(c fiber.Ctx) error {
// Get user ID from JWT claims in context (set by auth middleware)
claims, ok := c.Locals("jwt_claims").(*JWTClaims)
if !ok || claims == nil {
// claims, ok := c.Locals("jwt_claims").(*JWTClaims)
// if !ok || claims == nil {
// return c.Status(fiber.StatusUnauthorized).
// JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated)))
// }
// fmt.Printf("claims: %v\n", claims)
// var user model.Customer
// // Find user by ID
// if err := s.db.First(&user, claims.UserID).Error; err != nil {
// return err
// }
userLocals, ok := c.Locals(constdata.USER_LOCALES_NAME).(*model.UserSession)
if !ok {
return c.Status(fiber.StatusUnauthorized).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated)))
}
var user model.Customer
// Find user by ID
if err := s.db.First(&user, claims.UserID).Error; err != nil {
return err
user := model.Customer{
ID: userLocals.UserID,
Email: userLocals.Email,
Role: userLocals.Role,
LangID: userLocals.LangID,
CountryID: userLocals.CountryID,
IsActive: userLocals.IsActive,
}
// Parse language and country_id from query params

View File

@@ -1,8 +0,0 @@
{
"products-openai": {
"source": "openAi",
"model": "text-embedding-3-small",
"apiKey": "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",
"documentTemplate": "{{doc.Name}} is equipment used for {{doc.Description | truncatewords: 20}}"
}
}

View File

@@ -0,0 +1,230 @@
package meiliService
import "github.com/meilisearch/meilisearch-go"
type IndexationParams struct {
LocalizedAttributes []*meilisearch.LocalizedAttributes `json:"localizedAttributes"`
StopWords []string `json:"stopWords"`
Synonyms map[string][]string `json:"synonyms"`
}
var indexatio_params = map[uint]IndexationParams{
1: {
LocalizedAttributes: []*meilisearch.LocalizedAttributes{
{AttributePatterns: []string{"*"}, Locales: []string{"pol"}},
},
StopWords: []string{
"i", "w", "z", "na", "do", "się", "nie", "to", "że",
"a", "o", "jak", "ale", "po", "za", "przez", "przy",
"dla", "czy", "lub", "oraz", "ich", "jej", "jego",
"ten", "ta", "te", "tego", "tej", "tym", "tych",
"jest", "są", "być", "był", "była", "było", "będzie",
"już", "jeszcze", "też", "tylko", "więc", "jednak",
"co", "kto", "który", "która", "które", "ze", "by",
"ze", "im", "go", "je", "tu", "tam", "tak",
},
Synonyms: map[string][]string{
"plecak": {"plecaki", "plecaku", "plecaków", "tornister", "torba szkolna"},
"tornister": {"plecak", "torba szkolna", "plecaki"},
"piórnik": {"piórniki", "piórnika", "etui na przybory"},
"zeszyt": {"zeszyty", "zeszytu", "zeszytów", "notatnik", "notes"},
"kredka": {"kredki", "kredek", "kredkami", "ołówek kolorowy"},
"farba": {"farby", "farb", "farbami", "farba plakatowa", "tempera"},
"nożyczki": {"nożyczki dla dzieci", "nożyczki bezpieczne"},
"linijka": {"linijki", "linijka plastikowa", "linijka drewniana"},
"kalkulator": {"kalkulatory", "kalkulator szkolny", "kalkulator naukowy"},
"długopis": {"długopisy", "długopisu", "długopisów", "pisak", "pióro"},
"ołówek": {"ołówki", "ołówka", "ołówków", "grafitowy"},
"gumka": {"gumki", "gumka do mazania", "korektor"},
"temperówka": {"temperówki", "temperówka elektryczna"},
"plastelina": {"plasteliny", "masa plastyczna", "modelina", "glinka"},
"bibuła": {"bibułki", "bibuła marszczona", "krepa"},
"brystol": {"brystole", "brystolu", "karton", "tektura"},
"blok": {"bloki", "blok rysunkowy", "blok techniczny", "blok A4"},
"klej": {"kleje", "kleju", "klej w sztyfcie", "klej PVA", "klej UHU"},
"taśma": {"taśmy", "taśma klejąca", "taśma scotch"},
"marker": {"markery", "mazak", "mazaki", "flamaster", "flamastry"},
"pędzel": {"pędzle", "pędzla", "pędzli", "pędzel do farb"},
"plakat": {"plakaty", "plansza edukacyjna", "poster"},
"atlas": {"atlasy", "atlas geograficzny", "mapa"},
"słownik": {"słowniki", "słownika", "słownik angielski"},
"czytanka": {"czytanki", "lektura", "książka dla dzieci", "bajka"},
"zabawka": {"zabawki", "zabawek", "gra edukacyjna", "puzzle"},
"układanka": {"układanki", "puzzle", "mozaika"},
"klocki": {"klocek", "klocków", "lego", "duplo"},
"fartuch": {"fartuchy", "fartuch malarski", "fartuch plastyczny"},
"teczka": {"teczki", "teczka na dokumenty", "segregator", "portfolio"},
"koszulka": {"koszulki", "koszulka na dokumenty", "foliówka"},
"ekierka": {"ekierki", "kątownik"},
"cyrkiel": {"cyrkle", "kompas rysunkowy"},
"globus": {"globusy", "kula ziemska"},
"tablica": {"tablice", "tablica magnetyczna", "whiteboard", "flipchart"},
"magnez": {"magnesy", "magnes tablicowy"},
"kreda": {"kredy", "kreda tablicowa", "kreda kolorowa"},
"przedszkole": {"żłobek", "edukacja przedszkolna", "wiek przedszkolny"},
"szkoła": {"szkolny", "szkolna", "szkolne", "podstawówka"},
},
},
2: {
LocalizedAttributes: []*meilisearch.LocalizedAttributes{
{AttributePatterns: []string{"*"}, Locales: []string{"eng"}},
},
StopWords: []string{
"a", "an", "the", "and", "or", "but", "in", "on", "at",
"to", "for", "of", "with", "by", "from", "is", "are",
"was", "were", "be", "been", "being", "have", "has",
"had", "do", "does", "did", "will", "would", "could",
"should", "may", "might", "it", "its", "this", "that",
"these", "those", "as", "if", "so", "than", "up",
},
Synonyms: map[string][]string{
"backpack": {"rucksack", "school bag", "schoolbag", "knapsack"},
"pencil case": {"pencil pouch", "pencil box", "stationery case"},
"notebook": {"exercise book", "jotter", "notepad", "copybook"},
"colored pencil": {"colour pencil", "colouring pencil", "crayon pencil"},
"crayon": {"wax crayon", "colouring crayon", "wax pencil"},
"paint": {"poster paint", "tempera", "watercolour", "finger paint"},
"scissors": {"safety scissors", "kids scissors", "craft scissors"},
"ruler": {"measuring ruler", "plastic ruler", "wooden ruler"},
"calculator": {"scientific calculator", "school calculator"},
"pen": {"ballpoint", "ballpoint pen", "biro", "rollerball"},
"pencil": {"graphite pencil", "HB pencil", "drawing pencil"},
"eraser": {"rubber", "correction eraser", "white eraser"},
"sharpener": {"pencil sharpener", "electric sharpener"},
"plasticine": {"modelling clay", "play-doh", "clay", "dough"},
"tissue paper": {"crepe paper", "craft paper"},
"cardboard": {"card", "art board", "bristol board"},
"drawing pad": {"sketch pad", "art pad", "drawing block", "sketchbook"},
"glue": {"glue stick", "PVA glue", "craft glue", "adhesive"},
"tape": {"sticky tape", "sellotape", "scotch tape", "masking tape"},
"marker": {"felt tip", "felt-tip pen", "highlighter", "marker pen"},
"paintbrush": {"brush", "art brush", "watercolour brush"},
"poster": {"educational poster", "wall chart", "learning poster"},
"dictionary": {"word book", "vocabulary book", "language dictionary"},
"book": {"reading book", "children book", "picture book", "reader"},
"toy": {"educational toy", "learning toy", "kids toy"},
"puzzle": {"jigsaw", "jigsaw puzzle", "floor puzzle"},
"blocks": {"building blocks", "lego", "wooden blocks", "duplo"},
"apron": {"art apron", "painting apron", "smock"},
"folder": {"document folder", "ring binder", "portfolio"},
"sleeve": {"document sleeve", "plastic sleeve", "page protector"},
"compass": {"drawing compass", "geometry compass"},
"set square": {"triangle ruler", "geometry set"},
"globe": {"world globe", "earth globe"},
"whiteboard": {"dry erase board", "magnetic board", "notice board"},
"chalk": {"blackboard chalk", "coloured chalk", "sidewalk chalk"},
"kindergarten": {"preschool", "nursery", "early years", "pre-k"},
"school": {"primary school", "elementary school", "educational"},
},
},
3: {
LocalizedAttributes: []*meilisearch.LocalizedAttributes{
{AttributePatterns: []string{"*"}, Locales: []string{"deu"}},
},
StopWords: []string{
"und", "oder", "aber", "in", "an", "auf", "zu", "für",
"von", "mit", "durch", "bei", "nach", "über", "unter",
"ist", "sind", "war", "waren", "sein", "haben", "hat",
"der", "die", "das", "den", "dem", "des", "ein", "eine",
"einer", "einem", "einen", "eines", "sich", "auch",
"nicht", "noch", "schon", "nur", "so", "wie", "wenn",
"dann", "da", "hier", "dort", "ich", "du", "er", "sie",
"es", "wir", "ihr", "als", "am", "im", "ins",
},
Synonyms: map[string][]string{
"rucksack": {"schulrucksack", "schulranzen", "ranzen", "tornister"},
"schulranzen": {"rucksack", "tornister", "schultasche"},
"federmäppchen": {"mäppchen", "federmappe", "stiftemäppchen", "etui"},
"heft": {"schulheft", "schreibheft", "lineatur", "notizheft"},
"buntstift": {"buntstifte", "farbstift", "farbstifte", "malstift"},
"wachsmalstift": {"wachsmalstifte", "wachskreide", "malwachs"},
"farbe": {"schulfarbe", "plakatfarbe", "wasserfarbe", "fingerfarbe", "tempera"},
"schere": {"kinderschere", "bastelschere", "sicherheitsschere"},
"lineal": {"schullineal", "kunststofflineal", "holzlineal"},
"taschenrechner": {"schulrechner", "wissenschaftlicher rechner", "rechner"},
"kugelschreiber": {"kuli", "stift", "tintenroller", "füller"},
"bleistift": {"zeichenbleistift", "graphitstift", "schulbleistift"},
"radiergummi": {"radierer", "radiergummi", "tipp-ex"},
"anspitzer": {"bleistiftspitzer", "elektrischer anspitzer"},
"knete": {"knetmasse", "plastilin", "modellierton", "play-doh"},
"seidenpapier": {"krepppapier", "krepp", "bastelpapier"},
"karton": {"pappe", "zeichenkarton", "bristol"},
"zeichenblock": {"malblock", "skizzenblock", "zeichenpapier"},
"kleber": {"klebestift", "bastelkleber", "PVA-kleber", "UHU"},
"klebeband": {"tesa", "scotch", "klebeband transparent", "kreativband"},
"marker": {"filzstift", "faserstift", "textmarker", "edding"},
"pinsel": {"malpinsel", "aquarellpinsel", "zeichenpinsel"},
"poster": {"lernposter", "bildungsposter", "schulposter", "wandkarte"},
"wörterbuch": {"schulwörterbuch", "vokabelbuch", "lexikon"},
"buch": {"kinderbuch", "schulbuch", "lesebuch", "bilderbuch"},
"spielzeug": {"lernspielzeug", "pädagogisches spielzeug", "kinderspielzeug"},
"puzzle": {"legepuzzle", "bodenpuzzle", "steckpuzzle"},
"baustein": {"bausteine", "lego", "holzbausteine", "duplo"},
"schürze": {"malschürze", "bastelschürze", "kittel"},
"mappe": {"schulmappe", "hefter", "ringbuch", "ordner"},
"klarsichthülle": {"prospekthülle", "dokumentenhülle", "sichthülle"},
"zirkel": {"zeichenzirkel", "geometriezirkel"},
"geodreieck": {"winkelmesser", "zeichendreieck", "geometriedreieck"},
"globus": {"erdkugel", "weltkugel", "schulglob"},
"whiteboard": {"magnettafel", "schreibtafel", "flipchart"},
"kreide": {"tafelkreide", "schulkreide", "bunkreide"},
"kindergarten": {"kita", "krippe", "vorschule", "kiga"},
"schule": {"grundschule", "volksschule", "schulbedarf"},
},
},
4: {
LocalizedAttributes: []*meilisearch.LocalizedAttributes{
{AttributePatterns: []string{"*"}, Locales: []string{"ces"}},
},
StopWords: []string{
"a", "i", "v", "na", "do", "se", "ne", "to", "že",
"o", "jak", "ale", "po", "za", "pro", "při", "pro",
"nebo", "či", "anebo", "jejich", "jeho", "její",
"ten", "ta", "to", "toho", "té", "tom", "těch",
"je", "jsou", "být", "byl", "byla", "bylo", "bude",
"už", "ještě", "také", "jen", "tedy", "však",
"co", "kdo", "který", "která", "které", "ze",
"by", "mu", "ho", "ji", "tu", "tam", "tak",
},
Synonyms: map[string][]string{
"batoh": {"batohy", "batohu", "batohů", "školní batoh", "školní taška", "aktovka"},
"aktovka": {"aktovky", "školní aktovka", "batoh", "taška do školy"},
"penál": {"penály", "penálu", "pouzdro na tužky", "pouzdro na psací potřeby"},
"sešit": {"sešity", "sešitu", "sešitů", "poznámkový blok", "zápisník"},
"pastelka": {"pastelky", "pastelku", "pastelkách", "barevná tužka", "barevné tužky"},
"voskovka": {"voskovky", "vosková pastelka", "voskové pastelky"},
"barva": {"barvy", "barev", "školní barva", "plakátová barva", "temperová barva", "vodová barva", "prstová barva"},
"nůžky": {"dětské nůžky", "bezpečné nůžky", "školní nůžky"},
"pravítko": {"pravítka", "plastové pravítko", "dřevěné pravítko"},
"kalkulačka": {"kalkulačky", "školní kalkulačka", "vědecká kalkulačka"},
"propiska": {"propiska", "propisky", "kuličkové pero", "pero", "roller"},
"tužka": {"tužky", "grafitová tužka", "školní tužka", "kreslicí tužka"},
"guma": {"gumy", "guma na mazání", "mazací guma", "korektor"},
"ořezávátko": {"ořezávátka", "elektrické ořezávátko"},
"plastelína": {"plastelíny", "modelovací hmota", "modelína", "play-doh", "hlína"},
"hedvábný papír": {"krepový papír", "krepp", "krepa"},
"karton": {"kartony", "výkres", "výkresy", "bristol", "čtvrtka"},
"skicák": {"skicáky", "blok na kreslení", "kreslicí blok", "náčrtník"},
"lepidlo": {"lepidla", "lepicí tyčinka", "PVA lepidlo", "UHU", "lepidlo v tyčince"},
"lepicí páska": {"scotch", "průhledná páska", "izolepa"},
"fix": {"fixy", "fixů", "fixů", "popisovač", "popisovače", "zvýrazňovač"},
"štětec": {"štětce", "malířský štětec", "akvarelový štětec"},
"plakát": {"plakáty", "vzdělávací plakát", "výuková tabule", "nástěnná mapa"},
"slovník": {"slovníky", "školní slovník", "výkladový slovník"},
"kniha": {"knihy", "dětská kniha", "čítanka", "učebnice", "obrázkové knihy"},
"hračka": {"hračky", "vzdělávací hračka", "didaktická hračka"},
"puzzle": {"skládačka", "skládačky", "mozaika", "jigsaw"},
"kostky": {"stavebnice", "lego", "dřevěné kostky", "duplo"},
"zástěra": {"zástěry", "malířská zástěra", "ochranný plášť"},
"desky": {"složka", "složky", "pořadač", "kroužkový pořadač", "portfolio"},
"fólie": {"průhledná fólie", "eurodesky", "plastová fólie"},
"kružítko": {"kružítka", "rýsovací kružítko"},
"úhloměr": {"pravítko s úhloměrem", "trojúhelník", "rýsovací trojúhelník"},
"glóbus": {"glóbusy", "zeměkoule", "školní glóbus"},
"tabule": {"magnetická tabule", "whiteboard", "flipchart", "školní tabule"},
"křída": {"křídy", "barevná křída", "školní křída"},
"školka": {"mateřská škola", "MŠ", "předškolní věk", "jesle"},
"škola": {"školní", "základní škola", "ZŠ", "školní potřeby"},
},
},
}

View File

@@ -2,17 +2,23 @@ package meiliService
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/productDescriptionRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"github.com/meilisearch/meilisearch-go"
)
// MeiliIndexSettings holds the configurable index settings
type MeiliIndexSettings struct {
SearchableAttributes []string `json:"searchableAttributes"`
DisplayedAttributes []string `json:"displayedAttributes"`
FilterableAttributes []string `json:"filterableAttributes"`
SortableAttributes []string `json:"sortableAttributes"`
}
type MeiliService struct {
productDescriptionRepo productDescriptionRepo.UIProductDescriptionRepo
meiliClient meilisearch.ServiceManager
@@ -31,104 +37,172 @@ func New() *MeiliService {
}
}
func GetIndexName(id_lang uint) string {
return fmt.Sprintf("shop_%d_lang_%d", constdata.SHOP_ID, id_lang)
}
// ==================================== FOR TESTING ONLY ====================================
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)
indexName := GetIndexName(id_lang)
products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang)
for i := 0; i < len(products); i++ {
products[i].Description = cleanHTML(products[i].Description)
products[i].DescriptionShort = cleanHTML(products[i].DescriptionShort)
products[i].Usage = cleanHTML(products[i].Usage)
const batchSize = 500
offset := 0
for {
// Get batch of products from repo (includes scanning)
products, err := s.productDescriptionRepo.GetMeiliProducts(id_lang, offset, batchSize)
if err != nil {
return fmt.Errorf("failed to get products batch at offset %d: %w", offset, err)
}
// If no products returned, we're done
if len(products) == 0 {
break
}
// Add batch to index
if err := s.addBatchToIndex(indexName, products); err != nil {
return fmt.Errorf("failed to add batch to index: %w", err)
}
// Update offset for next batch
offset += batchSize
fmt.Printf("Indexed %d products (offset: %d)\n", len(products), offset)
}
primaryKey := "ProductID"
// Configure filterable attributes
filterableAttributes := []interface{}{
"product_id",
"category_id",
"category_ids",
"attr",
"feat",
"variations",
"price",
}
task, err := s.meiliClient.Index(indexName).UpdateFilterableAttributes(&filterableAttributes)
if err != nil {
return fmt.Errorf("failed to update filterable attributes: %w", err)
}
_, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
if err != nil {
return fmt.Errorf("failed to wait for filterable task: %w", err)
}
// Configure sortable attributes
sortableAttributes := []string{
"price",
"name",
"product_id",
"name",
"category_ids",
}
task, err = s.meiliClient.Index(indexName).UpdateSortableAttributes(&sortableAttributes)
if err != nil {
return fmt.Errorf("failed to update sortable attributes: %w", err)
}
task, err = s.meiliClient.Index(indexName).UpdateSettings(&meilisearch.Settings{
LocalizedAttributes: indexatio_params[id_lang].LocalizedAttributes,
Synonyms: indexatio_params[id_lang].Synonyms,
StopWords: indexatio_params[id_lang].StopWords,
})
if err != nil {
return fmt.Errorf("failed to update ranking rules: %w", err)
}
_, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
if err != nil {
return fmt.Errorf("failed to wait for sortable task: %w", err)
}
// Configure displayed attributes
displayedAttributes := []string{
"product_id",
"name",
"ean13",
"reference",
"variations",
"id_image",
"price",
"category_name",
}
task, err = s.meiliClient.Index(indexName).UpdateDisplayedAttributes(&displayedAttributes)
if err != nil {
return fmt.Errorf("failed to update displayed attributes: %w", err)
}
_, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
if err != nil {
return fmt.Errorf("failed to wait for displayed task: %w", err)
}
// Configure searchable attributes
searchableAttributes := []string{
"name",
"description",
"ean13",
"category_name",
"reference",
}
task, err = s.meiliClient.Index(indexName).UpdateSearchableAttributes(&searchableAttributes)
if err != nil {
return fmt.Errorf("failed to update searchable attributes: %w", err)
}
task, err = s.meiliClient.Index(indexName).UpdateFaceting(&meilisearch.Faceting{MaxValuesPerFacet: 5})
if err != nil {
return fmt.Errorf("failed to update ranking rules: %w", err)
}
// keyRequest := &meilisearch.Key{
// Key: "my secret key", // ✅ provide your own token
// Description: "Search-only key for frontend",
// Actions: []string{"search", "settings.get"},
// Indexes: []string{indexName}, // restrict to your index
// }
// createdKey, err := s.meiliClient.CreateKey(keyRequest)
// if err != nil {
// panic(err)
// }
// fmt.Println("Custom search key created:", createdKey.Key)
_, err = s.meiliClient.WaitForTask(task.TaskUID, 500*time.Millisecond)
if err != nil {
return fmt.Errorf("failed to wait for searchable task: %w", err)
}
return nil
}
// addBatchToIndex adds a batch of products to the Meilisearch index
func (s *MeiliService) addBatchToIndex(indexName string, products []model.MeiliSearchProduct) error {
primaryKey := "product_id"
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)
return fmt.Errorf("failed to add documents: %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",
"CategoryIDs",
}
task, err = s.meiliClient.Index(indexName).UpdateFilterableAttributes(&filterableAttributes)
finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 1000*time.Millisecond)
if err != nil {
return fmt.Errorf("meili AddDocuments error: %w", err)
return fmt.Errorf("failed to wait for task: %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",
"EAN13",
"Reference",
"Variations",
"CoverImage",
}
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
}
// ==================================== FOR TESTING ONLY ====================================
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: 4,
Facets: []string{
"CategoryID",
},
if finishedTask.Status == "failed" {
return fmt.Errorf("task failed: %v", finishedTask.Error)
}
// Perform search
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 'mat' in %s: %d hits\n", indexName, len(results.Hits))
return *results, nil
return nil
}
// Search performs a full-text search on the specified index
func (s *MeiliService) Search(id_lang uint, query string, id_category uint) (meilisearch.SearchResponse, error) {
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
indexName := GetIndexName(id_lang)
filter_query := "Active = 1"
if id_category != 0 {
@@ -165,7 +239,7 @@ func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) {
// GetIndexSettings retrieves the current settings for a specific index
func (s *MeiliService) GetIndexSettings(id_lang uint) (map[string]interface{}, error) {
indexName := "meili_products_shop" + strconv.FormatInt(constdata.SHOP_ID, 10) + "_lang" + strconv.FormatInt(int64(id_lang), 10)
indexName := GetIndexName(id_lang)
index := s.meiliClient.Index(indexName)
@@ -210,12 +284,62 @@ func (s *MeiliService) GetIndexSettings(id_lang uint) (map[string]interface{}, e
return result, nil
}
// remove all tags from HTML text
func cleanHTML(s string) string {
// Simple regex to remove all HTML tags
re := regexp.MustCompile(`<[^>]*>`)
result := re.ReplaceAllString(s, "")
// Replace multiple spaces with single space
result = regexp.MustCompile(`\s+`).ReplaceAllString(result, " ")
return strings.TrimSpace(result)
// UpdateIndexSettings updates the index settings (searchable, displayed, filterable, sortable attributes)
func (s *MeiliService) UpdateIndexSettings(id_lang uint, settings MeiliIndexSettings) error {
indexName := GetIndexName(id_lang)
index := s.meiliClient.Index(indexName)
// Update searchable attributes
if len(settings.SearchableAttributes) > 0 {
task, err := index.UpdateSearchableAttributes(&settings.SearchableAttributes)
if err != nil {
return fmt.Errorf("failed to update searchable attributes: %w", err)
}
_, err = s.meiliClient.WaitForTask(task.TaskUID, 1000*time.Millisecond)
if err != nil {
return fmt.Errorf("failed to wait for searchable attributes task: %w", err)
}
}
// Update displayed attributes
if len(settings.DisplayedAttributes) > 0 {
task, err := index.UpdateDisplayedAttributes(&settings.DisplayedAttributes)
if err != nil {
return fmt.Errorf("failed to update displayed attributes: %w", err)
}
_, err = s.meiliClient.WaitForTask(task.TaskUID, 1000*time.Millisecond)
if err != nil {
return fmt.Errorf("failed to wait for displayed attributes task: %w", err)
}
}
// Update filterable attributes
if len(settings.FilterableAttributes) > 0 {
var filterable []interface{}
for _, attr := range settings.FilterableAttributes {
filterable = append(filterable, attr)
}
task, err := index.UpdateFilterableAttributes(&filterable)
if err != nil {
return fmt.Errorf("failed to update filterable attributes: %w", err)
}
_, err = s.meiliClient.WaitForTask(task.TaskUID, 1000*time.Millisecond)
if err != nil {
return fmt.Errorf("failed to wait for filterable attributes task: %w", err)
}
}
// Update sortable attributes
if len(settings.SortableAttributes) > 0 {
task, err := index.UpdateSortableAttributes(&settings.SortableAttributes)
if err != nil {
return fmt.Errorf("failed to update sortable attributes: %w", err)
}
_, err = s.meiliClient.WaitForTask(task.TaskUID, 1000*time.Millisecond)
if err != nil {
return fmt.Errorf("failed to wait for sortable attributes task: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,233 @@
package searchservice
import (
"encoding/json"
"fmt"
"strconv"
"strings"
attributerepo "git.ma-al.com/goc_daniel/b2b/app/repos/attributeRepo"
categoryrepo "git.ma-al.com/goc_daniel/b2b/app/repos/categoryRepo"
featurerepo "git.ma-al.com/goc_daniel/b2b/app/repos/featureRepo"
searchrepo "git.ma-al.com/goc_daniel/b2b/app/repos/searchRepo"
)
type SearchService struct {
searchRepo searchrepo.UISearchRepo
attributeRepo attributerepo.UIAttributeRepo
featureRepo featurerepo.UIFeatureRepo
categoryRepo categoryrepo.UICategoryRepo
}
func New() *SearchService {
return &SearchService{
searchRepo: searchrepo.New(),
attributeRepo: attributerepo.New(),
featureRepo: featurerepo.New(),
categoryRepo: categoryrepo.New(),
}
}
func (s *SearchService) Search(index string, body []byte, idLang uint) (*searchrepo.SearchProxyResponse, error) {
resp, err := s.searchRepo.Search(index, body)
if err != nil {
return nil, err
}
if resp.StatusCode == 200 {
var data map[string]interface{}
if err := json.Unmarshal(resp.Body, &data); err == nil {
data = s.filterEmptyFacets(data)
data = s.enrichFacetTranslations(data, idLang)
resp.Body, _ = json.Marshal(data)
}
}
return resp, nil
}
func (s *SearchService) GetIndexSettings(index string) (*searchrepo.SearchProxyResponse, error) {
return s.searchRepo.GetIndexSettings(index)
}
func (s *SearchService) IsIndexNotFound(body []byte) bool {
var resp map[string]interface{}
if err := json.Unmarshal(body, &resp); err != nil {
return false
}
if code, ok := resp["code"].(string); ok {
return code == "index_not_found"
}
return false
}
func (s *SearchService) filterEmptyFacets(data map[string]interface{}) map[string]interface{} {
if facets, ok := data["facetDistribution"].(map[string]interface{}); ok {
filtered := make(map[string]interface{})
for k, v := range facets {
if m, ok := v.(map[string]interface{}); ok && len(m) > 0 {
filtered[k] = v
}
}
data["facetDistribution"] = filtered
}
return data
}
func (s *SearchService) enrichFacetTranslations(data map[string]interface{}, idLang uint) map[string]interface{} {
facets, ok := data["facetDistribution"].(map[string]interface{})
if !ok {
return data
}
groupIDs := []uint{}
attrIDs := []uint{}
featureIDs := []uint{}
featureValueIDs := []uint{}
categoryIDs := []uint{}
for key, values := range facets {
valueMap, ok := values.(map[string]interface{})
if !ok {
continue
}
if strings.HasPrefix(key, "attr.") {
groupIDStr := strings.TrimPrefix(key, "attr.")
groupID, err := strconv.ParseUint(groupIDStr, 10, 64)
if err != nil {
continue
}
groupIDs = append(groupIDs, uint(groupID))
for attrKey := range valueMap {
attrID, err := strconv.ParseUint(attrKey, 10, 64)
if err != nil {
continue
}
attrIDs = append(attrIDs, uint(attrID))
}
}
if strings.HasPrefix(key, "feat.") {
featureIDStr := strings.TrimPrefix(key, "feat.")
featureID, err := strconv.ParseUint(featureIDStr, 10, 64)
if err != nil {
continue
}
featureIDs = append(featureIDs, uint(featureID))
for valueKey := range valueMap {
valueID, err := strconv.ParseUint(valueKey, 10, 64)
if err != nil {
continue
}
featureValueIDs = append(featureValueIDs, uint(valueID))
}
}
if key == "category_ids" {
for catKey := range valueMap {
catID, err := strconv.ParseUint(catKey, 10, 64)
if err != nil {
continue
}
categoryIDs = append(categoryIDs, uint(catID))
}
}
}
attributeGroups, _ := s.attributeRepo.GetAttributeGroupsWithAttributes(groupIDs, attrIDs, idLang)
featureGroups, _ := s.featureRepo.GetFeaturesWithValues(featureIDs, featureValueIDs, idLang)
categoryTranslations, _ := s.categoryRepo.GetCategoryTranslations(categoryIDs, idLang)
translations := make(map[string]interface{})
for _, groupID := range groupIDs {
key := fmt.Sprintf("attr.%d", groupID)
group, ok := attributeGroups[groupID]
if !ok {
group = attributerepo.AttributeGroupWithAttrs{
GroupName: key,
Attrs: make(map[uint]attributerepo.AttributeWithColor),
}
}
valueTranslations := make(map[string]interface{})
for attrID, attr := range group.Attrs {
attrName := attr.Name
if attrName == "" {
attrName = strconv.FormatUint(uint64(attrID), 10)
}
entry := map[string]string{"t": attrName}
if attr.Color != "" {
entry["c"] = attr.Color
}
valueTranslations[strconv.FormatUint(uint64(attrID), 10)] = entry
}
groupName := group.GroupName
if groupName == "" {
groupName = key
}
translations[key] = map[string]interface{}{
"groupName": groupName,
"values": valueTranslations,
}
}
for _, featureID := range featureIDs {
key := fmt.Sprintf("feat.%d", featureID)
feature, ok := featureGroups[featureID]
if !ok {
feature = featurerepo.FeatureGroupWithValues{
FeatureName: key,
Values: make(map[uint]string),
}
}
valueTranslations := make(map[string]interface{})
for valueID, valueName := range feature.Values {
if valueName == "" {
valueName = strconv.FormatUint(uint64(valueID), 10)
}
valueTranslations[strconv.FormatUint(uint64(valueID), 10)] = map[string]string{
"t": valueName,
}
}
featureName := feature.FeatureName
if featureName == "" {
featureName = key
}
translations[key] = map[string]interface{}{
"groupName": featureName,
"values": valueTranslations,
}
}
categoryValueTranslations := make(map[string]interface{})
for _, catID := range categoryIDs {
catName := categoryTranslations[catID]
if catName == "" {
catName = strconv.FormatUint(uint64(catID), 10)
}
categoryValueTranslations[strconv.FormatUint(uint64(catID), 10)] = map[string]string{
"t": catName,
}
}
if len(categoryValueTranslations) > 0 {
translations["category_ids"] = map[string]interface{}{
"groupName": "category_ids",
"values": categoryValueTranslations,
}
}
data["facetTranslations"] = translations
return data
}