futriis/internal/engine/search.go

399 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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