Ruby on Rails: Concatenate results of Mongoid crit

2019-05-24 03:46发布

问题:

I'm pretty sure that I'm doing something wrong. Consider the following code:

criteria1 = Model.where(...)
criteria2 = Model.where(...)
results = (criteria1.to_a + criteria2.to_a)[offset..(offset + count_per_page - 1)]

This code concatenates results of two different criterias and get a certain number of results with a given offset (paging).

The problem in this code is implicit. The to_a method call actually loads all results of a criteria to the memory as an array.

Now consider a really huge collection... The to_a call slows all things down dramatically.

What I wish to do is something like this:

criteria1 = Model.where(...)
criteria2 = Model.where(...)

# A criteria, which returns results of the first criteria concatenated with results of the second criteria
criteria = criteria1 + criteria2
results = criteria.offset(offset).limit(count_per_page)

The important thing is that results of the second criteria goes after results of the first criteria.

Any clues how is it possible to achieve with Mongoid?

Thanks!

UPDATE

Gergo Erdosi suggested to use merge method. I've tried to use this and it is not what I'm looking for. The problem here is the following:

criteria1 = Model.where(:name => "John", :age => "23")
criteria2 = Model.where(:name => "Bob", :gender => "male")
criteria = criteria1.merge(criteria2)
p criteria.selector
# prints: { "name" => "Bob", :age => 23, :gender => "male" }

So here are two problems:

  1. merge doesn't produce OR, it overrides common keys of the first query with the second;
  2. Even if we use Model.or({ :name => "John" }, { :name => "Bob" }) or Model.in(:name => ["John", "Bob"]) results won't have the right order. I wish results of the first criteria go first and then results of the second criteria go after.

It is possible that I don't understand something and Gergo's answer is right. Do you have any other ideas? Thanks.

UPDATE 2

Thank you Gergo for helping me out here. Let's try a simple example in Mongo shell:

// Fill out test db with some simple documents.
for (var i = 0; i < 10; ++i) { db.users.insert({ name: i % 2 ? "John" : "Bob", age: Math.round(Math.random() * 100) }); }

// These queries give me the same order of documents.
db.users.find({ name: { $in: ["Bob", "John"] } });
db.users.find({ $or: [{ name: "Bob" }, { name: "John" }] });

// Like this:
{ "_id" : ObjectId("53732076b110ab9be7619a8e"), "name" : "Bob", "age" : 69 }
{ "_id" : ObjectId("53732076b110ab9be7619a8f"), "name" : "John", "age" : 63 }
{ "_id" : ObjectId("53732076b110ab9be7619a90"), "name" : "Bob", "age" : 25 }
{ "_id" : ObjectId("53732076b110ab9be7619a91"), "name" : "John", "age" : 72 }
// ...

// But I wish to get concatenated results of these queries:
db.users.find({ name: "Bob" });
db.users.find({ name: "John" });

// Like this (results of the first criteria go first):
{ "_id" : ObjectId("53732076b110ab9be7619a8e"), "name" : "Bob", "age" : 69 }
{ "_id" : ObjectId("53732076b110ab9be7619a90"), "name" : "Bob", "age" : 25 }
// ...
{ "_id" : ObjectId("53732076b110ab9be7619a8f"), "name" : "John", "age" : 63 }
{ "_id" : ObjectId("53732076b110ab9be7619a91"), "name" : "John", "age" : 72 }
// ...

Notice, that I cannot use a simple sorting here, because the data in the real application is more complex. In the real application criterias look like these:

// query variable is a string
exact_match_results = Model.where(:name => query)
inexact_match_results = Model.where(:name => /#{query}/i)

So we cannot just sort alphabetically here.

回答1:

Use the merge method:

criteria = criteria1.merge(criteria2)
results = criteria.offset(offset).limit(count_per_page)

You can see the details in the method description.

Edit: As pointed out, merge doesn't produce an OR query.

irb(main):010:0> Model.where(name: 'John').merge(Model.where(name: 'Bob'))
=> #<Mongoid::Criteria
  selector: {"name"=>"Bob"}
  options:  {}
  class:    Model
  embedded: false>

Which is not the expected behavior in this case. The reason is that merge uses Hash.merge which behaves this way. The relevant code from Criteria.merge:

selector.merge!(criteria.selector)

This can be illustrated as:

irb(main):011:0> {name: 'John'}.merge({name: 'Bob'})
=> {:name=>"Bob"}

Because of this, it's not easy to give a general advice on how to merge two criteria in a way that the result is an OR query. But with a little change in the criteria, it's possible. For example:

criteria1 = Model.any_of(name: 'John').where(age: '23')
criteria2 = Model.any_of(name: 'Bob').where(gender: 'male')

The result of the merge is an OR query which contains both names:

irb(main):014:0> criteria1.merge(criteria2)
=> #<Mongoid::Criteria
  selector: {"$or"=>[{"name"=>"John"}, {"name"=>"Bob"}], "age"=>"23", "gender"=>"male"}
  options:  {}
  class:    Model
  embedded: false>