All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.index.mapper.LookupRuntimeFieldType Maven / Gradle / Ivy

/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0 and the Server Side Public License, v 1; you may not use this file except
 * in compliance with, at your election, the Elastic License 2.0 or the Server
 * Side Public License, v 1.
 */

package org.elasticsearch.index.mapper;

import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.Query;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.document.DocumentField;
import org.elasticsearch.index.query.QueryShardException;
import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.search.fetch.subphase.FieldAndFormat;
import org.elasticsearch.search.fetch.subphase.LookupField;
import org.elasticsearch.search.lookup.SearchLookup;
import org.elasticsearch.search.lookup.SourceLookup;
import org.elasticsearch.xcontent.XContentBuilder;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES;

/**
 * A runtime field that retrieves fields from related indices.
 * 
 * {
 *     "type": "lookup",
 *     "target_index": "an_external_index",
 *     "input_field": "ip_address",
 *     "target_field": "host_ip",
 *     "fetch_fields": [
 *         "field-1",
 *         "field-2"
 *     ]
 * }
 * 
*/ public final class LookupRuntimeFieldType extends MappedFieldType { public static final RuntimeField.Parser PARSER = new RuntimeField.Parser(Builder::new); public static final String CONTENT_TYPE = "lookup"; private static class Builder extends RuntimeField.Builder { private final FieldMapper.Parameter targetIndex = FieldMapper.Parameter.stringParam( "target_index", false, RuntimeField.initializerNotSupported(), null ).addValidator(v -> { if (Strings.isEmpty(v)) { throw new IllegalArgumentException("[target_index] parameter must be specified"); } }); private final FieldMapper.Parameter inputField = FieldMapper.Parameter.stringParam( "input_field", false, RuntimeField.initializerNotSupported(), null ).addValidator(inputField -> { if (Strings.isEmpty(inputField)) { throw new IllegalArgumentException("[input_field] parameter must be specified"); } if (inputField.equals(name)) { throw new IllegalArgumentException("lookup field [" + name + "] can't use input from itself"); } }); private final FieldMapper.Parameter targetField = FieldMapper.Parameter.stringParam( "target_field", false, RuntimeField.initializerNotSupported(), null ).addValidator(targetField -> { if (Strings.isEmpty(targetField)) { throw new IllegalArgumentException("[target_field] parameter must be specified"); } }); private static FieldMapper.Parameter> newFetchFields() { final FieldMapper.Parameter> fetchFields = new FieldMapper.Parameter<>( "fetch_fields", false, List::of, (s, ctx, o) -> parseFetchFields(o), RuntimeField.initializerNotSupported(), XContentBuilder::field, Object::toString ); fetchFields.addValidator(fields -> { if (fields.isEmpty()) { throw new MapperParsingException("[fetch_fields] parameter must not be empty"); } }); return fetchFields; } @SuppressWarnings("rawtypes") private static List parseFetchFields(Object o) { final List values = (List) o; return values.stream().map(v -> { if (v instanceof Map m) { final String field = (String) m.get(FieldAndFormat.FIELD_FIELD.getPreferredName()); final String format = (String) m.get(FieldAndFormat.FORMAT_FIELD.getPreferredName()); if (field == null) { throw new MapperParsingException("[field] parameter of [fetch_fields] must be provided"); } return new FieldAndFormat(field, format); } else if (v instanceof String s) { return new FieldAndFormat(s, null); } else { throw new MapperParsingException("unexpected value [" + v + "] for [fetch_fields] parameter"); } }).toList(); } private final FieldMapper.Parameter> fetchFields = newFetchFields(); Builder(String name) { super(name); } @Override protected List> getParameters() { final List> parameters = new ArrayList<>(super.getParameters()); parameters.add(targetIndex); parameters.add(inputField); parameters.add(targetField); parameters.add(fetchFields); return parameters; } @Override protected RuntimeField createRuntimeField(MappingParserContext parserContext) { final LookupRuntimeFieldType ft = new LookupRuntimeFieldType( name, meta(), targetIndex.get(), inputField.get(), targetField.get(), fetchFields.get() ); return new LeafRuntimeField(name, ft, getParameters()); } @Override protected RuntimeField createChildRuntimeField( MappingParserContext parserContext, String parentName, Function parentScriptFactory ) { return createRuntimeField(parserContext); } } private final String lookupIndex; private final String inputField; private final String targetField; private final List fetchFields; private LookupRuntimeFieldType( String name, Map meta, String lookupIndex, String inputField, String targetField, List fetchFields ) { super(name, false, false, false, TextSearchInfo.NONE, meta); this.lookupIndex = lookupIndex; this.inputField = inputField; this.targetField = targetField; this.fetchFields = fetchFields; } @Override public ValueFetcher valueFetcher(SearchExecutionContext context, String format) { if (context.allowExpensiveQueries() == false) { throw new ElasticsearchException( "cannot be executed against lookup field [" + name() + "] while [" + ALLOW_EXPENSIVE_QUERIES.getKey() + "] is set to [false]." ); } return new LookupFieldValueFetcher(context); } @Override public String typeName() { return CONTENT_TYPE; } @Override public Query termQuery(Object value, SearchExecutionContext context) { throw new IllegalArgumentException("Cannot search on field [" + name() + "] since it is a lookup field."); } private class LookupFieldValueFetcher implements ValueFetcher { private final ValueFetcher inputFieldValueFetcher; LookupFieldValueFetcher(SearchExecutionContext context) { final MappedFieldType inputFieldType = context.getFieldType(inputField); // do not allow unmapped field if (inputFieldType == null) { throw new QueryShardException(context, "No field mapping can be found for the field with name [{}]", inputField); } this.inputFieldValueFetcher = inputFieldType.valueFetcher(context, null); } @Override public List fetchValues(SourceLookup lookup, List ignoredValues) throws IOException { assert false : "call #fetchDocumentField() instead"; throw new UnsupportedOperationException("call #fetchDocumentField() instead"); } @Override public DocumentField fetchDocumentField(String docName, SourceLookup lookup) throws IOException { final DocumentField inputDoc = inputFieldValueFetcher.fetchDocumentField(inputField, lookup); if (inputDoc == null || inputDoc.getValues().isEmpty()) { return null; } final List lookupFields = inputDoc.getValues().stream().map(input -> { final TermQueryBuilder query = new TermQueryBuilder(targetField, input.toString()); return new LookupField(lookupIndex, query, fetchFields, 1); }).toList(); return new DocumentField(docName, List.of(), List.of(), lookupFields); } @Override public void setNextReader(LeafReaderContext context) { inputFieldValueFetcher.setNextReader(context); } } }