ExSift
MongoDB-style query filtering for Elixir collections.
ExSift is an Elixir library inspired by sift.js that brings MongoDB's powerful query syntax to Elixir. Filter lists, maps, and any enumerable with an expressive, familiar query language.
Features
- MongoDB-compatible query syntax - Use the same operators you know from MongoDB
- Type-safe - Full Elixir typespecs and pattern matching
- Comprehensive operators - Support for comparison, logical, array, and special operators
- Nested property access - Query deeply nested maps with dot notation
- Regex support - Pattern matching with Elixir's
Regexmodule - Date/Time support - Compare
DateTime,NaiveDateTime, andDatetypes - Well-tested - 40+ tests covering all operators and edge cases
Installation
Add ex_sift to your list of dependencies in mix.exs:
def deps do
[
{:ex_sift, "~> 0.1.0"}
]
endQuick Start
data = [
%{name: "Alice", age: 30, city: "NYC"},
%{name: "Bob", age: 25, city: "SF"},
%{name: "Charlie", age: 35, city: "NYC"}
]
# Simple equality
ExSift.filter(data, %{city: "NYC"})
# => [%{name: "Alice", ...}, %{name: "Charlie", ...}]
# Comparison operators
ExSift.filter(data, %{age: %{"$gt" => 28}})
# => [%{name: "Alice", age: 30, ...}, %{name: "Charlie", age: 35, ...}]
# Multiple conditions
ExSift.filter(data, %{city: "NYC", age: %{"$gte" => 30}})
# => [%{name: "Alice", ...}, %{name: "Charlie", ...}]Supported Operators
Comparison Operators
$eq- Equals (same as direct value)ExSift.filter(data, %{age: %{"$eq" => 30}})$ne- Not equalsExSift.filter(data, %{city: %{"$ne" => "NYC"}})$gt- Greater thanExSift.filter(data, %{age: %{"$gt" => 25}})$gte- Greater than or equalExSift.filter(data, %{age: %{"$gte" => 30}})$lt- Less thanExSift.filter(data, %{age: %{"$lt" => 30}})$lte- Less than or equalExSift.filter(data, %{age: %{"$lte" => 25}})
Logical Operators
$and- All conditions must matchExSift.filter(data, %{ "$and" => [ %{age: %{"$gte" => 25}}, %{city: "NYC"} ] })$or- At least one condition must matchExSift.filter(data, %{ "$or" => [ %{age: %{"$lt" => 26}}, %{city: "LA"} ] })$nor- No conditions matchExSift.filter(data, %{ "$nor" => [ %{city: "NYC"}, %{city: "SF"} ] })$not- NegationExSift.filter(data, %{age: %{"$not" => %{"$lt" => 30}}})
Array Operators
$in- Value in arrayExSift.filter(data, %{city: %{"$in" => ["NYC", "LA", "SF"]}})$nin- Value not in arrayExSift.filter(data, %{city: %{"$nin" => ["NYC"]}})$all- Array contains all valuesExSift.filter(data, %{tags: %{"$all" => ["admin", "user"]}})$elemMatch- Array element matches querydata = [%{items: [%{id: 1, active: true}]}] ExSift.filter(data, %{items: %{"$elemMatch" => %{id: 1, active: true}}})$size- Array has specific lengthExSift.filter(data, %{tags: %{"$size" => 3}})
Other Operators
$exists- Field exists (not nil)ExSift.filter(data, %{email: %{"$exists" => true}}) ExSift.filter(data, %{phone: %{"$exists" => false}})$type- Type checkingExSift.filter(data, %{age: %{"$type" => "number"}}) ExSift.filter(data, %{name: %{"$type" => "string"}}) # Supported types: "string", "number", "integer", "float", # "boolean", "map", "list", "atom", "date", "datetime", "nil"$mod- Modulus operationExSift.filter(data, %{count: %{"$mod" => [5, 0]}}) # divisible by 5$regex- Regular expression matching# Using Elixir regex ExSift.filter(data, %{name: ~r/^[AB]/}) # Using $regex operator ExSift.filter(data, %{email: %{"$regex" => "@gmail\\.com$"}})
Advanced Usage
Nested Properties
Use dot notation to query nested maps:
data = [
%{user: %{profile: %{age: 30, verified: true}}},
%{user: %{profile: %{age: 25, verified: false}}}
]
ExSift.filter(data, %{"user.profile.age" => %{"$gt" => 28}})
# => [%{user: %{profile: %{age: 30, ...}}}]Complex Queries
Combine multiple operators for powerful filtering:
data = [
%{name: "Alice", age: 30, status: "active", tags: ["admin"]},
%{name: "Bob", age: 25, status: "inactive", tags: ["user"]},
%{name: "Charlie", age: 35, status: "active", tags: ["admin", "moderator"]}
]
ExSift.filter(data, %{
"$and" => [
%{age: %{"$gte" => 25, "$lte" => 35}},
%{status: "active"},
%{tags: %{"$in" => ["admin"]}}
]
})
# => [%{name: "Alice", ...}, %{name: "Charlie", ...}]Utility Functions
ExSift provides several utility functions beyond filter/2:
data = [%{a: 1}, %{a: 2}, %{a: 3}]
# Test a single item
ExSift.test(%{a: 1}, %{a: 1}) # => true
# Find first matching item
ExSift.find(data, %{a: 2}) # => %{a: 2}
# Check if any match
ExSift.any?(data, %{a: %{"$gt" => 2}}) # => true
# Check if all match
ExSift.all?(data, %{a: %{"$gte" => 1}}) # => true
# Count matches
ExSift.count(data, %{a: %{"$lt" => 3}}) # => 2
# Compile query for reuse
tester = ExSift.compile(%{a: %{"$gt" => 1}})
tester.(%{a: 2}) # => true
tester.(%{a: 1}) # => falseArchitecture
ExSift is built with three main modules:
ExSift- Main API and utility functionsExSift.Query- Query parsing and matching logicExSift.Operators- Operator implementations
The library leverages Elixir's pattern matching and protocol system for extensible, type-safe query operations.
Comparison with sift.js
ExSift is inspired by sift.js but adapted for Elixir's functional programming paradigm:
| Feature | sift.js | ExSift |
|---|---|---|
| Language | JavaScript/TypeScript | Elixir |
| Architecture | Operation classes with state | Pattern matching + pure functions |
| Extensibility | Custom operations via options | Protocol-based (future) |
| Type Safety | TypeScript generics | Dialyzer typespecs |
| Immutability | Depends on usage | Built-in (Elixir default) |
Performance
ExSift uses single-pass filtering with early termination where possible. All operations are implemented as pure functions without side effects.
For large datasets, consider:
-
Using
ExSift.compile/1to create reusable query functions -
Leveraging
ExSift.find/2orExSift.any?/2for early termination - Pre-filtering with simpler queries before complex ones
Testing
Run the test suite:
mix testExSift includes 40+ tests covering:
- All operators
- Nested property access
- Complex query combinations
- Edge cases and error handling
License
MIT License - See LICENSE file for details
Acknowledgments
Inspired by sift.js by Craig Condon.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.