Skip to content

Commit

Permalink
Replace parent-child with a join field.
Browse files Browse the repository at this point in the history
Implement hierarchical structures with Elastic join field, in place of
an obsolete (and removed) parent-child relationships.
  • Loading branch information
mrzasa committed Dec 20, 2021
1 parent 7d56754 commit 09113a8
Show file tree
Hide file tree
Showing 13 changed files with 741 additions and 71 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,23 @@ end

See the section on *Script fields* for details on calculating distance in a search.

### Join fields

You can use a [join field](https://www.elastic.co/guide/en/elasticsearch/reference/current/parent-join.html)
to implement parent-child relationships between documents.
It [replaces the old `parent_id` based parent-child mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html#parent-child-mapping-types)

To use it, you need to pass `relations`, `join_type` and `join_id` options:
```ruby
field :hierarchy_link, type: :join, relations: {question: %i[answer comment], answer: :vote, vote: :subvote}, join_type: :comment_type, join_id: :commented_id
```
assuming you have `comment_type` and `commented_id` fields in your model.

Note that when you reindex a parent, it's children and grandchildren will be reindexed as well.
This may require additional queries to the primary database and to elastisearch.

Also note that the join field doesn't support crutches (it should be a field directly defined on the model).

### Crutches™ technology

Assume you are defining your index like this (product has_many categories through product_categories):
Expand All @@ -456,7 +473,7 @@ class ProductsIndex < Chewy::Index
field :name
field :category_names, value: ->(product) { product.categories.map(&:name) } # or shorter just -> { categories.map(&:name) }
end
```
`
Then the Chewy reindexing flow will look like the following pseudo-code:
Expand Down
6 changes: 6 additions & 0 deletions lib/chewy/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ def initialize(type, import_errors)
super message
end
end

class InvalidJoinFieldType < Error
def initialize(join_field_type, join_field_name, relations)
super("`#{join_field_type}` set for the join field `#{join_field_name}` is not on the :relations list (#{relations})")
end
end
end
79 changes: 67 additions & 12 deletions lib/chewy/fields/base.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
module Chewy
module Fields
class Base
attr_reader :name, :options, :value, :children
attr_reader :name, :options, :children
attr_accessor :parent

JOIN_FIELD_EXTRA_OPTIONS = %i[join_id join_type].freeze

def initialize(name, value: nil, **options)
@name = name.to_sym
@options = {}
update_options!(**options)
@value = value
@children = []
@allowed_relations = find_allowed_relations(options[:relations]) # for join fields
end

def update_options!(**options)
@options = options
@join_type = options[:join_type]
@join_id = options[:join_id]
@options = options.reject { |k, _| JOIN_FIELD_EXTRA_OPTIONS.include?(k) }
end

def multi_field?
Expand Down Expand Up @@ -53,30 +58,66 @@ def compose(*objects)
{name => result}
end

def value
if join_field?
# memoize
@value ||= proc do |object|
validate_join_type!(value_by_name_proc(@join_type).call(object))
if value_by_name_proc(@join_id).call(object).present?
{
name: value_by_name_proc(@join_type).call(object),
parent: value_by_name_proc(@join_id).call(object)
}
else
value_by_name_proc(@join_type).call(object)
end
end
else
@value
end
end

private

def geo_point?
@options[:type].to_s == 'geo_point'
end

def join_field?
@options[:type].to_s == 'join'
end

def ignore_blank?
@options.fetch(:ignore_blank) { geo_point? }
end

def evaluate(objects)
object = objects.first

if value.is_a?(Proc)
if value.arity.zero?
object.instance_exec(&value)
elsif value.arity.negative?
value.call(*object)
else
value.call(*objects.first(value.arity))
end
value_by_proc(objects, value)
else
value_by_name(objects, value)
end
end

def value_by_proc(objects, value)
object = objects.first
if value.arity.zero?
object.instance_exec(&value)
elsif value.arity.negative?
value.call(*object)
else
message = value.is_a?(Symbol) || value.is_a?(String) ? value.to_sym : name
value.call(*objects.first(value.arity))
end
end

def value_by_name(objects, value)
object = objects.first
message = value.is_a?(Symbol) || value.is_a?(String) ? value.to_sym : name
value_by_name_proc(message).call(object)
end

def value_by_name_proc(message)
proc do |object|
if object.is_a?(Hash)
if object.key?(message)
object[message]
Expand All @@ -89,6 +130,20 @@ def evaluate(objects)
end
end

def validate_join_type!(type)
return unless type
return if @allowed_relations.include?(type.to_sym)

raise Chewy::InvalidJoinFieldType.new(type, @name, options[:relations])
end

def find_allowed_relations(relations)
return [] unless relations
return relations unless relations.is_a?(Hash)

(relations.keys + relations.values).flatten.uniq
end

def compose_children(value, *parent_objects)
return unless value

Expand Down
5 changes: 5 additions & 0 deletions lib/chewy/index/adapter/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ def raw_default_scope_where_ids_in(ids, converter)
object_class.connection.execute(sql).map(&converter)
end

def raw(scope, converter)
sql = scope.to_sql
object_class.connection.execute(sql).map(&converter)
end

def relation_class
::ActiveRecord::Relation
end
Expand Down
10 changes: 6 additions & 4 deletions lib/chewy/index/adapter/orm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,13 @@ def load(ids, **options)
additional_scope = options[options[:_index].to_sym].try(:[], :scope) || options[:scope]

loaded_objects = load_scope_objects(scope, additional_scope)
.index_by do |object|
object.public_send(primary_key).to_s
end
loaded_objects = raw(loaded_objects, options[:raw_import]) if options[:raw_import]

indexed_objects = loaded_objects.index_by do |object|
object.public_send(primary_key).to_s
end

ids.map { |id| loaded_objects[id.to_s] }
ids.map { |id| indexed_objects[id.to_s] }
end

private
Expand Down
Loading

0 comments on commit 09113a8

Please sign in to comment.