Skip to content

Commit f192906

Browse files
authored
GODRIVER-2859 Add search index management helpers (#1311)
1 parent 7de4d87 commit f192906

24 files changed

+2500
-4
lines changed

.evergreen/config.yml

+63
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,17 @@ functions:
688688
params:
689689
file: lb-expansion.yml
690690

691+
run-search-index-tests:
692+
- command: shell.exec
693+
type: test
694+
params:
695+
shell: "bash"
696+
working_dir: src/go.mongodb.org/mongo-driver
697+
script: |
698+
${PREPARE_SHELL}
699+
TEST_INDEX_URI="${TEST_INDEX_URI}" \
700+
make evg-test-search-index
701+
691702
stop-load-balancer:
692703
- command: shell.exec
693704
params:
@@ -2259,6 +2270,14 @@ tasks:
22592270
${PREPARE_SHELL}
22602271
./.evergreen/run-deployed-lambda-aws-tests.sh
22612272
2273+
- name: "test-search-index"
2274+
commands:
2275+
- func: "bootstrap-mongo-orchestration"
2276+
vars:
2277+
VERSION: "latest"
2278+
TOPOLOGY: "replica_set"
2279+
- func: "run-search-index-tests"
2280+
22622281
axes:
22632282
- id: version
22642283
display_name: MongoDB Version
@@ -2618,6 +2637,44 @@ task_groups:
26182637
tasks:
26192638
- test-aws-lambda-deployed
26202639

2640+
- name: test-search-index-task-group
2641+
setup_group:
2642+
- func: fetch-source
2643+
- func: prepare-resources
2644+
- command: subprocess.exec
2645+
params:
2646+
working_dir: src/go.mongodb.org/mongo-driver
2647+
binary: bash
2648+
add_expansions_to_env: true
2649+
env:
2650+
MONGODB_VERSION: "7.0"
2651+
args:
2652+
- ${DRIVERS_TOOLS}/.evergreen/atlas/setup-atlas-cluster.sh
2653+
- command: expansions.update
2654+
params:
2655+
file: src/go.mongodb.org/mongo-driver/atlas-expansion.yml
2656+
- command: shell.exec
2657+
params:
2658+
working_dir: src/go.mongodb.org/mongo-driver
2659+
shell: bash
2660+
script: |-
2661+
echo "TEST_INDEX_URI: ${MONGODB_URI}" > atlas-expansion.yml
2662+
- command: expansions.update
2663+
params:
2664+
file: src/go.mongodb.org/mongo-driver/atlas-expansion.yml
2665+
teardown_group:
2666+
- command: subprocess.exec
2667+
params:
2668+
working_dir: src/go.mongodb.org/mongo-driver
2669+
binary: bash
2670+
add_expansions_to_env: true
2671+
args:
2672+
- ${DRIVERS_TOOLS}/.evergreen/atlas/teardown-atlas-cluster.sh
2673+
setup_group_can_fail_task: true
2674+
setup_group_timeout_secs: 1800
2675+
tasks:
2676+
- test-search-index
2677+
26212678
buildvariants:
26222679
- name: static-analysis
26232680
display_name: "Static Analysis"
@@ -2766,6 +2823,12 @@ buildvariants:
27662823
tasks:
27672824
- test-aws-lambda-task-group
27682825

2826+
- matrix_name: "searchindex-test"
2827+
matrix_spec: { version: ["7.0"], os-faas-80: ["rhel80-large-go-1-20"] }
2828+
display_name: "Search Index ${version} ${os-faas-80}"
2829+
tasks:
2830+
- test-search-index-task-group
2831+
27692832
- name: testgcpkms-variant
27702833
display_name: "GCP KMS"
27712834
run_on:

Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ evg-test-load-balancers:
159159
go test $(BUILD_TAGS) ./mongo/integration -run TestLoadBalancerSupport -v -timeout $(TEST_TIMEOUT)s >> test.suite
160160
go test $(BUILD_TAGS) ./mongo/integration/unified -run TestUnifiedSpec -v -timeout $(TEST_TIMEOUT)s >> test.suite
161161

162+
.PHONY: evg-test-search-index
163+
evg-test-search-index:
164+
go test ./mongo/integration -run TestSearchIndexProse -v -timeout $(TEST_TIMEOUT)s >> test.suite
165+
162166
.PHONY: evg-test-ocsp
163167
evg-test-ocsp:
164168
go test -v ./mongo -run TestOCSP $(OCSP_TLS_SHOULD_SUCCEED) >> test.suite

bson/raw.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,19 @@ func (r Raw) LookupErr(key ...string) (RawValue, error) {
6060
// elements. If the document is not valid, the elements up to the invalid point will be returned
6161
// along with an error.
6262
func (r Raw) Elements() ([]RawElement, error) {
63-
elems, err := bsoncore.Document(r).Elements()
63+
doc := bsoncore.Document(r)
64+
if len(doc) == 0 {
65+
return nil, nil
66+
}
67+
elems, err := doc.Elements()
68+
if err != nil {
69+
return nil, err
70+
}
6471
relems := make([]RawElement, 0, len(elems))
6572
for _, elem := range elems {
6673
relems = append(relems, RawElement(elem))
6774
}
68-
return relems, err
75+
return relems, nil
6976
}
7077

7178
// Values returns this document as a slice of values. The returned slice will contain valid values.

mongo/collection.go

+7
Original file line numberDiff line numberDiff line change
@@ -1773,6 +1773,13 @@ func (coll *Collection) Indexes() IndexView {
17731773
return IndexView{coll: coll}
17741774
}
17751775

1776+
// SearchIndexes returns a SearchIndexView instance that can be used to perform operations on the search indexes for the collection.
1777+
func (coll *Collection) SearchIndexes() SearchIndexView {
1778+
return SearchIndexView{
1779+
coll: coll,
1780+
}
1781+
}
1782+
17761783
// Drop drops the collection on the server. This method ignores "namespace not found" errors so it is safe to drop
17771784
// a collection that does not exist on the server.
17781785
func (coll *Collection) Drop(ctx context.Context) error {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
// Copyright (C) MongoDB, Inc. 2023-present.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
package integration
8+
9+
import (
10+
"context"
11+
"os"
12+
"sync"
13+
"testing"
14+
"time"
15+
16+
"go.mongodb.org/mongo-driver/bson"
17+
"go.mongodb.org/mongo-driver/internal/assert"
18+
"go.mongodb.org/mongo-driver/internal/require"
19+
"go.mongodb.org/mongo-driver/internal/uuid"
20+
"go.mongodb.org/mongo-driver/mongo"
21+
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
22+
"go.mongodb.org/mongo-driver/mongo/options"
23+
)
24+
25+
func TestSearchIndexProse(t *testing.T) {
26+
t.Parallel()
27+
28+
const timeout = 5 * time.Minute
29+
30+
uri := os.Getenv("TEST_INDEX_URI")
31+
if uri == "" {
32+
t.Skip("skipping")
33+
}
34+
35+
opts := options.Client().ApplyURI(uri).SetTimeout(timeout)
36+
mt := mtest.New(t, mtest.NewOptions().ClientOptions(opts).MinServerVersion("7.0").Topologies(mtest.ReplicaSet))
37+
38+
mt.Run("case 1: Driver can successfully create and list search indexes", func(mt *mtest.T) {
39+
ctx := context.Background()
40+
41+
_, err := mt.Coll.InsertOne(ctx, bson.D{})
42+
require.NoError(mt, err, "failed to insert")
43+
44+
view := mt.Coll.SearchIndexes()
45+
46+
definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}}
47+
searchName := "test-search-index"
48+
opts := options.SearchIndexes().SetName(searchName)
49+
index, err := view.CreateOne(ctx, mongo.SearchIndexModel{
50+
Definition: definition,
51+
Options: opts,
52+
})
53+
require.NoError(mt, err, "failed to create index")
54+
require.Equal(mt, searchName, index, "unmatched name")
55+
56+
var doc bson.Raw
57+
for doc == nil {
58+
cursor, err := view.List(ctx, opts)
59+
require.NoError(mt, err, "failed to list")
60+
61+
if !cursor.Next(ctx) {
62+
break
63+
}
64+
if cursor.Current.Lookup("queryable").Boolean() {
65+
doc = cursor.Current
66+
} else {
67+
t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
68+
time.Sleep(5 * time.Second)
69+
}
70+
}
71+
require.NotNil(mt, doc, "got empty document")
72+
assert.Equal(mt, searchName, doc.Lookup("name").StringValue(), "unmatched name")
73+
expected, err := bson.Marshal(definition)
74+
require.NoError(mt, err, "failed to marshal definition")
75+
actual := doc.Lookup("latestDefinition").Value
76+
assert.Equal(mt, expected, actual, "unmatched definition")
77+
})
78+
79+
mt.Run("case 2: Driver can successfully create multiple indexes in batch", func(mt *mtest.T) {
80+
ctx := context.Background()
81+
82+
_, err := mt.Coll.InsertOne(ctx, bson.D{})
83+
require.NoError(mt, err, "failed to insert")
84+
85+
view := mt.Coll.SearchIndexes()
86+
87+
definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}}
88+
models := []mongo.SearchIndexModel{
89+
{
90+
Definition: definition,
91+
Options: options.SearchIndexes().SetName("test-search-index-1"),
92+
},
93+
{
94+
Definition: definition,
95+
Options: options.SearchIndexes().SetName("test-search-index-2"),
96+
},
97+
}
98+
indexes, err := view.CreateMany(ctx, models)
99+
require.NoError(mt, err, "failed to create index")
100+
require.Equal(mt, len(indexes), 2, "expected 2 indexes")
101+
for _, model := range models {
102+
require.Contains(mt, indexes, *model.Options.Name)
103+
}
104+
105+
getDocument := func(opts *options.SearchIndexesOptions) bson.Raw {
106+
for {
107+
cursor, err := view.List(ctx, opts)
108+
require.NoError(mt, err, "failed to list")
109+
110+
if !cursor.Next(ctx) {
111+
return nil
112+
}
113+
if cursor.Current.Lookup("queryable").Boolean() {
114+
return cursor.Current
115+
}
116+
t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
117+
time.Sleep(5 * time.Second)
118+
}
119+
}
120+
121+
var wg sync.WaitGroup
122+
wg.Add(len(models))
123+
for i := range models {
124+
go func(opts *options.SearchIndexesOptions) {
125+
defer wg.Done()
126+
127+
doc := getDocument(opts)
128+
require.NotNil(mt, doc, "got empty document")
129+
assert.Equal(mt, *opts.Name, doc.Lookup("name").StringValue(), "unmatched name")
130+
expected, err := bson.Marshal(definition)
131+
require.NoError(mt, err, "failed to marshal definition")
132+
actual := doc.Lookup("latestDefinition").Value
133+
assert.Equal(mt, expected, actual, "unmatched definition")
134+
}(models[i].Options)
135+
}
136+
wg.Wait()
137+
})
138+
139+
mt.Run("case 3: Driver can successfully drop search indexes", func(mt *mtest.T) {
140+
ctx := context.Background()
141+
142+
_, err := mt.Coll.InsertOne(ctx, bson.D{})
143+
require.NoError(mt, err, "failed to insert")
144+
145+
view := mt.Coll.SearchIndexes()
146+
147+
definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}}
148+
searchName := "test-search-index"
149+
opts := options.SearchIndexes().SetName(searchName)
150+
index, err := view.CreateOne(ctx, mongo.SearchIndexModel{
151+
Definition: definition,
152+
Options: opts,
153+
})
154+
require.NoError(mt, err, "failed to create index")
155+
require.Equal(mt, searchName, index, "unmatched name")
156+
157+
var doc bson.Raw
158+
for doc == nil {
159+
cursor, err := view.List(ctx, opts)
160+
require.NoError(mt, err, "failed to list")
161+
162+
if !cursor.Next(ctx) {
163+
break
164+
}
165+
if cursor.Current.Lookup("queryable").Boolean() {
166+
doc = cursor.Current
167+
} else {
168+
t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
169+
time.Sleep(5 * time.Second)
170+
}
171+
}
172+
require.NotNil(mt, doc, "got empty document")
173+
require.Equal(mt, searchName, doc.Lookup("name").StringValue(), "unmatched name")
174+
175+
err = view.DropOne(ctx, searchName)
176+
require.NoError(mt, err, "failed to drop index")
177+
for {
178+
cursor, err := view.List(ctx, opts)
179+
require.NoError(mt, err, "failed to list")
180+
181+
if !cursor.Next(ctx) {
182+
break
183+
}
184+
t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
185+
time.Sleep(5 * time.Second)
186+
}
187+
})
188+
189+
mt.Run("case 4: Driver can update a search index", func(mt *mtest.T) {
190+
ctx := context.Background()
191+
192+
_, err := mt.Coll.InsertOne(ctx, bson.D{})
193+
require.NoError(mt, err, "failed to insert")
194+
195+
view := mt.Coll.SearchIndexes()
196+
197+
definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}}
198+
searchName := "test-search-index"
199+
opts := options.SearchIndexes().SetName(searchName)
200+
index, err := view.CreateOne(ctx, mongo.SearchIndexModel{
201+
Definition: definition,
202+
Options: opts,
203+
})
204+
require.NoError(mt, err, "failed to create index")
205+
require.Equal(mt, searchName, index, "unmatched name")
206+
207+
getDocument := func() bson.Raw {
208+
for {
209+
cursor, err := view.List(ctx, opts)
210+
require.NoError(mt, err, "failed to list")
211+
212+
if !cursor.Next(ctx) {
213+
return nil
214+
}
215+
if cursor.Current.Lookup("queryable").Boolean() {
216+
return cursor.Current
217+
}
218+
t.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
219+
time.Sleep(5 * time.Second)
220+
}
221+
}
222+
223+
doc := getDocument()
224+
require.NotNil(mt, doc, "got empty document")
225+
require.Equal(mt, searchName, doc.Lookup("name").StringValue(), "unmatched name")
226+
227+
definition = bson.D{{"mappings", bson.D{{"dynamic", true}}}}
228+
err = view.UpdateOne(ctx, searchName, definition)
229+
require.NoError(mt, err, "failed to drop index")
230+
doc = getDocument()
231+
require.NotNil(mt, doc, "got empty document")
232+
assert.Equal(mt, searchName, doc.Lookup("name").StringValue(), "unmatched name")
233+
assert.Equal(mt, "READY", doc.Lookup("status").StringValue(), "unexpected status")
234+
expected, err := bson.Marshal(definition)
235+
require.NoError(mt, err, "failed to marshal definition")
236+
actual := doc.Lookup("latestDefinition").Value
237+
assert.Equal(mt, expected, actual, "unmatched definition")
238+
})
239+
240+
mt.Run("case 5: dropSearchIndex suppresses namespace not found errors", func(mt *mtest.T) {
241+
ctx := context.Background()
242+
243+
id, err := uuid.New()
244+
require.NoError(mt, err)
245+
246+
collection := mt.CreateCollection(mtest.Collection{
247+
Name: id.String(),
248+
}, false)
249+
250+
err = collection.SearchIndexes().DropOne(ctx, "foo")
251+
require.NoError(mt, err)
252+
})
253+
}

0 commit comments

Comments
 (0)