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 }