Document indexing and search
Fluxzero provides a powerful and flexible document store that lets you persist and query models using full-text search, filters, and time-based constraints.
This system is especially useful for:
- Querying across entities (e.g., active users, recent payments)
- Supporting projections for read APIs or dashboards
- Tracking workflows, external states, or business processes
- Replacing the need for a traditional read model database
Manual indexing
Section titled “Manual indexing”You can index any object manually using:
Fluxzero.index(myObject);Fluxzero.index(myObject)This stores myObject in the document store so it can be queried later via Fluxzero.search(...).
- If the object is annotated with
@Searchable, any declaredcollection,timestampPath, orendPathwill be used. - If a field is annotated with
@EntityId, it becomes the document ID. Otherwise, a random ID is generated. - Timestamps can be inferred from annotated paths or passed explicitly.
You can also specify the collection in which the object should be stored directly:
Fluxzero.index(myObject, "customCollection");Fluxzero.index(myObject, "customCollection")Searchable domain models
Section titled “Searchable domain models”Many models in Fluxzero (e.g. aggregates or stateful handlers) are automatically indexable:
@Aggregate(searchable = true)@Stateful(implicitly@Searchable)- Directly annotate any POJO with
@Searchable
@Aggregate(searchable = true)public record UserAccount(@EntityId UserId userId, UserProfile profile, boolean accountClosed) {}@Aggregate(searchable = true)data class UserAccount( @EntityId val userId: UserId, val profile: UserProfile, val accountClosed: Boolean)By default, the collection name is derived from the class’s simple name (UserAccount → "UserAccount"),
unless explicitly overridden via an annotation like @Aggregate, @Stateful or @Searchable or in the search/index
call:
@Aggregate(searchable = true, collection = "users", timestampPath = "profile/createdAt")@Aggregate(searchable = true, collection = "users", timestampPath = "profile/createdAt")Querying indexed documents
Section titled “Querying indexed documents”Use the fluent search(...) API:
List<UserAccount> admins = Fluxzero .search("users") .match("admin", "profile/role") .inLast(Duration.ofDays(30)) .sortBy("profile/lastLogin", true) .fetch(100);val admins: List<UserAccount> = Fluxzero .search("users") .match("admin", "profile/role") .inLast(Duration.ofDays(30)) .sortBy("profile/lastLogin", true) .fetch(100)Consistency after commands
Section titled “Consistency after commands”Search is optimized for current state, but indexing still follows the commit path. When a command directly updates a
searchable aggregate, Fluxzero waits for asynchronous after-handler aggregate commits before publishing the command
result by default. In many cases, calling sendCommandAndWait(...) and then querying the aggregate’s document can read
the just-committed state.
This does not cover downstream projections or side-effect indexing. If a document is indexed by an event handler or projection handler, treat it as eventually consistent and wait for that projection’s own completion signal when immediate read-after-write behavior matters.
You can also query by class:
List<UserAccount> users = Fluxzero .search(UserAccount.class) .match("Netherlands", "profile.country") .fetchAll();val users: List<UserAccount> = Fluxzero .search(UserAccount::class.java) .match("Netherlands", "profile.country") .fetchAll()Common filtering constraints
Section titled “Common filtering constraints”Fluxzero supports a rich set of constraints:
lookAhead("cat", paths...)– search-as-you-type lookupsquery("*text & (cat* | hat)", paths...)– full-text searchmatch(value, path)– field matchmatchFacet(name, value)– match field with@Facetbetween(min, max, path)– numeric or time rangessince(...),before(...),inLast(...)– temporal filtersanyExist(...)– match if any of the fields are present- Logical operations:
not(...),all(...),any(...)
Fluxzero.search("payments") .match("FAILED", "status") .inLast(Duration.ofDays(1)) .fetchAll();Fluxzero.search("payments") .match("FAILED", "status") .inLast(Duration.ofDays(1)) .fetchAll()Async search operations
Section titled “Async search operations”Search execution also has asynchronous variants for request handlers and web endpoints that can return a
CompletableFuture. Return the future directly instead of calling .join() inside the handler; this lets Fluxzero keep
processing other work while the search request is in flight.
fetchAsync(maxSize)/fetchAsync(maxSize, type)fetch matching documents asynchronously.countAsync()returns the matching document count asynchronously.aggregateAsync(fields...)andgroupBy(paths...).aggregateAsync(fields...)return search statistics asynchronously.facetStatsAsync()returns facet value counts asynchronously.
@HandleQueryCompletableFuture<List<UserAccount>> handle(SearchUsers query) { return Fluxzero.search(UserAccount.class) .lookAhead(query.term(), "profile.name", "email") .fetchAsync(50, UserAccount.class);}@HandleQueryfun handle(query: SearchUsers): CompletableFuture<List<UserAccount>> = Fluxzero.search(UserAccount::class.java) .lookAhead(query.term, "profile.name", "email") .fetchAsync(50, UserAccount::class.java)Matching facet fields
Section titled “Matching facet fields”If you’re filtering on a field that is marked with @Facet, it’s better to use:
.matchFacet("status", "archived").matchFacet("status", "archived")instead of:
.match("archived", "status").match("archived", "status")While both achieve the same result, matchFacet(...) is generally faster and more efficient.
Facet statistics
Section titled “Facet statistics”When a field or getter is annotated with @Facet, you can also retrieve facet statistics — e.g., how many documents
exist per value of a given property.
Example: Product breakdown by category and brand
Section titled “Example: Product breakdown by category and brand”@Searchablepublic record Product(@Facet String category, @Facet String brand, String name, BigDecimal price) {}
List<FacetStats> stats = Fluxzero.search(Product.class) .lookAhead("wireless") .facetStats();@Searchabledata class Product( @Facet val category: String, @Facet val brand: String, val name: String, val price: BigDecimal)
val stats: List<FacetStats> = Fluxzero.search(Product::class.java) .lookAhead("wireless") .facetStats()This gives you document counts per facet value:
[ { "name": "category", "value": "headphones", "count": 55 }, { "name": "brand", "value": "Acme", "count": 45 }, { "name": "brand", "value": "NoName", "count": 10 }]Each FacetStats object will contain:
- the facet name (e.g.,
category) - the distinct values (e.g.,
"electronics","clothing") - the number of documents per value
Use facetStatsAsync() when the surrounding handler returns a CompletableFuture.
Search index exclusion
Section titled “Search index exclusion”By default, all non-transient properties of a document are included in the search index. However, you can fine-tune what fields get indexed using the following annotations:
@SearchExclude
Section titled “@SearchExclude”Use @SearchExclude to exclude a field or type from search indexing. This prevents the property from being matched in
search queries, though it will still be present in the stored document and accessible at runtime.
public record Order(String id, Customer customer, @SearchExclude byte[] encryptedPayload) {}data class Order( val id: String, val customer: Customer, @SearchExclude val encryptedPayload: ByteArray)You can also exclude entire types:
@SearchExcludepublic record EncryptedData(byte[] value) {}@SearchExcludedata class EncryptedData(val value: ByteArray)In this case, none of the properties of EncryptedData will be indexed, unless you override selectively with
@SearchInclude.
@SearchInclude
Section titled “@SearchInclude”Use @SearchInclude to override an exclusion. This is functionally equivalent to @SearchExclude(false) and is
typically used on a field or class that would otherwise be excluded by inheritance or parent-level settings.
@SearchExcludepublic record BaseDocument(String internalNotes, @SearchInclude String publicSummary) {}@SearchExcludedata class BaseDocument( val internalNotes: String, @SearchInclude val publicSummary: String)Here, internalNotes will not be indexed, but publicSummary will be.
Behavior summary
Section titled “Behavior summary”| Annotation | Effect |
|---|---|
@SearchExclude | Prevents property/type from being indexed for search |
@SearchExclude(false) or @SearchInclude | Explicitly includes a property even if a parent type is excluded |
| No annotation | Field is included in the search index by default |
Rapid sorting and filtering
Section titled “Rapid sorting and filtering”To enable efficient range filters and sorted results in document searches, annotate properties with @Sortable:
public record Product(@Sortable BigDecimal price, @Sortable("releaseDate") Instant publishedAt) {}data class Product( @Sortable val price: BigDecimal, @Sortable("releaseDate") val publishedAt: Instant)This tells Fluxzero to pre-index these fields in a lexicographically sortable format. When you issue a search
with a between(...) constraint or .sortBy(...) clause, the Fluxzero Runtime can evaluate it directly in the data store —
without needing to load and compare documents in memory.
Optimized search example
Section titled “Optimized search example”List<Product> results = Fluxzero.search(Product.class) .between(10, 100, "price") .sortBy("releaseDate") .fetch(100);val results: List<Product> = Fluxzero.search(Product::class.java) .between(10, 100, "price") .sortBy("releaseDate") .fetch(100)What gets indexed
Section titled “What gets indexed”Fluxzero normalizes and encodes sortable fields depending on their value type:
| Type | Behavior |
|---|---|
| Numbers | Padded base-10 string (preserves order, supports negatives) |
| Instants | ISO-8601 timestamp format |
| Strings/Others | Normalized (lowercased, trimmed, diacritics removed) |
This ensures that sorting is consistent and correct across types and locales.
Nested and composite values
Section titled “Nested and composite values”If the sortable field is:
- A collection → Max element is indexed. Create a getter if you need sorting on the min element
- A map → Values are indexed using
key/propertyNamepath - A nested object annotated with
@Sortable→ ItstoString()is used - A POJO with
@Sortablefields → Those nested values are indexed with prefixed paths
Important notes
Section titled “Important notes”- No retroactive indexing: Adding
@Sortableto a field does not automatically reindex existing documents. - To apply sorting retroactively, trigger a reindex (e.g. with
@HandleDocumentand a bumped@Revision). - Sorting and filtering still happen within the Fluxzero Runtime, but without
@Sortablethe logic falls back to in-memory evaluation — which is much slower. - Avoid re-implementing sorting/filtering in application code; express them in the search query so execution stays in the Runtime.
Customizing returned fields
Section titled “Customizing returned fields”When performing a search, you can control which fields are included or excluded in the returned documents.
This is useful for:
- Hiding sensitive fields (e.g. private data, tokens)
- Reducing payload size
- Optimizing performance when only partial data is needed
Example
Section titled “Example”Given the following indexed document:
{ "id": "user123", "profile": { "name": "Alice", "email": "alice@example.com", "ssn": "123456789" }, "roles": [ "user", "admin" ]}You can exclude sensitive fields like so:
Fluxzero.search("users") .exclude("profile.ssn") .fetch(50);Fluxzero.search("users") .exclude("profile.ssn") .fetch(50)This will return:
{ "id": "user123", "profile": { "name": "Alice", "email": "alice@example.com" }, "roles": [ "user", "admin" ]}Streaming results
Section titled “Streaming results”Fluxzero supports efficient streaming of large result sets:
Fluxzero.search("auditTrail") .inLast(Duration.ofDays(7)) .stream().forEach(auditEvent -> process(auditEvent));Fluxzero.search("auditTrail") .inLast(Duration.ofDays(7)) .stream() .forEach { auditEvent -> process(auditEvent) }Deleting documents
Section titled “Deleting documents”To remove documents from the index:
Fluxzero.search("expiredTokens") .before(Instant.now()) .delete();Fluxzero.search("expiredTokens") .before(Instant.now()) .delete()Summary
Section titled “Summary”- Use
Fluxzero.index(...)to manually index documents. - Use
@Searchableto configure the collection name or time range for an object. - Use
@Aggregate(searchable = true)or@Statefulfor automatic indexing. - Use
Fluxzero.search(...)to query, stream, sort, and aggregate your documents. - Use
fetchAsync(...),countAsync(),aggregateAsync(...), andfacetStatsAsync()in asynchronous handlers.
© 2026 Fluxzero