Skip to content

Commit 0cf5da3

Browse files
authored
[8.15] Support semantic_text in object fields (#114601) (#115047)
* Support semantic_text in object fields (#114601) (cherry picked from commit f99321b) # Conflicts: # x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java # x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapperTests.java # x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/30_semantic_text_inference.yml * Fix build errors
1 parent 1970f6c commit 0cf5da3

File tree

7 files changed

+546
-1
lines changed

7 files changed

+546
-1
lines changed

docs/changelog/114601.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 114601
2+
summary: Support semantic_text in object fields
3+
area: Vector Search
4+
type: bug
5+
issues:
6+
- 114401

x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import org.elasticsearch.features.FeatureSpecification;
1111
import org.elasticsearch.features.NodeFeature;
12+
import org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper;
1213
import org.elasticsearch.xpack.inference.rank.textsimilarity.TextSimilarityRankRetrieverBuilder;
1314

1415
import java.util.Set;
@@ -23,4 +24,8 @@ public Set<NodeFeature> getFeatures() {
2324
return Set.of(TextSimilarityRankRetrieverBuilder.TEXT_SIMILARITY_RERANKER_RETRIEVER_SUPPORTED);
2425
}
2526

27+
@Override
28+
public Set<NodeFeature> getTestFeatures() {
29+
return Set.of(SemanticTextFieldMapper.SEMANTIC_TEXT_IN_OBJECT_FIELD_FIX);
30+
}
2631
}

x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ private void applyInferenceResponses(BulkItemRequest item, FieldInferenceRespons
396396
),
397397
indexRequest.getContentType()
398398
);
399-
newDocMap.put(fieldName, result);
399+
SemanticTextFieldMapper.insertValue(fieldName, newDocMap, result);
400400
}
401401
indexRequest.source(newDocMap, indexRequest.getContentType());
402402
}

