Files
b2b/app/service/meiliService/meiliService.go
2026-03-30 01:02:02 +02:00

346 lines
9.8 KiB
Go

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().MailiSearch.ServerURL,
meilisearch.WithAPIKey(config.Get().MailiSearch.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
}