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

org.dizitart.no2.collection.operation.FindOptimizer Maven / Gradle / Ivy

/*
 * Copyright (c) 2017-2021 Nitrite author or authors.
 *
 * Licensed 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.
 *
 */

package org.dizitart.no2.collection.operation;

import org.dizitart.no2.collection.FindOptions;
import org.dizitart.no2.collection.FindPlan;
import org.dizitart.no2.common.SortOrder;
import org.dizitart.no2.common.tuples.Pair;
import org.dizitart.no2.exceptions.FilterException;
import org.dizitart.no2.filters.*;
import org.dizitart.no2.index.IndexDescriptor;

import java.util.*;

import static org.dizitart.no2.common.Constants.DOC_ID;
import static org.dizitart.no2.common.util.Iterables.firstOrNull;
import static org.dizitart.no2.filters.Filter.and;
import static org.dizitart.no2.filters.Filter.or;

/**
 * @since 4.0
 * @author Anindya Chatterjee
 */
class FindOptimizer {

    public FindPlan optimize(Filter filter,
                             FindOptions findOptions,
                             Collection indexDescriptors) {
        FindPlan findPlan = createFilterPlan(indexDescriptors, filter);
        readSortOption(findOptions, findPlan);
        readLimitOption(findOptions, findPlan);

        if (findOptions != null) {
            findPlan.setCollator(findOptions.collator());
            findPlan.setDistinct(findOptions.distinct());
        }
        return findPlan;
    }

    private FindPlan createFilterPlan(Collection indexDescriptors, Filter filter) {
        if (filter instanceof AndFilter) {
            List filters = flattenAndFilter((AndFilter) filter);
            return createAndPlan(indexDescriptors, filters);
        } else if (filter instanceof OrFilter) {
            return createOrPlan(indexDescriptors, ((OrFilter) filter).getFilters());
        } else {
            List filters = Collections.singletonList(filter);
            return createAndPlan(indexDescriptors, filters);
        }
    }

    private List flattenAndFilter(AndFilter andFilter) {
        List flattenedFilters = new ArrayList<>();
        if (andFilter != null) {
            for (Filter filter : andFilter.getFilters()) {
                if (filter instanceof AndFilter) {
                    List filters = flattenAndFilter((AndFilter) filter);
                    flattenedFilters.addAll(filters);
                } else {
                    flattenedFilters.add(filter);
                }
            }
        }
        return flattenedFilters;
    }

    private FindPlan createOrPlan(Collection indexDescriptors, List filters) {
        FindPlan findPlan = new FindPlan();

        Set flattenedFilter = new HashSet<>();

        // flatten the or filter
        for (Filter filter : filters) {
            if (filter instanceof OrFilter) {
                flattenedFilter.addAll(((OrFilter) filter).getFilters());
            } else {
                flattenedFilter.add(filter);
            }
        }

        for (Filter filter : flattenedFilter) {
            FindPlan subPlan = createFilterPlan(indexDescriptors, filter);
            findPlan.getSubPlans().add(subPlan);
        }

        // check if all sub plan have index support
        for (FindPlan plan : findPlan.getSubPlans()) {
            if (plan.getIndexDescriptor() == null) {
                // if one of the sub plan doesn't have any index support
                // then it can not be optimized, instead the
                // original filter should be set as coll-scan filter
                // for the parent plan
                findPlan.getSubPlans().clear();
                // set the original or filter as coll scan filter
                findPlan.setCollectionScanFilter(or(filters.toArray(new Filter[0])));
                return findPlan;
            }
        }
        return findPlan;
    }