x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.elasticsearch.common.xcontent.support.XContentMapValues;
1919
import org.elasticsearch.core.Nullable;
2020
import org.elasticsearch.core.Tuple;
21+
import org.elasticsearch.features.NodeFeature;
2122
import org.elasticsearch.index.IndexSettings;
2223
import org.elasticsearch.index.IndexVersion;
2324
import org.elasticsearch.index.fielddata.FieldDataContext;
@@ -31,6 +32,7 @@
3132
import org.elasticsearch.index.mapper.Mapper;
3233
import org.elasticsearch.index.mapper.MapperBuilderContext;
3334
import org.elasticsearch.index.mapper.MapperMergeContext;
35+
import org.elasticsearch.index.mapper.MappingLookup;
3436
import org.elasticsearch.index.mapper.NestedObjectMapper;
3537
import org.elasticsearch.index.mapper.ObjectMapper;
3638
import org.elasticsearch.index.mapper.SimpleMappedFieldType;
@@ -78,6 +80,8 @@
7880
* A {@link FieldMapper} for semantic text fields.
7981
*/
8082
public class SemanticTextFieldMapper extends FieldMapper implements InferenceFieldMapper {
83+
public static final NodeFeature SEMANTIC_TEXT_IN_OBJECT_FIELD_FIX = new NodeFeature("semantic_text.in_object_field_fix");
84+
8185
public static final String CONTENT_TYPE = "semantic_text";
8286

8387
private final IndexSettings indexSettings;
@@ -327,6 +331,25 @@ public Object getOriginalValue(Map<String, Object> sourceAsMap) {
327331
return XContentMapValues.extractValue(TEXT_FIELD, fieldValueMap);
328332
}
329333

334+
@Override
335+
protected void doValidate(MappingLookup mappers) {
336+
int parentPathIndex = fullPath().lastIndexOf(leafName());
337+
if (parentPathIndex > 0) {
338+
// Check that the parent object field allows subobjects.
339+
// Subtract one from the parent path index to omit the trailing dot delimiter.
340+
ObjectMapper parentMapper = mappers.objectMappers().get(fullPath().substring(0, parentPathIndex - 1));
341+
if (parentMapper == null) {
342+
throw new IllegalStateException(CONTENT_TYPE + " field [" + fullPath() + "] does not have a parent object mapper");
343+
}
344+
345+
if (parentMapper.subobjects() == false) {
346+
throw new IllegalArgumentException(
347+
CONTENT_TYPE + " field [" + fullPath() + "] cannot be in an object field with subobjects disabled"
348+
);
349+
}
350+
}
351+
}
352+
330353
public static class SemanticTextFieldType extends SimpleMappedFieldType {
331354
private final String inferenceId;
332355
private final SemanticTextField.ModelSettings modelSettings;
@@ -484,6 +507,116 @@ public QueryBuilder semanticQuery(InferenceResults inferenceResults, float boost
484507
}
485508
}
486509

510+
/**
511+
* <p>
512+
* Insert or replace the path's value in the map with the provided new value. The map will be modified in-place.
513+
* If the complete path does not exist in the map, it will be added to the deepest (sub-)map possible.
514+
* </p>
515+
* <p>
516+
* For example, given the map:
517+
* </p>
518+
* <pre>
519+
* {
520+
* "path1": {
521+
* "path2": {
522+
* "key1": "value1"
523+
* }
524+
* }
525+
* }
526+
* </pre>
527+
* <p>
528+
* And the caller wanted to insert {@code "path1.path2.path3.key2": "value2"}, the method would emit the modified map:
529+
* </p>
530+
* <pre>
531+
* {
532+
* "path1": {
533+
* "path2": {
534+
* "key1": "value1",
535+
* "path3.key2": "value2"
536+
* }
537+
* }
538+
* }
539+
* </pre>
540+
*
541+
* @param path the value's path in the map.
542+
* @param map the map to search and modify in-place.
543+
* @param newValue the new value to assign to the path.
544+
*
545+
* @throws IllegalArgumentException If either the path cannot be fully traversed or there is ambiguity about where to insert the new
546+
* value.
547+
*/
548+
public static void insertValue(String path, Map<?, ?> map, Object newValue) {
549+
String[] pathElements = path.split("\\.");
550+
if (pathElements.length == 0) {
551+
return;
552+
}
553+
554+
List<SuffixMap> suffixMaps = extractSuffixMaps(pathElements, 0, map);
555+
if (suffixMaps.isEmpty()) {
556+
// This should never happen. Throw in case it does for some reason.
557+
throw new IllegalStateException("extractSuffixMaps returned an empty suffix map list");
558+
} else if (suffixMaps.size() == 1) {
559+
SuffixMap suffixMap = suffixMaps.get(0);
560+
suffixMap.map().put(suffixMap.suffix(), newValue);
561+
} else {
562+
throw new IllegalArgumentException(
563+
"Path [" + path + "] could be inserted in " + suffixMaps.size() + " distinct ways, it is ambiguous which one to use"
564+
);
565+
}
566+
}
567+
568+
private record SuffixMap(String suffix, Map<String, Object> map) {}
569+
570+
private static List<SuffixMap> extractSuffixMaps(String[] pathElements, int index, Object currentValue) {
571+
if (currentValue instanceof List<?> valueList) {
572+
List<SuffixMap> suffixMaps = new ArrayList<>(valueList.size());
573+
for (Object o : valueList) {
574+
suffixMaps.addAll(extractSuffixMaps(pathElements, index, o));
575+
}
576+
577+
return suffixMaps;
578+
} else if (currentValue instanceof Map<?, ?>) {
579+
@SuppressWarnings("unchecked")
580+
Map<String, Object> map = (Map<String, Object>) currentValue;
581+
List<SuffixMap> suffixMaps = new ArrayList<>(map.size());
582+
583+
String key = pathElements[index];
584+
while (index < pathElements.length) {
585+
if (map.containsKey(key)) {
586+
if (index + 1 == pathElements.length) {
587+
// We found the complete path
588+
suffixMaps.add(new SuffixMap(key, map));
589+
} else {
590+
// We've matched that path partially, keep traversing to try to match it fully
591+
suffixMaps.addAll(extractSuffixMaps(pathElements, index + 1, map.get(key)));
592+
}
593+
}
594+
595+
if (++index < pathElements.length) {
596+
key += "." + pathElements[index];
597+
}
598+
}
599+
600+
if (suffixMaps.isEmpty()) {
601+
// We checked for all remaining elements in the path, and they do not exist. This means we found a leaf map that we should
602+
// add the value to.
603+
suffixMaps.add(new SuffixMap(key, map));
604+
}
605+
606+
return suffixMaps;
607+
} else {
608+
throw new IllegalArgumentException(
609+
"Path ["
610+
+ String.join(".", Arrays.copyOfRange(pathElements, 0, index))
611+
+ "] has value ["
612+
+ currentValue
613+
+ "] of type ["
614+
+ currentValue.getClass().getSimpleName()
615+
+ "], which cannot be traversed into further"
616+
);
617+
}
618+
}
619+
487620
private static ObjectMapper createInferenceField(
488621
MapperBuilderContext context,
489622
IndexVersion indexVersionCreated,

0 commit comments

Comments
 (0)