// /futriis/internal/engine/search.go // Пакет engine реализует параллельный поиск по данным СУБД // Обеспечивает конкурентный поиск по нескольким тапплам, слайсам и кортежам // с использованием горутин и каналов для максимальной производительности package engine import ( "context" "fmt" "reflect" "strings" "sync" "time" "futriis/pkg/types" "futriis/pkg/utils" ) // SearchResult представляет результат поиска type SearchResult struct { TappleName string SliceName string TupleID string Fields map[string]interface{} Score float64 // Релевантность результата } // SearchQuery представляет параметры поиска type SearchQuery struct { Query string // Поисковый запрос Fields []string // Поля для поиска (пусто - все поля) Tapples []string // Тапплы для поиска (пусто - все) Slices []string // Слайсы для поиска (пусто - все) CaseSensitive bool // Учитывать регистр Fuzzy bool // Нечеткий поиск MaxResults int // Максимальное количество результатов Timeout time.Duration // Таймаут поиска Concurrency int // Количество параллельных рабочих } // SearchEngine выполняет параллельный поиск по данным type SearchEngine struct { engine *Engine } // NewSearchEngine создает новый поисковый движок func NewSearchEngine(engine *Engine) *SearchEngine { return &SearchEngine{ engine: engine, } } // Search выполняет параллельный поиск по данным func (se *SearchEngine) Search(ctx context.Context, query SearchQuery) ([]SearchResult, error) { // Устанавливаем значения по умолчанию if query.MaxResults <= 0 { query.MaxResults = 100 } if query.Concurrency <= 0 { query.Concurrency = 10 } if query.Timeout <= 0 { query.Timeout = 30 * time.Second } // Создаем контекст с таймаутом ctx, cancel := context.WithTimeout(ctx, query.Timeout) defer cancel() // Получаем все тапплы allTapples := se.engine.storage.GetTappleManager().GetAllTapples() // Фильтруем тапплы по запросу tapplesToSearch := make([]*types.Tapple, 0) if len(query.Tapples) == 0 { // Ищем по всем тапплам for _, tapple := range allTapples { tapplesToSearch = append(tapplesToSearch, tapple) } } else { // Ищем только по указанным тапплам for _, tappleName := range query.Tapples { if tapple, exists := allTapples[tappleName]; exists { tapplesToSearch = append(tapplesToSearch, tapple) } } } if len(tapplesToSearch) == 0 { return nil, fmt.Errorf("no tapples to search") } // Создаем каналы для результатов и заданий jobs := make(chan searchJob, len(tapplesToSearch)*10) results := make(chan SearchResult, query.MaxResults) done := make(chan bool) // Счетчик для отслеживания завершения всех горутин var wg sync.WaitGroup // Запускаем воркеры for i := 0; i < query.Concurrency; i++ { wg.Add(1) go se.searchWorker(ctx, i, jobs, results, &wg, query) } // Горутина для закрытия results после завершения всех воркеров go func() { wg.Wait() close(results) done <- true }() // Отправляем задания на поиск go se.dispatchJobs(ctx, tapplesToSearch, query, jobs) // Собираем результаты finalResults := make([]SearchResult, 0, query.MaxResults) for result := range results { finalResults = append(finalResults, result) // Если достигли максимума, отменяем контекст if len(finalResults) >= query.MaxResults { cancel() break } } // Ждем завершения всех горутин <-done return finalResults, nil } // searchJob представляет задание на поиск в одном слайсе type searchJob struct { TappleName string SliceName string Slice *types.Slice } // searchWorker выполняет поиск в слайсе func (se *SearchEngine) searchWorker(ctx context.Context, id int, jobs <-chan searchJob, results chan<- SearchResult, wg *sync.WaitGroup, query SearchQuery) { defer wg.Done() for { select { case <-ctx.Done(): return case job, ok := <-jobs: if !ok { return } // Получаем все кортежи из слайса tuples, err := se.getAllTuplesFromSlice(job.Slice) if err != nil { continue } // Ищем в каждом кортеже for tupleID, tuple := range tuples { select { case <-ctx.Done(): return default: if se.matchTuple(tuple, query) { select { case results <- SearchResult{ TappleName: job.TappleName, SliceName: job.SliceName, TupleID: tupleID, Fields: tuple.Fields, Score: se.calculateScore(tuple, query), }: case <-ctx.Done(): return } } } } } } } // dispatchJobs распределяет задания по воркерам func (se *SearchEngine) dispatchJobs(ctx context.Context, tapples []*types.Tapple, query SearchQuery, jobs chan<- searchJob) { defer close(jobs) for _, tapple := range tapples { // Получаем все слайсы в таппле slices := se.getAllSlicesFromTapple(tapple) for sliceName, slice := range slices { // Фильтруем по слайсам если указано if len(query.Slices) > 0 { found := false for _, s := range query.Slices { if s == sliceName { found = true break } } if !found { continue } } select { case <-ctx.Done(): return case jobs <- searchJob{ TappleName: tapple.Name, SliceName: sliceName, Slice: slice, }: } } } } // matchTuple проверяет, соответствует ли кортеж поисковому запросу func (se *SearchEngine) matchTuple(tuple *types.Tuple, query SearchQuery) bool { if query.Query == "" { return true } queryStr := query.Query if !query.CaseSensitive { queryStr = strings.ToLower(queryStr) } // Определяем поля для поиска fieldsToSearch := make([]string, 0) if len(query.Fields) == 0 { // Ищем по всем полям for fieldName := range tuple.Fields { fieldsToSearch = append(fieldsToSearch, fieldName) } } else { fieldsToSearch = query.Fields } // Ищем в указанных полях for _, fieldName := range fieldsToSearch { if value, exists := tuple.Fields[fieldName]; exists { strValue := fmt.Sprint(value) if !query.CaseSensitive { strValue = strings.ToLower(strValue) } if query.Fuzzy { // Простая реализация нечеткого поиска (расстояние Левенштейна) if se.fuzzyMatch(strValue, queryStr) { return true } } else { // Точное вхождение if strings.Contains(strValue, queryStr) { return true } } } } return false } // fuzzyMatch проверяет нечеткое соответствие строк func (se *SearchEngine) fuzzyMatch(str, query string) bool { // Если запрос длиннее строки, то не подходит if len(query) > len(str) { return false } // Проверяем, содержит ли строка все символы запроса в правильном порядке // (упрощенная реализация для демонстрации) strIdx := 0 queryIdx := 0 for strIdx < len(str) && queryIdx < len(query) { if str[strIdx] == query[queryIdx] { queryIdx++ } strIdx++ } return queryIdx == len(query) } // calculateScore вычисляет релевантность результата func (se *SearchEngine) calculateScore(tuple *types.Tuple, query SearchQuery) float64 { if query.Query == "" { return 1.0 } score := 0.0 queryStr := query.Query if !query.CaseSensitive { queryStr = strings.ToLower(queryStr) } for _, value := range tuple.Fields { strValue := fmt.Sprint(value) if !query.CaseSensitive { strValue = strings.ToLower(strValue) } // Чем больше совпадений, тем выше score if strings.Contains(strValue, queryStr) { // Дополнительные баллы за точное совпадение if strValue == queryStr { score += 2.0 } else { // Баллы за частичное совпадение (пропорционально длине) score += float64(len(queryStr)) / float64(len(strValue)) } } } return score } // getAllTuplesFromSlice возвращает все кортежи из слайса func (se *SearchEngine) getAllTuplesFromSlice(slice *types.Slice) (map[string]*types.Tuple, error) { if slice == nil { return nil, fmt.Errorf("slice is nil") } v := reflect.ValueOf(slice).Elem() field := v.FieldByName("tuples") if !field.IsValid() || field.Kind() != reflect.Map { return nil, fmt.Errorf("cannot access tuples field in slice") } result := make(map[string]*types.Tuple) iter := field.MapRange() for iter.Next() { key := iter.Key().String() value := iter.Value().Interface() if tuple, ok := value.(*types.Tuple); ok { result[key] = tuple } } return result, nil } // getAllSlicesFromTapple возвращает все слайсы из таппла func (se *SearchEngine) getAllSlicesFromTapple(tapple *types.Tapple) map[string]*types.Slice { v := reflect.ValueOf(tapple).Elem() field := v.FieldByName("slices") if !field.IsValid() || field.Kind() != reflect.Map { return make(map[string]*types.Slice) } result := make(map[string]*types.Slice) iter := field.MapRange() for iter.Next() { key := iter.Key().String() value := iter.Value().Interface() if slice, ok := value.(*types.Slice); ok { result[key] = slice } } return result } // FormatSearchResults форматирует результаты поиска для вывода func FormatSearchResults(results []SearchResult, query string) string { if len(results) == 0 { return utils.ColorYellow + "No results found" + utils.ColorReset } var sb strings.Builder sb.WriteString(utils.ColorCyan + fmt.Sprintf("Search results for '%s' (%d found):\n", query, len(results)) + utils.ColorReset) for i, result := range results { sb.WriteString(fmt.Sprintf("\n %d. ", i+1)) sb.WriteString(utils.ColorGreen + result.TappleName + utils.ColorReset) sb.WriteString("." + utils.ColorYellow + result.SliceName + utils.ColorReset) sb.WriteString(":" + utils.ColorPromptCode + result.TupleID + utils.ColorReset) if len(result.Fields) > 0 { sb.WriteString("\n Fields:\n") for k, v := range result.Fields { sb.WriteString(fmt.Sprintf(" %s: %v\n", k, v)) } } } return sb.String() }