GeoPointFieldMapper.java 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. /*
  2. * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
  3. * or more contributor license agreements. Licensed under the "Elastic License
  4. * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
  5. * Public License v 1"; you may not use this file except in compliance with, at
  6. * your election, the "Elastic License 2.0", the "GNU Affero General Public
  7. * License v3.0 only", or the "Server Side Public License, v 1".
  8. */
  9. package org.elasticsearch.index.mapper;
  10. import org.apache.lucene.document.Field;
  11. import org.apache.lucene.document.FieldType;
  12. import org.apache.lucene.document.LatLonDocValuesField;
  13. import org.apache.lucene.document.LatLonPoint;
  14. import org.apache.lucene.document.ShapeField;
  15. import org.apache.lucene.document.StoredField;
  16. import org.apache.lucene.geo.GeoEncodingUtils;
  17. import org.apache.lucene.geo.LatLonGeometry;
  18. import org.apache.lucene.index.DocValuesType;
  19. import org.apache.lucene.index.LeafReaderContext;
  20. import org.apache.lucene.search.IndexOrDocValuesQuery;
  21. import org.apache.lucene.search.Query;
  22. import org.apache.lucene.util.BytesRef;
  23. import org.apache.lucene.util.NumericUtils;
  24. import org.elasticsearch.ElasticsearchParseException;
  25. import org.elasticsearch.common.Explicit;
  26. import org.elasticsearch.common.geo.GeoFormatterFactory;
  27. import org.elasticsearch.common.geo.GeoPoint;
  28. import org.elasticsearch.common.geo.GeoUtils;
  29. import org.elasticsearch.common.geo.GeometryFormatterFactory;
  30. import org.elasticsearch.common.geo.ShapeRelation;
  31. import org.elasticsearch.common.geo.SimpleVectorTileFormatter;
  32. import org.elasticsearch.common.unit.DistanceUnit;
  33. import org.elasticsearch.core.CheckedConsumer;
  34. import org.elasticsearch.core.CheckedFunction;
  35. import org.elasticsearch.geometry.Point;
  36. import org.elasticsearch.index.IndexMode;
  37. import org.elasticsearch.index.IndexVersion;
  38. import org.elasticsearch.index.fielddata.FieldDataContext;
  39. import org.elasticsearch.index.fielddata.IndexFieldData;
  40. import org.elasticsearch.index.fielddata.SourceValueFetcherMultiGeoPointIndexFieldData;
  41. import org.elasticsearch.index.fielddata.plain.LatLonPointIndexFieldData;
  42. import org.elasticsearch.index.query.SearchExecutionContext;
  43. import org.elasticsearch.script.GeoPointFieldScript;
  44. import org.elasticsearch.script.Script;
  45. import org.elasticsearch.script.ScriptCompiler;
  46. import org.elasticsearch.script.SortedNumericDocValuesLongFieldScript;
  47. import org.elasticsearch.script.field.GeoPointDocValuesField;
  48. import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
  49. import org.elasticsearch.search.aggregations.support.TimeSeriesValuesSourceType;
  50. import org.elasticsearch.search.aggregations.support.ValuesSourceType;
  51. import org.elasticsearch.search.lookup.FieldValues;
  52. import org.elasticsearch.search.lookup.SearchLookup;
  53. import org.elasticsearch.search.runtime.GeoPointScriptFieldDistanceFeatureQuery;
  54. import org.elasticsearch.xcontent.CopyingXContentParser;
  55. import org.elasticsearch.xcontent.FilterXContentParserWrapper;
  56. import org.elasticsearch.xcontent.ToXContent;
  57. import org.elasticsearch.xcontent.XContentBuilder;
  58. import org.elasticsearch.xcontent.XContentParser;
  59. import java.io.IOException;
  60. import java.io.UncheckedIOException;
  61. import java.util.Collections;
  62. import java.util.List;
  63. import java.util.Map;
  64. import java.util.Objects;
  65. import java.util.Set;
  66. import java.util.function.Function;
  67. import static org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference.DOC_VALUES;
  68. /**
  69. * Field Mapper for geo_point types.
  70. *
  71. * Uses lucene 6 LatLonPoint encoding
  72. */
  73. public class GeoPointFieldMapper extends AbstractPointGeometryFieldMapper<GeoPoint> {
  74. public static final String CONTENT_TYPE = "geo_point";
  75. private static Builder builder(FieldMapper in) {
  76. return toType(in).builder;
  77. }
  78. private static GeoPointFieldMapper toType(FieldMapper in) {
  79. return (GeoPointFieldMapper) in;
  80. }
  81. public static final class Builder extends FieldMapper.Builder {
  82. final Parameter<Explicit<Boolean>> ignoreMalformed;
  83. final Parameter<Explicit<Boolean>> ignoreZValue = ignoreZValueParam(m -> builder(m).ignoreZValue.get());
  84. final Parameter<GeoPoint> nullValue;
  85. final Parameter<Boolean> indexed;
  86. final Parameter<Boolean> hasDocValues = Parameter.docValuesParam(m -> builder(m).hasDocValues.get(), true);
  87. final Parameter<Boolean> stored = Parameter.storeParam(m -> builder(m).stored.get(), false);
  88. private final Parameter<Script> script = Parameter.scriptParam(m -> builder(m).script.get());
  89. private final Parameter<OnScriptError> onScriptErrorParam = Parameter.onScriptErrorParam(
  90. m -> builder(m).onScriptErrorParam.get(),
  91. script
  92. );
  93. final Parameter<Map<String, String>> meta = Parameter.metaParam();
  94. private final ScriptCompiler scriptCompiler;
  95. private final IndexVersion indexCreatedVersion;
  96. private final Parameter<TimeSeriesParams.MetricType> metric; // either null, or POSITION if this is a time series metric
  97. private final Parameter<Boolean> dimension; // can only support time_series_dimension: false
  98. private final IndexMode indexMode; // either STANDARD or TIME_SERIES
  99. public Builder(
  100. String name,
  101. ScriptCompiler scriptCompiler,
  102. boolean ignoreMalformedByDefault,
  103. IndexVersion indexCreatedVersion,
  104. IndexMode mode
  105. ) {
  106. super(name);
  107. this.ignoreMalformed = ignoreMalformedParam(m -> builder(m).ignoreMalformed.get(), ignoreMalformedByDefault);
  108. this.nullValue = nullValueParam(
  109. m -> builder(m).nullValue.get(),
  110. (n, c, o) -> parseNullValue(o, ignoreZValue.get().value(), ignoreMalformed.get().value()),
  111. () -> null,
  112. XContentBuilder::field
  113. ).acceptsNull();
  114. this.scriptCompiler = Objects.requireNonNull(scriptCompiler);
  115. this.indexCreatedVersion = Objects.requireNonNull(indexCreatedVersion);
  116. this.script.precludesParameters(nullValue, ignoreMalformed, ignoreZValue);
  117. this.indexMode = mode;
  118. this.indexed = Parameter.indexParam(
  119. m -> toType(m).indexed,
  120. () -> indexMode != IndexMode.TIME_SERIES || getMetric().getValue() != TimeSeriesParams.MetricType.POSITION
  121. );
  122. addScriptValidation(script, indexed, hasDocValues);
  123. this.metric = TimeSeriesParams.metricParam(m -> toType(m).metricType, TimeSeriesParams.MetricType.POSITION).addValidator(v -> {
  124. if (v != null && hasDocValues.getValue() == false) {
  125. throw new IllegalArgumentException(
  126. "Field [" + TimeSeriesParams.TIME_SERIES_METRIC_PARAM + "] requires that [" + hasDocValues.name + "] is true"
  127. );
  128. }
  129. });
  130. // We allow `time_series_dimension` parameter to be parsed, but only allow it to be `false`
  131. this.dimension = TimeSeriesParams.dimensionParam(m -> false).addValidator(v -> {
  132. if (v) {
  133. throw new IllegalArgumentException(
  134. "Parameter [" + TimeSeriesParams.TIME_SERIES_DIMENSION_PARAM + "] cannot be set to geo_point"
  135. );
  136. }
  137. });
  138. }
  139. private Parameter<TimeSeriesParams.MetricType> getMetric() {
  140. return metric;
  141. }
  142. @Override
  143. protected Parameter<?>[] getParameters() {
  144. return new Parameter<?>[] {
  145. hasDocValues,
  146. indexed,
  147. stored,
  148. ignoreMalformed,
  149. ignoreZValue,
  150. nullValue,
  151. script,
  152. onScriptErrorParam,
  153. meta,
  154. dimension,
  155. metric };
  156. }
  157. public Builder docValues(boolean hasDocValues) {
  158. this.hasDocValues.setValue(hasDocValues);
  159. return this;
  160. }
  161. private static GeoPoint parseNullValue(Object nullValue, boolean ignoreZValue, boolean ignoreMalformed) {
  162. if (nullValue == null) {
  163. return null;
  164. }
  165. GeoPoint point = GeoUtils.parseGeoPoint(nullValue, ignoreZValue);
  166. if (ignoreMalformed == false) {
  167. if (point.lat() > 90.0 || point.lat() < -90.0) {
  168. throw new IllegalArgumentException("illegal latitude value [" + point.lat() + "]");
  169. }
  170. if (point.lon() > 180.0 || point.lon() < -180) {
  171. throw new IllegalArgumentException("illegal longitude value [" + point.lon() + "]");
  172. }
  173. } else {
  174. GeoUtils.normalizePoint(point);
  175. }
  176. return point;
  177. }
  178. private FieldValues<GeoPoint> scriptValues() {
  179. if (this.script.get() == null) {
  180. return null;
  181. }
  182. GeoPointFieldScript.Factory factory = scriptCompiler.compile(this.script.get(), GeoPointFieldScript.CONTEXT);
  183. return factory == null
  184. ? null
  185. : (lookup, ctx, doc, consumer) -> factory.newFactory(leafName(), script.get().getParams(), lookup, OnScriptError.FAIL)
  186. .newInstance(ctx)
  187. .runForDoc(doc, consumer);
  188. }
  189. @Override
  190. public FieldMapper build(MapperBuilderContext context) {
  191. boolean ignoreMalformedEnabled = ignoreMalformed.get().value();
  192. Parser<GeoPoint> geoParser = new GeoPointParser(
  193. leafName(),
  194. (parser) -> GeoUtils.parseGeoPoint(parser, ignoreZValue.get().value()),
  195. nullValue.get(),
  196. ignoreZValue.get().value(),
  197. ignoreMalformedEnabled,
  198. metric.get() != TimeSeriesParams.MetricType.POSITION,
  199. context.isSourceSynthetic() && ignoreMalformedEnabled
  200. );
  201. GeoPointFieldType ft = new GeoPointFieldType(
  202. context.buildFullName(leafName()),
  203. indexed.get() && indexCreatedVersion.isLegacyIndexVersion() == false,
  204. stored.get(),
  205. hasDocValues.get(),
  206. geoParser,
  207. nullValue.get(),
  208. scriptValues(),
  209. meta.get(),
  210. metric.get(),
  211. indexMode,
  212. context.isSourceSynthetic()
  213. );
  214. hasScript = script.get() != null;
  215. onScriptError = onScriptErrorParam.get();
  216. return new GeoPointFieldMapper(leafName(), ft, builderParams(this, context), geoParser, this);
  217. }
  218. }
  219. public static final TypeParser PARSER = createTypeParserWithLegacySupport(
  220. (n, c) -> new Builder(
  221. n,
  222. c.scriptCompiler(),
  223. IGNORE_MALFORMED_SETTING.get(c.getSettings()),
  224. c.indexVersionCreated(),
  225. c.getIndexSettings().getMode()
  226. )
  227. );
  228. private final Builder builder;
  229. private final FieldValues<GeoPoint> scriptValues;
  230. private final IndexVersion indexCreatedVersion;
  231. private final TimeSeriesParams.MetricType metricType;
  232. private final IndexMode indexMode;
  233. private final boolean indexed;
  234. public GeoPointFieldMapper(
  235. String simpleName,
  236. MappedFieldType mappedFieldType,
  237. BuilderParams builderParams,
  238. Parser<GeoPoint> parser,
  239. Builder builder
  240. ) {
  241. super(
  242. simpleName,
  243. mappedFieldType,
  244. builderParams,
  245. builder.ignoreMalformed.get(),
  246. builder.ignoreZValue.get(),
  247. builder.nullValue.get(),
  248. parser
  249. );
  250. this.builder = builder;
  251. this.scriptValues = builder.scriptValues();
  252. this.indexCreatedVersion = builder.indexCreatedVersion;
  253. this.metricType = builder.metric.get();
  254. this.indexMode = builder.indexMode;
  255. this.indexed = builder.indexed.get();
  256. }
  257. @Override
  258. public FieldMapper.Builder getMergeBuilder() {
  259. return new Builder(
  260. leafName(),
  261. builder.scriptCompiler,
  262. builder.ignoreMalformed.getDefaultValue().value(),
  263. indexCreatedVersion,
  264. indexMode
  265. ).init(this);
  266. }
  267. @Override
  268. protected void index(DocumentParserContext context, GeoPoint geometry) throws IOException {
  269. final boolean indexed = fieldType().isIndexed();
  270. final boolean hasDocValues = fieldType().hasDocValues();
  271. final boolean store = fieldType().isStored();
  272. if (indexed && hasDocValues) {
  273. context.doc().add(new LatLonPointWithDocValues(fieldType().name(), geometry.lat(), geometry.lon()));
  274. } else if (hasDocValues) {
  275. context.doc().add(new LatLonDocValuesField(fieldType().name(), geometry.lat(), geometry.lon()));
  276. } else if (indexed) {
  277. context.doc().add(new LatLonPoint(fieldType().name(), geometry.lat(), geometry.lon()));
  278. }
  279. if (store) {
  280. context.doc().add(new StoredField(fieldType().name(), geometry.toString()));
  281. }
  282. if (hasDocValues == false && (indexed || store)) {
  283. // When the field doesn't have doc values so that we can run exists queries, we also need to index the field name separately.
  284. context.addToFieldNames(fieldType().name());
  285. }
  286. // TODO phase out geohash (which is currently used in the CompletionSuggester)
  287. // we only expose the geohash value and disallow advancing tokens, hence we can reuse the same parser throughout multiple sub-fields
  288. DocumentParserContext parserContext = context.switchParser(new GeoHashMultiFieldParser(context.parser(), geometry.geohash()));
  289. multiFields().parse(this, context, () -> parserContext);
  290. }
  291. /**
  292. * Parser that pretends to be the main document parser, but exposes the provided geohash regardless of how the geopoint was provided
  293. * in the incoming document. We rely on the fact that consumers are only ever call {@link XContentParser#textOrNull()} and never
  294. * advance tokens, which is explicitly disallowed by this parser.
  295. */
  296. static class GeoHashMultiFieldParser extends FilterXContentParserWrapper {
  297. private final String value;
  298. GeoHashMultiFieldParser(XContentParser innerParser, String value) {
  299. super(innerParser);
  300. this.value = value;
  301. }
  302. @Override
  303. public String textOrNull() throws IOException {
  304. return value;
  305. }
  306. @Override
  307. public Token currentToken() {
  308. return Token.VALUE_STRING;
  309. }
  310. @Override
  311. public Token nextToken() throws IOException {
  312. throw new UnsupportedOperationException();
  313. }
  314. }
  315. @Override
  316. protected void indexScriptValues(
  317. SearchLookup searchLookup,
  318. LeafReaderContext readerContext,
  319. int doc,
  320. DocumentParserContext documentParserContext
  321. ) {
  322. this.scriptValues.valuesForDoc(searchLookup, readerContext, doc, point -> {
  323. try {
  324. index(documentParserContext, point);
  325. } catch (IOException e) {
  326. throw new UncheckedIOException(e); // only thrown by MultiFields which is always null
  327. }
  328. });
  329. }
  330. @Override
  331. protected String contentType() {
  332. return CONTENT_TYPE;
  333. }
  334. public static class GeoPointFieldType extends AbstractPointFieldType<GeoPoint> implements GeoShapeQueryable {
  335. private final TimeSeriesParams.MetricType metricType;
  336. public static final GeoFormatterFactory<GeoPoint> GEO_FORMATTER_FACTORY = new GeoFormatterFactory<>(
  337. List.of(new SimpleVectorTileFormatter())
  338. );
  339. private final FieldValues<GeoPoint> scriptValues;
  340. private final IndexMode indexMode;
  341. private final boolean isSyntheticSource;
  342. private GeoPointFieldType(
  343. String name,
  344. boolean indexed,
  345. boolean stored,
  346. boolean hasDocValues,
  347. Parser<GeoPoint> parser,
  348. GeoPoint nullValue,
  349. FieldValues<GeoPoint> scriptValues,
  350. Map<String, String> meta,
  351. TimeSeriesParams.MetricType metricType,
  352. IndexMode indexMode,
  353. boolean isSyntheticSource
  354. ) {
  355. super(name, indexed, stored, hasDocValues, parser, nullValue, meta);
  356. this.scriptValues = scriptValues;
  357. this.metricType = metricType;
  358. this.indexMode = indexMode;
  359. this.isSyntheticSource = isSyntheticSource;
  360. }
  361. // only used in test
  362. public GeoPointFieldType(String name, TimeSeriesParams.MetricType metricType, IndexMode indexMode) {
  363. this(name, true, false, true, null, null, null, Collections.emptyMap(), metricType, indexMode, false);
  364. }
  365. // only used in test
  366. public GeoPointFieldType(String name) {
  367. this(name, null, null);
  368. }
  369. @Override
  370. public String typeName() {
  371. return CONTENT_TYPE;
  372. }
  373. @Override
  374. public boolean isSearchable() {
  375. return isIndexed() || hasDocValues();
  376. }
  377. @Override
  378. protected Function<List<GeoPoint>, List<Object>> getFormatter(String format) {
  379. return GEO_FORMATTER_FACTORY.getFormatter(format, p -> new Point(p.getLon(), p.getLat()));
  380. }
  381. @Override
  382. public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
  383. if (scriptValues == null) {
  384. return super.valueFetcher(context, format);
  385. }
  386. Function<List<GeoPoint>, List<Object>> formatter = getFormatter(format != null ? format : GeometryFormatterFactory.GEOJSON);
  387. return FieldValues.valueListFetcher(scriptValues, formatter, context);
  388. }
  389. @Override
  390. public Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, LatLonGeometry... geometries) {
  391. failIfNotIndexedNorDocValuesFallback(context);
  392. final ShapeField.QueryRelation luceneRelation;
  393. if (relation == ShapeRelation.INTERSECTS && isPointGeometry(geometries)) {
  394. // For point queries and intersects, lucene does not match points that are encoded
  395. // to Integer.MAX_VALUE because the use of ComponentPredicate for speeding up queries.
  396. // We use contains instead.
  397. luceneRelation = ShapeField.QueryRelation.CONTAINS;
  398. } else {
  399. luceneRelation = relation.getLuceneRelation();
  400. }
  401. Query query;
  402. if (isIndexed()) {
  403. query = LatLonPoint.newGeometryQuery(fieldName, luceneRelation, geometries);
  404. if (hasDocValues()) {
  405. Query dvQuery = LatLonDocValuesField.newSlowGeometryQuery(fieldName, luceneRelation, geometries);
  406. query = new IndexOrDocValuesQuery(query, dvQuery);
  407. }
  408. } else {
  409. query = LatLonDocValuesField.newSlowGeometryQuery(fieldName, luceneRelation, geometries);
  410. }
  411. return query;
  412. }
  413. private static boolean isPointGeometry(LatLonGeometry[] geometries) {
  414. return geometries.length == 1 && geometries[0] instanceof org.apache.lucene.geo.Point;
  415. }
  416. @Override
  417. public IndexFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext) {
  418. FielddataOperation operation = fieldDataContext.fielddataOperation();
  419. if (operation == FielddataOperation.SEARCH) {
  420. failIfNoDocValues();
  421. }
  422. ValuesSourceType valuesSourceType = indexMode == IndexMode.TIME_SERIES && metricType == TimeSeriesParams.MetricType.POSITION
  423. ? TimeSeriesValuesSourceType.POSITION
  424. : CoreValuesSourceType.GEOPOINT;
  425. if ((operation == FielddataOperation.SEARCH || operation == FielddataOperation.SCRIPT) && hasDocValues()) {
  426. return new LatLonPointIndexFieldData.Builder(name(), valuesSourceType, GeoPointDocValuesField::new);
  427. }
  428. if (operation == FielddataOperation.SCRIPT) {
  429. SearchLookup searchLookup = fieldDataContext.lookupSupplier().get();
  430. Set<String> sourcePaths = fieldDataContext.sourcePathsLookup().apply(name());
  431. return new SourceValueFetcherMultiGeoPointIndexFieldData.Builder(
  432. name(),
  433. valuesSourceType,
  434. valueFetcher(sourcePaths, null, null),
  435. searchLookup,
  436. GeoPointDocValuesField::new
  437. );
  438. }
  439. throw new IllegalStateException("unknown field data type [" + operation.name() + "]");
  440. }
  441. @Override
  442. public Query distanceFeatureQuery(Object origin, String pivot, SearchExecutionContext context) {
  443. failIfNotIndexedNorDocValuesFallback(context);
  444. GeoPoint originGeoPoint;
  445. if (origin instanceof GeoPoint) {
  446. originGeoPoint = (GeoPoint) origin;
  447. } else if (origin instanceof String) {
  448. originGeoPoint = GeoUtils.parseFromString((String) origin);
  449. } else {
  450. throw new IllegalArgumentException(
  451. "Illegal type ["
  452. + origin.getClass()
  453. + "] for [origin]! "
  454. + "Must be of type [geo_point] or [string] for geo_point fields!"
  455. );
  456. }
  457. double pivotDouble = DistanceUnit.DEFAULT.parse(pivot, DistanceUnit.DEFAULT);
  458. if (isIndexed()) {
  459. // As we already apply boost in AbstractQueryBuilder::toQuery, we always passing a boost of 1.0 to distanceFeatureQuery
  460. return LatLonPoint.newDistanceFeatureQuery(name(), 1.0f, originGeoPoint.lat(), originGeoPoint.lon(), pivotDouble);
  461. } else {
  462. return new GeoPointScriptFieldDistanceFeatureQuery(
  463. new Script(""),
  464. ctx -> new SortedNumericDocValuesLongFieldScript(name(), context.lookup(), ctx),
  465. name(),
  466. originGeoPoint.lat(),
  467. originGeoPoint.lon(),
  468. pivotDouble
  469. );
  470. }
  471. }
  472. /**
  473. * If field is a time series metric field, returns its metric type
  474. * @return the metric type or null
  475. */
  476. @Override
  477. public TimeSeriesParams.MetricType getMetricType() {
  478. return metricType;
  479. }
  480. @Override
  481. public BlockLoader blockLoader(BlockLoaderContext blContext) {
  482. if (blContext.fieldExtractPreference() == DOC_VALUES && hasDocValues()) {
  483. return new BlockDocValuesReader.LongsBlockLoader(name());
  484. }
  485. // There are two scenarios possible once we arrive here:
  486. //
  487. // * Stored source - we'll just use blockLoaderFromSource
  488. // * Synthetic source. However, because of the fieldExtractPreference() check above it is still possible that doc_values are
  489. // present here.
  490. // So we have two subcases:
  491. // - doc_values are enabled - _ignored_source field does not exist since we have doc_values. We will use
  492. // blockLoaderFromSource which reads "native" synthetic source.
  493. // - doc_values are disabled - we know that _ignored_source field is present and use a special block loader.
  494. if (isSyntheticSource && hasDocValues() == false) {
  495. return blockLoaderFromFallbackSyntheticSource(blContext);
  496. }
  497. return blockLoaderFromSource(blContext);
  498. }
  499. }
  500. /** GeoPoint parser implementation */
  501. private static class GeoPointParser extends PointParser<GeoPoint> {
  502. private final boolean storeMalformedDataForSyntheticSource;
  503. GeoPointParser(
  504. String field,
  505. CheckedFunction<XContentParser, GeoPoint, IOException> objectParser,
  506. GeoPoint nullValue,
  507. boolean ignoreZValue,
  508. boolean ignoreMalformed,
  509. boolean allowMultipleValues,
  510. boolean storeMalformedDataForSyntheticSource
  511. ) {
  512. super(field, objectParser, nullValue, ignoreZValue, ignoreMalformed, allowMultipleValues);
  513. this.storeMalformedDataForSyntheticSource = storeMalformedDataForSyntheticSource;
  514. }
  515. protected GeoPoint validate(GeoPoint in) {
  516. if (ignoreMalformed == false) {
  517. if (in.lat() > 90.0 || in.lat() < -90.0) {
  518. throw new IllegalArgumentException("illegal latitude value [" + in.lat() + "] for " + field);
  519. }
  520. if (in.lon() > 180.0 || in.lon() < -180) {
  521. throw new IllegalArgumentException("illegal longitude value [" + in.lon() + "] for " + field);
  522. }
  523. } else {
  524. if (isNormalizable(in.lat()) && isNormalizable(in.lon())) {
  525. GeoUtils.normalizePoint(in);
  526. } else {
  527. throw new ElasticsearchParseException("cannot normalize the point - not a number");
  528. }
  529. }
  530. return in;
  531. }
  532. private static boolean isNormalizable(double coord) {
  533. return Double.isNaN(coord) == false && Double.isInfinite(coord) == false;
  534. }
  535. @Override
  536. protected GeoPoint createPoint(double x, double y) {
  537. return new GeoPoint(y, x);
  538. }
  539. @Override
  540. public GeoPoint normalizeFromSource(GeoPoint point) {
  541. // normalize during parsing
  542. return point;
  543. }
  544. @Override
  545. protected void parseAndConsumeFromObject(
  546. XContentParser parser,
  547. CheckedConsumer<GeoPoint, IOException> consumer,
  548. MalformedValueHandler malformedHandler
  549. ) throws IOException {
  550. XContentParser parserWithCustomization = parser;
  551. XContentBuilder malformedDataForSyntheticSource = null;
  552. if (storeMalformedDataForSyntheticSource) {
  553. if (parser.currentToken() == XContentParser.Token.START_OBJECT
  554. || parser.currentToken() == XContentParser.Token.START_ARRAY) {
  555. // We have a complex structure so we'll memorize it while parsing.
  556. var copyingParser = new CopyingXContentParser(parser);
  557. malformedDataForSyntheticSource = copyingParser.getBuilder();
  558. parserWithCustomization = copyingParser;
  559. } else {
  560. // We have a single value (e.g. a string) that is potentially malformed, let's simply remember it.
  561. malformedDataForSyntheticSource = XContentBuilder.builder(parser.contentType().xContent()).copyCurrentStructure(parser);
  562. }
  563. }
  564. try {
  565. GeoPoint point = objectParser.apply(parserWithCustomization);
  566. consumer.accept(validate(point));
  567. } catch (Exception e) {
  568. malformedHandler.notify(e, malformedDataForSyntheticSource);
  569. }
  570. }
  571. }
  572. @Override
  573. protected void onMalformedValue(DocumentParserContext context, XContentBuilder malformedDataForSyntheticSource, Exception cause)
  574. throws IOException {
  575. super.onMalformedValue(context, malformedDataForSyntheticSource, cause);
  576. if (malformedDataForSyntheticSource != null) {
  577. context.doc().add(IgnoreMalformedStoredValues.storedField(fullPath(), malformedDataForSyntheticSource));
  578. }
  579. }
  580. @Override
  581. protected SyntheticSourceSupport syntheticSourceSupport() {
  582. if (fieldType().hasDocValues()) {
  583. return new SyntheticSourceSupport.Native(
  584. () -> new SortedNumericDocValuesSyntheticFieldLoader(fullPath(), leafName(), ignoreMalformed()) {
  585. final GeoPoint point = new GeoPoint();
  586. @Override
  587. protected void writeValue(XContentBuilder b, long value) throws IOException {
  588. point.reset(GeoEncodingUtils.decodeLatitude((int) (value >>> 32)), GeoEncodingUtils.decodeLongitude((int) value));
  589. point.toXContent(b, ToXContent.EMPTY_PARAMS);
  590. }
  591. }
  592. );
  593. }
  594. return super.syntheticSourceSupport();
  595. }
  596. /**
  597. * Utility class that allows adding index and doc values in one field
  598. */
  599. public static class LatLonPointWithDocValues extends Field {
  600. public static final FieldType TYPE = new FieldType();
  601. static {
  602. TYPE.setDimensions(2, Integer.BYTES);
  603. TYPE.setDocValuesType(DocValuesType.SORTED_NUMERIC);
  604. TYPE.freeze();
  605. }
  606. // holds the doc value value.
  607. private final long docValue;
  608. public LatLonPointWithDocValues(String name, double latitude, double longitude) {
  609. super(name, TYPE);
  610. final byte[] bytes;
  611. if (fieldsData == null) {
  612. bytes = new byte[8];
  613. fieldsData = new BytesRef(bytes);
  614. } else {
  615. bytes = ((BytesRef) fieldsData).bytes;
  616. }
  617. final int latitudeEncoded = GeoEncodingUtils.encodeLatitude(latitude);
  618. final int longitudeEncoded = GeoEncodingUtils.encodeLongitude(longitude);
  619. NumericUtils.intToSortableBytes(latitudeEncoded, bytes, 0);
  620. NumericUtils.intToSortableBytes(longitudeEncoded, bytes, Integer.BYTES);
  621. docValue = (((long) latitudeEncoded) << 32) | (longitudeEncoded & 0xFFFFFFFFL);
  622. }
  623. @Override
  624. public Number numericValue() {
  625. return docValue;
  626. }
  627. @Override
  628. public String toString() {
  629. StringBuilder result = new StringBuilder();
  630. result.append(getClass().getSimpleName());
  631. result.append(" <");
  632. result.append(name);
  633. result.append(':');
  634. byte[] bytes = ((BytesRef) fieldsData).bytes;
  635. result.append(GeoEncodingUtils.decodeLatitude(bytes, 0));
  636. result.append(',');
  637. result.append(GeoEncodingUtils.decodeLongitude(bytes, Integer.BYTES));
  638. result.append('>');
  639. return result.toString();
  640. }
  641. }
  642. }