rsolr 0.12.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +29 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +2 -0
  5. data/CHANGES.txt +63 -260
  6. data/Gemfile +13 -0
  7. data/README.rdoc +177 -63
  8. data/Rakefile +19 -0
  9. data/lib/rsolr/char.rb +6 -0
  10. data/lib/rsolr/client.rb +344 -86
  11. data/lib/rsolr/document.rb +66 -0
  12. data/lib/rsolr/error.rb +182 -0
  13. data/lib/rsolr/field.rb +87 -0
  14. data/lib/rsolr/generator.rb +5 -0
  15. data/lib/rsolr/json.rb +60 -0
  16. data/lib/rsolr/response.rb +95 -0
  17. data/lib/rsolr/uri.rb +25 -0
  18. data/lib/rsolr/version.rb +7 -0
  19. data/lib/rsolr/xml.rb +150 -0
  20. data/lib/rsolr.rb +47 -35
  21. data/rsolr.gemspec +44 -31
  22. data/spec/api/client_spec.rb +423 -0
  23. data/spec/api/document_spec.rb +48 -0
  24. data/spec/api/error_spec.rb +158 -0
  25. data/spec/api/json_spec.rb +248 -0
  26. data/spec/api/pagination_spec.rb +31 -0
  27. data/spec/api/rsolr_spec.rb +31 -0
  28. data/spec/api/uri_spec.rb +37 -0
  29. data/spec/api/xml_spec.rb +255 -0
  30. data/spec/fixtures/basic_configs/_rest_managed.json +1 -0
  31. data/spec/fixtures/basic_configs/currency.xml +67 -0
  32. data/spec/fixtures/basic_configs/lang/stopwords_en.txt +54 -0
  33. data/spec/fixtures/basic_configs/protwords.txt +21 -0
  34. data/spec/fixtures/basic_configs/schema.xml +530 -0
  35. data/spec/fixtures/basic_configs/solrconfig.xml +572 -0
  36. data/spec/fixtures/basic_configs/stopwords.txt +14 -0
  37. data/spec/fixtures/basic_configs/synonyms.txt +29 -0
  38. data/spec/integration/solr5_spec.rb +38 -0
  39. data/spec/lib/rsolr/client_spec.rb +19 -0
  40. data/spec/spec_helper.rb +94 -0
  41. metadata +228 -54
  42. data/lib/rsolr/connection/net_http.rb +0 -48
  43. data/lib/rsolr/connection/requestable.rb +0 -43
  44. data/lib/rsolr/connection/utils.rb +0 -73
  45. data/lib/rsolr/connection.rb +0 -9
  46. data/lib/rsolr/message/document.rb +0 -48
  47. data/lib/rsolr/message/field.rb +0 -20
  48. data/lib/rsolr/message/generator.rb +0 -89
  49. data/lib/rsolr/message.rb +0 -8
