RUAL Documentation

Storage Guide

A hands-on guide to working with RUAL storage: creating your first document, building search queries, modelling data without relations, and using full-text search effectively.

This guide builds on the Storage documentation and walks you through practical examples of working with RUAL's built-in storage system. If you haven't read the storage docs yet, we recommend starting there to understand the fundamentals of metadata, document updates, and expiry.

Creating Your First Document

To create a document, you need two things: a storage reference and a set of mutations that define the fields. Here is the block chain for creating a document:

  1. Add a storage block and select the storage you want to write to.
  2. Add a mutations_set_bp_field_multiple block to define the fields and their values.
  3. Add a function_create_document block, connect the storage and mutations to its in-pins, and connect the flow pin from your flow.

The function_create_document block returns the created document including the generated _meta data with its unique guid.

Created Document Output
{ "username": "joe", "email": "joe@example.com", "role": "editor", "_meta": { "guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc", "expiry": -1, "entity": 1, "removed": 0, "created": 1706396400, "updated": 1706396400, "ums": 1706396400803, "cms": 1706396400362, "update_hash": "60e27209fa93063b5605e41605c2722ed428cae0" } }

Field Name Rules

Field names are always stored in lowercase. If you use UserName, it will be stored as username. Fields cannot include dots (.) — if they do, RUAL automatically converts them into a nested object structure.

Updating Documents

RUAL uses a transaction-based update system through mutations. Each update is processed sequentially, ensuring that concurrent updates don't overwrite each other. This makes it safe to use counters and array operations.

To update a document, use the function_update_document block with the document's guid and the mutations you want to apply.

Common mutation operations include:

Block Operation
mutations_set_bp_field_multiple Set one or more fields to specific values.
mutations_set_custom_field Set a field using a dynamic key and value.
mutations_increment_by_field Increment a numeric field by a given amount.
mutations_decrement_by_field Decrement a numeric field by a given amount.
mutations_add_to_array_by_field Add a unique value to an array field.
mutations_remove_from_array_by_field Remove a value from an array field.
mutations_remove_by_field_multiple Remove one or more fields from the document entirely.
mutations_add Combine multiple mutation sets into one.

Getting a Document by GUID

The fastest way to retrieve a document is by its _meta.guid using the function_get_document block. This method is faster than searching and is easier for the system to cache. Whenever possible, prefer get document over search queries for individual document retrieval.

To retrieve multiple documents at once, use the function_get_documents block with an array of GUIDs.

Building Search Queries

Search queries are built by connecting query blocks together and feeding them into a function_search block. A typical search flow looks like this:

  1. Create the query conditions using blocks like query_bool_term_fields (exact match on a field).
  2. Wrap them in a boolean query using query_bool_filter, query_bool_must, or query_bool_should.
  3. Optionally add sorting with query_sort_field and a limit with query_limit or a number block connected to the limit pin.
  4. Combine all query parts using query_and (multiple queries).
  5. Connect the final query output to the query in-pin of function_search.
  6. Select the storage on the function_search block.

Search Query Example

Below is an example of a search that finds documents in a messages storage where user_guid matches a specific value, sorted by creation date descending, limited to 10 results:

Block Purpose Connects To
value_default The user GUID to search for query_bool_term_fields (user_guid field)
query_bool_term_fields Match documents where user_guid equals the value query_bool_filter
query_bool_filter Wrap the term query in a filter context query_and
query_sort_field Sort by _meta.created descending query_and
query_and Combine filter and sort into one query function_search (query pin)
number_default (10) Limit results to 10 function_search (limit pin)

The function_search block returns an array of matching documents. For queries that should return at most one result, use function_search_single_result instead.

Query Types Overview

Query Block What It Does
query_bool_must All provided queries must match (AND logic).
query_bool_should One or more queries should match (OR logic).
query_bool_filter Filters results using a boolean query (does not affect scoring).
query_bool_must_not All provided queries must not match (exclusion).
query_bool_term_fields Exact match on a selected field.
query_bool_range_field Range comparison (greater than, less than) on a numeric field.
query_bool_range_date_field Range comparison on a date field.
query_bool_exists_bpfield Check if a field exists in the document.
query_bool_wildcard_string_field Wildcard matching on a field value.
query_bool_prefix_string_field Check if a field value starts with a given prefix.
query_bool_term_fuzzy Find documents with terms similar to the search term (fuzzy matching).

