Finders & Aggregations
Locate records by field values, compute aggregates, pluck specific columns, and perform atomic instance operations on your models.
Finder Methods
Finder methods provide convenient shortcuts for common lookup patterns. They return model instances (or null when no match is found).
FIND_BY Find by Exact Field Match
Returns the first record whose field equals the given value, or null if none is found.
# Find by exact field match
user = User.find_by("email", "[email protected]")
FIRST_BY Find with Ordering
Returns the first record matching the field value, with ordering applied.
# Find with ordering (first by field value)
user = User.first_by("name", "Alice")
FIND_OR_CREATE_BY Find or Create
Returns an existing record if one matches, or creates a new one. An optional third argument provides additional fields for creation.
# Find or create - returns existing or creates new
user = User.find_or_create_by("email", "[email protected]")
user = User.find_or_create_by("email", "[email protected]", { "name": "New User" })
DYNAMIC FINDERS Dynamic Finder Methods
Automatically generated finders for any field combination. Method name encodes the field names and conditions.
# Single field finder — find_by_fieldname(value)
user = User.find_by_email("[email protected]")
# Two-field finder — find_by_field1_and_field2(val1, val2)
user = User.find_by_email_and_active("[email protected]", true)
# Three+ field combinations
post = Post.find_by_title_and_published_and_author_id("Hello", true, 123)
These methods return the first matching record or null if not found.
Aggregations
Aggregation methods let you compute sums, averages, minimums, maximums, and grouped totals directly in the database.
QueryBuilder mode: Aggregation methods (sum, avg, min, max, group_by) return a QueryBuilder in aggregation mode. Chain with .first to execute and get the result, or .to_query to inspect the generated SDBQL.
SUM Sum
Compute the sum of a numeric field. Chain .first to execute.
# Sum — chain .first to execute
total = User.where("age > @a", { "a": 18 }).sum("balance").first
AVG Average
# Average
avg = User.avg("score").first
MIN Minimum
# Minimum
min_score = User.min("score").first
MAX Maximum
# Maximum
max_score = User.max("views").first
GROUP_BY Group By Aggregation
Group records by a field and apply an aggregation function. Chain .all for grouped results.
# Group by aggregation — chain .all for grouped results
by_country = User.group_by("country", "sum", "balance").all
# Returns: [{ group: "US", result: 1000 }, { group: "FR", result: 500 }, ...]
INSPECT Inspecting Generated Queries
Use .to_query to see the SDBQL that a QueryBuilder will execute.
# Inspect the generated query
q = User.where("active = @a", { "a": true }).sum("balance").to_query
# => FOR doc IN users FILTER doc.active == @a RETURN SUM(doc.balance)
Pluck
Use pluck to retrieve only specific field values instead of full model instances. This is more efficient when you only need a subset of fields.
SINGLE FIELD Pluck a Single Field
Returns a flat array of values. Chain .all to execute.
# Get array of single field values — chain .all to execute
names = User.where("active = @a", { "a": true }).pluck("name").all
# Returns: ["Alice", "Bob", "Charlie"]
MULTIPLE FIELDS Pluck Multiple Fields
Returns an array of objects with the requested keys.
# Get multiple fields as objects
users = User.pluck("name", "email").all
# Returns: [{ name: "Alice", email: "[email protected]" }, ...]
Exists
Check whether any records match a query without fetching them. Returns a boolean.
# Check if records exist — chain .first to execute (returns boolean)
exists = User.where("role = @r", { "r": "admin" }).exists.first
# Returns: true or false
# Inspect the generated query
q = User.where("role = @r", { "r": "admin" }).exists.to_query
# => FOR doc IN users FILTER doc.role == @r LIMIT 1 RETURN true
Instance Increment/Decrement
Perform atomic updates and utility operations on individual model instances.
INCREMENT / DECREMENT Atomic Field Updates
Atomically add or subtract from a numeric field in the database. The default step is 1.
user = User.find("user_id")
# Atomic increment/decrement (CAS via `_rev` with bounded retry)
user.increment("view_count") # +1
user.increment("view_count", 5) # +5
user.decrement("stock") # -1
Each call drives an optimistic compare-and-swap loop against SoliDB rather than a plain read-modify-write on the in-memory instance:
- Re-fetch the document to read the current field value and its
_rev. - Compute
current ± delta. - PUT the new value with an
If-Match: <rev>header. - If another writer raced in between, the DB returns
409 Conflictand the loop retries (up to 10 attempts).
On success the in-memory instance's field and _rev are refreshed. Concurrent increments cannot lose updates — every successful PUT was the unique continuation of the rev it read.
Under extreme contention all 10 retries can fail; the call returns an error like "increment failed: Atomic update of users.view_count failed after 10 attempts (too much contention)" instead of silently dropping the update. See Models for the same details from the model-API surface.
TOUCH Update Timestamp
Updates the _updated_at timestamp without modifying any other fields.
# Update timestamp only
user.touch # Updates _updated_at
RELOAD Refresh from Database
Re-fetches the record from the database, discarding any unsaved local changes.
# Refresh from database
user.reload
Vector / Similarity Search
Soli supports AI-native vector similarity search as a first-class database primitive.
Chain .similar(query, field?, top_k?) onto any query to rank results by semantic relevance.
.SIMILAR() Basic Similarity Search
Finds records whose embedding field is closest to the query text.
Results include a _similarity_score field.
# Semantic search with chained filters
results = Post.where("category == 'docs'").similar("how to deploy").all
# Access the similarity score
for (post in results)
print(post.title + " (score: " + str(post._similarity_score) + ")")
end
OPTIONS Custom Field & Top-K
Specify a different embedding field name and control how many results to return.
# Custom field and top-5 results
results = Product
.where("active == true")
.similar("red shoes", "title_embedding", 5)
.all
# With filter and custom parameters
results = Product
.where("price <= @max", {"max": 50})
.similar("comfortable running shoes", "description_vec", 20)
.all
Configuration
Embeddings are generated by calling an OpenAI-compatible API. Set these environment variables:
SOLI_EMBEDDING_API_KEY=sk-... # Required
SOLI_EMBEDDING_URL=https://api.openai.com/v1/embeddings # Default
SOLI_EMBEDDING_MODEL=text-embedding-3-small # Default
When the API key is not set, .similar() returns an empty result set.
The current implementation computes cosine similarity client-side in Rust after
fetching matching documents from SolidB.
SolidDB Native Vector Search
SolidDB provides native vector search with HNSW indexes, VECTOR_SIMILARITY() in SDBQL,
scalar quantization (4x memory reduction), and hybrid search combining vector + fulltext.
Create a vector index on your embedding field for production-scale workloads:
curl -X POST http://localhost:6745/_api/database/solidb/vector/products \
-H "Content-Type: application/json" \
-d '{
"name": "embedding_idx",
"field": "embedding",
"dimension": 1536,
"metric": "cosine"
}'
See the SolidDB Vector Search docs and Hybrid Search docs for SDBQL functions, REST API, and production tuning.