@@ -0,0 +1,182 @@
1
+ require 'json'
2
+
3
+ module RSolr::Error
4
+
5
+ module URICleanup
6
+ # Removes username and password from URI object.
7
+ def clean_uri(uri)
8
+ uri = uri.dup
9
+ uri.password = "REDACTED" if uri.password
10
+ uri.user = "REDACTED" if uri.user
11
+ uri
12
+ end
13
+ end
14
+
15
+ module SolrContext
16
+ include URICleanup
17
+
18
+ attr_accessor :request, :response
19
+
20
+ def to_s
21
+ m = "#{super.to_s}"
22
+ if response
23
+ m << " - #{response[:status]} #{Http::STATUS_CODES[response[:status].to_i]}"
24
+ details = parse_solr_error_response response[:body]
25
+ m << "\nError: #{details}\n" if details
26
+ end
27
+ p = "\nURI: #{clean_uri(request[:uri]).to_s}"
28
+ p << "\nRequest Headers: #{request[:headers].inspect}" if request[:headers]
29
+ p << "\nRequest Data: #{request[:data].inspect}" if request[:data]
30
+ p << "\n"
31
+ p << "\nBacktrace: " + self.backtrace[0..10].join("\n")
32
+ m << p
33
+ m
34
+ end
35
+
36
+ protected
37
+
38
+ def parse_solr_error_response body
39
+ begin
40
+ # Default JSON response, try to parse and retrieve error message
41
+ if response[:headers] && response[:headers]["content-type"].start_with?("application/json")
42
+ begin
43
+ parsed_body = JSON.parse(body)
44
+ info = parsed_body && parsed_body["error"] && parsed_body["error"]["msg"]
45
+ rescue JSON::ParserError
46
+ end
47
+ end
48
+ return info if info
49
+
50
+ # legacy analysis, I think trying to handle wt=ruby responses without
51
+ # a full parse?
52
+ if body =~ /<pre>/
53
+ info = body.scan(/<pre>(.*)<\/pre>/mi)[0]
54
+ elsif body =~ /'msg'=>/
55
+ info = body.scan(/'msg'=>(.*)/)[0]
56
+ end
57
+ info = info.join if info.respond_to? :join
58
+ info ||= body # body might not contain <pre> or msg elements
59
+
60
+ partial = info.to_s.split("\n")[0..10]
61
+ partial.join("\n").gsub("&gt;", ">").gsub("&lt;", "<")
62
+ rescue
63
+ nil
64
+ end
65
+ end
66
+ end
67
+
68
+ class ConnectionRefused < ::Errno::ECONNREFUSED
69
+ include URICleanup
70
+
71
+ def initialize(request)
72
+ request[:uri] = clean_uri(request[:uri])
73
+
74
+ super(request.inspect)
75
+ end
76
+ end
77
+
78
+ class Http < RuntimeError
79
+
80
+ include SolrContext
81
+
82
+ # ripped right from ActionPack
83
+ # Defines the standard HTTP status codes, by integer, with their
84
+ # corresponding default message texts.
85
+ # Source: http://www.iana.org/assignments/http-status-codes
86
+ STATUS_CODES = {
87
+ 100 => "Continue",
88
+ 101 => "Switching Protocols",
89
+ 102 => "Processing",
90
+
91
+ 200 => "OK",
92
+ 201 => "Created",
93
+ 202 => "Accepted",
94
+ 203 => "Non-Authoritative Information",
95
+ 204 => "No Content",
96
+ 205 => "Reset Content",
97
+ 206 => "Partial Content",
98
+ 207 => "Multi-Status",
99
+ 226 => "IM Used",
100
+
101
+ 300 => "Multiple Choices",
102
+ 301 => "Moved Permanently",
103
+ 302 => "Found",
104
+ 303 => "See Other",
105
+ 304 => "Not Modified",
106
+ 305 => "Use Proxy",
107
+ 307 => "Temporary Redirect",
108
+
109
+ 400 => "Bad Request",
110
+ 401 => "Unauthorized",
111
+ 402 => "Payment Required",
112
+ 403 => "Forbidden",
113
+ 404 => "Not Found",
114
+ 405 => "Method Not Allowed",
115
+ 406 => "Not Acceptable",
116
+ 407 => "Proxy Authentication Required",
117
+ 408 => "Request Timeout",
118
+ 409 => "Conflict",
119
+ 410 => "Gone",
120
+ 411 => "Length Required",
121
+ 412 => "Precondition Failed",
122
+ 413 => "Request Entity Too Large",
123
+ 414 => "Request-URI Too Long",
124
+ 415 => "Unsupported Media Type",
125
+ 416 => "Requested Range Not Satisfiable",
126
+ 417 => "Expectation Failed",
127
+ 422 => "Unprocessable Entity",
128
+ 423 => "Locked",
129
+ 424 => "Failed Dependency",
130
+ 426 => "Upgrade Required",
131
+
132
+ 500 => "Internal Server Error",
133
+ 501 => "Not Implemented",
134
+ 502 => "Bad Gateway",
135
+ 503 => "Service Unavailable",
136
+ 504 => "Gateway Timeout",
137
+ 505 => "HTTP Version Not Supported",
138
+ 507 => "Insufficient Storage",
139
+ 510 => "Not Extended"
140
+ }
141
+
142
+ def initialize request, response
143
+ response = response_with_force_encoded_body(response)
144
+ @request, @response = request, response
145
+ end
146
+
147
+ private
148
+
149
+ def response_with_force_encoded_body(response)
150
+ response[:body] = response[:body].force_encoding('UTF-8') if response
151
+ response
152
+ end
153
+ end
154
+
155
+ # Thrown if the :wt is :ruby
156
+ # but the body wasn't succesfully parsed/evaluated
157
+ class InvalidResponse < Http
158
+
159
+ end
160
+
161
+ # Subclasses Rsolr::Error::Http for legacy backwards compatibility
162
+ # purposes, because earlier RSolr 2 didn't distinguish these
163
+ # from Http errors.
164
+ #
165
+ # In RSolr 3, it could make sense to `< Timeout::Error` instead,
166
+ # analagous to ConnectionRefused above
167
+ class Timeout < Http
168
+ end
169
+
170
+ # Thrown if the :wt is :ruby
171
+ # but the body wasn't succesfully parsed/evaluated
172
+ class InvalidJsonResponse < InvalidResponse
173
+
174
+ end
175
+
176
+ # Thrown if the :wt is :ruby
177
+ # but the body wasn't succesfully parsed/evaluated
178
+ class InvalidRubyResponse < InvalidResponse
179
+
180
+ end
181
+
182
+ end
@@ -0,0 +1,87 @@
1
+ module RSolr
2
+ class Field
3
+ def self.instance(attrs, value)
4
+ attrs = attrs.dup
5
+ field_type = attrs.delete(:type) { value.class.name }
6
+
7
+ klass = if field_type.is_a? String
8
+ class_for_field(field_type)
9
+ elsif field_type.is_a? Class
10
+ field_type
11
+ else
12
+ self
13
+ end
14
+
15
+ klass.new(attrs, value)
16
+ end
17
+
18
+ def self.class_for_field(field_type)
19
+ potential_class_name = field_type + 'Field'.freeze
20
+ search_scope = Module.nesting[1]
21
+ search_scope.const_defined?(potential_class_name, false) ? search_scope.const_get(potential_class_name) : self
22
+ end
23
+ private_class_method :class_for_field
24
+
25
+ # "attrs" is a hash for setting the "doc" xml attributes
26
+ # "value" is the text value for the node
27
+ attr_accessor :attrs, :source_value
28
+
29
+ # "attrs" must be a hash
30
+ # "value" should be something that responds to #_to_s
31
+ def initialize(attrs, source_value)
32
+ @attrs = attrs
33
+ @source_value = source_value
34
+ end
35
+
36
+ # the value of the "name" attribute
37
+ def name
38
+ attrs[:name]
39
+ end
40
+
41
+ def value
42
+ source_value
43
+ end
44
+
45
+ def as_json
46
+ if attrs[:update]
47
+ { attrs[:update] => value }
48
+ elsif attrs.any? { |k, _| k != :name }
49
+ hash = attrs.dup
50
+ hash.delete(:name)
51
+ hash.merge(value: value)
52
+ else
53
+ value
54
+ end
55
+ end
56
+ end
57
+
58
+ class DateField < Field
59
+ def value
60
+ Time.utc(source_value.year, source_value.mon, source_value.mday).iso8601
61
+ end
62
+ end
63
+
64
+ class TimeField < Field
65
+ def value
66
+ source_value.getutc.strftime('%FT%TZ')
67
+ end
68
+ end
69
+
70
+ class DateTimeField < Field
71
+ def value
72
+ source_value.to_time.getutc.iso8601
73
+ end
74
+ end
75
+
76
+ class DocumentField < Field
77
+ def value
78
+ return RSolr::Document.new(source_value) if source_value.respond_to? :each_pair
79
+
80
+ super
81
+ end
82
+
83
+ def as_json
84
+ value.as_json
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,5 @@
1
+ module RSolr
2
+ class Generator
3
+
4
+ end
5
+ end
data/lib/rsolr/json.rb ADDED
@@ -0,0 +1,60 @@
1
+ require 'json'
2
+
3
+ module RSolr::JSON
4
+ class Generator < RSolr::Generator
5
+ CONTENT_TYPE = 'application/json'.freeze
6
+
7
+ def content_type
8
+ CONTENT_TYPE
9
+ end
10
+
11
+ def add data, add_attrs = {}
12
+ add_attrs ||= {}
13
+ data = RSolr::Array.wrap(data)
14
+
15
+ if add_attrs.empty? && data.none? { |doc| doc.is_a?(RSolr::Document) && !doc.attrs.empty? }
16
+ data.map do |doc|
17
+ doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
18
+ yield doc if block_given?
19
+ doc.as_json
20
+ end.to_json
21
+ else
22
+ i = 0
23
+ data.each_with_object({}) do |doc, hash|
24
+ doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
25
+ yield doc if block_given?
26
+ hash["add__UNIQUE_RSOLR_SUFFIX_#{i += 1}"] = add_attrs.merge(doc.attrs).merge(doc: doc.as_json)
27
+ end.to_json.gsub(/__UNIQUE_RSOLR_SUFFIX_\d+/, '')
28
+ end
29
+ end
30
+
31
+ # generates a commit message
32
+ def commit(opts = {})
33
+ opts ||= {}
34
+ { commit: opts }.to_json
35
+ end
36
+
37
+ # generates a optimize message
38
+ def optimize(opts = {})
39
+ opts ||= {}
40
+ { optimize: opts }.to_json
41
+ end
42
+
43
+ # generates a rollback message
44
+ def rollback
45
+ { rollback: {} }.to_json
46
+ end
47
+
48
+ # generates a delete message
49
+ # "ids" can be a single value or array of values
50
+ def delete_by_id(ids)
51
+ { delete: ids }.to_json
52
+ end
53
+
54
+ # generates a delete message
55
+ # "queries" can be a single value or an array of values
56
+ def delete_by_query(queries)
57
+ { delete: { query: queries } }.to_json
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,95 @@
1
+ module RSolr::Response
2
+
3
+ def self.included(base)
4
+ unless base < Hash
5
+ raise ArgumentError, "RSolr::Response expects to included only in (sub)classes of Hash; got included in '#{base}' instead."
6
+ end
7
+ base.send(:attr_reader, :request, :response)
8
+ end
9
+
10
+ def initialize_rsolr_response(request, response, result)
11
+ @request = request
12
+ @response = response
13
+ self.merge!(result)
14
+ if self["response"] && self["response"]["docs"].is_a?(::Array)
15
+ docs = PaginatedDocSet.new(self["response"]["docs"])
16
+ docs.per_page = request[:params]["rows"]
17
+ docs.page_start = request[:params]["start"]
18
+ docs.page_total = self["response"]["numFound"].to_s.to_i
19
+ self["response"]["docs"] = docs
20
+ end
21
+ end
22
+
23
+ def with_indifferent_access
24
+ if defined?(::RSolr::HashWithIndifferentAccessWithResponse)
25
+ ::RSolr::HashWithIndifferentAccessWithResponse.new(request, response, self)
26
+ else
27
+ if defined?(ActiveSupport::HashWithIndifferentAccess)
28
+ RSolr.const_set("HashWithIndifferentAccessWithResponse", Class.new(ActiveSupport::HashWithIndifferentAccess))
29
+ RSolr::HashWithIndifferentAccessWithResponse.class_eval <<-eos
30
+ include RSolr::Response
31
+ def initialize(request, response, result)
32
+ super()
33
+ initialize_rsolr_response(request, response, result)
34
+ end
35
+ eos
36
+ ::RSolr::HashWithIndifferentAccessWithResponse.new(request, response, self)
37
+ else
38
+ raise RuntimeError, "HashWithIndifferentAccess is not currently defined"
39
+ end
40
+ end
41
+ end
42
+
43
+ # A response module which gets mixed into the solr ["response"]["docs"] array.
44
+ class PaginatedDocSet < ::Array
45
+
46
+ attr_accessor :page_start, :per_page, :page_total
47
+ if not (Object.const_defined?("RUBY_ENGINE") and Object::RUBY_ENGINE=='rbx')
48
+ alias_method(:start,:page_start)
49
+ alias_method(:start=,:page_start=)
50
+ alias_method(:total,:page_total)
51
+ alias_method(:total=,:page_total=)
52
+ end
53
+
54
+ # Returns the current page calculated from 'rows' and 'start'
55
+ def current_page
56
+ return 1 if start < 1
57
+ per_page_normalized = per_page < 1 ? 1 : per_page
58
+ @current_page ||= (start / per_page_normalized).ceil + 1
59
+ end
60
+
61
+ # Calcuates the total pages from 'numFound' and 'rows'
62
+ def total_pages
63
+ @total_pages ||= per_page > 0 ? (total / per_page.to_f).ceil : 1
64
+ end
65
+
66
+ # returns the previous page number or 1
67
+ def previous_page
68
+ @previous_page ||= (current_page > 1) ? current_page - 1 : 1
69
+ end
70
+
71
+ # returns the next page number or the last
72
+ def next_page
73
+ @next_page ||= (current_page == total_pages) ? total_pages : current_page+1
74
+ end
75
+
76
+ def has_next?
77
+ current_page < total_pages
78
+ end
79
+
80
+ def has_previous?
81
+ current_page > 1
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+
88
+ class RSolr::HashWithResponse < Hash
89
+ include RSolr::Response
90
+
91
+ def initialize(request, response, result)
92
+ super()
93
+ initialize_rsolr_response(request, response, result || {})
94
+ end
95
+ end
data/lib/rsolr/uri.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'uri'
2
+
3
+ module RSolr::Uri
4
+ # Creates a Solr based query string.
5
+ # Keys that have arrays values are set multiple times:
6
+ # params_to_solr(:q => 'query', :fq => ['a', 'b'])
7
+ # is converted to:
8
+ # ?q=query&fq=a&fq=b
9
+ # @param [boolean] escape false if no URI escaping is to be performed. Default true.
10
+ # @return [String] Solr query params as a String, suitable for use in a url
11
+ def self.params_to_solr(params, escape = true)
12
+ return URI.encode_www_form(params.reject{|k,v| k.to_s.empty? || v.to_s.empty?}) if escape
13
+
14
+ # escape = false if we are here
15
+ mapped = params.map do |k, v|
16
+ next if v.to_s.empty?
17
+ if v.class == ::Array
18
+ params_to_solr(v.map { |x| [k, x] }, false)
19
+ else
20
+ "#{k}=#{v}"
21
+ end
22
+ end
23
+ mapped.compact.join("&")
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ module RSolr
2
+ VERSION = "2.6.0"
3
+
4
+ def self.version
5
+ VERSION
6
+ end
7
+ end
data/lib/rsolr/xml.rb ADDED
@@ -0,0 +1,150 @@
1
+ module RSolr::Xml
2
+ Document = RSolr::Document
3
+ Field = RSolr::Field
4
+
5
+ class Generator < RSolr::Generator
6
+ class << self
7
+ attr_accessor :use_nokogiri
8
+
9
+ def builder_proc
10
+ if use_nokogiri
11
+ require 'nokogiri' unless defined?(::Nokogiri::XML::Builder)
12
+ :nokogiri_build
13
+ else
14
+ require 'builder' unless defined?(::Builder::XmlMarkup)
15
+ :builder_build
16
+ end
17
+ end
18
+ end
19
+ self.use_nokogiri = defined?(::Nokogiri::XML::Builder) ? true : false
20
+
21
+ CONTENT_TYPE = 'text/xml'.freeze
22
+
23
+ def content_type
24
+ CONTENT_TYPE
25
+ end
26
+
27
+
28
+ def nokogiri_build &block
29
+ b = ::Nokogiri::XML::Builder.new do |xml|
30
+ block_given? ? yield(xml) : xml
31
+ end
32
+ '<?xml version="1.0" encoding="UTF-8"?>'+b.to_xml(:indent => 0, :encoding => 'UTF-8', :save_with => ::Nokogiri::XML::Node::SaveOptions::AS_XML | ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION).strip
33
+ end
34
+ protected :nokogiri_build
35
+
36
+ def builder_build &block
37
+ b = ::Builder::XmlMarkup.new(:indent => 0, :margin => 0, :encoding => 'UTF-8')
38
+ b.instruct!
39
+ block_given? ? yield(b) : b
40
+ end
41
+ protected :builder_build
42
+
43
+ def build &block
44
+ self.send(self.class.builder_proc,&block)
45
+ end
46
+
47
+ # generates "add" xml for updating solr
48
+ # "data" can be a hash or an array of hashes.
49
+ # - each hash should be a simple key=>value pair representing a solr doc.
50
+ # If a value is an array, multiple fields will be created.
51
+ #
52
+ # "add_attrs" can be a hash for setting the add xml element attributes.
53
+ #
54
+ # This method can also accept a block.
55
+ # The value yielded to the block is a Message::Document; for each solr doc in "data".
56
+ # You can set xml element attributes for each "doc" element or individual "field" elements.
57
+ #
58
+ # For example:
59
+ #
60
+ # solr.add({:id=>1, :nickname=>'Tim'}, {:boost=>5.0, :commitWithin=>1.0}) do |doc_msg|
61
+ # doc_msg.attrs[:boost] = 10.00 # boost the document
62
+ # nickname = doc_msg.field_by_name(:nickname)
63
+ # nickname.attrs[:boost] = 20 if nickname.value=='Tim' # boost a field
64
+ # end
65
+ #
66
+ # would result in an add element having the attributes boost="10.0"
67
+ # and a commitWithin="1.0".
68
+ # Each doc element would have a boost="10.0".
69
+ # The "nickname" field would have a boost="20.0"
70
+ # if the doc had a "nickname" field with the value of "Tim".
71
+ #
72
+ def add data, add_attrs = nil, &block
73
+ add_attrs ||= {}
74
+ data = RSolr::Array.wrap(data)
75
+ build do |xml|
76
+ xml.add(add_attrs) do |add_node|
77
+ data.each do |doc|
78
+ doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
79
+ yield doc if block_given?
80
+ doc_node_builder = to_xml(doc)
81
+ self.class.use_nokogiri ? add_node.doc_(doc.attrs,&doc_node_builder) : add_node.doc(doc.attrs,&doc_node_builder)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # generates a <commit/> message
88
+ def commit opts = nil
89
+ opts ||= {}
90
+ build {|xml| xml.commit(opts) }
91
+ end
92
+
93
+ # generates a <optimize/> message
94
+ def optimize opts = nil
95
+ opts ||= {}
96
+ build {|xml| xml.optimize(opts) }
97
+ end
98
+
99
+ # generates a <rollback/> message
100
+ def rollback
101
+ build {|xml| xml.rollback({}) }
102
+ end
103
+
104
+ # generates a <delete><id>ID</id></delete> message
105
+ # "ids" can be a single value or array of values
106
+ def delete_by_id ids
107
+ ids = RSolr::Array.wrap(ids)
108
+ build do |xml|
109
+ xml.delete do |delete_node|
110
+ ids.each do |id|
111
+ self.class.use_nokogiri ? delete_node.id_(id) : delete_node.id(id)
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ # generates a <delete><query>ID</query></delete> message
118
+ # "queries" can be a single value or an array of values
119
+ def delete_by_query(queries)
120
+ queries = RSolr::Array.wrap(queries)
121
+ build do |xml|
122
+ xml.delete do |delete_node|
123
+ queries.each { |query| delete_node.query(query) }
124
+ end
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def to_xml(doc)
131
+ lambda do |doc_node|
132
+ doc.fields.each do |field_obj|
133
+ value = field_obj.value
134
+
135
+ if field_obj.name.to_s == RSolr::Document::CHILD_DOCUMENT_KEY
136
+ child_node_builder = to_xml(field_obj.value)
137
+ self.class.use_nokogiri ? doc_node.doc_(&child_node_builder) : doc_node.doc(&child_node_builder)
138
+ elsif value.is_a?(Hash) && value.length == 1 && field_obj.attrs[:update].nil?
139
+ update_attr, real_value = value.first
140
+ doc_node.field real_value, field_obj.attrs.merge(update: update_attr)
141
+ elsif value.nil?
142
+ doc_node.field field_obj.value, field_obj.attrs.merge(null: true)
143
+ else
144
+ doc_node.field field_obj.value, field_obj.attrs
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end