Mongory

Let you query everywhere !

This is a Mongo-like in-memory query DSL for Ruby.

Mongory lets you filter and query in-memory collections using syntax and semantics similar to MongoDB. It is designed for expressive chaining, symbolic operators, and composable matchers.

Table of Contents

Requirements

  • Ruby >= 2.6.0
  • No external database required

Installation & Quick Start

Installation

Install manually:

gem install mongory

Or add to your Gemfile:

gem 'mongory'

Before installing: install build tools

Mongory ships with an optional native C extension. Before installing Mongory, make sure these following precompiled package support your platform:

  • x86_64-linux-musl (Linux)
  • aarch64-linux-musl (Linux)
  • x86_64-linux (Linux)
  • aarch64-linux (Linux)
  • x86_64-darwin (MacOS Intel)
  • arm64-darwin (MacOS Apple Silicon)
  • x64-mingw32 (Windows)
  • x64-mingw-ucrt (Windows)

If your platform excludes, make sure your system has a C build toolchain (gcc/clang and make). Install the toolchain with the following commands for your platform:

  • Debian/Ubuntu (including ruby:*-slim base images)

    apt-get update && apt-get install -y build-essential
    
  • Alpine

    apk add --no-cache build-base
    
  • CentOS/RHEL

    yum groupinstall -y "Development Tools"
    
  • Fedora

    dnf groupinstall -y "Development Tools"
    
  • Amazon Linux

    yum groupinstall -y "Development Tools"
    
  • macOS

    xcode-select --install
    

Basic Usage

records = [
  { 'name' => 'Jack', 'age' => 18, 'gender' => 'M' },
  { 'name' => 'Jill', 'age' => 15, 'gender' => 'F' },
  { 'name' => 'Bob',  'age' => 21, 'gender' => 'M' },
  { 'name' => 'Mary', 'age' => 18, 'gender' => 'F' }
]

# Basic query with conditions
result = records.mongory
  .where(:age.gte => 18)
  .or({ :name => /J/ }, { :name.eq => 'Bob' })

# Using limit to restrict results
# Note: limit executes immediately and affects subsequent conditions
limited = records.mongory
  .limit(2)                    # Only process first 2 records
  .where(:age.gte => 18)       # Conditions apply to limited set

Rails Generator

You can install a starter configuration with:

rails g mongory:install

This will generate config/initializers/mongory.rb and set up:

  • Optional symbol operator snippets (e.g. :age.gt => 18)
  • Class registration (e.g. Array, ActiveRecord::Relation, etc.)
  • Custom value/key converters for your ORM

Positioning

Mongory is designed to serve two types of users:

  1. For MongoDB users:

    • Seamless integration with familiar query syntax
    • Extends query capabilities for non-indexed fields
    • No additional learning cost
  2. For non-MongoDB users:

    • Initial learning cost for MongoDB-style syntax
    • Long-term benefits:
      • Improved code readability
      • Better development efficiency
      • Lower maintenance costs
    • Ideal for teams valuing code quality and maintainability

Integration with MongoDB

Mongory is designed to complement MongoDB, not replace it. Here's how to use them together:

  1. Use MongoDB for:

    • Queries with indexes
    • Persistent data operations
    • Large-scale data processing
  2. Use Mongory for:

    • Queries without indexes
    • Complex in-memory calculations
    • Temporary data filtering needs

Example:

# First use MongoDB for indexed queries
users = User.where(status: 'active')  # Uses MongoDB index

# Then use Mongory for non-indexed fields
active_users = users.mongory
  .where(:last_login.gte => 1.week.ago)  # No index on last_login
  .where(:tags.elem_match => { :name => 'ruby' })  # Complex array query

Creating Custom Matchers

Using the Generator

You can generate a new matcher using:

rails g mongory:matcher class_in

This will:

  1. Create a new matcher file at lib/mongory/matchers/class_in_matcher.rb
  2. Create a spec file at spec/mongory/matchers/class_in_matcher_spec.rb
  3. Update config/initializers/mongory.rb to require the new matcher

