Skip to content

Commit 07be2d8

Browse files
bkimmigmorganlutz
bkimmig
authored andcommitted
Adds support for dense_vector type
A dense_vector field stores dense vectors of float values. The maximum number of dimensions that can be in a vector should not exceed 2048. A dense_vector field is a single-valued field. Closes spring-projects#1700
1 parent 1c549b7 commit 07be2d8

File tree

7 files changed

+100
-3
lines changed

7 files changed

+100
-3
lines changed

Diff for: src/main/java/org/springframework/data/elasticsearch/annotations/Field.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
* @author Peter-Josef Meisch
3535
* @author Xiao Yu
3636
* @author Aleksei Arsenev
37+
* @author Brian Kimmig
38+
* @author Morgan Lutz
3739
*/
3840
@Retention(RetentionPolicy.RUNTIME)
3941
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@@ -157,14 +159,14 @@
157159

158160
/**
159161
* if true, the field will be stored in Elasticsearch even if it has a null value
160-
*
162+
*
161163
* @since 4.1
162164
*/
163165
boolean storeNullValue() default false;
164166

165167
/**
166168
* to be used in combination with {@link FieldType#Rank_Feature}
167-
*
169+
*
168170
* @since 4.1
169171
*/
170172
boolean positiveScoreImpact() default true;
@@ -185,4 +187,11 @@
185187
* @since 4.1
186188
*/
187189
NullValueType nullValueType() default NullValueType.String;
190+
191+
/**
192+
* to be used in combination with {@link FieldType#Dense_Vector}
193+
*
194+
* @since 4.2
195+
*/
196+
int dims() default -1;
188197
}

Diff for: src/main/java/org/springframework/data/elasticsearch/annotations/FieldType.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
* @author Zeng Zetang
2323
* @author Peter-Josef Meisch
2424
* @author Aleksei Arsenev
25+
* @author Brian Kimmig
26+
* @author Morgan Lutz
2527
*/
2628
public enum FieldType {
2729
Auto, //
@@ -57,5 +59,7 @@ public enum FieldType {
5759
/** @since 4.1 */
5860
Rank_Features, //
5961
/** since 4.2 */
60-
Wildcard //
62+
Wildcard, //
63+
/** @since 4.2 */
64+
Dense_Vector //
6165
}

Diff for: src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java

+9
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
* @author Xiao Yu
2828
* @author Peter-Josef Meisch
2929
* @author Aleksei Arsenev
30+
* @author Brian Kimmig
31+
* @author Morgan Lutz
3032
*/
3133
@Retention(RetentionPolicy.RUNTIME)
3234
@Target(ElementType.ANNOTATION_TYPE)
@@ -140,4 +142,11 @@
140142
* @since 4.1
141143
*/
142144
NullValueType nullValueType() default NullValueType.String;
145+
146+
/**
147+
* to be used in combination with {@link FieldType#Dense_Vector}
148+
*
149+
* @since 4.2
150+
*/
151+
int dims() default -1;
143152
}

Diff for: src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java

