class GenericFunctionQuery extends Query {

    private final Function function;
    private final LuceneCollectorExpression[] expressions;
    private final Input<Boolean> condition;

    GenericFunctionQuery(Function function,
                         Collection<? extends LuceneCollectorExpression<?>> expressions,
                         Input<Boolean> condition) {
        this.function = function;
        // inner loop iterates over expressions - call toArray to avoid iterator allocations
        this.expressions = expressions.toArray(new LuceneCollectorExpression[0]);
        this.condition = condition;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        GenericFunctionQuery that = (GenericFunctionQuery) o;

        return function.equals(that.function);
    }

    @Override
    public int hashCode() {
        return function.hashCode();
    }

    @Override
    public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
        return new Weight(this) {
            @Override
            public boolean isCacheable(LeafReaderContext ctx) {
                if (SymbolVisitors.any(s -> s instanceof Function fn && !fn.signature().isDeterministic(), function)) {
                    return false;
                }
                var fields = new ArrayList<String>();
                RefVisitor.visitRefs(function, ref -> fields.add(ref.storageIdent()));
                return DocValues.isCacheable(ctx, fields.toArray(new String[0]));
            }

            @Override
            public Explanation explain(LeafReaderContext context, int doc) throws IOException {<FILL_FUNCTION_BODY>}

            @Override
            public Scorer scorer(LeafReaderContext context) throws IOException {
                return new ConstantScoreScorer(this, 0f, scoreMode, getTwoPhaseIterator(context));
            }
        };
    }

    @Override
    public void visit(QueryVisitor visitor) {
    }

    private FilteredTwoPhaseIterator getTwoPhaseIterator(final LeafReaderContext context) throws IOException {
        for (LuceneCollectorExpression<?> expression : expressions) {
            expression.setNextReader(new ReaderContext(context));
        }
        return new FilteredTwoPhaseIterator(context.reader(), condition, expressions);
    }

    @Override
    public String toString(String field) {
        return function.toString();
    }

    private static class FilteredTwoPhaseIterator extends TwoPhaseIterator {

        private final Input<Boolean> condition;
        private final LuceneCollectorExpression[] expressions;
        private final Bits liveDocs;

        FilteredTwoPhaseIterator(LeafReader reader,
                                 Input<Boolean> condition,
                                 LuceneCollectorExpression[] expressions) {
            super(DocIdSetIterator.all(reader.maxDoc()));
            this.liveDocs = reader.getLiveDocs() == null
                                ? new Bits.MatchAllBits(reader.maxDoc())
                                : reader.getLiveDocs();
            this.condition = condition;
            this.expressions = expressions;
        }

        @Override
        public boolean matches() throws IOException {
            int doc = approximation.docID();
            if (!liveDocs.get(doc)) {
                return false;
            }
            for (LuceneCollectorExpression<?> expression : expressions) {
                expression.setNextDocId(doc);
            }
            return InputCondition.matches(condition);
        }

        @Override
        public float matchCost() {
            // Arbitrary number, we don't have a way to get the cost of the condition
            return 10;
        }
    }
}

                final Scorer s = scorer(context);
                final boolean match;
                final TwoPhaseIterator twoPhase = s.twoPhaseIterator();
                if (twoPhase == null) {
                    match = s.iterator().advance(doc) == doc;
                } else {
                    match = twoPhase.approximation().advance(doc) == doc && twoPhase.matches();
                }
                if (match) {
                    assert s.score() == 0f : "score must be 0";
                    return Explanation.match(0f, "Match on id " + doc);
                } else {
                    return Explanation.match(0f, "No match on id " + doc);
                }
            
<no_super_class>