eintopf.info@v0.13.16/service/eventsearch/eventsearch.go (about)

     1  // Copyright (C) 2022 The Eintopf authors
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    15  
    16  package eventsearch
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"log"
    22  	"sync"
    23  	"time"
    24  
    25  	"eintopf.info/service/event"
    26  	"eintopf.info/service/group"
    27  	"eintopf.info/service/oqueue"
    28  	"eintopf.info/service/place"
    29  	"eintopf.info/service/revent"
    30  	"eintopf.info/service/search"
    31  )
    32  
    33  // -go:generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/eventsearch Service --output=../../internal/mock/eventsearch_service.go --package=mock --mock-name=EventSearchService
    34  type Service interface {
    35  	// Search makes a search in the search database for events. The result can
    36  	// be modified using Options.
    37  	Search(ctx context.Context, opts Options) (*Result, error)
    38  	// Index takes one or many event and adds them to the search index.
    39  	Index(e ...*event.Event) error
    40  	// LastModified returns the time the index was last modified.
    41  	LastModified() time.Time
    42  	// Reindex makes sure, that the state of the search index is the same as in the
    43  	// event database.
    44  	Reindex(ctx context.Context) error
    45  }
    46  
    47  // Options is a set of options, that can be used to modify a search result.
    48  // swagger:parameters eventsearch eventsearch
    49  type Options struct {
    50  	// Query is the search query used for a text search.
    51  	Query string `json:"query"`
    52  
    53  	// Sort is the field, that should be sorted by.
    54  	// When left empty, the default sorting is used.
    55  	Sort string `json:"sort"`
    56  
    57  	// SortDescending defines the sort order.
    58  	SortDescending bool `json:"sortAscending"`
    59  
    60  	// Page is the current page.
    61  	Page int `json:"page"`
    62  
    63  	// PageSize defines the number of events returned per page.
    64  	//
    65  	// PageSize is infinite when set to 0.
    66  	PageSize int `json:"pageSize"`
    67  
    68  	// Filters is a list of filters, that reduce the search result. All filters
    69  	// are combined with AND logic in addition with the search query.
    70  	Filters Filters `json:"filters"`
    71  
    72  	// Aggregations is a map of aggregations. Each map key corresponds to a
    73  	// bucket of aggregated items with the same ke.
    74  	Aggregations Aggregations `json:"aggregations"`
    75  
    76  	// Omit takes a list of fields, tha will be removed from all response
    77  	// objects recursively.
    78  	Omit []string `json:"omit"`
    79  }
    80  
    81  // Filter is an event filter, that can reduce an event search.
    82  //
    83  //go:generate go run ../../scripts/gen/go/searchfilter/main.go -modelPath eventdocument.yaml -path filter.go -packageName eventsearch
    84  type Filter interface {
    85  	// SearchFilter converts the the filter to a search.Filter.
    86  	SearchFilter() search.Filter
    87  }
    88  
    89  // Filters is a list of filters. They can be converted to a list of search.Filter.
    90  type Filters []Filter
    91  
    92  // SearchFilters converts the list of filters to a list of search filters.
    93  func (s Filters) SearchFilters() []search.Filter {
    94  	searchFilters := make([]search.Filter, 0, len(s))
    95  	for _, f := range s {
    96  		searchFilters = append(searchFilters, f.SearchFilter())
    97  	}
    98  	return searchFilters
    99  }
   100  
   101  // Aggregation is an event aggregation.
   102  //
   103  //go:generate go run ../../scripts/gen/go/searchaggregation/main.go -modelPath eventdocument.yaml -path aggregation.go -packageName eventsearch
   104  type Aggregation interface {
   105  	// SearchAggregation converts the aggregation to a search.Aggregation.
   106  	SearchAggregation() search.Aggregation
   107  }
   108  
   109  // Aggregations maps a name to a aggregation. This allows multiple
   110  // aggregations on the same field with different filters.
   111  type Aggregations map[string]Aggregation
   112  
   113  func (s Aggregations) SearchAggregations() map[string]search.Aggregation {
   114  	searchAggregations := make(map[string]search.Aggregation, len(s))
   115  	for k, v := range s {
   116  		searchAggregations[k] = v.SearchAggregation()
   117  	}
   118  	return searchAggregations
   119  }
   120  
   121  // Result contains an event search result.
   122  // swagger:response eventsearchResult
   123  type Result struct {
   124  	// Events are the events found for the current pagination.
   125  	Events []*EventDocument `json:"events"`
   126  
   127  	// Total is the total number of events in the search result.
   128  	// It is independet of the current pagination.
   129  	Total uint64 `json:"total"`
   130  
   131  	// Buckets is a set of aggregation buckets.
   132  	// The map key corresponds to aggregation name.
   133  	Buckets map[string]search.Bucket `json:"buckets"`
   134  }
   135  
   136  // ServiceImpl is an implementation of Service.
   137  type ServiceImpl struct {
   138  	searchService         search.Service
   139  	eventService          event.Storer
   140  	groupService          group.Service
   141  	placeService          place.Service
   142  	repeatingEventService revent.Service
   143  
   144  	lastModified  time.Time
   145  	lastModifiedM *sync.Mutex
   146  }
   147  
   148  // NewService returns a new ServiceImpl.
   149  func NewService(
   150  	searchService search.Service,
   151  	eventService event.Storer,
   152  	groupService group.Service,
   153  	placeService place.Service,
   154  	repeatingEventService revent.Service,
   155  ) *ServiceImpl {
   156  	s := &ServiceImpl{
   157  		searchService:         searchService,
   158  		eventService:          eventService,
   159  		groupService:          groupService,
   160  		placeService:          placeService,
   161  		repeatingEventService: repeatingEventService,
   162  
   163  		lastModified:  time.Now(),
   164  		lastModifiedM: &sync.Mutex{},
   165  	}
   166  
   167  	return s
   168  }
   169  
   170  func (s ServiceImpl) Search(ctx context.Context, opts Options) (*Result, error) {
   171  	searchOpts := &search.Options{
   172  		Query:          opts.Query,
   173  		Sort:           opts.Sort,
   174  		SortDescending: opts.SortDescending,
   175  		Page:           opts.Page,
   176  		PageSize:       opts.PageSize,
   177  		Filters: append(
   178  			opts.Filters.SearchFilters(),
   179  			// Always filter for type event.
   180  			&search.TermsFilter{Field: "type", Terms: []string{"event"}},
   181  		),
   182  		Aggregations: opts.Aggregations.SearchAggregations(),
   183  	}
   184  
   185  	rawResult, err := s.searchService.Search(ctx, searchOpts)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	result := &Result{
   190  		Total:   rawResult.Total,
   191  		Events:  []*EventDocument{},
   192  		Buckets: rawResult.Buckets,
   193  	}
   194  	result.Buckets = convertSearchBuckets(result.Buckets, opts.Aggregations)
   195  	for _, hit := range rawResult.Hits {
   196  		if hit.Type != "event" {
   197  			log.Println("eventsearch: hit is not of type event")
   198  			continue
   199  		}
   200  		e := &EventDocument{}
   201  		e.indexID = hit.ID
   202  		err := hit.Unmarshal(e)
   203  		if err != nil {
   204  			return nil, err
   205  		}
   206  		for _, omit := range opts.Omit {
   207  			switch omit {
   208  			case "description":
   209  				e.Description = ""
   210  				for _, c := range e.Children {
   211  					c.Description = ""
   212  				}
   213  				for _, o := range e.Organizers {
   214  					if o.Group != nil {
   215  						o.Group.Description = ""
   216  					}
   217  				}
   218  				if e.Location != nil && e.Location.Place != nil {
   219  					e.Location.Place.Description = ""
   220  				}
   221  				if e.RepeatingEvent != nil {
   222  					e.RepeatingEvent.Description = ""
   223  				}
   224  			}
   225  		}
   226  		result.Events = append(result.Events, e)
   227  	}
   228  
   229  	return result, nil
   230  }
   231  
   232  func (s *ServiceImpl) LastModified() time.Time {
   233  	s.lastModifiedM.Lock()
   234  	defer s.lastModifiedM.Unlock()
   235  
   236  	return s.lastModified
   237  }
   238  
   239  func (s *ServiceImpl) updateLastModified() {
   240  	s.lastModifiedM.Lock()
   241  	defer s.lastModifiedM.Unlock()
   242  
   243  	s.lastModified = time.Now()
   244  }
   245  
   246  // ConsumeOperation consumes an event operations of the type create, update and
   247  // delete. Depending on wether the model is searchable, it performs an index or
   248  // a delete operation on the search index.
   249  func (s *ServiceImpl) ConsumeOperation(op oqueue.Operation) error {
   250  	switch op := op.(type) {
   251  	case event.CreateOperation:
   252  		if op.Event.Indexable() {
   253  			defer s.updateLastModified()
   254  			return s.Index(op.Event)
   255  		}
   256  	case event.UpdateOperation:
   257  		if op.Event.Indexable() {
   258  			defer s.updateLastModified()
   259  			return s.Index(op.Event)
   260  		} else {
   261  			defer s.updateLastModified()
   262  			return s.searchService.Delete("event", op.Event.ID)
   263  		}
   264  	case event.DeleteOperation:
   265  		defer s.updateLastModified()
   266  		// Collect all events with the given id, to properly delete multiday
   267  		// events.
   268  		result, err := s.Search(context.Background(), Options{
   269  			Filters: Filters{IDFilter{ID: op.ID}},
   270  		})
   271  		if err != nil {
   272  			return fmt.Errorf("delete: find existing events: %s", err)
   273  		}
   274  		for _, e := range result.Events {
   275  			err = s.searchService.Delete("event", e.indexID)
   276  			if err != nil {
   277  				return fmt.Errorf("delete: %s", err)
   278  			}
   279  		}
   280  	}
   281  
   282  	return nil
   283  }
   284  
   285  // Reindex makes sure, that the state of the search index is the same as in the
   286  // event database.
   287  func (s *ServiceImpl) Reindex(ctx context.Context) error {
   288  	// Collect all event ids, that should be in the search index.
   289  	events, _, err := s.eventService.Find(ctx, nil)
   290  	if err != nil {
   291  		return err
   292  	}
   293  	indexableIDs := []string{}
   294  	indexableEvents := []*event.Event{}
   295  	for _, event := range events {
   296  		if event.Indexable() {
   297  			indexableEvents = append(indexableEvents, event)
   298  			indexableIDs = append(indexableIDs, event.ID)
   299  		}
   300  	}
   301  	s.Index(indexableEvents...)
   302  
   303  	// Collect all events, that should not be in the search index and remove them.
   304  	result, err := s.Search(ctx, Options{
   305  		Filters: []Filter{
   306  			&NotFilter{&IDsFilter{IDs: indexableIDs}},
   307  		},
   308  	})
   309  	if err != nil {
   310  		return err
   311  	}
   312  	if result.Total > 0 {
   313  		log.Printf("Reindex(events): deleting %d events from index\n", len(result.Events))
   314  		for _, e := range result.Events {
   315  			s.searchService.Delete("event", e.indexID)
   316  		}
   317  	}
   318  
   319  	s.lastModifiedM.Lock()
   320  	s.lastModified = time.Now()
   321  	s.lastModifiedM.Unlock()
   322  
   323  	return nil
   324  }