Skip to content

Conversation

@triceo
Copy link
Collaborator

@triceo triceo commented Jan 16, 2026

Before

    Constraint speakerUnavailableTimeslot(ConstraintFactory factory) {
        return factory.forEach(Talk.class)
                .join(Speaker.class,
                        filtering((talk, speaker) -> talk.hasSpeaker(speaker)
                                && speaker.getUnavailableTimeslots().contains(talk.getTimeslot())))
                .penalize(...)
                .asConstraint(...);
    }

After (bigO order of magnitude more scalable)

    Constraint speakerUnavailableTimeslot(ConstraintFactory factory) {
        return factory.forEach(Talk.class)
                .join(Speaker.class,
                        containing(Talk::getSpeakers, speaker -> speaker),
                        containedIn(Talk::getTimeslot, Speaker::getUnavailableTimeslots))
                .penalize(...)
                .asConstraint(...);
    }

This is mostly a work of @ge0ffrey, I just rebased it and smoothened some rough edges.
Replaces #1999.

triceo and others added 6 commits January 16, 2026 07:56
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces three new collection-based joiners (containing, containedIn, and intersecting) to provide significant performance improvements through efficient indexing. The PR also includes several internal refactorings: renaming getElement() to element(), isEmpty() to isRemovable(), and KeyRetriever to KeyUnpacker.

Changes:

  • Added new joiner types: containing, containedIn, and intersecting with comprehensive API support across all joiner arities
  • Implemented specialized indexers (ContainingIndexer, ContainedInIndexer, IntersectingIndexer) for efficient query performance
  • Refactored internal APIs for better consistency and clarity

Reviewed changes

Copilot reviewed 44 out of 44 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc Documents new joiner types
core/src/main/java/ai/timefold/solver/core/api/score/stream/Joiners.java Adds public API for containing, containedIn, and intersecting joiners across all arities
core/src/main/java/ai/timefold/solver/core/impl/bavet/common/joiner/JoinerType.java Adds CONTAINING, CONTAINED_IN, INTERSECTING enum values with flip logic
core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ContainingIndexer.java New indexer implementation for containing joiner
core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ContainedInIndexer.java New indexer implementation for containedIn joiner
core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IntersectingIndexer.java New indexer implementation for intersecting joiner
core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java Updated to create new indexer types
core/src/main/java/ai/timefold/solver/core/impl/util/CompositeListEntry.java New record for storing tuples in multiple downstream indexers
core/src/main/java/ai/timefold/solver/core/impl/util/ListEntry.java Simplified interface (removed isRemoved, renamed getElement to element)
core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/KeyUnpacker.java Renamed from KeyRetriever
Test files Updated tests for new joiners and refactored naming
Comments suppressed due to low confidence (2)

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexer.java:112

  • This method overrides Indexer.get; it is advisable to add an Override annotation.
    core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexer.java:103
  • This method overrides Indexer.iterator; it is advisable to add an Override annotation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@TomCools TomCools left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs look good to me

Copy link
Contributor

@Christopher-Chianelli Christopher-Chianelli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have at least 1 test that go through the ScoreDirector that uses these Joiners. Additionally, I do not think these Joiners will work if the backing collection changes (such as a InverseShadowVariable) given the same size assertions in the code (IIRC, everything is updated only when calculateScore is called, and not at before/after variable change, so the indexer will encounter a different sized collection if a shadow variable changes).

/**
* See {@link EqualIndexer} for explanation of the parameters.
*/
private final Map<Key_, Indexer<T>> downstreamIndexerMap = new HashMap<>(16, 0.5f);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These HashMap parameters were found after testing IIRC. Might be better to have them in CollectionUtils so they don't need to be copied and pasted everywhere.

Comment on lines +42 to +43
this.modifyKeyUnpacker = Objects.requireNonNull((KeyUnpacker<KeyCollection_>) keyUnpacker);
this.queryKeyUnpacker = Objects.requireNonNull(keyUnpacker);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two fields always point to the same thing (but with different generics)?

Comment on lines +73 to +76
throw new IllegalStateException("""
Impossible state: the tuple (%s) with composite key (%s) has a different number of children (%d) \
than the index key collection size (%d)."""
.formatted(entry, modifyCompositeKey, children.size(), indexKeyCollection.size()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to add to the Javadoc using variable collection properties such as PlanningListVariable or InverseShadowVariable is not allowed (since those will trigger this exception as they change size).

Comment on lines +46 to +47
this.modifyKeyUnpacker = Objects.requireNonNull((KeyUnpacker<KeyCollection_>) keyUnpacker);
this.queryKeyUnpacker = Objects.requireNonNull((KeyUnpacker<KeyCollection_>) keyUnpacker);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two fields will always have the exact same value (with the exact same generics)

@triceo
Copy link
Collaborator Author

triceo commented Jan 17, 2026

Moving back to draft, as there are score corruptions.

@triceo triceo marked this pull request as draft January 17, 2026 06:36
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 44 out of 44 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (2)

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexer.java:112

  • This method overrides Indexer.get; it is advisable to add an Override annotation.
    core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexer.java:103
  • This method overrides Indexer.iterator; it is advisable to add an Override annotation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Collaborator Author

@triceo triceo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a conversation with Geoff, we'll do the following:

  • The joiners are removed from public API, therefore they can not be used.
  • They are also removed from the documentation. There is no public trace of them existing.
  • But they are being merged; Geoffrey will eventually get to finishing this work, but we want to avoid further merge conflicts until he does.

This entire piece of work will be re-reviewed as a whole, once the fixes for the score corruptions have been made. I'm leaving all the remaining comments here open, so that during the next round of review, we know what's left to resolve.

@triceo triceo marked this pull request as ready for review January 23, 2026 07:21
Copilot AI review requested due to automatic review settings January 23, 2026 07:21
@triceo triceo linked an issue Jan 23, 2026 that may be closed by this pull request
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 45 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexer.java:112

  • This method overrides Indexer.get; it is advisable to add an Override annotation.
    core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexer.java:103
  • This method overrides Indexer.iterator; it is advisable to add an Override annotation.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
10.1% Duplication on New Code (required ≤ 5%)

See analysis details on SonarQube Cloud

@triceo triceo merged commit f3fe406 into TimefoldAI:main Jan 23, 2026
37 of 40 checks passed
@triceo triceo deleted the contains branch January 23, 2026 07:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unit test for IndexerFactory: IndexerFactoryTest

4 participants