I'm using ElasticSearch with Tire to index and search some ActiveRecord models, and I've been searching for the "right" way to index and search associations. I haven't found what seems like a best practice for this, so I wanted to ask if anyone has an approach that they think works really well.
As an example setup (this is made up but illustrates the problem), let's say we have a book, with chapters. Each book has a title and author, and a bunch of chapters. Each chapter has text. We want to index the book's fields and the chapters' text so you can search for a book by author, or for any book with certain words in it.
class Book < ActiveRecord::Base include Tire::Model::Search include Tire::Model::Callbacks has_many :chapters mapping do indexes :title, :analyzer => 'snowball', :boost => 100 indexes :author, :analyzer => 'snowball' indexes :chapters, type: 'object', properties: { chapter_text: { type: 'string', analyzer: 'snowball' } } end end class Chapter < ActiveRecord::Base belongs_to :book end
So then I do the search with:
s = Book.search do query { string query_string } end
That doesn't work, even though it seems like that indexing should do it. If instead I index:
indexes :chapters, :as => 'chapters.map{|c| c.chapter_text}.join('|'), :analyzer => 'snowball'
That makes the text searchable, but obviously it's not a nice hack and it loses the actual associated object. I've tried variations of the searching, like:
s = Book.search do query do boolean do should { string query_string } should { string "chapters.chapter_text:#{query_string}" } end end end
With no luck there, either. If anyone has a good, clear example of indexing and searching associated ActiveRecord objects using Tire, it seems like that would be a really good addition to the knowledge base here.
Thanks for any ideas and contributions.
The support for ActiveRecord associations in Tire is working, but requires couple of tweaks inside your application. There's no question the library should do better job here, and in the future it certainly will.
That said, here is a full-fledged example of Tire configuration to work with Rails' associations in elasticsearch: active_record_associations.rb
Let me highlight couple of things here.
First, you have to ensure you notify the parent model of the association about changes in the association.
Given we have a Chapter
model, which “belongs to” a Book
, we need to do:
class Chapter < ActiveRecord::Base belongs_to :book, touch: true end
In this way, when we do something like:
book.chapters.create text: "Lorem ipsum...."
The book
instance is notified about the added chapter.
With this part sorted, we need to notify Tire about the change, and update the elasticsearch index accordingly:
class Book < ActiveRecord::Base has_many :chapters after_touch() { tire.update_index } end
(There's no question Tire should intercept after_touch
notifications by itself, and not force you to do this. It is, on the other hand, a testament of how easy is to work your way around the library limitations in a manner which does not hurt your eyes.)
Despite the README mentions you have to disable automatic "adding root key in JSON" in Rails < 3.1, many people forget it, so you have to include it in the class definition as well:
self.include_root_in_json = false
Now comes the meat of our work -- defining proper mapping for our documents (models):
mapping do indexes :title, type: 'string', boost: 10, analyzer: 'snowball' indexes :created_at, type: 'date' indexes :chapters do indexes :text, analyzer: 'snowball' end end
Notice we index title
with boosting, created_at
as "date", and chapter text from the associated model. All the data are effectively “de-normalized” as a single document in elasticsearch (if such a term would make slight sense).
As the last step, we have to properly serialize the document in the elasticsearch index. Notice how we can leverage the convenient to_json
method from ActiveRecord:
def to_indexed_json to_json( include: { chapters: { only: [:text] } } ) end
With all this setup in place, we can search in properties in both the Book
and the Chapter
parts of our document.
Please run the active_record_associations.rb Ruby file linked at the beginning to see the full picture.
For further information, please refer to these resources:
See this StackOverflow answer: ElasticSearch & Tire: Using Mapping and to_indexed_json for more information about mapping
/ to_indexed_json
interplay.
See this StackOverflow answer: Index the results of a method in ElasticSearch (Tire + ActiveRecord) to see how to fight n+1 queries when indexing models with associations.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With