The generated matcher will:

  • Be named ClassInMatcher
  • Register the operator as $classIn makes it available in queries
  • Enable :field.class_in query snippet (If symbol snippet enabled)

Example usage of the generated matcher:

records.mongory.where(:value.class_in => [Integer, String])

Manual Creation

If you prefer to create matchers manually, here's an example:

class ClassInMatcher < Mongory::Matchers::AbstractMatcher
  def match(subject)
    @condition.any? { |klass| subject.is_a?(klass) }
  end

  def check_validity!
    raise TypeError, '$classIn needs an array.' unless @condition.is_a?(Array)
    @condition.each do |klass|
      raise TypeError, '$classIn needs an array of class.' unless klass.is_a?(Class)
    end
  end
end

Mongory::Matchers.register(:class_in, '$classIn', ClassInMatcher)

[{a: 1}].mongory.where(:a.class_in => [Integer]).first
# => { a: 1 }

You can define any matcher behavior and attach it to a $operator of your choice. Matchers can be composed, validated, and traced just like built-in ones.

Core Concepts & API Reference

Registering Models

To allow calling .mongory on collections, use register:

Mongory.register(Array)
Mongory.register(ActiveRecord::Relation)
User.where(status: 'active').mongory.where(:age.gte => 18, :name.regex => "^S.+")

This injects a .mongory method via an internal extension module. Internally, the query is compiled into a matcher tree.

Method Description Example
where Adds a condition to filter records where(age: { :$gte => 18 })
not Adds a negated condition not(age: { :$lt => 18 })
and Combines conditions with $and and({ age: { :$gte => 18 } }, { name: /J/ })
or Combines conditions with $or or({ age: { :$gte => 18 } }, { name: /J/ })
any_of Combines conditions with $or inside an $and block any_of({ age: { :$gte => 18 } }, { name: /J/ })
in Checks if a value is in a set in(age: [18, 19, 20])
nin Checks if a value is not in a set nin(age: [18, 19, 20])
limit Limits the number of records returned. This method executes immediately and affects subsequent conditions. limit(2)
pluck Extracts selected fields from matching records pluck(:name)
with_context Sets a custom context for the query. Useful for controlling data conversion and sharing configuration across matchers. with_context(merchant: merchant)

Context Configuration

The with_context method allows you to customize the query execution environment:

# Share configuration across matchers
records.mongory
  .with_context(custom_option: true)
  .where(:status => 'active')
  .where(:age.gte => 18)

This will share a mutatable, but stable context object to all matchers in matcher tree. To get your custom option, using @context.config in your custom matcher.

Debugging

You can use explain to visualize the matcher tree structure:

records = [
  { name: 'John', age: 25, status: 'active' },
  { name: 'Jane', age: 30, status: 'inactive' }
]

query = records.mongory
  .where(:age.gte => 18)
  .any_of(
    { :status => 'active' },
    { :name.regex => /^J/ }
  )

query.explain

Output:

And: {"age"=>{"$gte"=>18}, "$or"=>[{"status"=>"active"}, {"name"=>{"$regex"=>/^J/}}]}
├─ Field: "age" to match: {"$gte"=>18}
│  └─ Gte: 18
└─ Or: [{"status"=>"active"}, {"name"=>{"$regex"=>/^J/}}]
   ├─ Field: "status" to match: "active"
   │  └─ Eq: "active"
   └─ Field: "name" to match: {"$regex"=>/^J/}
      └─ Regex: /^J/

This helps you understand how your query is being processed and can be useful for debugging complex conditions.

Or use trace for detailed matching process:

# Execute your query
query = Mongory.build_query(users).where(age: { :$gt => 18 })
query.trace do |user|
  puts user
end

The debug output will show detailed matching process with full class names:

QueryMatcher Matched, condition: {"age"=>{"$gt"=>18}}, record: {"age"=>25}
  AndMatcher Matched, condition: {"age"=>{"$gt"=>18}}, record: {"age"=>25}
    FieldMatcher Matched, condition: {"$gt"=>18}, field: "age", record: {"age"=>25}
      GtMatcher Matched, condition: 18, record: 25

