diff --git a/internal/engine/search.go b/internal/engine/search.go new file mode 100644 index 0000000..266d87f --- /dev/null +++ b/internal/engine/search.go @@ -0,0 +1,398 @@ +// /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() +}