eintopf.info@v0.13.16/service/search/search_test.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 search_test
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"strconv"
    22  	"sync"
    23  	"testing"
    24  	"time"
    25  
    26  	"eintopf.info/service/search"
    27  	"eintopf.info/test"
    28  )
    29  
    30  type doc struct {
    31  	ID   string
    32  	A    string    `json:"a"`
    33  	Int  int       `json:"b"`
    34  	C    string    `json:"c"`
    35  	D    time.Time `json:"d"`
    36  	E    string    `json:"e"`
    37  	Bool bool      `json:"bool"`
    38  }
    39  
    40  func (d *doc) Identifier() string {
    41  	return d.ID
    42  }
    43  
    44  func (d *doc) Type() string {
    45  	return "mytype"
    46  }
    47  
    48  func (d *doc) QueryText() string {
    49  	if d.A == "" {
    50  		return "a"
    51  	}
    52  	return d.A
    53  }
    54  
    55  func (d *doc) SearchFields() map[string]interface{} {
    56  	return map[string]interface{}{
    57  		"id":   d.ID,
    58  		"a":    d.A,
    59  		"Int":  d.Int,
    60  		"c":    d.C,
    61  		"d":    d.D,
    62  		"e":    d.E,
    63  		"bool": d.Bool,
    64  	}
    65  }
    66  
    67  func TestSearchQuery(t *testing.T) {
    68  	testSearchTestcases(t, []testcase{
    69  		{
    70  			name:     "returns nothing without a match",
    71  			docs:     []doc{{ID: "1", A: "something", Int: 42}},
    72  			query:    "nothing",
    73  			wantHits: []doc{},
    74  		}, {
    75  			name:     "empty query returns all documents",
    76  			docs:     []doc{{ID: "1", A: "something", Int: 42}, {ID: "2", A: "nothing", Int: 42}},
    77  			query:    "",
    78  			wantHits: []doc{{ID: "1", A: "something", Int: 42}, {ID: "2", A: "nothing", Int: 42}},
    79  		}, {
    80  			name:     "matches on a word",
    81  			docs:     []doc{{ID: "1", A: "something", Int: 42}},
    82  			query:    "something",
    83  			opts:     nil,
    84  			wantHits: []doc{{ID: "1", A: "something", Int: 42}},
    85  		}, {
    86  			name:     "matches on part of the text",
    87  			docs:     []doc{{ID: "1", A: "from to", Int: 42}},
    88  			query:    "from",
    89  			opts:     nil,
    90  			wantHits: []doc{{ID: "1", A: "from to", Int: 42}},
    91  		}, {
    92  			name:     "matches on multiple words in the query",
    93  			docs:     []doc{{ID: "1", A: "from to", Int: 42}},
    94  			query:    "from to",
    95  			opts:     nil,
    96  			wantHits: []doc{{ID: "1", A: "from to", Int: 42}},
    97  		}, {
    98  			name:     "Fahradtaschen -> Fahradtasche",
    99  			docs:     []doc{{ID: "1", A: "Fahradtaschen", Int: 42}},
   100  			query:    "fahradtasche",
   101  			opts:     nil,
   102  			wantHits: []doc{{ID: "1", A: "Fahradtaschen", Int: 42}},
   103  		}, {
   104  			name: "finds multple docs",
   105  			docs: []doc{
   106  				{ID: "1", A: "something", Int: 42},
   107  				{ID: "2", A: "something", Int: 43},
   108  			},
   109  			query: "something",
   110  			opts:  &search.Options{Sort: "id"},
   111  			wantHits: []doc{
   112  				{ID: "1", A: "something", Int: 42},
   113  				{ID: "2", A: "something", Int: 43},
   114  			},
   115  		}, {
   116  			name: "doesn't always return everything",
   117  			docs: []doc{
   118  				{ID: "1", A: "something", Int: 42},
   119  				{ID: "2", A: "nothing", Int: 43},
   120  			},
   121  			query:    "something",
   122  			opts:     nil,
   123  			wantHits: []doc{{ID: "1", A: "something", Int: 42}},
   124  		},
   125  	})
   126  }
   127  
   128  func TestSearchSort(t *testing.T) {
   129  	testSearchTestcases(t, []testcase{
   130  		{
   131  			name:     "sort number field",
   132  			docs:     []doc{{ID: "2", Int: 2}, {ID: "1", Int: 1}},
   133  			query:    "",
   134  			opts:     &search.Options{Sort: "Int"},
   135  			wantHits: []doc{{ID: "1", Int: 1}, {ID: "2", Int: 2}},
   136  		},
   137  		{
   138  			name:     "sort string field",
   139  			docs:     []doc{{ID: "1", A: "foo"}, {ID: "2", A: "bar"}},
   140  			query:    "",
   141  			opts:     &search.Options{Sort: "a"},
   142  			wantHits: []doc{{ID: "2", A: "bar"}, {ID: "1", A: "foo"}},
   143  		},
   144  		{
   145  			name:     "sort string field (ignores upper/lower case)",
   146  			docs:     []doc{{ID: "1", A: "Foo"}, {ID: "2", A: "bar"}},
   147  			query:    "",
   148  			opts:     &search.Options{Sort: "a"},
   149  			wantHits: []doc{{ID: "2", A: "bar"}, {ID: "1", A: "Foo"}},
   150  		},
   151  		{
   152  			name: "sort time field",
   153  			docs: []doc{
   154  				{ID: "1", D: time.Date(2022, 10, 20, 10, 10, 10, 10, time.UTC)},
   155  				{ID: "2", D: time.Date(2022, 10, 10, 10, 10, 10, 10, time.UTC)},
   156  			},
   157  			query: "",
   158  			opts:  &search.Options{Sort: "d"},
   159  			wantHits: []doc{
   160  				{ID: "2", D: time.Date(2022, 10, 10, 10, 10, 10, 10, time.UTC)},
   161  				{ID: "1", D: time.Date(2022, 10, 20, 10, 10, 10, 10, time.UTC)},
   162  			},
   163  		},
   164  		{
   165  			name:     "sort descending",
   166  			docs:     []doc{{ID: "2", Int: 2}, {ID: "1", Int: 1}},
   167  			query:    "",
   168  			opts:     &search.Options{Sort: "b", SortDescending: true},
   169  			wantHits: []doc{{ID: "2", Int: 2}, {ID: "1", Int: 1}},
   170  		},
   171  	})
   172  }
   173  
   174  func TestSearchPagination(t *testing.T) {
   175  	three := uint64(3)
   176  	testSearchTestcases(t, []testcase{
   177  		{
   178  			name:      "NoPagination",
   179  			docs:      []doc{{ID: "1"}, {ID: "2"}, {ID: "3"}},
   180  			query:     "",
   181  			opts:      &search.Options{Sort: "id", Page: 0, PageSize: 0},
   182  			wantHits:  []doc{{ID: "1"}, {ID: "2"}, {ID: "3"}},
   183  			wantTotal: &three,
   184  		}, {
   185  			name:      "FirstPage",
   186  			docs:      []doc{{ID: "1"}, {ID: "2"}, {ID: "3"}},
   187  			query:     "",
   188  			opts:      &search.Options{Sort: "id", Page: 0, PageSize: 2},
   189  			wantHits:  []doc{{ID: "1"}, {ID: "2"}},
   190  			wantTotal: &three,
   191  		}, {
   192  			name:      "SecondPage",
   193  			docs:      []doc{{ID: "1"}, {ID: "2"}, {ID: "3"}},
   194  			query:     "",
   195  			opts:      &search.Options{Sort: "id", Page: 1, PageSize: 2},
   196  			wantHits:  []doc{{ID: "3"}},
   197  			wantTotal: &three,
   198  		},
   199  	})
   200  }
   201  
   202  func TestSearchFilterTerms(t *testing.T) {
   203  	testSearchTestcases(t, []testcase{
   204  		{
   205  			name: "String1",
   206  			docs: []doc{{ID: "1", C: "foo"}, {ID: "2", C: "bar"}},
   207  			opts: &search.Options{Filters: []search.Filter{
   208  				&search.TermsFilter{
   209  					Field: "c",
   210  					Terms: []string{"foo"},
   211  				},
   212  			}},
   213  			wantHits: []doc{{ID: "1", C: "foo"}},
   214  		}, {
   215  			name: "String2",
   216  			docs: []doc{{ID: "1", C: "foo bar"}, {ID: "2", C: "bar"}},
   217  			opts: &search.Options{Filters: []search.Filter{
   218  				&search.TermsFilter{
   219  					Field: "c",
   220  					Terms: []string{"bar"},
   221  				},
   222  			}},
   223  			wantHits: []doc{{ID: "2", C: "bar"}, {ID: "1", C: "foo bar"}},
   224  		}, {
   225  			name: "UUID",
   226  			docs: []doc{{ID: "aee609c7-4fea-4a1c-8acf-0799a3bebadd", C: "foo"}, {ID: "39396d6e-fe59-4fea-899a-387d716a8968", C: "bar"}},
   227  			opts: &search.Options{Filters: []search.Filter{
   228  				&search.TermsFilter{
   229  					Field: "id",
   230  					Terms: []string{"aee609c7-4fea-4a1c-8acf-0799a3bebadd"},
   231  				},
   232  			}},
   233  			wantHits: []doc{{ID: "aee609c7-4fea-4a1c-8acf-0799a3bebadd", C: "foo"}},
   234  		}, {
   235  			name: "Integer",
   236  			docs: []doc{{ID: "1", Int: 1}, {ID: "2", Int: 2}},
   237  			opts: &search.Options{Filters: []search.Filter{
   238  				&search.TermsFilter{
   239  					Field: "Int",
   240  					Terms: []string{"1"},
   241  				},
   242  			}},
   243  			wantHits: []doc{}, // can't find int via terms filter
   244  		}, {
   245  			name: "MultipleTerms",
   246  			docs: []doc{
   247  				{ID: "1", C: "foo"},
   248  				{ID: "2", C: "bar"},
   249  				{ID: "3", C: "baz"},
   250  			},
   251  			opts: &search.Options{Sort: "c", Filters: []search.Filter{
   252  				&search.TermsFilter{
   253  					Field: "c",
   254  					Terms: []string{"foo", "baz"},
   255  				},
   256  			}},
   257  			wantHits: []doc{
   258  				{ID: "3", C: "baz"},
   259  				{ID: "1", C: "foo"},
   260  			},
   261  		},
   262  	})
   263  }
   264  
   265  func TestSearchFilterDateRange(t *testing.T) {
   266  	testSearchTestcases(t, []testcase{
   267  		{
   268  			name: "DateRangeFilter",
   269  			docs: []doc{
   270  				{ID: "1", D: time.Date(2022, 4, 22, 12, 0, 0, 0, time.UTC)},
   271  				{ID: "2", D: time.Date(2022, 4, 22, 14, 0, 0, 0, time.UTC)},
   272  			},
   273  			opts: &search.Options{Filters: []search.Filter{
   274  				&search.DateRangeFilter{
   275  					Field: "d",
   276  					Min:   time.Date(2022, 4, 22, 11, 0, 0, 0, time.UTC),
   277  					Max:   time.Date(2022, 4, 22, 13, 0, 0, 0, time.UTC),
   278  				},
   279  			}},
   280  			wantHits: []doc{{ID: "1", D: time.Date(2022, 4, 22, 12, 0, 0, 0, time.UTC)}},
   281  		}, {
   282  			name: "DateRangeFilter2",
   283  			docs: []doc{
   284  				{ID: "1", D: now().Add(1 * time.Hour)},
   285  				{ID: "2", D: now().Add(-1 * time.Hour)},
   286  			},
   287  			opts: &search.Options{Filters: []search.Filter{
   288  				&search.DateRangeFilter{
   289  					Field: "d",
   290  					Min:   now(),
   291  				},
   292  			}},
   293  			wantHits: []doc{{ID: "1", D: now().Add(1 * time.Hour)}},
   294  		},
   295  	})
   296  }
   297  
   298  func TestSearchFilterBool(t *testing.T) {
   299  	testSearchTestcases(t, []testcase{
   300  		{
   301  			name: "True",
   302  			docs: []doc{
   303  				{ID: "1", Bool: true},
   304  				{ID: "2", Bool: false},
   305  				{ID: "3", Bool: true},
   306  			},
   307  			opts: &search.Options{Sort: "id", Filters: []search.Filter{
   308  				&search.BoolFilter{
   309  					Field: "bool",
   310  					Value: true,
   311  				},
   312  			}},
   313  			wantHits: []doc{
   314  				{ID: "1", Bool: true},
   315  				{ID: "3", Bool: true},
   316  			},
   317  		}, {
   318  			name: "False",
   319  			docs: []doc{
   320  				{ID: "1", Bool: true},
   321  				{ID: "2", Bool: false},
   322  				{ID: "3", Bool: true},
   323  			},
   324  			opts: &search.Options{Sort: "id", Filters: []search.Filter{
   325  				&search.BoolFilter{
   326  					Field: "bool",
   327  					Value: false,
   328  				},
   329  			}},
   330  			wantHits: []doc{
   331  				{ID: "2", Bool: false},
   332  			},
   333  		},
   334  	})
   335  }
   336  
   337  func TestSearchFilterNumericRange(t *testing.T) {
   338  	testSearchTestcases(t, []testcase{
   339  		{
   340  			name: "Min",
   341  			docs: []doc{
   342  				{ID: "1", Int: 1},
   343  				{ID: "2", Int: 2},
   344  				{ID: "3", Int: 3},
   345  			},
   346  			opts: &search.Options{Sort: "id", Filters: []search.Filter{
   347  				&search.NumericRangeFilter{
   348  					Field: "Int",
   349  					Min:   f64ptr(2),
   350  				},
   351  			}},
   352  			wantHits: []doc{
   353  				{ID: "2", Int: 2},
   354  				{ID: "3", Int: 3},
   355  			},
   356  		},
   357  	})
   358  }
   359  
   360  func TestSearchFilterMultiple(t *testing.T) {
   361  	testSearchTestcases(t, []testcase{
   362  		{
   363  			name: "MultipleFilters",
   364  			docs: []doc{
   365  				{ID: "1", A: "f", C: "foo"},
   366  				{ID: "2", A: "b", C: "bar"},
   367  				{ID: "3", A: "a", C: "baz"},
   368  			},
   369  			opts: &search.Options{Filters: []search.Filter{
   370  				&search.TermsFilter{
   371  					Field: "a",
   372  					Terms: []string{"f"},
   373  				},
   374  				&search.TermsFilter{
   375  					Field: "c",
   376  					Terms: []string{"foo"},
   377  				},
   378  			}},
   379  			wantHits: []doc{
   380  				{ID: "1", A: "f", C: "foo"},
   381  			},
   382  		},
   383  	})
   384  }
   385  
   386  func TestSearchAggregation(t *testing.T) {
   387  	time1 := time.Date(2019, 1, 12, 20, 0, 0, 0, time.UTC)
   388  	time2 := time.Date(2019, 2, 12, 20, 0, 0, 0, time.UTC)
   389  	testSearchTestcases(t, []testcase{
   390  		{
   391  			name: "Terms",
   392  			docs: []doc{
   393  				{ID: "1", C: "foo"},
   394  				{ID: "2", C: "bar"},
   395  				{ID: "3", C: "bar"},
   396  			},
   397  			opts: &search.Options{Aggregations: map[string]search.Aggregation{
   398  				"foo": {
   399  					Field: "c",
   400  					Type:  search.TermsAggregation,
   401  				},
   402  			}},
   403  			wantBuckets: map[string]search.Bucket{
   404  				"foo": search.TermsBucket([]search.Term{
   405  					{Term: "bar", Count: 2},
   406  					{Term: "foo", Count: 1},
   407  				}),
   408  			},
   409  		}, {
   410  			name: "DateRange",
   411  			docs: []doc{{ID: "1", D: time1}, {ID: "2", D: time2}},
   412  			opts: &search.Options{Aggregations: map[string]search.Aggregation{
   413  				"foo": {
   414  					Field: "d",
   415  					Type:  search.DateRangeAggregation,
   416  				},
   417  			}},
   418  			wantBuckets: map[string]search.Bucket{
   419  				"foo": search.DateRangeBucket{
   420  					Min: time1,
   421  					Max: time2,
   422  				},
   423  			},
   424  		}, {
   425  			name: "WithTermsFilter",
   426  			docs: []doc{{ID: "1", A: "f", C: "foo"}, {ID: "2", A: "b", C: "bar"}},
   427  			opts: &search.Options{Aggregations: map[string]search.Aggregation{
   428  				"foo": {
   429  					Field: "c",
   430  					Type:  search.TermsAggregation,
   431  					Filters: []search.Filter{
   432  						&search.TermsFilter{
   433  							Field: "a",
   434  							Terms: []string{"f"},
   435  						},
   436  					},
   437  				},
   438  			}},
   439  			wantBuckets: map[string]search.Bucket{
   440  				"foo": search.TermsBucket([]search.Term{
   441  					{Term: "foo", Count: 1},
   442  				}),
   443  			},
   444  		},
   445  	})
   446  }
   447  
   448  type testcase struct {
   449  	name        string
   450  	docs        []doc
   451  	query       string
   452  	opts        *search.Options
   453  	wantHits    []doc
   454  	wantBuckets map[string]search.Bucket
   455  	wantTotal   *uint64
   456  }
   457  
   458  func testSearchTestcases(t *testing.T, testcases []testcase) {
   459  	t.Helper()
   460  	for _, tc := range testcases {
   461  		t.Run(tc.name, func(tt *testing.T) {
   462  			index, cleanupIndex := test.CreateBleveTestIndex(tc.name)
   463  			tt.Cleanup(cleanupIndex)
   464  			s, err := search.NewService(index, time.Second, 1, 1, time.UTC)
   465  			if err != nil {
   466  				tt.Errorf("search.NewService unexpectedly failed: %s", err)
   467  			}
   468  			defer s.Stop()
   469  
   470  			for _, doc := range tc.docs {
   471  				err = s.Index(&doc)
   472  				if err != nil {
   473  					tt.Errorf("Index() unexpectedly failed: %s", err)
   474  				}
   475  			}
   476  
   477  			if tc.opts == nil {
   478  				tc.opts = &search.Options{}
   479  			}
   480  			tc.opts.Query = tc.query
   481  			result, err := s.Search(context.Background(), tc.opts)
   482  			if err != nil {
   483  				tt.Errorf("Search(%s, opts) unexpectedly failed: %s", tc.query, err)
   484  			}
   485  
   486  			if tc.wantHits != nil {
   487  				hits := []doc{}
   488  				for _, rawHit := range result.Hits {
   489  					var hit doc
   490  					err = rawHit.Unmarshal(&hit)
   491  					if err != nil {
   492  						tt.Errorf("hit.Unmarshal unexpectedly failed: %s", err)
   493  					}
   494  					hits = append(hits, hit)
   495  				}
   496  				if fmt.Sprint(hits) != fmt.Sprint(tc.wantHits) {
   497  					tt.Errorf("hits don't match:\nwant: %v\ngot:  %v", tc.wantHits, hits)
   498  				}
   499  			}
   500  
   501  			if tc.wantBuckets != nil {
   502  				if fmt.Sprint(result.Buckets) != fmt.Sprint(tc.wantBuckets) {
   503  					tt.Errorf("buckets don't match: \nwant: %v\ngot:  %v", tc.wantBuckets, result.Buckets)
   504  				}
   505  			}
   506  
   507  			if tc.wantTotal != nil {
   508  				if *tc.wantTotal != result.Total {
   509  					tt.Errorf("total don't match: want %d, got %d", &tc.wantTotal, result.Total)
   510  				}
   511  			}
   512  		})
   513  	}
   514  }
   515  
   516  func TestIndexConcurrently(t *testing.T) {
   517  	index, cleanupIndex := test.CreateBleveTestIndex("TestIndexConcurrently")
   518  	t.Cleanup(cleanupIndex)
   519  	s, err := search.NewService(index, time.Second, 1, 1, time.UTC)
   520  	if err != nil {
   521  		t.Fatalf("search.NewService() failed unexpectedly: %s", err)
   522  	}
   523  	defer s.Stop()
   524  
   525  	wg := sync.WaitGroup{}
   526  
   527  	for _, d := range makeDocs(10) {
   528  		dd := &d
   529  		wg.Add(1)
   530  		go func(i search.Indexable) {
   531  			s.Index(i)
   532  			wg.Done()
   533  		}(dd)
   534  	}
   535  
   536  	wg.Wait()
   537  }
   538  
   539  func TestSearchServiceWithExistingIndex(t *testing.T) {
   540  	index, cleanupIndex := test.CreateBleveTestIndex("TestSearchServiceWithExistingIndex")
   541  	t.Cleanup(cleanupIndex)
   542  
   543  	s, err := search.NewService(index, time.Second, 1, 1, time.UTC)
   544  	if err != nil {
   545  		t.Fatalf("search.NewService() failed unexpectedly: %s", err)
   546  	}
   547  	s.Stop()
   548  
   549  	s, err = search.NewService(index, time.Second, 1, 1, time.UTC)
   550  	if err != nil {
   551  		t.Fatalf("search.NewService() failed with existing index: %s", err)
   552  	}
   553  	s.Stop()
   554  }
   555  
   556  func TestSearchCancel(t *testing.T) {
   557  	index, cleanupIndex := test.CreateBleveTestIndex("TestSearchCancel")
   558  	t.Cleanup(cleanupIndex)
   559  
   560  	s, err := search.NewService(index, time.Second*10, 1, 1, time.UTC)
   561  	if err != nil {
   562  		t.Fatalf("search.NewService() failed unexpectedly: %s", err)
   563  	}
   564  	t.Cleanup(s.Stop)
   565  	// Add documents to the index, such that a search is not instant.
   566  	for i := 0; i < 100; i++ {
   567  		s.Index(&doc{ID: fmt.Sprintf("%d", i), A: "f", C: "foo"})
   568  	}
   569  
   570  	ctx, cancel := context.WithCancel(context.Background())
   571  	go func() { cancel() }()
   572  	_, err = s.Search(ctx, nil)
   573  	if err == nil || err.Error() != "context canceled" {
   574  		t.Errorf("err should be 'context canceled', got: %s", err)
   575  	}
   576  }
   577  
   578  func TestSearchCancelTimeout(t *testing.T) {
   579  	index, cleanupIndex := test.CreateBleveTestIndex("TestSearchCancelTimeout")
   580  	t.Cleanup(cleanupIndex)
   581  
   582  	s, err := search.NewService(index, time.Nanosecond, 1, 1, time.UTC)
   583  	if err != nil {
   584  		t.Fatalf("search.NewService() failed unexpectedly: %s", err)
   585  	}
   586  	t.Cleanup(s.Stop)
   587  
   588  	_, err = s.Search(context.Background(), nil)
   589  	if err == nil || err.Error() != "context deadline exceeded" {
   590  		t.Errorf("err should be 'context deadline exceeded', got: %s", err)
   591  	}
   592  }
   593  
   594  func makeDocs(count int) []doc {
   595  	docs := make([]doc, 0, count)
   596  	for i := 0; i < count; i++ {
   597  		docs = append(docs, doc{
   598  			A: "djasd",
   599  			D: time.Unix(0, 0),
   600  		})
   601  	}
   602  	return docs
   603  }
   604  
   605  func makeAggregations(count int) map[string]search.Aggregation {
   606  	aggregations := make(map[string]search.Aggregation)
   607  	for i := 0; i < count; i++ {
   608  		aggregations[strconv.Itoa(i)+"_terms"] = search.Aggregation{
   609  			Type:  search.TermsAggregation,
   610  			Field: "a",
   611  		}
   612  		aggregations[strconv.Itoa(i)+"_daterange"] = search.Aggregation{
   613  			Type:  search.DateRangeAggregation,
   614  			Field: "d",
   615  		}
   616  	}
   617  	return aggregations
   618  }
   619  
   620  func BenchmarkSearch(b *testing.B) {
   621  	benchcases := []struct {
   622  		name  string
   623  		docs  []doc
   624  		query string
   625  		opts  *search.Options
   626  	}{
   627  		{
   628  			name:  "Normal",
   629  			docs:  makeDocs(1000),
   630  			query: "",
   631  		}, {
   632  			name:  "AggregationSmall",
   633  			docs:  makeDocs(100),
   634  			query: "",
   635  			opts: &search.Options{
   636  				Aggregations: makeAggregations(2),
   637  			},
   638  		}, {
   639  			name:  "AggregationBig",
   640  			docs:  makeDocs(100),
   641  			query: "",
   642  			opts: &search.Options{
   643  				Aggregations: makeAggregations(100),
   644  			},
   645  		},
   646  	}
   647  
   648  	for _, bc := range benchcases {
   649  		b.Run(bc.name, func(bb *testing.B) {
   650  			index, cleanupIndex := test.CreateBleveTestIndex(bc.name)
   651  			bb.Cleanup(cleanupIndex)
   652  
   653  			s, err := search.NewService(index, time.Second, 1, 1, time.UTC)
   654  			if err != nil {
   655  				bb.Fatalf("search.NewService() failed unexpectedly: %s", err)
   656  			}
   657  			defer s.Stop()
   658  
   659  			for _, doc := range bc.docs {
   660  				err = s.Index(&doc)
   661  				if err != nil {
   662  					bb.Fatalf("Index() unexpectedly failed: %s", err)
   663  				}
   664  			}
   665  
   666  			bb.ResetTimer()
   667  			for n := 0; n < bb.N; n++ {
   668  				bc.opts.Query = bc.query
   669  				_, err := s.Search(context.Background(), bc.opts)
   670  				if err != nil {
   671  					bb.Fatalf("Search(%s, opts) unexpectedly failed: %s", "foo", err)
   672  				}
   673  			}
   674  		})
   675  	}
   676  }
   677  
   678  func now() time.Time {
   679  	n := time.Now()
   680  	return time.Date(n.Year(), n.Month(), n.Hour(), n.Minute(), 0, 0, 0, time.UTC)
   681  }
   682  
   683  func f64ptr(f float64) *float64 {
   684  	return &f
   685  }