399 lines
11 KiB
Go
399 lines
11 KiB
Go
// /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()
|
||
}
|