+12
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
*
4040
* @author Peter-Josef Meisch
4141
* @author Aleksei Arsenev
42+
* @author Brian Kimmig
43+
* @author Morgan Lutz
4244
* @since 4.0
4345
*/
4446
public final class MappingParameters {
@@ -65,6 +67,7 @@ public final class MappingParameters {
6567
static final String FIELD_PARAM_NULL_VALUE = "null_value";
6668
static final String FIELD_PARAM_POSITION_INCREMENT_GAP = "position_increment_gap";
6769
static final String FIELD_PARAM_POSITIVE_SCORE_IMPACT = "positive_score_impact";
70+
static final String FIELD_PARAM_DIMS = "dims";
6871
static final String FIELD_PARAM_SCALING_FACTOR = "scaling_factor";
6972
static final String FIELD_PARAM_SEARCH_ANALYZER = "search_analyzer";
7073
static final String FIELD_PARAM_STORE = "store";
@@ -94,6 +97,7 @@ public final class MappingParameters {
9497
private final NullValueType nullValueType;
9598
private final Integer positionIncrementGap;
9699
private final boolean positiveScoreImpact;
100+
private final Integer dims;
97101
private final String searchAnalyzer;
98102
private final double scalingFactor;
99103
private final Similarity similarity;
@@ -153,6 +157,8 @@ private MappingParameters(Field field) {
153157
|| (maxShingleSize >= 2 && maxShingleSize <= 4), //
154158
"maxShingleSize must be in inclusive range from 2 to 4 for field type search_as_you_type");
155159
positiveScoreImpact = field.positiveScoreImpact();
160+
dims = field.dims();
161+
Assert.isTrue(dims <= 2048, "The maximum number of dimensions that can be in a vector should not exceed 2048.");
156162
Assert.isTrue(field.enabled() || type == FieldType.Object, "enabled false is only allowed for field type object");
157163
enabled = field.enabled();
158164
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
@@ -191,6 +197,8 @@ private MappingParameters(InnerField field) {
191197
|| (maxShingleSize >= 2 && maxShingleSize <= 4), //
192198
"maxShingleSize must be in inclusive range from 2 to 4 for field type search_as_you_type");
193199
positiveScoreImpact = field.positiveScoreImpact();
200+
dims = field.dims();
201+
Assert.isTrue(dims <= 2048, "The maximum number of dimensions that can be in a vector should not exceed 2048.");
194202
enabled = true;
195203
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
196204
}
@@ -323,6 +331,10 @@ public void writeTypeAndParametersTo(XContentBuilder builder) throws IOException
323331
builder.field(FIELD_PARAM_POSITIVE_SCORE_IMPACT, positiveScoreImpact);
324332
}
325333

334+
if (dims >= 1) {
335+
builder.field(FIELD_PARAM_DIMS, dims);
336+
}
337+
326338
if (!enabled) {
327339
builder.field(FIELD_PARAM_ENABLED, enabled);
328340
}

Diff for: src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderIntegrationTests.java

+19
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@
8282
* @author Peter-Josef Meisch
8383
* @author Xiao Yu
8484
* @author Roman Puchkovskiy
85+
* @author Brian Kimmig
86+
* @author Morgan Lutz
8587
*/
8688
@SpringIntegrationTest
8789
@ContextConfiguration(classes = { ElasticsearchRestTemplateConfiguration.class })
@@ -271,6 +273,16 @@ void shouldWriteWildcardFieldMapping() {
271273
indexOps.putMapping();
272274
}
273275

276+
@Test // #1700
277+
@DisplayName("should write dense_vector field mapping")
278+
void shouldWriteDenseVectorFieldMapping() {
279+
280+
IndexOperations indexOps = operations.indexOps(DenseVectorEntity.class);
281+
indexOps.create();
282+
indexOps.putMapping();
283+
indexOps.delete();
284+
}
285+
274286
@Test // #1370
275287
@DisplayName("should write mapping for disabled entity")
276288
void shouldWriteMappingForDisabledEntity() {
@@ -657,4 +669,11 @@ static class DisabledMappingProperty {
657669
@Field(type = Text) private String text;
658670
@Mapping(enabled = false) @Field(type = Object) private Object object;
659671
}
672+
673+
@Data
674+
@Document(indexName = "densevector-test")
675+
static class DenseVectorEntity {
676+
@Id private String id;
677+
@Field(type = Dense_Vector, dims = 3) private float[] dense_vector;
678+
}
660679
}

Diff for: src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java

+27
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242

4343
import org.elasticsearch.search.suggest.completion.context.ContextMapping;
4444
import org.json.JSONException;
45+
import org.junit.jupiter.api.Assertions;
4546
import org.junit.jupiter.api.DisplayName;
4647
import org.junit.jupiter.api.Test;
4748
import org.springframework.data.annotation.Id;
@@ -69,6 +70,8 @@
6970
* @author Peter-Josef Meisch
7071
* @author Xiao Yu
7172
* @author Roman Puchkovskiy
73+
* @author Brian Kimmig
74+
* @author Morgan Lutz
7275
*/
7376
public class MappingBuilderUnitTests extends MappingContextBaseTests {
7477

@@ -506,6 +509,23 @@ void shouldWriteRankFeatureProperties() throws JSONException {
506509
assertEquals(expected, mapping, false);
507510
}
508511

512+
@Test // #1700
513+
@DisplayName("should write dense_vector properties")
514+
void shouldWriteDenseVectorProperties() throws JSONException {
515+
String expected = "{\n" + //
516+
" \"properties\": {\n" + //
517+
" \"my_vector\": {\n" + //
518+
" \"type\": \"dense_vector\",\n" + //
519+
" \"dims\": 16\n" + //
520+
" }\n" + //
521+
" }\n" + //
522+
"}\n"; //
523+
524+
String mapping = getMappingBuilder().buildPropertyMapping(DenseVectorEntity.class);
525+
526+
assertEquals(expected, mapping, false);
527+
}
528+
509529
@Test // #1370
510530
@DisplayName("should not write mapping when enabled is false on entity")
511531
void shouldNotWriteMappingWhenEnabledIsFalseOnEntity() throws JSONException {
@@ -963,6 +983,13 @@ static class RankFeatureEntity {
963983
@Field(type = FieldType.Rank_Features) private Map<String, Integer> topics;
964984
}
965985

986+
@Data
987+
static class DenseVectorEntity {
988+
989+
@Id private String id;
990+
@Field(type = FieldType.Dense_Vector, dims = 16) private float[] my_vector;
991+
}
992+
966993
@Data
967994
@Mapping(enabled = false)
968995
static class DisabledMappingEntity {

Diff for: src/test/java/org/springframework/data/elasticsearch/core/index/MappingParametersTest.java

+17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.springframework.data.elasticsearch.core.index;
22

33
import static org.assertj.core.api.Assertions.*;
4+
import static org.springframework.data.elasticsearch.annotations.FieldType.Dense_Vector;
45
import static org.springframework.data.elasticsearch.annotations.FieldType.Object;
56

67
import java.lang.annotation.Annotation;
@@ -17,6 +18,8 @@
1718

1819
/**
1920
* @author Peter-Josef Meisch
21+
* @author Brian Kimmig
22+
* @author Morgan Lutz
2023
*/
2124
public class MappingParametersTest extends MappingContextBaseTests {
2225

@@ -66,6 +69,16 @@ void shouldAllowEnabledFalseOnlyOnObjectFields() {
6669
assertThatThrownBy(() -> MappingParameters.from(annotation)).isInstanceOf(IllegalArgumentException.class);
6770
}
6871

72+
@Test // #1700
73+
@DisplayName("should not allow dims length greater than 2048")
74+
void shouldNotAllowDimsLengthGreaterThan2048() {
75+
ElasticsearchPersistentEntity<?> failEntity = elasticsearchConverter.get().getMappingContext()
76+
.getRequiredPersistentEntity(InvalidDenseVectorClass.class);
77+
Annotation annotation = failEntity.getRequiredPersistentProperty("dense_vector").findAnnotation(Field.class);
78+
79+
assertThatThrownBy(() -> MappingParameters.from(annotation)).isInstanceOf(IllegalArgumentException.class);
80+
}
81+
6982
static class AnnotatedClass {
7083
@Nullable @Field private String field;
7184
@Nullable @MultiField(mainField = @Field,
@@ -79,4 +92,8 @@ static class AnnotatedClass {
7992
static class InvalidEnabledFieldClass {
8093
@Nullable @Field(type = FieldType.Text, enabled = false) private String disabledObject;
8194
}
95+
96+
static class InvalidDenseVectorClass {
97+
@Field(type = Dense_Vector, dims = 2049) private float[] dense_vector;
98+
}
8299
}

0 commit comments

Comments
 (0)