Skip to content

Commit f44822a

Browse files
committed
Rework geo mappers to index value by value (elastic#71696)
The various geo field mappers are organised in a hierarchy that shares parsing and indexing code. This ends up over-complicating things, particularly when we have some mappers that accept multiple values and others that only accept singletons. It also leads to confusing behaviour around ignore_malformed behaviour: geo fields will ignore all values if a single one is badly formed, while all other field mappers will only ignore the problem value and index the rest. Finally, this structure makes adding index-time scripts to geo_point needlessly complex. This commit refactors the indexing logic of the hierarchy to move the individual value indexing logic into the concrete implementations, and aligns the ignore_malformed behaviour with that of other mappers. It contains two breaking changes: * The geo field mappers no longer check for external field values on the parse context. This added considerable complication to the refactored parse methods, and is unused anywhere in our codebase, but may impact plugin-based field mappers which expect to use geo fields as multifields * The geo_point field mapper now passes geohashes to its multifields one-by-one, instead of formatting them into a comma-delimited string and passing them all at once. Completion multifields using this as an input should still behave as normal because by default they would split this combined geohash string on the commas in any case, but keyword subfields may look different. Fixes elastic#69601
1 parent 88f6eb7 commit f44822a

30 files changed

+318
-565
lines changed

libs/x-content/src/main/java/org/elasticsearch/common/xcontent/support/MapXContentParser.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
import org.elasticsearch.common.xcontent.DeprecationHandler;
1212
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
1313
import org.elasticsearch.common.xcontent.XContentLocation;
14+
import org.elasticsearch.common.xcontent.XContentParser;
1415
import org.elasticsearch.common.xcontent.XContentType;
1516

1617
import java.io.IOException;
1718
import java.math.BigDecimal;
1819
import java.math.BigInteger;
1920
import java.nio.CharBuffer;
21+
import java.util.Collections;
2022
import java.util.Iterator;
2123
import java.util.List;
2224
import java.util.Map;
@@ -30,6 +32,17 @@ public class MapXContentParser extends AbstractXContentParser {
3032
private TokenIterator iterator;
3133
private boolean closed;
3234

35+
public static XContentParser wrapObject(Object sourceMap) throws IOException {
36+
XContentParser parser = new MapXContentParser(
37+
NamedXContentRegistry.EMPTY,
38+
DeprecationHandler.IGNORE_DEPRECATIONS,
39+
Collections.singletonMap("dummy_field", sourceMap), XContentType.JSON);
40+
parser.nextToken(); // start object
41+
parser.nextToken(); // field name
42+
parser.nextToken(); // field value
43+
return parser;
44+
}
45+
3346
public MapXContentParser(NamedXContentRegistry xContentRegistry, DeprecationHandler deprecationHandler, Map<String, Object> map,
3447
XContentType xContentType) {
3548
super(xContentRegistry, deprecationHandler);

server/src/internalClusterTest/java/org/elasticsearch/index/mapper/ExternalValuesMapperIntegrationIT.java

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,11 @@
99
package org.elasticsearch.index.mapper;
1010

1111
import org.elasticsearch.action.search.SearchResponse;
12-
import org.elasticsearch.common.geo.ShapeRelation;
13-
import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
1412
import org.elasticsearch.common.xcontent.XContentFactory;
1513
import org.elasticsearch.index.query.QueryBuilders;
1614
import org.elasticsearch.plugins.Plugin;
1715
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
1816
import org.elasticsearch.test.ESIntegTestCase;
19-
import org.locationtech.jts.geom.Coordinate;
2017

2118
import java.util.Collection;
2219
import java.util.Collections;
@@ -99,19 +96,6 @@ public void testExternalValues() throws Exception {
9996

10097
assertThat(response.getHits().getTotalHits().value, equalTo((long) 1));
10198

102-
response = client().prepareSearch("test-idx")
103-
.setPostFilter(QueryBuilders.geoDistanceQuery("field.point").point(42.0, 51.0).distance("1km"))
104-
.execute().actionGet();
105-
106-
assertThat(response.getHits().getTotalHits().value, equalTo((long) 1));
107-
108-
response = client().prepareSearch("test-idx")
109-
.setPostFilter(QueryBuilders.geoShapeQuery("field.shape",
110-
new EnvelopeBuilder(new Coordinate(-101, 46), new Coordinate(-99, 44))).relation(ShapeRelation.WITHIN))
111-
.execute().actionGet();
112-
113-
assertThat(response.getHits().getTotalHits().value, equalTo((long) 1));
114-
11599
response = client().prepareSearch("test-idx")
116100
.setPostFilter(QueryBuilders.termQuery("field.field", "foo"))
117101
.execute().actionGet();

server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java

Lines changed: 41 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,29 @@
77
*/
88
package org.elasticsearch.index.mapper;
99

10-
import org.apache.lucene.index.IndexableField;
1110
import org.apache.lucene.search.Query;
11+
import org.apache.lucene.util.SetOnce;
12+
import org.elasticsearch.common.CheckedConsumer;
1213
import org.elasticsearch.common.Explicit;
1314
import org.elasticsearch.common.geo.GeoJsonGeometryFormat;
14-
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
15-
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
1615
import org.elasticsearch.common.xcontent.XContentParser;
17-
import org.elasticsearch.common.xcontent.XContentType;
1816
import org.elasticsearch.common.xcontent.support.MapXContentParser;
1917
import org.elasticsearch.index.analysis.NamedAnalyzer;
2018
import org.elasticsearch.index.query.SearchExecutionContext;
2119

2220
import java.io.IOException;
2321
import java.io.UncheckedIOException;
24-
import java.text.ParseException;
2522
import java.util.ArrayList;
2623
import java.util.Collections;
2724
import java.util.List;
2825
import java.util.Map;
26+
import java.util.function.Consumer;
2927
import java.util.function.Function;
3028

3129
/**
3230
* Base field mapper class for all spatial field types
3331
*/
34-
public abstract class AbstractGeometryFieldMapper<Parsed, Processed> extends FieldMapper {
32+
public abstract class AbstractGeometryFieldMapper<T> extends FieldMapper {
3533

3634
public static Parameter<Explicit<Boolean>> ignoreMalformedParam(Function<FieldMapper, Explicit<Boolean>> initializer,
3735
boolean ignoreMalformedByDefault) {
@@ -42,52 +40,33 @@ public static Parameter<Explicit<Boolean>> ignoreZValueParam(Function<FieldMappe
4240
return Parameter.explicitBoolParam("ignore_z_value", true, initializer, true);
4341
}
4442

45-
/**
46-
* Interface representing an preprocessor in geometry indexing pipeline
47-
*/
48-
public interface Indexer<Parsed, Processed> {
49-
Processed prepareForIndexing(Parsed geometry);
50-
Class<Processed> processedClass();
51-
List<IndexableField> indexShape(ParseContext context, Processed shape);
52-
}
53-
5443
/**
5544
* Interface representing parser in geometry indexing pipeline.
5645
*/
57-
public abstract static class Parser<Parsed> {
46+
public abstract static class Parser<T> {
5847
/**
59-
* Parse the given xContent value to an object of type {@link Parsed}. The value can be
48+
* Parse the given xContent value to one or more objects of type {@link T}. The value can be
6049
* in any supported format.
6150
*/
62-
public abstract Parsed parse(XContentParser parser) throws IOException, ParseException;
51+
public abstract void parse(
52+
XContentParser parser,
53+
CheckedConsumer<T, IOException> consumer,
54+
Consumer<Exception> onMalformed) throws IOException;
6355

6456
/**
6557
* Given a parsed value and a format string, formats the value into a plain Java object.
6658
*
6759
* Supported formats include 'geojson' and 'wkt'. The different formats are defined
6860
* as subclasses of {@link org.elasticsearch.common.geo.GeometryFormat}.
6961
*/
70-
public abstract Object format(Parsed value, String format);
62+
public abstract Object format(T value, String format);
7163

72-
/**
73-
* Parses the given value, then formats it according to the 'format' string.
74-
*
75-
* Used by value fetchers to validate and format geo objects
76-
*/
77-
public Object parseAndFormatObject(Object value, String format) {
78-
Parsed geometry;
79-
try (XContentParser parser = new MapXContentParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE,
80-
Collections.singletonMap("dummy_field", value), XContentType.JSON)) {
81-
parser.nextToken(); // start object
82-
parser.nextToken(); // field name
83-
parser.nextToken(); // field value
84-
geometry = parse(parser);
64+
private void fetchFromSource(Object sourceMap, Consumer<Object> consumer, String format) {
65+
try (XContentParser parser = MapXContentParser.wrapObject(sourceMap)) {
66+
parse(parser, v -> consumer.accept(format(v, format)), e -> {}); /* ignore malformed */
8567
} catch (IOException e) {
8668
throw new UncheckedIOException(e);
87-
} catch (ParseException e) {
88-
throw new RuntimeException(e);
8969
}
90-
return format(geometry, format);
9170
}
9271
}
9372

@@ -113,19 +92,22 @@ public final Query termQuery(Object value, SearchExecutionContext context) {
11392
public final ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
11493
String geoFormat = format != null ? format : GeoJsonGeometryFormat.NAME;
11594

116-
Function<Object, Object> valueParser = value -> geometryParser.parseAndFormatObject(value, geoFormat);
11795
if (parsesArrayValue) {
11896
return new ArraySourceValueFetcher(name(), context) {
11997
@Override
12098
protected Object parseSourceValue(Object value) {
121-
return valueParser.apply(value);
99+
List<Object> values = new ArrayList<>();
100+
geometryParser.fetchFromSource(value, values::add, geoFormat);
101+
return values;
122102
}
123103
};
124104
} else {
125105
return new SourceValueFetcher(name(), context) {
126106
@Override
127107
protected Object parseSourceValue(Object value) {
128-
return valueParser.apply(value);
108+
SetOnce<Object> holder = new SetOnce<>();
109+
geometryParser.fetchFromSource(value, holder::set, geoFormat);
110+
return holder.get();
129111
}
130112
};
131113
}
@@ -134,26 +116,24 @@ protected Object parseSourceValue(Object value) {
134116

135117
private final Explicit<Boolean> ignoreMalformed;
136118
private final Explicit<Boolean> ignoreZValue;
137-
private final Indexer<Parsed, Processed> indexer;
138-
private final Parser<Parsed> parser;
119+
private final Parser<T> parser;
139120

140121
protected AbstractGeometryFieldMapper(String simpleName, MappedFieldType mappedFieldType,
141122
Map<String, NamedAnalyzer> indexAnalyzers,
142123
Explicit<Boolean> ignoreMalformed, Explicit<Boolean> ignoreZValue,
143124
MultiFields multiFields, CopyTo copyTo,
144-
Indexer<Parsed, Processed> indexer, Parser<Parsed> parser) {
125+
Parser<T> parser) {
145126
super(simpleName, mappedFieldType, indexAnalyzers, multiFields, copyTo, false, null);
146127
this.ignoreMalformed = ignoreMalformed;
147128
this.ignoreZValue = ignoreZValue;
148-
this.indexer = indexer;
149129
this.parser = parser;
150130
}
151131

152132
protected AbstractGeometryFieldMapper(String simpleName, MappedFieldType mappedFieldType,
153133
Explicit<Boolean> ignoreMalformed, Explicit<Boolean> ignoreZValue,
154134
MultiFields multiFields, CopyTo copyTo,
155-
Indexer<Parsed, Processed> indexer, Parser<Parsed> parser) {
156-
this(simpleName, mappedFieldType, Collections.emptyMap(), ignoreMalformed, ignoreZValue, multiFields, copyTo, indexer, parser);
135+
Parser<T> parser) {
136+
this(simpleName, mappedFieldType, Collections.emptyMap(), ignoreMalformed, ignoreZValue, multiFields, copyTo, parser);
157137
}
158138

159139
@Override
@@ -166,60 +146,25 @@ protected void parseCreateField(ParseContext context) throws IOException {
166146
throw new UnsupportedOperationException("Parsing is implemented in parse(), this method should NEVER be called");
167147
}
168148

169-
protected abstract void addStoredFields(ParseContext context, Processed geometry);
170-
protected abstract void addDocValuesFields(String name, Processed geometry, List<IndexableField> fields, ParseContext context);
171-
protected abstract void addMultiFields(ParseContext context, Processed geometry) throws IOException;
149+
/**
150+
* Build an index document using a parsed geometry
151+
* @param context the ParseContext holding the document
152+
* @param geometry the parsed geometry object
153+
*/
154+
protected abstract void index(ParseContext context, T geometry) throws IOException;
172155

173-
/** parsing logic for geometry indexing */
174156
@Override
175-
public void parse(ParseContext context) throws IOException {
176-
MappedFieldType mappedFieldType = fieldType();
177-
178-
try {
179-
Processed shape = context.parseExternalValue(indexer.processedClass());
180-
if (shape == null) {
181-
Parsed geometry = parser.parse(context.parser());
182-
if (geometry == null) {
183-
return;
184-
}
185-
shape = indexer.prepareForIndexing(geometry);
186-
}
187-
188-
List<IndexableField> fields = new ArrayList<>();
189-
if (mappedFieldType.isSearchable() || mappedFieldType.hasDocValues()) {
190-
fields.addAll(indexer.indexShape(context, shape));
191-
}
192-
193-
// indexed:
194-
List<IndexableField> indexedFields = new ArrayList<>();
195-
if (mappedFieldType.isSearchable()) {
196-
indexedFields.addAll(fields);
197-
}
198-
// stored:
199-
if (fieldType().isStored()) {
200-
addStoredFields(context, shape);
201-
}
202-
// docValues:
203-
if (fieldType().hasDocValues()) {
204-
addDocValuesFields(mappedFieldType.name(), shape, fields, context);
205-
} else if (fieldType().isStored() || fieldType().isSearchable()) {
206-
createFieldNamesField(context);
207-
}
208-
209-
// add the indexed fields to the doc:
210-
for (IndexableField field : indexedFields) {
211-
context.doc().add(field);
212-
}
213-
214-
// add multifields (e.g., used for completion suggester)
215-
addMultiFields(context, shape);
216-
} catch (Exception e) {
217-
if (ignoreMalformed.value() == false) {
218-
throw new MapperParsingException("failed to parse field [{}] of type [{}]", e, fieldType().name(),
219-
fieldType().typeName());
220-
}
221-
context.addIgnoredField(mappedFieldType.name());
222-
}
157+
public final void parse(ParseContext context) throws IOException {
158+
parser.parse(context.parser(), v -> index(context, v), e -> {
159+
if (ignoreMalformed()) {
160+
context.addIgnoredField(fieldType().name());
161+
} else {
162+
throw new MapperParsingException(
163+
"Failed to parse field [" + fieldType().name() + "] of type [" + contentType() + "]",
164+
e
165+
);
166+
}
167+
});
223168
}
224169

225170
public boolean ignoreMalformed() {

0 commit comments

Comments
 (0)