package meiliService import ( "fmt" "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 } func New() *MeiliService { client := meilisearch.New( config.Get().MeiliSearch.ServerURL, meilisearch.WithAPIKey(config.Get().MeiliSearch.ApiKey), ) return &MeiliService{ meiliClient: client, productDescriptionRepo: productDescriptionRepo.New(), } } 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 := GetIndexName(id_lang) 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) } // 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("failed to add documents: %w", err) } finishedTask, err := s.meiliClient.WaitForTask(task.TaskUID, 1000*time.Millisecond) if err != nil { return fmt.Errorf("failed to wait for task: %w", err) } if finishedTask.Status == "failed" { return fmt.Errorf("task failed: %v", finishedTask.Error) } 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 := GetIndexName(id_lang) filter_query := "Active = 1" if id_category != 0 { // Use CategoryIDs to include products from child categories filter_query += fmt.Sprintf(" AND CategoryIDs = %d", id_category) } searchReq := &meilisearch.SearchRequest{ Limit: 30, Facets: []string{ "CategoryID", }, Filter: filter_query, } results, err := s.meiliClient.Index(indexName).Search(query, searchReq) if err != nil { fmt.Printf("Meilisearch search error: %v\n", err) return meilisearch.SearchResponse{}, err } return *results, nil } // HealthCheck checks if Meilisearch is healthy and accessible func (s *MeiliService) HealthCheck() (*meilisearch.Health, error) { health, err := s.meiliClient.Health() if err != nil { return nil, fmt.Errorf("meilisearch health check failed: %w", err) } return health, nil } // GetIndexSettings retrieves the current settings for a specific index func (s *MeiliService) GetIndexSettings(id_lang uint) (map[string]interface{}, error) { indexName := GetIndexName(id_lang) index := s.meiliClient.Index(indexName) result := make(map[string]interface{}) // Get searchable attributes searchable, err := index.GetSearchableAttributes() if err == nil { result["searchableAttributes"] = searchable } // Get filterable attributes filterable, err := index.GetFilterableAttributes() if err == nil { result["filterableAttributes"] = filterable } // Get displayed attributes displayed, err := index.GetDisplayedAttributes() if err == nil { result["displayedAttributes"] = displayed } // Get ranking rules ranking, err := index.GetRankingRules() if err == nil { result["rankingRules"] = ranking } // Get distinct attribute distinct, err := index.GetDistinctAttribute() if err == nil && distinct != nil { result["distinctAttribute"] = *distinct } // Get typo tolerance typo, err := index.GetTypoTolerance() if err == nil { result["typoTolerance"] = typo } return result, nil } // 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 }