|
This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Data Meilisearch 0.13.0! |
Query Methods
Repository query methods can derive Meilisearch filters from method names or declare native filter and search-text templates. The module supports search-backed finders and derived filter-based count, existence, and delete methods; use MeilisearchOperations for dynamic search composition and richer search options.
Query Creation
Spring Data parses derived query method names when the repository is created. For example, findByGenreAndRatingGreaterThan(String genre, int rating) requires both predicates to match. The following table lists the supported keywords and the filters they produce.
| Keyword | Sample | Meilisearch filter |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Both predicates must match ( |
|
|
|
An empty collection passed to an In predicate is rejected at invocation with an IllegalArgumentException that identifies the method and operator. An empty collection passed to NotIn contributes no filter. With Or, each branch must retain at least one effective predicate; a branch consisting only of an empty NotIn is rejected rather than turning the whole query into an unfiltered search or delete.
StartingWith requires a String property marked filterable in Meilisearch and a String parameter. Null or empty prefixes are rejected before searching or deleting. It matches a literal prefix of the field value, not a token anywhere in the text; findByTitleStartingWith("Star") matches Star Trek but not The Star. The filter value is quoted and escaped. The server must run Meilisearch v1.17.0 or newer: STARTS WITH became stable in v1.17.0 and does not require enabling the experimental containsFilter feature. Older servers reject the filter at request time; this module does not emulate it with full-text search or client-side filtering.
For an equality predicate with an @Nullable argument, passing null matches both an explicitly null field and a missing field. IsNull matches those same two states. IsNotNull requires a present, non-null field, while Exists also matches an explicitly null field. The mapping converter omits null-valued properties when saving documents, so a missing field is the usual result of saving a null property.
Count, Existence, and Delete Methods
Derived predicates can also be used with the following methods:
| Method pattern | Return type | Behavior |
|---|---|---|
|
|
Returns the exact number of matching documents using Meilisearch’s |
|
|
Checks for a matching document using search with a limit of one. |
|
|
Deletes matching documents with Meilisearch’s native filter-delete API and waits for the task to succeed. Numeric return types report the deleted-document count; a failed task or unavailable deletion details fails the invocation instead of returning a count. |
These methods use the same supported predicates listed above, including grouped Or branches. Every property used in a predicate must be configured as filterable. Count, existence, and delete methods do not accept a static OrderBy suffix or runtime Sort or Pageable parameters. A delete or remove method must produce a non-empty effective filter; an unfiltered method or predicates that produce no filter are rejected rather than deleting every document.
Method Return Types
A finder method may return the repository’s domain type T directly, or one flat container: Optional<T>, List<T>, Iterable<T>, or Page<T>. Entity subtypes, projections, and nested containers are rejected. Entity and Optional methods require at most one match. When no hit matches, an entity method returns null by default; if its repository package uses Spring’s @NonNullApi, an unannotated entity method instead throws EmptyResultDataAccessException, while an explicitly @Nullable method may return null. Optional returns the sole hit or Optional.empty(). Either single-result form throws IncorrectResultSizeDataAccessException if multiple hits match. List and Iterable contain matching entity content.
Inherited repository type variables are resolved in the concrete repository’s domain context. Method-level type variables are rejected, including inside a container, because the finder can only materialize the repository’s exact domain type.
Sorting and Paging
For finder methods, static ordering uses an OrderBy suffix, for example findByGenreOrderByRatingDesc(String genre). A Sort parameter provides runtime ordering, and a Pageable parameter provides an offset, limit, and optional sort.
A paged Page<Entity> uses the total reported by the same Meilisearch paginated search response; it does not run a separate exact-count query. pagination.maxTotalHits limits the search result window that Meilisearch can return, even when the reported total is larger. A Page method must declare a Pageable parameter.
A paged List or Iterable with Pageable returns only that requested window and has no total. An unpaged List or Iterable fetches all matching hits in chunks of 1,000 and requires Meilisearch to report an exact total. It fails rather than return partial content if the server’s pagination.maxTotalHits cap prevents complete retrieval, if the exact total changes while fetching, or if Meilisearch does not report an exact total. Configure @Pagination(maxTotalHits = …) when the application needs a different search limit; the default cap is 1,000. Passing Pageable.unpaged() requests the complete-result behavior; an unpaged Page reports the number of fetched results.
Index Settings
String arguments are values, not filter expressions. They are emitted as quoted and escaped Meilisearch filter literals; do not add quotes or filter syntax to the argument. Numeric and boolean values are emitted as literals. Every field used in a predicate—including predicates for count, existence, and delete methods—must be configured as filterable, and every field used for sorting must be configured as sortable. For example:
Nested property paths use dotted Meilisearch field names. For example, findByDetailsDirector(String director) filters details.director = "Director" for an entity with a details.director property. The segments are the mapped Java field names; custom aliases for these fields are not supported. Configure details.director as a filterable attribute; a filter on the top-level details attribute does not substitute for that nested path.
A collection of nested objects can also be filtered by a nested field. For example, findByCreditsDirector("Director") filters credits.director and matches a document with at least one matching object in its credits array. Configure credits.director as filterable. If the path is missing, IsNull matches it under the same missing-field semantics described above. Predicates on different nested fields are combined as document-level filters; they do not require values to come from the same array element.
@Setting(
filterableAttributes = { "genre", "rating", "available" },
sortableAttributes = { "rating" }
)
@Document(indexUid = "movies")
class Movie {
@Id String id;
String genre;
int rating;
boolean available;
}
interface MovieRepository extends MeilisearchRepository<Movie, String> {
Page<Movie> findByGenreInAndRatingGreaterThanEqual(
Collection<String> genres, int rating, Pageable pageable);
}
Those index settings must be applied to Meilisearch before the search runs. See Settings for annotation and programmatic configuration.
Declared Searches
Use io.vanslog.spring.data.meilisearch.repository.Query when a method name would be unwieldy or the search needs both full-text terms and explicit filters. The annotation’s value and filter attributes are aliases for a native Meilisearch filter template; q is a separate full-text search template. The primary string is a filter expression, not a search term or a JSON search request.
import java.util.Collection;
import java.util.List;
import io.vanslog.spring.data.meilisearch.annotations.Document;
import io.vanslog.spring.data.meilisearch.annotations.Setting;
import io.vanslog.spring.data.meilisearch.repository.MeilisearchRepository;
import io.vanslog.spring.data.meilisearch.repository.Query;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@Document(indexUid = "products")
@Setting(
filterableAttributes = { "category", "price", "active" },
searchableAttributes = { "title" },
sortableAttributes = { "price" }
)
class Product {
@Id String id;
String title;
String category;
int price;
boolean active;
}
interface ProductRepository extends MeilisearchRepository<Product, String> {
@Query("category = ?0 AND price <= ?1")
List<Product> affordable(String category, int maximumPrice);
@Query(q = "?0", filter = "category = ?1 AND price <= ?2")
Page<Product> search(String text, String category, int maximumPrice, Pageable pageable);
@Query(q = "?0")
List<Product> fullText(String text);
@Query(filter = "category IN ?0 AND active = true")
List<Product> inCategories(Collection<String> categories, Sort sort);
@Query(filter = "category = ?0 AND price <= ?2")
List<Product> byCategoryAndPrice(String category, Pageable pageable, int maximumPrice);
}
class ProductSearch {
private final ProductRepository products;
ProductSearch(ProductRepository products) {
this.products = products;
}
Page<Product> findAffordableBooks() {
return products.search("sci-fi?", "books", 25, PageRequest.of(0, 20));
}
}
The example’s @Setting declares the index attributes required by its filters, full-text search, and sorting. Repository creation can apply those settings; see Automatic Settings Application. The invocation passes ? inside runtime search text. It remains text because placeholders are parsed from the template before values are bound.
@Query("category = ?0") and @Query(filter = "category = ?0") are equivalent aliases. To use the named form of the annotated search method above, remove its annotation and load the matching Product.search filter and Product.search.q text templates shown below; the same invocation binds the same q and filter values.
An annotated search replaces query derivation: method-name predicates, count/exists/delete prefixes, and static OrderBy suffixes do not contribute to it.
Declared searches use MeilisearchOperations and the existing converter and finder execution path. They support only flat result forms using the repository’s domain type T: T, Optional<T>, List<T>, Iterable<T>, and Page<T>. Return declarations using an entity subtype, projections, or nested container types are not supported. Use a Pageable parameter for a Page result. Runtime Sort and Pageable remain supported for collection/page finders; single-result finders do not accept Pageable. Declared count, exists, delete, streams, Slice, Limit, and scrolling are not supported.
Generic parameters and result types inherited from a repository base interface are resolved against the concrete repository’s type arguments before validation. The resolved types must still fit the supported binding and result forms; a parameter explicitly declared as Serializable, for example, does not become supported just because the caller supplies a string.
Method-level result variables, such as <S extends Product> Optional<S> or <S extends Product> List<? extends S>, are not concrete result declarations and are rejected even when their upper bound is the repository domain type. This restriction also applies to variables nested in wildcard bounds, generic owners, or array components. The repository materializes its fixed domain type; callers cannot select a different subtype through a method type argument. Concrete domain wildcards and inherited repository type variables remain supported when they resolve to a supported result form.
Parameter Binding
Positional placeholders use a ? followed by one or more ASCII digits, such as ?0 or ?12, and refer to zero-based method parameter positions, including positions occupied by special parameters. Unlike the JPA reference’s ?1 example, the index here is zero-based and is not the ordinal among bindable parameters. Sort, Pageable, and other special parameters cannot be bound. In the example, byCategoryAndPrice(String category, Pageable pageable, int maximumPrice) uses ?0 and ?2; ?1 names the special Pageable and is invalid. Every bindable method argument must appear in the filter or search-text template.
Filter placeholders must occupy complete value positions; do not quote them or interpolate them into field names, operators, or existing string literals.
For example, use category = ?0, not category = "?0" or ?0 = true.
Strings are quoted and escaped so a value such as x" OR active = true remains one literal rather than adding a condition.
Control characters and literal backslash escape sequences remain distinct filter values.
Supported value positions include comparison right-hand sides, both bounds of price ?0 TO ?1, STARTS WITH and CONTAINS operands, scalar entries in category IN [?0, ?1], and complete flat collections/arrays in category IN ?0 or category NOT IN ?0.
Collection and array element declarations must support scalar binding. Known unsupported element types, including domain objects, nested collections, and arrays of collections or arrays, fail during repository creation. Inherited generic element types are resolved against the repository interface before this check. Raw collections, unresolved type variables, and Object element declarations retain runtime value validation.
Numeric placeholders are also supported in _geoRadius, _geoBoundingBox, and _geoPolygon arguments.
This binding support does not enable experimental operators or emulate operators missing from the server version.
Boolean and finite standard numeric values are emitted as native literals.
BigInteger and BigDecimal are supported; Date values use epoch milliseconds.
Numeric binding preserves the literal’s integer precision and decimal scale without converting through floating point; this does not change Meilisearch’s own numeric storage or comparison semantics.
Number parameter declarations are supported, but runtime numeric values must have one of the exact standard boxed primitive, BigInteger, or BigDecimal classes. Custom numeric subclasses are rejected rather than trusting their string representation as filter syntax.
Strings, characters, enum names, temporal values, and UUIDs are quoted.
These are native literal representations: the binder does not infer a property’s type from the filter string.
Null values, non-finite numbers, unsupported value types, empty collections/arrays, null collection elements, and nested collections are rejected before searching.
Unlike derived NotIn, a declared NOT IN with an empty bound collection does not silently remove the condition.
Search-text templates are scanned for positional placeholders: every ? in q must begin a placeholder with ASCII digits, such as ?0; there is no quoted-literal exception. Put a literal question mark in the bound argument instead, as in the invocation above. Runtime text is inserted after template parsing and its ? characters are not interpreted again. Search-text values are not filter-quoted and retain Meilisearch’s normal full-text syntax.
A q-only search that binds to blank text is rejected rather than becoming an unrestricted placeholder search. Blank search text is allowed when the declaration also supplies a filter.
Malformed placeholders (including ?name, ?-1, a bare ?, or a marker using non-ASCII digits), invalid placeholder positions, out-of-range indices, special-parameter references, incompatible declared parameter types, and unused bindable parameters fail during repository creation. A non-ASCII digit immediately following an ASCII index, such as the Arabic-Indic digit in ?0٠, is also invalid; this includes supplementary Unicode digits. Literal Unicode text separated from the index and runtime argument text are not restricted by the placeholder digit rule.
A ? outside a quoted filter literal is placeholder syntax; placeholders inside quoted filter literals are not supported.
The binder checks substitution boundaries, not the entire server query language: actual filter syntax, operator availability, and index settings are validated by Meilisearch.
The failure boundary depends on what is wrong:
| When | Examples |
|---|---|
Repository creation |
Conflicting |
Method invocation, before the search request |
Invalid runtime values such as |
Meilisearch request |
The complete filter expression, field availability/settings, server-supported operators, and server-version requirements are checked by Meilisearch. Placeholder validation does not parse or emulate the full filter language; unsupported syntax or unavailable server features fail at request time. |
Field Names and Settings
Native filter templates refer to stored Meilisearch index field names, including dotted nested paths. The module does not rewrite Java property names inside a filter string. Runtime sort properties continue to use the repository’s Java-property mapping. Fields used in filters must be configured as filterable, fields used in sorting as sortable, and full-text search uses the index’s searchable attributes.
Named Searches
Load named templates from a properties resource using the existing namedQueriesLocation configuration:
@EnableMeilisearchRepositories(
basePackages = "com.example.products",
namedQueriesLocation = "classpath:meilisearch-named-queries.properties")
class RepositoryConfig {}
The key <DomainSimpleName>.<methodName> stores the filter template.
An optional companion key ending in .q stores the search-text template.
Either key can declare a search, and both use the same parameter binding and finder rules as @Query.
Product.search=category = ?1 AND price <= ?2
Product.search.q=?0
Product.inCategories=category IN ?0 AND active = true
Product.fullText.q=?0
Use normal Java properties escaping in the resource. Overloaded methods share the same named-query key; each method must be compatible with that template. Annotations take precedence over named templates under declared-query lookup.
Query Lookup Strategies
The strategy names follow Spring Data Commons' query lookup strategy contract, with this module’s declaration sources and precedence:
| Strategy | Resolution order | If no declaration matches |
|---|---|---|
|
Annotated |
Derives a supported query from the method name. |
|
Annotated |
Fails while the repository is created; it does not derive a query. |
|
Method-name derivation only; annotations and named templates are ignored. |
Attempts to derive a supported query from the method name. |
An invalid selected declaration fails repository creation rather than falling through to a named template or derived query. See Repository Configuration.
Unsupported Query Methods
The supported predicate and return-type matrix is intentionally narrow. The repository rejects unsupported derived methods during bootstrap, with the method and unsupported operator or return type in the error. In particular:
-
Containing,EndingWith,Like, andRegexare unsupported text-pattern predicates.Containingwould require the experimentalCONTAINSfilter, which is not enabled by this module. Full-text search throughMeilisearchOperationswith aBasicQueryis a different, tokenized search mode, not an equivalent literal field-prefix or substring filter. -
Filter-based count, existence, and delete methods are supported only through the patterns and return types listed above; they do not accept sorting or paging. The standard repository
count()and delete methods are separate operations and are unchanged. -
Declared searches do not accept raw request JSON, Elasticsearch Query DSL, SpEL, or named placeholders. Use
MeilisearchOperationsand the existing Query models for advanced request options. -
Interface or class projections,
Slice, streams, scalar-valued finder results, and return types outside the supported method matrix are not supported.
These syntaxes are store-specific rather than interchangeable:
-
Spring Data JPA’s
@Queryreference describes JPQL and native SQL (JPA query methods). -
Spring Data MongoDB documents JSON-based repository queries (MongoDB JSON-based query methods).
-
Spring Data Elasticsearch documents JSON query bodies and SpEL support (Elasticsearch
@Query).
Spring Data Meilisearch’s @Query uses the native filter and q templates described on this page instead.
For search modes outside this subset, use MeilisearchOperations directly; it exposes explicit Meilisearch query construction without bypassing the operations abstraction.
Using MeilisearchOperations for Custom Queries
Use MeilisearchRepository for the built-in CRUD, ID lookup, collection listing, sorting, pageable methods, declared searches, and supported derived finder, count, existence, and filter-delete methods. Use MeilisearchOperations for dynamically composed searches or richer search result data, such as facets, similar-document search, or federated multi-search.
For a mapped domain type such as Movie, inject MeilisearchOperations when you need to compose a search query directly:
import io.vanslog.spring.data.meilisearch.core.MeilisearchOperations;
import io.vanslog.spring.data.meilisearch.core.SearchHits;
import io.vanslog.spring.data.meilisearch.core.query.BasicQuery;
class MovieSearchService {
private final MeilisearchOperations operations;
MovieSearchService(MeilisearchOperations operations) {
this.operations = operations;
}
SearchHits<Movie> search(String text) {
BasicQuery query = BasicQuery.builder().withQ(text).build();
return operations.search(query, Movie.class);
}
}
This service-level call returns Meilisearch search hits. Repository query methods provide derived filters and declared filter/full-text searches with finder return forms; use MeilisearchOperations when search needs dynamic composition, advanced options, facets, similar-document search, or federated multi-search.
Base Repository Paging, Sorting, and Result Limits
Derived finder and existence methods use search. Derived count methods use documents/fetch, and derived filter-delete methods use the native index filter-delete API. The standard repository methods have separate behavior:
-
A paged
findAll(Pageable)is search-backed. ItsPagetotal comes from the total reported by Meilisearch search results.pagination.maxTotalHitscan cap returned content without capping that reported total. -
An unpaged
findAll(Pageable)uses the document-list API, retrieves all documents into memory, and sets thePagetotal to the retrieved count. It is not limited bymaxTotalHits. -
findAll()also lists all documents through the document API. SortedfindAll(Sort)and sorted unpagedfindAll(Pageable)count first, then fetch up to that count; additions between those requests may be absent. -
Sorting document listings requires a sortable index attribute and Meilisearch 1.16 or later.
findAllById(…)uses native ID-list retrieval, available from Meilisearch 1.14, with no fallback on older servers. -
Document-list reads are not capped by search
maxTotalHitsand ignoredisplayedAttributes. -
Empty
saveAll(…),deleteAll(Iterable), anddeleteAllById(…)operations complete without a server request. The no-argumentdeleteAll()still deletes all documents for the entity type.
For full method-to-API behavior, see Method Behavior at a Glance.