    private FindPlan createAndPlan(Collection indexDescriptors, List filters) {
        FindPlan findPlan = new FindPlan();
        Set indexScanFilters = new LinkedHashSet<>();
        Set columnScanFilters = new LinkedHashSet<>();

        // find out set id filter (if any)
        planForIdFilter(findPlan, filters);

        // find out if there are any index only filter with index
        planForIndexOnlyFilters(findPlan, indexScanFilters, indexDescriptors, filters);

        // if no id filter found or no index only filter found, scan for matching index
        if (findPlan.getByIdFilter() == null && indexScanFilters.isEmpty()) {
            planForIndexScanningFilters(findPlan, indexScanFilters, indexDescriptors, filters);
        }

        // plan for column scan filters
        planForCollectionScanningFilters(findPlan, indexScanFilters, columnScanFilters, filters);

        IndexScanFilter indexScanFilter;
        if (indexScanFilters.size() == 1) {
            indexScanFilter = new IndexScanFilter(Collections.singletonList(firstOrNull(indexScanFilters)));
            findPlan.setIndexScanFilter(indexScanFilter);
        } else if (indexScanFilters.size() > 1) {
            indexScanFilter = new IndexScanFilter(indexScanFilters);
            findPlan.setIndexScanFilter(indexScanFilter);
        }

        if (columnScanFilters.size() == 1) {
            findPlan.setCollectionScanFilter(firstOrNull(columnScanFilters));
        } else if (columnScanFilters.size() > 1) {
            Filter andFilter = and(columnScanFilters.toArray(new Filter[0]));
            findPlan.setCollectionScanFilter(andFilter);
        }

        return findPlan;
    }

    private void planForIdFilter(FindPlan findPlan, List filters) {
        for (Filter filter : filters) {
            if (filter instanceof EqualsFilter) {
                EqualsFilter equalsFilter = (EqualsFilter) filter;

                // handle byId filter specially
                if (equalsFilter.getField().equals(DOC_ID)) {
                    findPlan.setByIdFilter(equalsFilter);
                }
                break;
            }
        }
    }

    private void planForIndexOnlyFilters(FindPlan findPlan, Set indexScanFilters,
                                         Collection indexDescriptors, List filters) {
        // find out if there are any filter which does not support covered queries
        List indexOnlyFilters = new ArrayList<>();
        for (Filter filter : filters) {
            if (filter instanceof IndexOnlyFilter) {
                IndexOnlyFilter indexScanFilter = (IndexOnlyFilter) filter;
                if (isCompatibleFilter(indexOnlyFilters, indexScanFilter)) {
                    // if filter is compatible with already identified index only filter then add
                    indexOnlyFilters.add(indexScanFilter);
                } else {
                    throw new FilterException("A query can not have multiple index only filters");
                }
            }
        }

        // populate index descriptor for the index only filters
        if (!indexOnlyFilters.isEmpty()) {

            // get any index only filter from the set
            IndexOnlyFilter anyFilter = indexOnlyFilters.get(0);
            for (IndexDescriptor indexDescriptor : indexDescriptors) {

                // check the index type match between filter and index descriptor
                if (anyFilter.supportedIndexType().equals(indexDescriptor.getIndexType())) {
                    // choose the index descriptor and filters
                    findPlan.setIndexDescriptor(indexDescriptor);
                    indexScanFilters.addAll(indexOnlyFilters);
                    break;
                }
            }

            if (findPlan.getIndexDescriptor() == null) {
                throw new FilterException(anyFilter.getField() + " is not indexed with "
                    + anyFilter.supportedIndexType() + " index");
            }
        }
    }

    private boolean isCompatibleFilter(List indexOnlyFilters, IndexOnlyFilter filter) {
        if (indexOnlyFilters.isEmpty()) {
            return true;
        } else {
            IndexOnlyFilter comparableFilter = indexOnlyFilters.get(0);
            return comparableFilter.canBeGrouped(filter);
        }
    }

