Skip to content

Commit 6cd54df

Browse files
Introduce SearchIndexDefinition -> up next a litte documentation
1 parent 40c223c commit 6cd54df

File tree

5 files changed

+247
-16
lines changed

5 files changed

+247
-16
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ public interface SearchIndexDefinition {
4141
*/
4242
String getType();
4343

44+
/**
45+
* Returns the index document for this index without any potential entity context resolving field name mappings. The
46+
* resulting document contains the index name, type and {@link #getDefinition(TypeInformation, MappingContext)
47+
* definition}.
48+
*
49+
* @return never {@literal null}.
50+
*/
51+
default Document getRawIndexDocument() {
52+
return getIndexDocument(null, null);
53+
}
54+
4455
/**
4556
* Returns the index document for this index in the context of a potential entity to resolve field name mappings. The
4657
* resulting document contains the index name, type and {@link #getDefinition(TypeInformation, MappingContext)
@@ -51,7 +62,7 @@ public interface SearchIndexDefinition {
5162
* @return never {@literal null}.
5263
*/
5364
default Document getIndexDocument(@Nullable TypeInformation<?> entity,
54-
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
65+
@Nullable MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
5566

5667
Document document = new Document();
5768
document.put("name", getName());
@@ -70,6 +81,5 @@ default Document getIndexDocument(@Nullable TypeInformation<?> entity,
7081
* @return never {@literal null}.
7182
*/
7283
Document getDefinition(@Nullable TypeInformation<?> entity,
73-
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext);
74-
84+
@Nullable MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext);
7585
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.index;
17+
18+
import java.util.function.Supplier;
19+
20+
import org.bson.Document;
21+
import org.springframework.data.mapping.context.MappingContext;
22+
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
23+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
24+
import org.springframework.data.util.Lazy;
25+
import org.springframework.data.util.TypeInformation;
26+
import org.springframework.lang.Nullable;
27+
28+
/**
29+
* Index information for a MongoDB Search Index.
30+
*
31+
* @author Christoph Strobl
32+
*/
33+
public class SearchIndexInfo {
34+
35+
private final @Nullable Object id;
36+
private final SearchIndexStatus status;
37+
private final Lazy<SearchIndexDefinition> indexDefinition;
38+
39+
SearchIndexInfo(@Nullable Object id, SearchIndexStatus status, Supplier<SearchIndexDefinition> indexDefinition) {
40+
this.id = id;
41+
this.status = status;
42+
this.indexDefinition = Lazy.of(indexDefinition);
43+
}
44+
45+
public static SearchIndexInfo parse(String source) {
46+
return of(Document.parse(source));
47+
}
48+
49+
public static SearchIndexInfo of(Document indexDocument) {
50+
51+
Object id = indexDocument.get("id");
52+
SearchIndexStatus status = SearchIndexStatus.valueOf(indexDocument.get("status", "DOES_NOT_EXIST"));
53+
54+
return new SearchIndexInfo(id, status, () -> readIndexDefinition(indexDocument));
55+
}
56+
57+
/**
58+
* The id of the index. Can be {@literal null}, eg. for an index not yet created.
59+
*
60+
* @return can be {@literal null}.
61+
*/
62+
@Nullable
63+
public Object getId() {
64+
return id;
65+
}
66+
67+
/**
68+
* @return the current status of the index.
69+
*/
70+
public SearchIndexStatus getStatus() {
71+
return status;
72+
}
73+
74+
/**
75+
* @return the current index definition.
76+
*/
77+
public SearchIndexDefinition getIndexDefinition() {
78+
return indexDefinition.get();
79+
}
80+
81+
private static SearchIndexDefinition readIndexDefinition(Document document) {
82+
83+
String type = document.get("type", "search");
84+
if (type.equals("vectorSearch")) {
85+
return VectorIndex.of(document);
86+
}
87+
88+
return new SearchIndexDefinition() {
89+
90+
@Override
91+
public String getName() {
92+
return document.getString("name");
93+
}
94+
95+
@Override
96+
public String getType() {
97+
return type;
98+
}
99+
100+
@Override
101+
public Document getDefinition(@Nullable TypeInformation<?> entity,
102+
@Nullable MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
103+
if (document.containsKey("latestDefinition")) {
104+
return document.get("latestDefinition", new Document());
105+
}
106+
return document.get("definition", new Document());
107+
}
108+
109+
@Override
110+
public String toString() {
111+
return getDefinition(null, null).toJson();
112+
}
113+
};
114+
}
115+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,11 @@ public String getType() {
112112

113113
@Override
114114
public Document getDefinition(@Nullable TypeInformation<?> entity,
115-
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
115+
@Nullable MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
116116

117-
if (fields.isEmpty()) {
118-
throw new IllegalStateException("At least one vector or filter field must be added to the index");
119-
}
120-
121-
MongoPersistentEntity<?> persistentEntity = entity != null ? mappingContext.getPersistentEntity(entity) : null;
117+
MongoPersistentEntity<?> persistentEntity = entity != null
118+
? (mappingContext != null ? mappingContext.getPersistentEntity(entity) : null)
119+
: null;
122120

123121
Document definition = new Document();
124122
List<Document> fields = new ArrayList<>();
@@ -150,15 +148,35 @@ private VectorIndex addField(SearchField filterField) {
150148
return this;
151149
}
152150

151+
@Override
152+
public String toString() {
153+
return "VectorIndex{" + "name='" + name + '\'' + ", fields=" + fields + ", type='" + getType() + '\'' + '}';
154+
}
155+
153156
// /** instead of index info */
154-
// public static void from(Document document) {
155-
//
156-
// }
157+
static VectorIndex of(Document document) {
158+
159+
VectorIndex index = new VectorIndex(document.getString("name"));
160+
String definitionKey = document.containsKey("latestDefinition") ? "latestDefinition" : "definition";
161+
Document definition = document.get(definitionKey, Document.class);
162+
for (Object entry : definition.get("fields", List.class)) {
163+
if (entry instanceof Document field) {
164+
if (field.get("type").equals("vector")) {
165+
index.addField(new VectorIndexField(field.getString("path"), "vector", field.getInteger("numDimensions"),
166+
field.getString("similarity"), field.getString("quantization")));
167+
} else {
168+
index.addField(new VectorFilterField(field.getString("path"), "filter"));
169+
}
170+
}
171+
}
172+
173+
return index;
174+
}
157175

158176
private String resolvePath(String path, @Nullable MongoPersistentEntity<?> persistentEntity,
159-
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
177+
@Nullable MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
160178

161-
if (persistentEntity == null) {
179+
if (persistentEntity == null || mappingContext == null) {
162180
return path;
163181
}
164182

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.index;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import org.junit.jupiter.params.ParameterizedTest;
21+
import org.junit.jupiter.params.provider.ValueSource;
22+
23+
/**
24+
* @author Christoph Strobl
25+
*/
26+
class SearchIndexInfoUnitTests {
27+
28+
@ParameterizedTest
29+
@ValueSource(strings = { """
30+
{
31+
"id": "679b7637a580c270015ef6fb",
32+
"name": "vector_index",
33+
"type": "vectorSearch",
34+
"status": "READY",
35+
"queryable": true,
36+
"latestVersion": 0,
37+
"latestDefinition": {
38+
"fields": [
39+
{
40+
"type": "vector",
41+
"path": "plot_embedding",
42+
"numDimensions": 1536,
43+
"similarity": "euclidean"
44+
}
45+
]
46+
}
47+
}""", """
48+
{
49+
id: '648b4ad4d697b73bf9d2e5e1',
50+
name: 'search-index',
51+
status: 'PENDING',
52+
queryable: false,
53+
latestDefinition: {
54+
mappings: { dynamic: false, fields: { text: { type: 'string' } } }
55+
}
56+
}""", """
57+
{
58+
name: 'search-index-not-yet-created',
59+
definition: {
60+
mappings: { dynamic: false, fields: { text: { type: 'string' } } }
61+
}
62+
}""", """
63+
{
64+
name: 'vector-index-with-filter',
65+
type: "vectorSearch",
66+
definition: {
67+
fields: [
68+
{
69+
type: "vector",
70+
path: "plot_embedding",
71+
numDimensions: 1536,
72+
similarity: "euclidean"
73+
}, {
74+
type: "filter",
75+
path: "year"
76+
}
77+
]
78+
}
79+
}""" })
80+
void parsesIndexInfo(String indexInfoSource) {
81+
82+
SearchIndexInfo indexInfo = SearchIndexInfo.parse(indexInfoSource);
83+
84+
if (indexInfo.getId() != null) {
85+
assertThat(indexInfo.getId()).isInstanceOf(String.class);
86+
}
87+
assertThat(indexInfo.getStatus()).isNotNull();
88+
assertThat(indexInfo.getIndexDefinition()).isNotNull();
89+
}
90+
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ void dropIndex() {
108108
indexOps.dropIndex(idx.getName());
109109

110110
assertThat(readRawIndexInfo(idx.getName())).isNull();
111-
112111
}
113112

114113
@Test // GH-4706
@@ -207,5 +206,4 @@ static class Movie {
207206

208207
@Field("plot_embedding") Double[] plotEmbedding;
209208
}
210-
211209
}

0 commit comments

Comments
 (0)