Search Tips

Case-Sensitive Searches Search queries in RUAL storage are case-sensitive. If you store a value as "Joe" and search for "joe", the document will not be found. It is good practice to store searchable values in lowercase to ensure accurate search results.
  • Use get document when possible: Retrieving a document by _meta.guid is the fastest method and is easy to cache. Avoid creating custom unique identifiers and searching for them.
  • Use query_bool_filter for most queries: Filter context is more efficient than must because it does not calculate relevance scores.
  • Limit your results: Always set a reasonable limit on search results. Smaller result sets are faster to return and process.
  • Select specific fields: Use the query_source_by_field block to return only the fields you need, reducing data transfer.
  • Stream large result sets: For queries that return many documents, use function_search_stream to process results as they arrive rather than loading everything into memory.

Real-Time Search Results

By default, search queries do not wait for the most recent inserts to be indexed. If you create a document and immediately search for it, it might not appear in the results yet. In most cases this slight delay is acceptable.

If your use case requires the absolute latest data, you can use the disable cache query block. This forces the system to wait until all pending inserts are indexed before executing the search. However, in high-insert environments, this can introduce significant delays.

Learn More Read about real-time search results and when to use the disable cache option.

Data Modelling Without Relations

RUAL storage is schema-less and does not support relationships between storage entities. This means you cannot do traditional SQL-style joins. Instead, you should denormalize your data: include all the information you need within each document.

Example: Orders and Customers

In a traditional relational database, you would have a separate customers table and reference the customer ID from the orders table. In RUAL, you should include the relevant customer information directly in the order document:

Denormalized Order Document
{ "order_number": "ORD-2024-0042", "status": "shipped", "total": 15990, "customer_name": "joe doe", "customer_email": "joe@example.com", "customer_guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc", "items": [ { "name": "widget", "quantity": 3, "price": 5330 } ], "_meta": {} }

This way, when you search for orders, you immediately have the customer name and email available without needing a second query. If the customer changes their name, you can update all their order documents using a storage event on the customers storage.

Denormalization Strategies

  • Embed frequently accessed data: Include the data you display most often directly in the document. For example, store customer_name on every order instead of just customer_guid.
  • Keep a reference GUID: Always store the guid of related documents so you can use get document when you need the full, up-to-date data.
  • Use storage events for sync: When a source document changes (e.g., customer name), use a storage event to update all documents that embed that data.
  • Store counters separately: Instead of using aggregation queries to count documents, maintain a counter field on a parent document and increment it when child documents are created. Alternatively, store aggregated stats in Redis cache.

Full-Text Search

RUAL's storage includes a built-in full-text search engine. To take advantage of it:

Remember that search queries are case-sensitive. Store searchable text in lowercase and convert user input to lowercase before searching to ensure consistent results.

Document Lifecycle

Understanding the full lifecycle of a document helps you design robust data flows:

Operation Block Notes
Create function_create_document Returns the created document with _meta. Triggers on_created and on_saved storage events.
Read function_get_document Fastest retrieval method by guid. Preferred over search for single documents.
Search function_search Full-text search with query blocks. Results may have a slight indexing delay.
Update function_update_document Transaction-based. Creates a revision if enabled. Triggers on_updated and on_saved events.
Remove (soft) function_remove_document Marks as removed but data stays. Excluded from search by default. Can be restored.
Restore function_restore_document Restores a removed document back to active state.
Delete (permanent) function_delete_document Permanently deletes. Cannot be recovered unless revisions are enabled.
Best Practice Use remove (soft delete) in most cases instead of permanent delete. This allows recovery if needed. You can set up a repeating event to permanently purge removed documents after a retention period.

Caching with Redis

For high-traffic applications, caching search results in Redis is critical to maintain performance. Instead of hitting storage for every page visit, check Redis first and only query storage when the cache is empty or expired.

Common caching patterns include:

  • Cache frequently accessed search results with a TTL (time to live) using redis cache blocks.
  • Invalidate or update the cache when documents change, using storage events.
  • Store aggregated statistics in Redis and update them periodically with repeating events.
Common Pitfalls Learn about proper cache usage, real-time search, and avoiding aggregation misuse.