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.
- 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
Add ex_sift to your list of dependencies in mix.exs:
def deps do
[
{:ex_sift, "~> 0.1.0"}
]
enddata = [
%{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", ...}]-
$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}})
-
$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}}})
-
$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}})
-
$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$"}})
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, ...}}}]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", ...}]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}) # => falseExSift 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.
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) |
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
Run the test suite:
mix testExSift includes 40+ tests covering:
- All operators
- Nested property access
- Complex query combinations
- Edge cases and error handling
MIT License - See LICENSE file for details
Inspired by sift.js by Craig Condon. Created by Sahilpohare
Contributions are welcome! Please feel free to submit a Pull Request.