Upload files to "internal/engine"
This commit is contained in:
parent
dde2e7636b
commit
1d786e356d
398
internal/engine/search.go
Normal file
398
internal/engine/search.go
Normal 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()
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user