Skip to content

Commit 98efe55

Browse files
feat: Add Configurable Dynamic Indexing for Atlas Search (#3867)
* Add type_sets field to search_index resource * Add type_sets and mappings_dynamic_config fields to singular and plural data source * Refactor function into accepting multiple fields * Add cannonical function for JSON * Ensure mappings_dynamic_config is not set when mappings_dynamic is being used * Refactor acceptance tests for less verbosity * Add changelog entry * Add documentation * Extract hashtype implementation for better readability * Address PR comments - documentation * Add acceptance test step for omitted type_set * Rephrase documentation for mappings_dynamic_config * Address docs PR comment suggestions * Update changelog sentences to be in 3rd person * Refactor read operation for resource and single data source on mappings field * Refactor canonical JSON function for simplicity purposes * Remove unnecessary checks for resource read on mappings_dynamic and mappings_dynamic_config
1 parent 603f19d commit 98efe55

File tree

9 files changed

+427
-39
lines changed

9 files changed

+427
-39
lines changed

.changelog/3867.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
```release-note:enhancement
2+
resource/mongodbatlas_search_index: Adds `type_sets` field to support Atlas Search configurable dynamic type sets
3+
```
4+
5+
```release-note:enhancement
6+
resource/mongodbatlas_search_index: Adds `mappings_dynamic_config` (mutually exclusive with `mappings_dynamic`) to support object form of `mappings.dynamic`
7+
```
8+
9+
```release-note:enhancement
10+
data-source/mongodbatlas_search_index: Exposes `mappings_dynamic_config` and `type_sets`
11+
```
12+
13+
```release-note:enhancement
14+
data-source/mongodbatlas_search_indexes: Exposes `mappings_dynamic_config` and `type_sets` per result
15+
```

docs/data-sources/search_index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,16 @@ data "mongodbatlas_search_index" "test" {
3535
* `collection_name` - Name of the collection the index is on.
3636
* `database` - Name of the database the collection is in.
3737
* `mappings_dynamic` - Flag indicating whether the index uses dynamic or static mappings.
38+
* `mappings_dynamic_config` - JSON object for `mappings.dynamic` when Atlas returns an object (Please see the documentation for [dynamic and static mappings](https://www.mongodb.com/docs/atlas/atlas-search/index-definitions/#field-mapping-examples)). Mutually exclusive with `mappings_dynamic`.
3839
* `mappings_fields` - Object containing one or more field specifications.
3940
* `search_analyzer` - [Analyzer](https://docs.atlas.mongodb.com/reference/atlas-search/analyzers/#std-label-analyzers-ref) to use when searching the index.
4041
* `synonyms` - Synonyms mapping definition to use in this index.
4142
* `synonyms.#.name` - Name of the [synonym mapping definition](https://docs.atlas.mongodb.com/reference/atlas-search/synonyms/#std-label-synonyms-ref).
4243
* `synonyms.#.source_collection` - Name of the source MongoDB collection for the synonyms.
4344
* `synonyms.#.analyzer` - Name of the [analyzer](https://docs.atlas.mongodb.com/reference/atlas-search/analyzers/#std-label-analyzers-ref) to use with this synonym mapping.
4445
* `stored_source` - String that can be "true" (store all fields), "false" (default, don't store any field), or a JSON string that contains the list of fields to store (include) or not store (exclude) on Atlas Search. To learn more, see [Stored Source Fields](https://www.mongodb.com/docs/atlas/atlas-search/stored-source-definition/).
46+
* `type_sets` - Set of type set definitions (when present). Each item includes:
47+
* `name` - Type set name.
48+
* `types` - JSON array string describing the types for the set.
4549

4650
For more information see: [MongoDB Atlas API Reference.](https://docs.atlas.mongodb.com/atlas-search/) - [and MongoDB Atlas API - Search](https://docs.atlas.mongodb.com/reference/api/atlas-search/) Documentation for more information.

docs/data-sources/search_indexes.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,17 @@ data "mongodbatlas_search_indexes" "test" {
4040
* `analyzers` - [Custom analyzers](https://docs.atlas.mongodb.com/reference/atlas-search/analyzers/custom/#std-label-custom-analyzers) to use in this index (this is an array of objects).
4141
* `collection_name` - (Required) Name of the collection the index is on.
4242
* `database` - (Required) Name of the database the collection is in.
43-
* `mappings_dynamic` - Flag indicating whether the index uses dynamic or static mappings.
43+
* `mappings_dynamic` - Flag indicating whether the index uses dynamic or static mappings. Mutually exclusive with `mappings_dynamic_config`.
44+
* `mappings_dynamic_config` - JSON object for `mappings.dynamic` when Atlas returns an object (Please see the documentation for [dynamic and static mappings](https://www.mongodb.com/docs/atlas/atlas-search/index-definitions/#field-mapping-examples)). Mutually exclusive with `mappings_dynamic`.
4445
* `mappings_fields` - Object containing one or more field specifications.
4546
* `search_analyzer` - [Analyzer](https://docs.atlas.mongodb.com/reference/atlas-search/analyzers/#std-label-analyzers-ref) to use when searching the index.
4647
* `synonyms` - Synonyms mapping definition to use in this index.
4748
* `synonyms.#.name` - Name of the [synonym mapping definition](https://docs.atlas.mongodb.com/reference/atlas-search/synonyms/#std-label-synonyms-ref).
4849
* `synonyms.#.source_collection` - Name of the source MongoDB collection for the synonyms.
4950
* `synonyms.#.analyzer` - Name of the [analyzer](https://docs.atlas.mongodb.com/reference/atlas-search/analyzers/#std-label-analyzers-ref) to use with this synonym mapping.
5051
* `stored_source` - String that can be "true" (store all fields), "false" (default, don't store any field), or a JSON string that contains the list of fields to store (include) or not store (exclude) on Atlas Search. To learn more, see [Stored Source Fields](https://www.mongodb.com/docs/atlas/atlas-search/stored-source-definition/).
52+
* `type_sets` - Set of type set definitions (when present). Each item includes:
53+
* `name` - Type set name.
54+
* `types` - JSON array string describing the types for the set.
5155

5256
For more information see: [MongoDB Atlas API Reference.](https://docs.atlas.mongodb.com/atlas-search/) - [and MongoDB Atlas API - Search](https://docs.atlas.mongodb.com/reference/api/atlas-search/) Documentation for more information.

docs/resources/search_index.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,33 @@ EOF
113113
}
114114
```
115115

116+
### Configurable dynamic (typeSets + dynamic object)
117+
```terraform
118+
resource "mongodbatlas_search_index" "conf-dynamic" {
119+
project_id = "<PROJECT_ID>"
120+
cluster_name = "<CLUSTER_NAME>"
121+
collection_name = "collection_test"
122+
database = "database_test"
123+
name = "conf-dynamic"
124+
type = "search"
125+
126+
# mappings.dynamic as an object referencing a type set
127+
mappings_dynamic_config = <<-EOF
128+
{ "typeSet": "type_set_name" }
129+
EOF
130+
131+
# Define the referenced type set
132+
type_sets {
133+
name = "type_set_name"
134+
types = <<-EOF
135+
[
136+
{ "type": "string" }
137+
]
138+
EOF
139+
}
140+
}
141+
```
142+
116143
## Argument Reference
117144

118145
* `type` - (Optional) Type of index: `search` or `vectorSearch`. Default type is `search`.
@@ -152,7 +179,7 @@ EOF
152179

153180
* `database` - (Required) Name of the database the collection is in.
154181

155-
* `mappings_dynamic` - Indicates whether the search index uses dynamic or static mapping. For dynamic mapping, set the value to `true`. For static mapping, specify the fields to index using `mappings_fields`
182+
* `mappings_dynamic` - Indicates whether the search index uses dynamic or static mapping. For default dynamic mapping, set the value to `true`. For static mapping, specify the fields to index using `mappings_fields`. Mutually exclusive with `mappings_dynamic_config`.
156183

157184
* `mappings_fields` - attribute is required in search indexes when `mappings_dynamic` is false. This field needs to be a JSON string in order to be decoded correctly.
158185
```terraform
@@ -190,6 +217,12 @@ EOF
190217
EOF
191218
```
192219

220+
* `mappings_dynamic_config` - (Optional) JSON object for `mappings.dynamic` when using configurable dynamic. See the MongoDB documentation for further information on [Static and Dynamic Mapping](https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/#std-label-fts-field-mappings). Mutually exclusive with `mappings_dynamic`.
221+
222+
* `type_sets` - (Optional) One or more blocks defining configurable dynamic type sets. Atlas only persists/returns `typeSets` when `mappings.dynamic` is an object referencing a `typeSet` name.
223+
* `name` - (Required) Name of the type set.
224+
* `types` - (Optional) JSON array describing the types.
225+
193226
* `search_analyzer` - [Analyzer](https://docs.atlas.mongodb.com/reference/atlas-search/analyzers/#std-label-analyzers-ref) to use when searching the index. Defaults to [lucene.standard](https://docs.atlas.mongodb.com/reference/atlas-search/analyzers/standard/#std-label-ref-standard-analyzer)
194227
* `synonyms` - Synonyms mapping definition to use in this index.
195228

internal/service/searchindex/data_source_search_index.go

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ func returnSearchIndexDSSchema() map[string]*schema.Schema {
5858
Type: schema.TypeBool,
5959
Computed: true,
6060
},
61+
"mappings_dynamic_config": {
62+
Type: schema.TypeString,
63+
Computed: true,
64+
},
6165
"mappings_fields": {
6266
Type: schema.TypeString,
6367
Computed: true,
@@ -98,6 +102,22 @@ func returnSearchIndexDSSchema() map[string]*schema.Schema {
98102
Type: schema.TypeString,
99103
Computed: true,
100104
},
105+
"type_sets": {
106+
Type: schema.TypeSet,
107+
Computed: true,
108+
Elem: &schema.Resource{
109+
Schema: map[string]*schema.Schema{
110+
"name": {
111+
Type: schema.TypeString,
112+
Computed: true,
113+
},
114+
"types": {
115+
Type: schema.TypeString,
116+
Computed: true,
117+
},
118+
},
119+
},
120+
},
101121
}
102122
}
103123

@@ -160,18 +180,8 @@ func dataSourceMongoDBAtlasSearchIndexRead(ctx context.Context, d *schema.Resour
160180
}
161181

162182
if searchIndex.LatestDefinition.Mappings != nil {
163-
if err := d.Set("mappings_dynamic", searchIndex.LatestDefinition.Mappings.Dynamic); err != nil {
164-
return diag.Errorf("error setting `mappings_dynamic` for search index (%s): %s", d.Id(), err)
165-
}
166-
167-
if fields := searchIndex.LatestDefinition.Mappings.Fields; fields != nil && conversion.HasElementsSliceOrMap(*fields) {
168-
searchIndexMappingFields, err := marshalSearchIndex(*fields)
169-
if err != nil {
170-
return diag.FromErr(err)
171-
}
172-
if err := d.Set("mappings_fields", searchIndexMappingFields); err != nil {
173-
return diag.Errorf("error setting `mappings_fields` for for search index (%s): %s", d.Id(), err)
174-
}
183+
if diags := setMappingsAttributesFromDefinition(d, searchIndex.LatestDefinition.Mappings); diags != nil {
184+
return diags
175185
}
176186
}
177187

@@ -186,6 +196,24 @@ func dataSourceMongoDBAtlasSearchIndexRead(ctx context.Context, d *schema.Resour
186196
}
187197
}
188198

199+
if typeSets := searchIndex.LatestDefinition.GetTypeSets(); len(typeSets) > 0 {
200+
var flattened []map[string]any
201+
for _, t := range typeSets {
202+
entry := map[string]any{"name": t.Name}
203+
if types := t.GetTypes(); len(types) > 0 {
204+
j, err := marshalSearchIndex(types)
205+
if err != nil {
206+
return diag.FromErr(err)
207+
}
208+
entry["types"] = j
209+
}
210+
flattened = append(flattened, entry)
211+
}
212+
if err := d.Set("type_sets", flattened); err != nil {
213+
return diag.Errorf("error setting `type_sets` for search index (%s): %s", d.Id(), err)
214+
}
215+
}
216+
189217
storedSource := searchIndex.LatestDefinition.GetStoredSource()
190218
strStoredSource, errStoredSource := MarshalStoredSource(storedSource)
191219
if errStoredSource != nil {

internal/service/searchindex/data_source_search_indexes.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,17 @@ func flattenSearchIndexes(searchIndexes []admin.SearchIndexResponse, projectID,
105105
}
106106

107107
if searchIndexes[i].LatestDefinition.Mappings != nil {
108-
searchIndexesMap[i]["mappings_dynamic"] = searchIndexes[i].LatestDefinition.Mappings.Dynamic
108+
switch v := searchIndexes[i].LatestDefinition.Mappings.GetDynamic().(type) {
109+
case bool:
110+
searchIndexesMap[i]["mappings_dynamic"] = v
111+
case map[string]any:
112+
j, err := marshalSearchIndex(v)
113+
if err != nil {
114+
return nil, err
115+
}
116+
searchIndexesMap[i]["mappings_dynamic_config"] = j
117+
default:
118+
}
109119

110120
if conversion.HasElementsSliceOrMap(searchIndexes[i].LatestDefinition.Mappings.Fields) {
111121
searchIndexMappingFields, err := marshalSearchIndex(searchIndexes[i].LatestDefinition.Mappings.Fields)
@@ -116,6 +126,22 @@ func flattenSearchIndexes(searchIndexes []admin.SearchIndexResponse, projectID,
116126
}
117127
}
118128

129+
if typeSets := searchIndexes[i].LatestDefinition.GetTypeSets(); len(typeSets) > 0 {
130+
var flattened []map[string]any
131+
for _, ts := range typeSets {
132+
entry := map[string]any{"name": ts.Name}
133+
if types := ts.GetTypes(); len(types) > 0 {
134+
j, err := marshalSearchIndex(types)
135+
if err != nil {
136+
return nil, err
137+
}
138+
entry["types"] = j
139+
}
140+
flattened = append(flattened, entry)
141+
}
142+
searchIndexesMap[i]["type_sets"] = flattened
143+
}
144+
119145
if analyzers := searchIndexes[i].LatestDefinition.GetAnalyzers(); len(analyzers) > 0 {
120146
searchIndexAnalyzers, err := marshalSearchIndex(analyzers)
121147
if err != nil {

internal/service/searchindex/model_search_index.go

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,19 @@ func unmarshalSearchIndexMappingFields(str string) (map[string]any, diag.Diagnos
6060
return fields, nil
6161
}
6262

63-
func unmarshalSearchIndexFields(str string) ([]map[string]any, diag.Diagnostics) {
64-
fields := []map[string]any{}
63+
func unmarshalJSONArrayForAttr(str, attr string) ([]map[string]any, diag.Diagnostics) {
64+
arr := []map[string]any{}
6565
if str == "" {
66-
return fields, nil
66+
return arr, nil
6767
}
68-
if err := json.Unmarshal([]byte(str), &fields); err != nil {
69-
return nil, diag.Errorf("cannot unmarshal search index attribute `fields` because it has an incorrect format")
68+
if err := json.Unmarshal([]byte(str), &arr); err != nil {
69+
return nil, diag.Errorf("cannot unmarshal search index attribute `%s` because it has an incorrect format", attr)
7070
}
71+
return arr, nil
72+
}
7173

72-
return fields, nil
74+
func unmarshalSearchIndexFields(str string) ([]map[string]any, diag.Diagnostics) {
75+
return unmarshalJSONArrayForAttr(str, "fields")
7376
}
7477

7578
func UnmarshalSearchIndexAnalyzersFields(str string) ([]admin.AtlasSearchAnalyzer, diag.Diagnostics) {
@@ -85,6 +88,35 @@ func UnmarshalSearchIndexAnalyzersFields(str string) ([]admin.AtlasSearchAnalyze
8588
return fields, nil
8689
}
8790

91+
func expandSearchIndexTypeSets(d *schema.ResourceData) ([]admin.SearchTypeSets, diag.Diagnostics) {
92+
var result []admin.SearchTypeSets
93+
94+
v, ok := d.GetOk("type_sets")
95+
if !ok {
96+
return result, nil
97+
}
98+
99+
for _, raw := range v.(*schema.Set).List() {
100+
item := raw.(map[string]any)
101+
102+
ts := admin.SearchTypeSets{
103+
Name: item["name"].(string),
104+
}
105+
106+
if s, ok := item["types"].(string); ok && s != "" {
107+
arr, diags := unmarshalJSONArrayForAttr(s, "type_sets.types")
108+
if diags != nil {
109+
return nil, diags
110+
}
111+
ts.Types = conversion.ToAnySlicePointer(&arr)
112+
}
113+
114+
result = append(result, ts)
115+
}
116+
117+
return result, nil
118+
}
119+
88120
func MarshalStoredSource(obj any) (string, error) {
89121
if obj == nil {
90122
return "", nil
@@ -127,3 +159,29 @@ func resourceSearchIndexRefreshFunc(ctx context.Context, clusterName, projectID,
127159
return searchIndex, status, nil
128160
}
129161
}
162+
163+
func canonicalizeJSONString(s string) string {
164+
if s == "" {
165+
return ""
166+
}
167+
var v any
168+
if err := json.Unmarshal([]byte(s), &v); err != nil {
169+
return s
170+
}
171+
by, err := json.Marshal(v)
172+
if err != nil {
173+
return s
174+
}
175+
return string(by)
176+
}
177+
178+
func hashTypeSetElement(v interface{}) int {
179+
m := v.(map[string]interface{})
180+
name := ""
181+
if nv, ok := m["name"].(string); ok {
182+
name = nv
183+
}
184+
typesStr, _ := m["types"].(string)
185+
canon := canonicalizeJSONString(typesStr)
186+
return schema.HashString(name + "|" + canon)
187+
}

0 commit comments

Comments
 (0)