Skip to content

Scoping by attribute fields

calaveraDeluxe edited this page Feb 24, 2017 · 10 revisions

The basics of scoping

Scoping in Sunspot is the equivalent of placing conditions on a database query – values against which to compare fields are passed in and processed verbatim. Scopes in Sunspot’s DSL are built using the with method:

Sunspot.search(Post) do
  with(:published_at).less_than(Time.now)
end

This will send a query to Sunspot that is roughly the equivalent of the MySQL query:

SELECT `posts`.* FROM `posts` WHERE `published_at` < NOW();

The argument to the with method is always the name of an attribute field (e.g., one defined using a type other than text). The chained method is the name of a restriction; the available restrictions are:

equal_to
Match all documents for whom this field contains exactly this value. For multi-valued fields, one of the values must match exactly (this pattern extends to all types of restrictions).
less_than
Match all documents for whom this field contains a value less than the given value.
greater_than
Match all documents for whom this field contains a value greater than the given value.
less_than_or_equal_to
Match all documents for whom this field contains a value less than or equal to the given value.
greater_than_or_equal_to
Match all documents for whom this field contains a value greater than or equal to the given value.
between
Match all documents for whom this field is between the values in the given range, inclusive. Value should be a Range or another object that responds to first and last.
any_of
Match all documents for whom this field contains any of the given collection of values. Value should be an Array or other Enumerable.
all_of
Match all documents for whom this field contains all of the given collection of values. This is only meaningful when applied to multi-valued fields. Value should be an Array or other Enumerable.

Shorthand restrictions

Sunspot also provides shorthand restrictions, wherein a second argument is passed to with; the restriction type is determined based on the class of the second argument:

- If an Array is passed, an any_of restriction is created.
- If a Range is passed, a between restriction is created.
- Otherwise, an equal_to restriction is created.

For example, the following pairs of restriction calls are equivalent:

with(:blog_id, 1)
with(:blog_id).equal_to(1)

with(:average_rating, 3.0..5.0)
with(:average_rating).between(3.0..5.0)

with(:category_ids, [1, 3, 5])
with(:category_ids).any_of([1, 3, 5])

Scoping by empty values

It’s perfectly legal to pass nil into an equal_to restriction:

Sunspot.search(Post) do
  with(:expired_at, nil)
end

This will simply scope the search to results who do not have any value indexed in the expired_at field. Passing nil as a value into any other restriction type is not permitted.

Note that, other than passing nil into an equality restriction, no restriction will ever match a field that has no value. For instance, neither with(:published_at).less_than(Time.now) nor with(:published_at).greater_than(Time.now) will match documents which have no published_at set at all. This will come as a surprise to those who are used to SQL queries but is a natural fact of the way Solr’s range queries work. See the Disjunctions and conjunctions section below for information on how a restriction can be built that matches both values less than a given value and documents that don’t have the field indexed at all.

Negating restrictions

Opposite the with method is the without method, which can be used anywhere the with method can be used and in the same ways; it simply negates the restriction:

Sunspot.search(Post) do
  without(:category_ids, 2)
end

The above will match all documents who do not have the value 2 in their category_ids field.

Exclusion by object identity

The without method also has a special function that does not have an analog in with, namely to exclude a specific instance from the search results. Passing an object that has an adapter registered with Sunspot will create this special type of restriction:

post = Post.find(params[:id])
Sunspot.search(Post) do
  without(post)
end

Combining restrictions

The simplest way to combine restrictions is to specify more than one within the search block; this will match documents to whom all of the restrictions apply:

Sunspot.search(Post) do
  with(:published_at).less_than(Time.now)
  with(:blog_id, 1)
end

The above will match all documents whose published date is in the past, and whose blog_id is 1.

Disjunctions and conjunctions

To combine scopes using OR semantics, use the any_of method to group restrictions into a disjunction:

Sunspot.search(Post) do
  any_of do
    with(:expired_at).greater_than(Time.now)
    with(:expired_at, nil)
  end
end

The above will match all documents whose expiration date is either in the future, or not set at all.

Inside a disjunction, the all_of method can be used to nest a set of restrictions combined with AND semantics:

Sunspot.search(Post) do
  any_of do
    with(:featured, true)
    all_of do
      with(:published_at).less_than(Time.now)
      with(:expired_at).greater_than(Time.now)
    end
  end
end

If desired, conjunctions and disjunctions can be nested to indefinite depths.