repo_description: The Official Golang driver for MongoDB
repo_topics: database driver go golang golang-library mongodb
code: // Copyright (C) MongoDB, Inc. 2023-present.
  //
  // Licensed under the Apache License, Version 2.0 (the "License"); you may
  // not use this file except in compliance with the License. You may obtain
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
  
  package integration
  
  import (
  	"context"
  	"os"
  	"sync"
  	"testing"
  	"time"
  
  	"go.mongodb.org/mongo-driver/v2/bson"
  	"go.mongodb.org/mongo-driver/v2/internal/assert"
  	"go.mongodb.org/mongo-driver/v2/internal/integration/mtest"
  	"go.mongodb.org/mongo-driver/v2/internal/mongoutil"
  	"go.mongodb.org/mongo-driver/v2/internal/require"
  	"go.mongodb.org/mongo-driver/v2/internal/uuid"
  	"go.mongodb.org/mongo-driver/v2/mongo"
  	"go.mongodb.org/mongo-driver/v2/mongo/options"
  	"go.mongodb.org/mongo-driver/v2/mongo/readconcern"
  	"go.mongodb.org/mongo-driver/v2/mongo/writeconcern"
  )
  
  func TestSearchIndexProse(t *testing.T) {
  	t.Parallel()
  
  	const timeout = 5 * time.Minute
  
  	uri := os.Getenv("SEARCH_INDEX_URI")
  	if uri == "" {
  		t.Skip("skipping")
  	}
  
  	opts := options.Client().ApplyURI(uri).SetTimeout(timeout)
  	mt := mtest.New(t, mtest.NewOptions().ClientOptions(opts).MinServerVersion("7.0").Topologies(mtest.ReplicaSet))
  
  	mt.Run("case 1: Driver can successfully create and list search indexes", func(mt *mtest.T) {
  		ctx := context.Background()
  
  		_, err := mt.Coll.InsertOne(ctx, bson.D{})
  		require.NoError(mt, err, "failed to insert")
  
  		view := mt.Coll.SearchIndexes()
  
  		definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}}
  		searchName := "test-search-index"
  		opts := options.SearchIndexes().SetName(searchName)
  		index, err := view.CreateOne(ctx, mongo.SearchIndexModel{
  			Definition: definition,
  			Options:    opts,
  		})
  		require.NoError(mt, err, "failed to create index")
  		require.Equal(mt, searchName, index, "unmatched name")
  
  		awaitCtx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
  		defer cancel()
  
  		var doc bson.Raw
  		for doc == nil {
  			cursor, err := view.List(awaitCtx, opts)
  			require.NoError(mt, err, "failed to list")
  
  			if !cursor.Next(awaitCtx) {
  				break
  			}
  			name := cursor.Current.Lookup("name").StringValue()
  			queryable := cursor.Current.Lookup("queryable").Boolean()
  			if name == searchName && queryable {
  				doc = cursor.Current
  			} else {
  				mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
  				time.Sleep(5 * time.Second)
  			}
  		}
  		require.NotNil(mt, doc, "got empty document")
  		actual := doc.Lookup("latestDefinition", "mappings", "dynamic").Boolean()
  		assert.False(mt, actual, "expected latestDefinition.mappings.dynamic to be false")
  	})
  
  	mt.Run("case 2: Driver can successfully create multiple indexes in batch", func(mt *mtest.T) {
  		ctx := context.Background()
  
  		_, err := mt.Coll.InsertOne(ctx, bson.D{})
  		require.NoError(mt, err, "failed to insert")
  
  		view := mt.Coll.SearchIndexes()
  
  		definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}}
  		models := []mongo.SearchIndexModel{
  			{
  				Definition: definition,
  				Options:    options.SearchIndexes().SetName("test-search-index-1"),
  			},
  			{
  				Definition: definition,
  				Options:    options.SearchIndexes().SetName("test-search-index-2"),
  			},
  		}
  		indexes, err := view.CreateMany(ctx, models)
  		require.NoError(mt, err, "failed to create index")
  		require.Equal(mt, len(indexes), 2, "expected 2 indexes")
  		for _, model := range models {
  			args, err := mongoutil.NewOptions[options.SearchIndexesOptions](model.Options)
  			require.NoError(mt, err, "failed to construct options from builder")
  
  			require.Contains(mt, indexes, *args.Name)
  		}
  
  		getDocument := func(ctx context.Context, opts *options.SearchIndexesOptionsBuilder) bson.Raw {
  			for {
  				cursor, err := view.List(ctx, opts)
  				require.NoError(mt, err, "failed to list")
  
  				if !cursor.Next(ctx) {
  					return nil
  				}
  				name := cursor.Current.Lookup("name").StringValue()
  				queryable := cursor.Current.Lookup("queryable").Boolean()
  
  				args, err := mongoutil.NewOptions[options.SearchIndexesOptions](opts)
  				require.NoError(mt, err, "failed to construct options from builder")
  
  				if name == *args.Name && queryable {
  					return cursor.Current
  				}
  				mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
  				time.Sleep(5 * time.Second)
  			}
  		}
  
  		var wg sync.WaitGroup
  		wg.Add(len(models))
  		for i := range models {
  			go func(opts *options.SearchIndexesOptionsBuilder) {
  				defer wg.Done()
  
  				ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
  				defer cancel()
  
  				doc := getDocument(ctx, opts)
  				require.NotNil(mt, doc, "got empty document")
  
  				args, err := mongoutil.NewOptions[options.SearchIndexesOptions](opts)
  				require.NoError(mt, err, "failed to construct options from builder")
  
  				assert.Equal(mt, *args.Name, doc.Lookup("name").StringValue(), "unmatched name")
  
  				actual := doc.Lookup("latestDefinition", "mappings", "dynamic").Boolean()
  				assert.False(mt, actual, "expected latestDefinition.mappings.dynamic to be false")
  			}(models[i].Options)
  		}
  		wg.Wait()
  	})
  
  	mt.Run("case 3: Driver can successfully drop search indexes", func(mt *mtest.T) {
  		ctx := context.Background()
  
  		_, err := mt.Coll.InsertOne(ctx, bson.D{})
  		require.NoError(mt, err, "failed to insert")
  
  		view := mt.Coll.SearchIndexes()
  
  		definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}}
  		searchName := "test-search-index"
  		opts := options.SearchIndexes().SetName(searchName)
  		index, err := view.CreateOne(ctx, mongo.SearchIndexModel{
  			Definition: definition,
  			Options:    opts,
  		})
  		require.NoError(mt, err, "failed to create index")
  		require.Equal(mt, searchName, index, "unmatched name")
  
  		createOneCtx, createOneCancel := context.WithTimeout(context.Background(), 1*time.Minute)
  		defer createOneCancel()
  		var doc bson.Raw
  		for doc == nil {
  			cursor, err := view.List(createOneCtx, opts)
  			require.NoError(mt, err, "failed to list")
  
  			if !cursor.Next(createOneCtx) {
  				break
  			}
  			name := cursor.Current.Lookup("name").StringValue()
  			queryable := cursor.Current.Lookup("queryable").Boolean()
  			if name == searchName && queryable {
  				doc = cursor.Current
  			} else {
  				mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
  				time.Sleep(5 * time.Second)
  			}
  		}
  		require.NotNil(mt, doc, "got empty document")
  
  		err = view.DropOne(ctx, searchName)
  		require.NoError(mt, err, "failed to drop index")
  		dropOneCtx, dropOneCancel := context.WithTimeout(context.Background(), 1*time.Minute)
  		defer dropOneCancel()
  		for {
  			cursor, err := view.List(dropOneCtx, opts)
  			require.NoError(mt, err, "failed to list")
  
  			if !cursor.Next(dropOneCtx) {
  				break
  			}
  			mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
  			time.Sleep(5 * time.Second)
  		}
  	})
  
  	mt.Run("case 4: Driver can update a search index", func(mt *mtest.T) {
  		ctx := context.Background()
  
  		_, err := mt.Coll.InsertOne(ctx, bson.D{})
  		require.NoError(mt, err, "failed to insert")
  
  		view := mt.Coll.SearchIndexes()
  
  		definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}}
  		searchName := "test-search-index"
  		opts := options.SearchIndexes().SetName(searchName)
  		index, err := view.CreateOne(ctx, mongo.SearchIndexModel{
  			Definition: definition,
  			Options:    opts,
  		})
  		require.NoError(mt, err, "failed to create index")
  		require.Equal(mt, searchName, index, "unmatched name")
  
  		createOneCtx, createOneCancel := context.WithTimeout(context.Background(), 1*time.Minute)
  		defer createOneCancel()
  		var doc bson.Raw
  		for doc == nil {
  			cursor, err := view.List(createOneCtx, opts)
  			require.NoError(mt, err, "failed to list")
  
  			if !cursor.Next(createOneCtx) {
  				break
  			}
  			name := cursor.Current.Lookup("name").StringValue()
  			queryable := cursor.Current.Lookup("queryable").Boolean()
  			if name == searchName && queryable {
  				doc = cursor.Current
  			} else {
  				mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
  				time.Sleep(5 * time.Second)
  			}
  		}
  		require.NotNil(mt, doc, "got empty document")
  
  		definition = bson.D{{"mappings", bson.D{{"dynamic", true}}}}
  		err = view.UpdateOne(ctx, searchName, definition)
  		require.NoError(mt, err, "failed to update index")
  		updateOneCtx, updateOneCancel := context.WithTimeout(context.Background(), 2*time.Minute)
  		defer updateOneCancel()
  		doc = nil
  		for doc == nil {
  			cursor, err := view.List(updateOneCtx, opts)
  			require.NoError(mt, err, "failed to list")
  
  			if !cursor.Next(updateOneCtx) {
  				break
  			}
  			name := cursor.Current.Lookup("name").StringValue()
  			queryable := cursor.Current.Lookup("queryable").Boolean()
  			status := cursor.Current.Lookup("status").StringValue()
  			if name == searchName && queryable && status == "READY" {
  				doc = cursor.Current
  			} else {
  				mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
  				time.Sleep(5 * time.Second)
  			}
  		}
  		require.NotNil(mt, doc, "got empty document")
  		require.True(mt, doc.Lookup("latestDefinition", "mappings", "dynamic").Boolean(),
  			"expected latestDefinition.mappings.dynamic to be true")
  	})
  
  	mt.Run("case 5: dropSearchIndex suppresses namespace not found errors", func(mt *mtest.T) {
  		ctx := context.Background()
  
  		id, err := uuid.New()
  		require.NoError(mt, err)
  
  		collection := mt.CreateCollection(mtest.Collection{
  			Name: id.String(),
  		}, false)
  
  		err = collection.SearchIndexes().DropOne(ctx, "foo")
  		require.NoError(mt, err)
  	})
  
  	mt.RunOpts("case 6: Driver can successfully create and list search indexes with non-default readConcern and writeConcern",
  		mtest.NewOptions().CollectionOptions(options.Collection().SetWriteConcern(writeconcern.W1()).SetReadConcern(readconcern.Majority())),
  		func(mt *mtest.T) {
  			ctx := context.Background()
  
  			_, err := mt.Coll.InsertOne(ctx, bson.D{})
  			require.NoError(mt, err, "failed to insert")
  
  			view := mt.Coll.SearchIndexes()
  
  			definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}}
  			const searchName = "test-search-index-case6"
  			opts := options.SearchIndexes().SetName(searchName)
  			index, err := view.CreateOne(ctx, mongo.SearchIndexModel{
  				Definition: definition,
  				Options:    opts,
  			})
  			require.NoError(mt, err, "failed to create index")
  			require.Equal(mt, searchName, index, "unmatched name")
  			awaitCtx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
  			defer cancel()
  			var doc bson.Raw
  			for doc == nil {
  				cursor, err := view.List(awaitCtx, opts)
  				require.NoError(mt, err, "failed to list")
  
  				if !cursor.Next(awaitCtx) {
  					break
  				}
  				name := cursor.Current.Lookup("name").StringValue()
  				queryable := cursor.Current.Lookup("queryable").Boolean()
  				if name == searchName && queryable {
  					doc = cursor.Current
  				} else {
  					mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
  					time.Sleep(5 * time.Second)
  				}
  			}
  			require.NotNil(mt, doc, "got empty document")
  			actual := doc.Lookup("latestDefinition", "mappings", "dynamic").Boolean()
  			assert.False(mt, actual, "expected latestDefinition.mappings.dynamic to be false")
  		})
  
  	case7CollName, err := uuid.New()
  	assert.NoError(mt, err, "failed to create random collection name for case #7")
  
  	mt.RunOpts("case 7: Driver can successfully handle search index types when creating indexes",
  		mtest.NewOptions().CollectionName(case7CollName.String()),
  		func(mt *mtest.T) {
  			ctx := context.Background()
  
  			_, err := mt.Coll.InsertOne(ctx, bson.D{})
  			require.NoError(mt, err, "failed to insert")
  
  			view := mt.Coll.SearchIndexes()
  
  			definition := bson.D{{"mappings", bson.D{{"dynamic", false}}}}
  			indexName := "test-search-index-case7-implicit"
  			opts := options.SearchIndexes().SetName(indexName)
  			index, err := view.CreateOne(ctx, mongo.SearchIndexModel{
  				Definition: definition,
  				Options:    opts,
  			})
  			require.NoError(mt, err, "failed to create index")
  			require.Equal(mt, indexName, index, "unmatched name")
  			implicitCtx, implicitCancel := context.WithTimeout(context.Background(), 1*time.Minute)
  			defer implicitCancel()
  			var doc bson.Raw
  			for doc == nil {
  				cursor, err := view.List(implicitCtx, opts)
  				require.NoError(mt, err, "failed to list")
  
  				if !cursor.Next(implicitCtx) {
  					break
  				}
  				name := cursor.Current.Lookup("name").StringValue()
  				queryable := cursor.Current.Lookup("queryable").Boolean()
  				indexType := cursor.Current.Lookup("type").StringValue()
  				if name == indexName && queryable {
  					doc = cursor.Current
  					assert.Equal(mt, indexType, "search")
  				} else {
  					mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
  					time.Sleep(5 * time.Second)
  				}
  			}
  
  			indexName = "test-search-index-case7-explicit"
  			opts = options.SearchIndexes().SetName(indexName).SetType("search")
  			index, err = view.CreateOne(ctx, mongo.SearchIndexModel{
  				Definition: definition,
  				Options:    opts,
  			})
  			require.NoError(mt, err, "failed to create index")
  			require.Equal(mt, indexName, index, "unmatched name")
  			explicitCtx, explicitCancel := context.WithTimeout(context.Background(), 1*time.Minute)
  			defer explicitCancel()
  			doc = nil
  			for doc == nil {
  				cursor, err := view.List(explicitCtx, opts)
  				require.NoError(mt, err, "failed to list")
  
  				if !cursor.Next(explicitCtx) {
  					break
  				}
  				name := cursor.Current.Lookup("name").StringValue()
  				queryable := cursor.Current.Lookup("queryable").Boolean()
  				indexType := cursor.Current.Lookup("type").StringValue()
  				if name == indexName && queryable {
  					doc = cursor.Current
  					assert.Equal(mt, indexType, "search")
  				} else {
  					mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
  					time.Sleep(5 * time.Second)
  				}
  			}
  
  			indexName = "test-search-index-case7-vector"
  			type vectorDefinitionField struct {
  				Type          string `bson:"type"`
  				Path          string `bson:"path"`
  				NumDimensions int    `bson:"numDimensions"`
  				Similarity    string `bson:"similarity"`
  			}
  
  			type vectorDefinition struct {
  				Fields []vectorDefinitionField `bson:"fields"`
  			}
  
  			opts = options.SearchIndexes().SetName(indexName).SetType("vectorSearch")
  			index, err = view.CreateOne(ctx, mongo.SearchIndexModel{
  				Definition: vectorDefinition{
  					Fields: []vectorDefinitionField{{"vector", "path", 1536, "euclidean"}},
  				},
  				Options: opts,
  			})
  			require.NoError(mt, err, "failed to create index")
  			require.Equal(mt, indexName, index, "unmatched name")
  			vectorCtx, vectorCancel := context.WithTimeout(context.Background(), 1*time.Minute)
  			defer vectorCancel()
  			doc = nil
  			for doc == nil {
  				cursor, err := view.List(vectorCtx, opts)
  				require.NoError(mt, err, "failed to list")
  
  				if !cursor.Next(vectorCtx) {
  					break
  				}
  				name := cursor.Current.Lookup("name").StringValue()
  				queryable := cursor.Current.Lookup("queryable").Boolean()
  				indexType := cursor.Current.Lookup("type").StringValue()
  				if name == indexName && queryable {
  					doc = cursor.Current
  					assert.Equal(mt, indexType, "vectorSearch")
  				} else {
  					mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
  					time.Sleep(5 * time.Second)
  				}
  			}
  		})
  
  	case8CollName, err := uuid.New()
  	assert.NoError(mt, err, "failed to create random collection name for case #8")
  
  	mt.RunOpts("case 8: Driver requires explicit type to create a vector search index",
  		mtest.NewOptions().CollectionName(case8CollName.String()),
  		func(mt *mtest.T) {
  			ctx := context.Background()
  
  			_, err := mt.Coll.InsertOne(ctx, bson.D{})
  			require.NoError(mt, err, "failed to insert")
  
  			view := mt.Coll.SearchIndexes()
  
  			type vectorDefinitionField struct {
  				Type          string `bson:"type"`
  				Path          string `bson:"path"`
  				NumDimensions int    `bson:"numDimensions"`
  				Similarity    string `bson:"similarity"`
  			}
  
  			type vectorDefinition struct {
  				Fields []vectorDefinitionField `bson:"fields"`
  			}
  
  			const indexName = "test-search-index-case7-vector"
  			opts := options.SearchIndexes().SetName(indexName)
  			_, err = view.CreateOne(ctx, mongo.SearchIndexModel{
  				Definition: vectorDefinition{
  					Fields: []vectorDefinitionField{{"vector", "plot_embedding", 1536, "euclidean"}},
  				},
  				Options: opts,
  			})
  			assert.ErrorContains(mt, err, "Attribute mappings missing")
  		})
  
  	mt.Run("case 9: Drivers use server default for unspecified name (`default`) and type (`search`)", func(mt *mtest.T) {
  		cases := []struct {
  			name string
  			opts *options.SearchIndexesOptionsBuilder
  		}{
  			{name: "empty options", opts: options.SearchIndexes()},
  			{name: "nil options", opts: nil},
  		}
  
  		for _, tc := range cases {
  			mt.Run(tc.name, func(mt *mtest.T) {
  				view := mt.Coll.SearchIndexes()
  				definition := bson.D{
  					{"mappings", bson.D{
  						{"dynamic", true},
  					}},
  				}
  
  				indexName, err := view.CreateOne(context.Background(), mongo.SearchIndexModel{
  					Definition: definition,
  					Options:    tc.opts,
  				})
  				require.NoError(mt, err, "failed to create index")
  				require.Equal(mt, "default", indexName)
  
  				ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
  				defer cancel()
  
  				var doc bson.Raw
  				for doc == nil {
  					cursor, err := view.List(ctx, tc.opts)
  					require.NoError(mt, err, "failed to list")
  
  					if !cursor.Next(ctx) {
  						break
  					}
  					name := cursor.Current.Lookup("name").StringValue()
  					queryable := cursor.Current.Lookup("queryable").Boolean()
  					indexType := cursor.Current.Lookup("type").StringValue()
  					if name == indexName && queryable {
  						doc = cursor.Current
  						require.Equal(mt, indexType, "search")
  					} else {
  						mt.Logf("cursor: %s, sleep 5 seconds...", cursor.Current.String())
  						time.Sleep(5 * time.Second)
  					}
  				}
  			})
  		}
  	})
  }
  