// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.prelude.querytransform; import com.yahoo.prelude.query.AndItem; import com.yahoo.prelude.query.CompositeItem; import com.yahoo.prelude.query.EquivItem; import com.yahoo.prelude.query.HasIndexItem; import com.yahoo.prelude.query.IndexedItem; import com.yahoo.prelude.query.Item; import com.yahoo.prelude.query.NearItem; import com.yahoo.prelude.query.NotItem; import com.yahoo.prelude.query.NullItem; import com.yahoo.prelude.query.OrItem; import com.yahoo.prelude.query.RankItem; import com.yahoo.prelude.query.SimpleIndexedItem; import com.yahoo.prelude.query.SubstringItem; import com.yahoo.prelude.query.WeakAndItem; import com.yahoo.search.Query; import com.yahoo.search.query.Model; import com.yahoo.search.result.Hit; /** * @author baldersheim */ // TODO: This overlaps with QueryCanonicalizer public class QueryRewrite { private enum Recall { RECALLS_EVERYTHING, RECALLS_NOTHING, UNKNOWN_RECALL } /** * Optimize multiple NotItems under and or by collapsing them in to one and leaving * the positive ones behind in its place and moving itself with the original and as its positive item * and the union of all the negative items of all the original NotItems as its negative items. */ public static void optimizeAndNot(Query query) { Item root = query.getModel().getQueryTree().getRoot(); Item possibleNewRoot = optimizeAndNot(root); if (root != possibleNewRoot) { query.getModel().getQueryTree().setRoot(possibleNewRoot); } } /** * Optimizes the given query tree based on its {@link Model#getRestrict()} parameter, if any. */ public static void optimizeByRestrict(Query query) { if (query.getModel().getRestrict().size() != 1) { return; } Item root = query.getModel().getQueryTree().getRoot(); if (optimizeByRestrict(root, query.getModel().getRestrict().iterator().next()) == Recall.RECALLS_NOTHING) { query.getModel().getQueryTree().setRoot(new NullItem()); } } /** * Collapses all single-child {@link CompositeItem}s into their parent item. */ public static void collapseSingleComposites(Query query) { Item oldRoot = query.getModel().getQueryTree().getRoot(); Item newRoot = collapseSingleComposites(oldRoot); if (oldRoot != newRoot) { query.getModel().getQueryTree().setRoot(newRoot); } } /** * Replaces and {@link SimpleIndexedItem} searching in the {@link Hit#SDDOCNAME_FIELD} with an item * appropriate for the search node. */ public static void rewriteSddocname(Query query) { Item oldRoot = query.getModel().getQueryTree().getRoot(); Item newRoot = rewriteSddocname(oldRoot); if (oldRoot != newRoot) { query.getModel().getQueryTree().setRoot(newRoot); } } // ------------------- End public API private static Item optimizeAndNot(Item node) { if (node instanceof CompositeItem) { return extractAndNotRecursively((CompositeItem) node); } return node; } private static CompositeItem extractAndNotRecursively(CompositeItem parent) { for (int i = 0; i < parent.getItemCount(); i++) { Item child = parent.getItem(i); Item possibleNewChild = optimizeAndNot(child); if (child != possibleNewChild) { parent.setItem(i, possibleNewChild); } } if (parent instanceof AndItem) { return extractAndNot((AndItem) parent); } return parent; } private static CompositeItem extractAndNot(AndItem parent) { NotItem theOnlyNot = null; for (int i = 0; i < parent.getItemCount(); i++) { Item child = parent.getItem(i); if (child instanceof NotItem) { NotItem thisNot = (NotItem) child; parent.setItem(i, thisNot.getPositiveItem()); if (theOnlyNot == null) { theOnlyNot = thisNot; theOnlyNot.setPositiveItem(parent); } else { for (int j=1; j < thisNot.getItemCount(); j++) { theOnlyNot.addNegativeItem(thisNot.getItem(j)); } } } } return (theOnlyNot != null) ? theOnlyNot : parent; } private static Recall optimizeByRestrict(Item item, String restrictParam) { if (item instanceof SimpleIndexedItem) { return optimizeIndexedItemByRestrict((SimpleIndexedItem)item, restrictParam); } else if (item instanceof NotItem) { return optimizeNotItemByRestrict((NotItem)item, restrictParam); } else if (item instanceof CompositeItem) { return optimizeCompositeItemByRestrict((CompositeItem)item, restrictParam); } else { return Recall.UNKNOWN_RECALL; } } private static Recall optimizeIndexedItemByRestrict(SimpleIndexedItem item, String restrictParam) { if (!Hit.SDDOCNAME_FIELD.equals(item.getIndexName())) { return Recall.UNKNOWN_RECALL; } // a query term searching for sddocname will either recall everything or nothing, depending on whether // the term matches the restrict parameter or not return restrictParam.equals(item.getIndexedString()) ? Recall.RECALLS_EVERYTHING : Recall.RECALLS_NOTHING; } private static Recall optimizeNotItemByRestrict(NotItem item, String restrictParam) { // first item is the positive one if (optimizeByRestrict(item.getItem(0), restrictParam) == Recall.RECALLS_NOTHING) { return Recall.RECALLS_NOTHING; } // all the remaining items are negative ones for (int i = item.getItemCount(); --i >= 1; ) { Item child = item.getItem(i); switch (optimizeByRestrict(child, restrictParam)) { case RECALLS_EVERYTHING: return Recall.RECALLS_NOTHING; case RECALLS_NOTHING: item.removeItem(i); break; } } return Recall.UNKNOWN_RECALL; } private static Recall optimizeCompositeItemByRestrict(CompositeItem item, String restrictParam) { Recall recall = Recall.UNKNOWN_RECALL; for (int i = item.getItemCount(); --i >= 0; ) { switch (optimizeByRestrict(item.getItem(i), restrictParam)) { case RECALLS_EVERYTHING: if ((item instanceof OrItem) || (item instanceof EquivItem)) { removeOtherNonrankedChildren(item, i); recall = Recall.RECALLS_EVERYTHING; } else if ((item instanceof AndItem) || (item instanceof NearItem) || (item instanceof WeakAndItem)) { if ( ! isRanked(item.getItem(i)) && item.items().size() > 1) { item.removeItem(i); } } else if (item instanceof RankItem) { // empty } else { throw new UnsupportedOperationException(item.getClass().getName()); } break; case RECALLS_NOTHING: if ((item instanceof OrItem) || (item instanceof EquivItem) && item.items().size() > 1) { item.removeItem(i); } else if ((item instanceof AndItem) || (item instanceof NearItem) || (item instanceof WeakAndItem)) { return Recall.RECALLS_NOTHING; } else if (item instanceof RankItem) { if (i == 0) return Recall.RECALLS_NOTHING; item.removeItem(i); } else { throw new UnsupportedOperationException(item.getClass().getName()); } break; } } return recall; } private static void removeOtherNonrankedChildren(CompositeItem parent, int indexOfChildToKeep) { Item childToKeep = parent.getItem(indexOfChildToKeep); for (int i = parent.getItemCount(); --i >= 0; ) { Item child = parent.getItem(i); if ( child != childToKeep && ! parent.getItem(i).isRanked()) parent.removeItem(i); } } private static boolean isRanked(Item item) { if (item instanceof CompositeItem) { for (Item child : ((CompositeItem)item).items()) if (isRanked(child)) return true; return false; } else if (item instanceof HasIndexItem && Hit.SDDOCNAME_FIELD.equals(((HasIndexItem)item).getIndexName())) { return false; // No point in ranking by sddocname } else { return item.isRanked(); } } private static Item collapseSingleComposites(Item item) { if (!(item instanceof CompositeItem)) { return item; } CompositeItem parent = (CompositeItem)item; int numChildren = parent.getItemCount(); for (int i = 0; i < numChildren; ++i) { Item oldChild = parent.getItem(i); Item newChild = collapseSingleComposites(oldChild); if (oldChild != newChild) { parent.setItem(i, newChild); } } return parent.extractSingleChild().orElse(item); } private static Item rewriteSddocname(Item item) { if (item instanceof CompositeItem) { CompositeItem parent = (CompositeItem)item; for (int i = 0, len = parent.getItemCount(); i < len; ++i) { Item oldChild = parent.getItem(i); Item newChild = rewriteSddocname(oldChild); if (oldChild != newChild) { parent.setItem(i, newChild); } } } else if (item instanceof SimpleIndexedItem) { SimpleIndexedItem oldItem = (SimpleIndexedItem)item; if (Hit.SDDOCNAME_FIELD.equals(oldItem.getIndexName())) { SubstringItem newItem = new SubstringItem(oldItem.getIndexedString()); newItem.setIndexName("[documentmetastore]"); return newItem; } } return item; } }