// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.schema.processing; import com.yahoo.config.application.api.DeployLogger; import com.yahoo.schema.RankProfileRegistry; import com.yahoo.document.DataType; import com.yahoo.document.Field; import com.yahoo.schema.Schema; import com.yahoo.schema.document.SDField; import com.yahoo.vespa.documentmodel.SummaryField; import com.yahoo.vespa.documentmodel.SummaryTransform; import com.yahoo.vespa.indexinglanguage.ExpressionConverter; import com.yahoo.vespa.indexinglanguage.expressions.*; import com.yahoo.vespa.model.container.search.QueryProfiles; import java.util.*; /** * This processor modifies all indexing scripts so that they output to the owning field by default. It also prevents * any output expression from writing to any field except for the owning field. Finally, for SummaryExpression, * this processor expands to write all appropriate summary fields. * * @author Simon Thoresen Hult */ public class IndexingOutputs extends Processor { public IndexingOutputs(Schema schema, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) { super(schema, deployLogger, rankProfileRegistry, queryProfiles); } @Override public void process(boolean validate, boolean documentsOnly) { for (SDField field : schema.allConcreteFields()) { ScriptExpression script = field.getIndexingScript(); if (script == null) continue; Set summaryFields = new TreeSet<>(); findSummaryTo(schema, field, summaryFields, summaryFields); MyConverter converter = new MyConverter(schema, field, summaryFields, validate); field.setIndexingScript((ScriptExpression)converter.convert(script)); } } public void findSummaryTo(Schema schema, SDField field, Set dynamicSummary, Set staticSummary) { var summaryFields = schema.getSummaryFields(field); if (summaryFields.isEmpty()) { fillSummaryToFromField(field, dynamicSummary, staticSummary); } else { fillSummaryToFromSearch(schema, field, summaryFields, dynamicSummary, staticSummary); } } private void fillSummaryToFromSearch(Schema schema, SDField field, List summaryFields, Set dynamicSummary, Set staticSummary) { for (SummaryField summaryField : summaryFields) { fillSummaryToFromSummaryField(schema, field, summaryField, dynamicSummary, staticSummary); } } private void fillSummaryToFromSummaryField(Schema schema, SDField field, SummaryField summaryField, Set dynamicSummary, Set staticSummary) { SummaryTransform summaryTransform = summaryField.getTransform(); String summaryName = summaryField.getName(); if (summaryTransform.isDynamic() && summaryField.getSourceCount() > 2) { // Avoid writing to summary fields that have more than a single input field, as that is handled by the // summary rewriter in the search core. return; } if (summaryTransform.isDynamic()) { DataType fieldType = field.getDataType(); if (!DynamicSummaryTransformUtils.summaryFieldIsPopulatedBySourceField(fieldType)) { if (!DynamicSummaryTransformUtils.isSupportedType(fieldType)) { warn(schema, field, "Dynamic summaries are only supported for fields of type " + "string and array, ignoring summary field '" + summaryField.getName() + "' for sd field '" + field.getName() + "' of type " + fieldType.getName() + "."); } return; } dynamicSummary.add(summaryName); } else if (summaryTransform != SummaryTransform.ATTRIBUTE && summaryTransform != SummaryTransform.TOKENS) { staticSummary.add(summaryName); } } private static void fillSummaryToFromField(SDField field, Set dynamicSummary, Set staticSummary) { for (SummaryField summaryField : field.getSummaryFields().values()) { String summaryName = summaryField.getName(); if (summaryField.getTransform().isDynamic()) { dynamicSummary.add(summaryName); } else { staticSummary.add(summaryName); } } } private class MyConverter extends ExpressionConverter { final Schema schema; final Field field; final Set summaryFields; final boolean validate; MyConverter(Schema schema, Field field, Set summaryFields, boolean validate) { this.schema = schema; this.field = field; this.summaryFields = summaryFields.isEmpty() ? Collections.singleton(field.getName()) : summaryFields; this.validate = validate; } @Override protected boolean shouldConvert(Expression exp) { if ( ! (exp instanceof OutputExpression)) { return false; } String fieldName = ((OutputExpression)exp).getFieldName(); if (fieldName == null) { return true; // inject appropriate field name } if ( validate && ! fieldName.equals(field.getName())) { fail(schema, field, "Indexing expression '" + exp + "' attempts to write to a field other than '" + field.getName() + "'."); } return false; } @Override protected Expression doConvert(Expression exp) { List ret = new LinkedList<>(); if (exp instanceof AttributeExpression) { ret.add(new AttributeExpression(field.getName())); } else if (exp instanceof IndexExpression) { ret.add(new IndexExpression(field.getName())); } else if (exp instanceof SummaryExpression) { for (String fieldName : summaryFields) { ret.add(new SummaryExpression(fieldName)); } } else { throw new UnsupportedOperationException(exp.getClass().getName()); } return new StatementExpression(ret); } } }