LanceScanPlan.java

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF 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.

package org.apache.doris.datasource.lance.source;

import org.apache.doris.spi.Split;
import org.apache.doris.thrift.TExternalSearchRequest;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/** Fixed planning decisions; the scheduler may still attach backend information to each Split. */
final class LanceScanPlan {
    enum SearchKind {
        /** Ordinary SELECT, including scalar filters and metadata COUNT. */
        NORMAL,
        /** Vector candidates merged by a Doris TopN above the scan. */
        VECTOR,
        /** FTS candidates restricted to committed inverted-index coverage. */
        FULL_TEXT;

        boolean isExternalSearch() {
            return this != NORMAL;
        }

        /** A TVF must carry exactly one search request; NORMAL has no search request. */
        static SearchKind fromSearchRequest(TExternalSearchRequest request) {
            if (request == null || !request.isSetSearchQuery()) {
                throw new IllegalArgumentException("Lance external search request requires search_query");
            }
            boolean hasVector = request.getSearchQuery().isSetVectorSearch();
            boolean hasFullText = request.getSearchQuery().isSetFullTextSearch();
            if (hasVector == hasFullText) {
                throw new IllegalArgumentException("Lance external search query must set exactly one search kind");
            }
            return hasVector ? VECTOR : FULL_TEXT;
        }
    }

    /** Explains FE segment selection, not whether the BE may automatically use a Lance index. */
    enum VectorIndexStatus {
        /** No vector planning result exists yet (also used by ordinary and FTS scans). */
        NOT_PLANNED,
        /** The request explicitly sets use_index=false. */
        DISABLED,
        /** No discoverable vector index matches the requested field. */
        NO_MATCH,
        /** The selected index has a different or unknown metric. */
        METRIC_MISMATCH,
        /** At least one segment has no fragment coverage metadata. */
        UNKNOWN_COVERAGE,
        /** All segment coverage lies outside the selected snapshot's visible fragments. */
        NO_VISIBLE_COVERAGE,
        /** FE produced segment splits, possibly with additional uncovered-fragment splits. */
        USED
    }

    final VectorIndexStatus vectorIndexStatus;
    private final List<Split> splits;
    final long version;
    final int fragmentCount;
    final int fragmentsPerSplit;
    final int indexSegmentCount;
    final int indexedFragmentCount;
    final int unindexedFragmentCount;
    final String scalarIndexName;

    LanceScanPlan(List<Split> splits, long version, int fragments, int fragmentsPerSplit,
            int indexSegments, int indexFragments, int unindexedFragments, String scalarIndexName,
            VectorIndexStatus vectorIndexStatus) {
        this.vectorIndexStatus = vectorIndexStatus;
        this.splits = Collections.unmodifiableList(new ArrayList<>(splits));
        this.version = version;
        this.fragmentCount = fragments;
        this.fragmentsPerSplit = fragmentsPerSplit;
        this.indexSegmentCount = indexSegments;
        this.indexedFragmentCount = indexFragments;
        this.unindexedFragmentCount = unindexedFragments;
        this.scalarIndexName = scalarIndexName;
    }

    static LanceScanPlan empty() {
        return new LanceScanPlan(Collections.emptyList(), -1, 0, 0, 0, 0, 0, null, VectorIndexStatus.NOT_PLANNED);
    }

    /** Backend assignment shuffles its input list; keep that mutation outside the stored plan. */
    List<Split> createSchedulingSplits() {
        return new ArrayList<>(splits);
    }
}