Upload files to "internal/engine"

This commit is contained in:
Григорий Сафронов 2026-03-01 00:48:40 +00:00
parent dde2e7636b
commit 1d786e356d

398
internal/engine/search.go Normal file
View File

@ -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()
}