/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.search.lookup; import org.apache.lucene.index.LeafReaderContext; import org.opensearch.index.fielddata.IndexFieldData; import org.opensearch.index.mapper.MappedFieldType; import org.opensearch.index.mapper.MapperService; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Supplier; /** * Orchestrator class for search phase lookups * * @opensearch.internal */ public class SearchLookup { /** * The maximum depth of field dependencies. * When a runtime field's doc values depends on another runtime field's doc values, * which depends on another runtime field's doc values and so on, it can * make a very deep stack, which we want to limit. */ private static final int MAX_FIELD_CHAIN_DEPTH = 5; /** * The chain of fields for which this lookup was created, used for detecting * loops caused by runtime fields referring to other runtime fields. The chain is empty * for the "top level" lookup created for the entire search. When a lookup is used to load * fielddata for a field, we fork it and make sure the field name name isn't in the chain, * then add it to the end. So the lookup for the a field named {@code a} will be {@code ["a"]}. If * that field looks up the values of a field named {@code b} then * {@code b}'s chain will contain {@code ["a", "b"]}. */ private final Set fieldChain; private final DocLookup docMap; private final SourceLookup sourceLookup; private final FieldsLookup fieldsLookup; private final BiFunction, IndexFieldData> fieldDataLookup; /** * Create the top level field lookup for a search request. Provides a way to look up fields from doc_values, * stored fields, or _source. */ public SearchLookup( MapperService mapperService, BiFunction, IndexFieldData> fieldDataLookup ) { this.fieldChain = Collections.emptySet(); docMap = new DocLookup( mapperService, fieldType -> fieldDataLookup.apply(fieldType, () -> forkAndTrackFieldReferences(fieldType.name())) ); sourceLookup = new SourceLookup(); fieldsLookup = new FieldsLookup(mapperService); this.fieldDataLookup = fieldDataLookup; } /** * Create a new {@link SearchLookup} that looks fields up the same as the one provided as argument, * while also tracking field references starting from the provided field name. It detects cycles * and prevents resolving fields that depend on more than {@link #MAX_FIELD_CHAIN_DEPTH} fields. * @param searchLookup the existing lookup to create a new one from * @param fieldChain the chain of fields that required the field currently being loaded */ private SearchLookup(SearchLookup searchLookup, Set fieldChain) { this.fieldChain = Collections.unmodifiableSet(fieldChain); this.docMap = new DocLookup( searchLookup.docMap.mapperService(), fieldType -> searchLookup.fieldDataLookup.apply(fieldType, () -> forkAndTrackFieldReferences(fieldType.name())) ); this.sourceLookup = searchLookup.sourceLookup; this.fieldsLookup = searchLookup.fieldsLookup; this.fieldDataLookup = searchLookup.fieldDataLookup; } /** * Creates a copy of the current {@link SearchLookup} that looks fields up in the same way, but also tracks field references * in order to detect cycles and prevent resolving fields that depend on more than {@link #MAX_FIELD_CHAIN_DEPTH} other fields. * @param field the field being referred to, for which fielddata needs to be loaded * @return the new lookup * @throws IllegalArgumentException if a cycle is detected in the fields required to build doc values, or if the field * being resolved depends on more than {@link #MAX_FIELD_CHAIN_DEPTH} */ public final SearchLookup forkAndTrackFieldReferences(String field) { Objects.requireNonNull(field, "field cannot be null"); Set newFieldChain = new LinkedHashSet<>(fieldChain); if (newFieldChain.add(field) == false) { String message = String.join(" -> ", newFieldChain) + " -> " + field; throw new IllegalArgumentException("Cyclic dependency detected while resolving runtime fields: " + message); } if (newFieldChain.size() > MAX_FIELD_CHAIN_DEPTH) { throw new IllegalArgumentException("Field requires resolving too many dependent fields: " + String.join(" -> ", newFieldChain)); } return new SearchLookup(this, newFieldChain); } public LeafSearchLookup getLeafSearchLookup(LeafReaderContext context) { return new LeafSearchLookup(context, docMap.getLeafDocLookup(context), sourceLookup, fieldsLookup.getLeafFieldsLookup(context)); } public DocLookup doc() { return docMap; } public SourceLookup source() { return sourceLookup; } }