The debug output includes:

  • The matcher tree structure with full class names
  • Each matcher's condition and record value
  • Color-coded results (green for matched, red for mismatched, purple for errors)
  • Field names highlighted in gray background
  • Detailed matching process for each record

Supported Operators

Category Operators
Comparison $eq, $ne, $gt, $gte, $lt, $lte
Set $in, $nin
Boolean $and, $or, $not
Pattern $regex
Presence $exists, $present
Nested Match $elemMatch, $every
Other $size

Note: Some operators are Mongory-specific and not available in MongoDB:

  • $present: Checks if a field is considered "present" (not nil, not empty, not KEY_NOT_FOUND)
    • Similar to $exists but evaluates truthiness of the value
    • Example: where(:name.present => true)
  • $every: Checks if all elements in an array match the given condition
    • Similar to $elemMatch but requires all elements to match
    • At least one element in an array, or returns false
    • Example: where(:tags.every => { :priority.gt => 5 })

Example:

# $present: Check if field is present (not nil, not empty)
records.mongory.where(:name.present => true)  # name is present
records.mongory.where(:name.present => false) # name is not present

# $every: Check if all array elements match condition
records.mongory.where(:tags.every => { :priority.gt => 5 })  # all tags have priority > 5

FAQ

Q: How does Mongory compare to MongoDB?

A: Mongory provides similar query syntax but operates entirely in memory. It's ideal for:

  • Small to medium datasets
  • Complex in-memory filtering
  • Testing MongoDB-like queries without a database

Q: Can I use Mongory with large datasets?

A: Yes, but consider:

  • Memory usage
  • Query complexity
  • Caching strategies
  • Using limit early in the chain

Q: How do I handle errors?

begin
  result = records.mongory.where(invalid: :condition)
rescue Mongory::Error => e
  # Handle error
end

Troubleshooting

  1. Debugging Queries

    records.mongory.where(:age => 18).trace.to_a
    

    If using C extension:

    records.mongory.c.where(:age => 18).trace.to_a
    
  2. Common Issues

    • Symbol snippets not working? Call Mongory.enable_symbol_snippets!
    • Complex queries slow? Use explain to analyze
    • Memory issues? Consider pagination or streaming

Best Practices

  1. Query Composition

    # Good: Use method chaining
    records.mongory
      .where(:age.gte => 18)
      .where(:status => 'active')
      .limit(10)
    
    # Bad: Avoid redundant query creation
    query = records.mongory.where(:age.gte => 18)
    query = query.where(:status => 'active')  # Unnecessary
    
  2. Performance Tips

    # Use limit to restrict result set
    records.mongory.limit(100).where(:age.gte => 18)
    
    # Use fast mode for better performance
    records.mongory.where(:age.gte => 18).fast
    
    # Use explain to analyze complex queries
    query = records.mongory.where(:$or => [...])
    query.explain
    
  3. Code Organization

    # Encapsulate common queries as methods
    class User
      def active_adults
        friends.mongory
          .where(:age.gte => 18)
          .where(:status => 'active')
      end
    end
    

Limitations

  1. Data Size

    • Suitable for small to medium datasets
    • Large datasets may impact performance
    • Proc-based implementation helps with memory usage
    • Context system provides better resource management
  2. Query Complexity

    • Complex queries may affect performance
    • Not all MongoDB operators are supported
    • Proc-based implementation improves complex query performance
    • Context system allows better control over query execution
  3. Memory Usage

    • All operations are performed in memory
    • Consider memory constraints

Contributing

Contributions are welcome! Here's how you can help:

  1. Fork the repository.
  2. Create a new branch for each significant change.
  3. Write tests for your changes.
  4. Send a pull request.

Please ensure your code adheres to the project's style guide and that all tests pass before submitting.

Code of Conduct

Everyone interacting in the Mongory-rb project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the code of conduct.

License

MIT. See LICENSE file.