    private void planForIndexScanningFilters(FindPlan findPlan, Set indexScanFilters,
                                             Collection indexDescriptors, List filters) {
        // descending sort based on cardinality of indices, consider the higher cardinality index first
        NavigableMap> indexFilterMap = new TreeMap<>(Collections.reverseOrder());

        for (IndexDescriptor indexDescriptor : indexDescriptors) {
            List fieldNames = indexDescriptor.getFields().getFieldNames();

            List indexedFilters = new ArrayList<>();
            for (String fieldName : fieldNames) {
                boolean matchFound = false;
                for (Filter filter : filters) {
                    if (filter instanceof ComparableFilter) {
                        String filterFieldName = ((ComparableFilter) filter).getField();
                        if (filterFieldName.equals(fieldName)) {
                            indexedFilters.add((ComparableFilter) filter);
                            matchFound = true;
                            break;
                        }
                    }
                }

                if (!matchFound) {
                    // match not found, so can't consider this index
                    break;
                }
            }

            if (!indexedFilters.isEmpty()) {
                indexFilterMap.put(indexDescriptor, indexedFilters);
            }
        }

        for (Map.Entry> entry : indexFilterMap.entrySet()) {
            // consider the filter combination if it encompasses more fields
            // than the previously selected filter
            if (entry.getValue().size() > indexScanFilters.size()) {
                // maintain the order in set
                indexScanFilters.addAll(entry.getValue());
                findPlan.setIndexDescriptor(entry.getKey());
            }
        }
    }

    private void planForCollectionScanningFilters(FindPlan findPlan, Set indexScanFilters,
                                                  Set columnScanFilters, List filters) {
        for (Filter filter : filters) {
            // ignore the elected filters for index scan and
            // insert rest of the filters for column scan
            // NOTE: for byId filter, index scan filters will always be empty
            if (!(filter instanceof ComparableFilter) || !indexScanFilters.contains(filter)) {
                // ignore the byId filter (if any) for column scan
                if (filter != findPlan.getByIdFilter()) {
                    columnScanFilters.add(filter);
                }
            }
        }

        // validate whether column scanning is supported for each filter,
        // if there is no index scan available
        if (indexScanFilters.isEmpty()) {
            validateCollectionScanFilters(columnScanFilters);
        }
    }

    private void validateCollectionScanFilters(Collection filters) {
        for (Filter filter : filters) {
            if (filter instanceof IndexOnlyFilter) {
                throw new FilterException("Collection scan is not supported for the filter " + filter);
            } else if (filter instanceof TextFilter) {
                throw new FilterException(((TextFilter) filter).getField() + " is not full-text indexed");
            }
        }
    }

    private void readSortOption(FindOptions findOptions, FindPlan findPlan) {
        IndexDescriptor indexDescriptor = findPlan.getIndexDescriptor();
        if (findOptions != null && findOptions.orderBy() != null) {
            // get sort spec for find
            List> findSortSpec = findOptions.orderBy().getSortingOrders();

            if (indexDescriptor != null) {
                // get index field names
                List indexedFieldNames = indexDescriptor.getFields().getFieldNames();

                boolean canUseIndex = false;
                Map indexScanOrder = new HashMap<>();

                if (indexedFieldNames.size() >= findSortSpec.size()) {
                    // if all fields of the sort spec is covered by index, then only
                    // sorting can take help of index

                    int length = findSortSpec.size();
                    for (int i = 0; i < length; i++) {
                        String indexFieldName = indexedFieldNames.get(i);
                        Pair findPair = findSortSpec.get(i);
                        if (!indexFieldName.equals(findPair.getFirst())) {
                            // field mismatch in sort spec, can't use index for sorting
                            canUseIndex = false;
                            break;
                        } else {
                            canUseIndex = true;
                            boolean reverseScan = false;

                            SortOrder findSortOrder = findPair.getSecond();
                            if (findSortOrder != SortOrder.Ascending) {
                                // if sort order is different, reverse scan in index
                                reverseScan = true;
                            }

                            // add to index scan order
                            indexScanOrder.put(indexFieldName, reverseScan);
                        }
                    }
                }

                if (canUseIndex) {
                    findPlan.setIndexScanOrder(indexScanOrder);
                } else {
                    findPlan.setBlockingSortOrder(findSortSpec);
                }
            } else {
                // no find options, so consider the index sorting order
                findPlan.setBlockingSortOrder(findSortSpec);
            }
        }
    }

    private void readLimitOption(FindOptions findOptions, FindPlan findPlan) {
        if (findOptions != null) {
            findPlan.setLimit(findOptions.limit());
            findPlan.setSkip(findOptions.skip());
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy