Currently, predicates/sorts can be re-used by calling .toPredicate()/toSort() on the terminal context in the DSL. The user gets a SearchPredicate/SearchSort instance that can be reused in multiple queries, even in different threads.
But, there are rough edges:
SearchPredicate/SearchSort are not actually immutable. They internally store the builders that were used by the DSL, so if you keep a reference to the DSL contexts around, you should be able to change the builders, and thus the SearchPredicate/SearchSort. Maybe we should somehow "freeze" the builders when we build the SearchPredicate/SearchSort, so that trying to call a method on the DSL after that will trigger an exception?
When reusing a SearchPredicate/SearchSort, we do not check that the search target is compatible with the one the objects were originally created for, beyond the usual technology check (Lucene/Elasticsearch). For example if you create a SearchPredicate while targeting the index book, then you re-use it while targeting the index user, something will eventually go wrong, but we won't detect it. Maybe we should? Ideally, when a SearchPredicate/SearchSort/etc. was created with a target A, and is reused in a target B, we should make sure that B is a subset of A. If we do that, worst case, the fields referenced in the predicate/sort/etc. do not exist, but if they do we are sure they are compatible. => This is no longer true, we do check that the same indexes are targeted.
The DSL contexts can also be reused. They shouldn't be, because they reference objects that are no longer useful once the predicate/sort/etc. is built and should be garbage-collected, but everything will work if you just store the DSL contexts somewhere and re-use them. Maybe we should prevent that somehow? This is less critical, however, since it's mostly about memory consumption.