xtf-ruby 1.0.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/APACHE-2-LICENSE +13 -0
- data/CHANGELOG.md +12 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +50 -0
- data/LICENSE.txt +202 -0
- data/NOTICE.txt +1 -0
- data/README.md +51 -0
- data/Rakefile +9 -0
- data/lib/xtf/result/element/base.rb +54 -0
- data/lib/xtf/result/element/doc_hit.rb +61 -0
- data/lib/xtf/result/element/facet.rb +29 -0
- data/lib/xtf/result/element/group.rb +37 -0
- data/lib/xtf/result/element/result.rb +91 -0
- data/lib/xtf/result/element.rb +13 -0
- data/lib/xtf/result.rb +6 -0
- data/lib/xtf/ruby/version.rb +7 -0
- data/lib/xtf/ruby.rb +10 -0
- data/lib/xtf/search/element/all_docs.rb +23 -0
- data/lib/xtf/search/element/and.rb +13 -0
- data/lib/xtf/search/element/base.rb +32 -0
- data/lib/xtf/search/element/clause.rb +57 -0
- data/lib/xtf/search/element/exact.rb +8 -0
- data/lib/xtf/search/element/facet.rb +26 -0
- data/lib/xtf/search/element/near.rb +17 -0
- data/lib/xtf/search/element/not.rb +7 -0
- data/lib/xtf/search/element/or.rb +13 -0
- data/lib/xtf/search/element/or_near.rb +11 -0
- data/lib/xtf/search/element/phrase.rb +28 -0
- data/lib/xtf/search/element/query.rb +42 -0
- data/lib/xtf/search/element/range.rb +44 -0
- data/lib/xtf/search/element/result_data.rb +18 -0
- data/lib/xtf/search/element/section_type.rb +19 -0
- data/lib/xtf/search/element/term.rb +51 -0
- data/lib/xtf/search/element.rb +15 -0
- data/lib/xtf/search.rb +11 -0
- data/lib/xtf/xml.rb +85 -0
- data/lib/xtf.rb +31 -0
- data/sig/xtf/ruby.rbs +6 -0
- metadata +158 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
class XTF::Result::Element::Result
|
2
|
+
# require 'rubygems'
|
3
|
+
# require 'libxml_helper'
|
4
|
+
DEFAULT_DOCS_PER_PAGE = 50
|
5
|
+
attr_reader :tag_name,
|
6
|
+
:xml,
|
7
|
+
:doc,
|
8
|
+
:search,
|
9
|
+
:query,
|
10
|
+
:query_time,
|
11
|
+
:total_docs,
|
12
|
+
:start_doc,
|
13
|
+
:end_doc,
|
14
|
+
:doc_hits,
|
15
|
+
:docs_per_page,
|
16
|
+
:facets
|
17
|
+
alias :hits :doc_hits
|
18
|
+
|
19
|
+
def initialize(xml=nil, search=nil)
|
20
|
+
start = Time.now
|
21
|
+
@tag_name = "crossQueryResult"
|
22
|
+
@doc_hits = []
|
23
|
+
@facets = []
|
24
|
+
parse_xml(xml) if xml
|
25
|
+
@search = search
|
26
|
+
# RAILS_DEFAULT_LOGGER.debug("~~~~~ #{self.class.name} initialized #{@doc_hits.size} hits and #{@facets.size} facets in #{Time.now - start} seconds.")
|
27
|
+
end
|
28
|
+
|
29
|
+
# Parses the important bits out of the XTF search results using LibXML-Ruby.
|
30
|
+
def parse_xml(xml)
|
31
|
+
@xml = xml.to_s.gsub(/\s+/, " ").strip
|
32
|
+
@doc = XML::Document.parse_string(@xml).root
|
33
|
+
|
34
|
+
# the query metadata
|
35
|
+
@query_time = @doc.at('/crossQueryResult')['queryTime']
|
36
|
+
@total_docs = @doc.at('/crossQueryResult')['totalDocs']
|
37
|
+
@start_doc = @doc.at('/crossQueryResult')['startDoc']
|
38
|
+
@end_doc = @doc.at('/crossQueryResult')['endDoc']
|
39
|
+
@docs_per_page = @doc.at("/crossQueryResult/query")['maxDocs'] rescue DEFAULT_DOCS_PER_PAGE
|
40
|
+
|
41
|
+
@query = @doc.at('/crossQueryResult/query').to_s
|
42
|
+
# the docHits
|
43
|
+
# TODO deal with PDFS. This currently filters them from the results
|
44
|
+
@doc_hits = @doc.search('./docHit').collect { |h| XTF::Result::Element::DocHit.create(h, @query) }.compact
|
45
|
+
|
46
|
+
@facets = @doc.search('/crossQueryResult/facet').collect { |f| XTF::Result::Element::Facet.new(f, @query) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def empty?
|
50
|
+
self.hits.size < 1
|
51
|
+
end
|
52
|
+
|
53
|
+
def previous_start_doc
|
54
|
+
diff = start_doc.to_i - docs_per_page.to_i
|
55
|
+
diff < 1 ? "1" : diff.to_s
|
56
|
+
end
|
57
|
+
alias :prev_start_doc :previous_start_doc
|
58
|
+
|
59
|
+
def next_start_doc
|
60
|
+
sum = start_doc.to_i + docs_per_page.to_i
|
61
|
+
sum > total_docs.to_i ? start_doc : sum.to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
def last_start_doc
|
65
|
+
(((total_docs.to_i-1) / docs_per_page.to_i) * docs_per_page.to_i + 1).to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
def total_pages
|
69
|
+
(((total_docs.to_i-1) / docs_per_page.to_i) + 1).to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
def current_page
|
73
|
+
(((start_doc.to_i-1) / docs_per_page.to_i) + 1).to_s
|
74
|
+
end
|
75
|
+
|
76
|
+
def start_doc_for_page(page)
|
77
|
+
return "1" if page.to_i <= 1
|
78
|
+
return self.last_start_doc if page.to_i >= self.total_pages.to_i
|
79
|
+
(docs_per_page.to_i * (page.to_i - 1) + 1).to_s
|
80
|
+
end
|
81
|
+
|
82
|
+
def next_page_query_string()
|
83
|
+
"startDoc=#{self.next_start_doc}&docsPerPage=#{self.docs_per_page}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def previous_page_query_string()
|
87
|
+
"startDoc=#{self.previous_start_doc}&docsPerPage=#{self.docs_per_page}"
|
88
|
+
end
|
89
|
+
alias :prev_page_query_string :previous_page_query_string
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module XTF
|
2
|
+
module Result
|
3
|
+
module Element
|
4
|
+
end
|
5
|
+
end
|
6
|
+
end
|
7
|
+
$:.unshift(File.dirname(__FILE__))
|
8
|
+
require 'element/base'
|
9
|
+
require 'element/doc_hit'
|
10
|
+
require 'element/group'
|
11
|
+
require 'element/facet'
|
12
|
+
require 'element/result'
|
13
|
+
#Dir[File.dirname(__FILE__) + "/element/*.rb"].each { |file| require(file) }
|
data/lib/xtf/result.rb
ADDED
data/lib/xtf/ruby.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# This class is used inside a +Query+ to retrieve all docs. It's primary purpose is
|
2
|
+
# for use with +Facet+s:
|
3
|
+
#
|
4
|
+
# query = Query.new
|
5
|
+
# query.content << AllDocs.new
|
6
|
+
# query.content << Facet.new('word')
|
7
|
+
#
|
8
|
+
class XTF::Search::Element::AllDocs
|
9
|
+
|
10
|
+
attr_reader :tag_name
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@tag_name = "all_docs"
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_xml_node
|
18
|
+
XTF::XML::Element.new(self.tag_name.to_s.camelize(:lower))
|
19
|
+
end
|
20
|
+
def to_xml
|
21
|
+
to_xml_node.to_s
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class XTF::Search::Element::And < XTF::Search::Element::Clause
|
2
|
+
attribute_keys BASE_ATTRIBUTE_KEYS, :fields, :use_proximity, :slop
|
3
|
+
|
4
|
+
def initialize(*args)
|
5
|
+
@tag_name = "and"
|
6
|
+
params = args[0] || {}
|
7
|
+
raise ArgumentError, "Provide :field or :fields, but not both" if params.key?(:field) && params.key?(:fields)
|
8
|
+
raise ArgumentError, ":fields requires :slop" if params.key?(:fields) && !params.key?(:slop)
|
9
|
+
raise ArgumentError, ":slop requires :fields" if params.key?(:slop) && !params.key?(:fields)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class XTF::Search::Element::Base
|
2
|
+
BASE_ATTRIBUTE_KEYS = [:field, :max_snippets, :boost]
|
3
|
+
|
4
|
+
def self.attribute_keys(*args)
|
5
|
+
array = args.flatten
|
6
|
+
list = array.inspect
|
7
|
+
class_eval(%Q{def attribute_keys() #{list} end}, __FILE__, __LINE__)
|
8
|
+
array.each do |k|
|
9
|
+
attr_accessor k
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attribute_keys *BASE_ATTRIBUTE_KEYS
|
14
|
+
|
15
|
+
attr_reader :tag_name
|
16
|
+
|
17
|
+
# Takes a +Hash+ of attributes and sets them. Silently ignores erroneous keys.
|
18
|
+
def initialize(*args)
|
19
|
+
params = args[0]
|
20
|
+
attribute_keys.each { |k| self.__send__("#{k}=", params[k]) if params.key?(k) } if params
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns a +Hash+ of the attributes listed in +ATTRIBUTE_KEYS+ with their values.
|
24
|
+
# The keys are +Symbol+s.
|
25
|
+
def attributes
|
26
|
+
self.attribute_keys.inject({}) do |hash, key|
|
27
|
+
hash[key] = self.__send__(key) if self.__send__(key)
|
28
|
+
hash
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class XTF::Search::Element::Clause < XTF::Search::Element::Base
|
2
|
+
VALID_TAG_NAMES = %w{phrase exact and or or_near orNear not near range}
|
3
|
+
|
4
|
+
# an Array that contains any number of clauses and/or terms
|
5
|
+
attr_accessor :content
|
6
|
+
|
7
|
+
# convenience to create a +Term+ from a +String+ and insert it into +content+
|
8
|
+
attr_accessor :term
|
9
|
+
|
10
|
+
attr_accessor :section_type #available on all elements except <not> and <facet>
|
11
|
+
|
12
|
+
# This is a factory method for creating subclasses directly from +Clause+.
|
13
|
+
# The tag_name may be passed as the first argument or as the value to the key
|
14
|
+
# +:tag_name+ or +'tag_name'+
|
15
|
+
def self.create(*args)
|
16
|
+
tag_name = args.shift.to_s if args[0].is_a?(String) || args[0].is_a?(Symbol)
|
17
|
+
params = (args[0] || {}).symbolize_keys
|
18
|
+
tag_name = params.delete(:tag_name) unless tag_name
|
19
|
+
|
20
|
+
raise ArgumentError, "need tag_name for XTF::Search::Element::Clause" unless tag_name
|
21
|
+
raise ArgumentError, "tag_name #{tag_name} not valid for XTF::Search::Element::Clause. Must be one of: #{VALID_TAG_NAMES.join(', ')}" unless VALID_TAG_NAMES.include?(tag_name)
|
22
|
+
|
23
|
+
klass = eval("XTF::Search::Element::#{tag_name.to_s.camelize}") # scope the name to avoid conflicts, especially with Range
|
24
|
+
klass.new(params)
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(*args)
|
28
|
+
params = args[0] || {}
|
29
|
+
self.content = params.delete(:content) || []
|
30
|
+
self.term = params.delete(:term) if params.has_key?(:term)
|
31
|
+
super(params)
|
32
|
+
|
33
|
+
raise ArgumentError, "need tag_name for XTF::Search::Element::Clause (maybe you should call Clause.create(:tag_name) ? )" unless @tag_name
|
34
|
+
raise ArgumentError, "tag_name #{@tag_name} not valid for XTF::Search::Element::Clause. Must be one of: #{VALID_TAG_NAMES.join(', ')}" unless VALID_TAG_NAMES.include?(@tag_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Accepts a +Term+ or a +String+ which is converted to a +Term+ and adds it to the +content+.
|
38
|
+
def term=(value)
|
39
|
+
self.content << (value.is_a?(XTF::Search::Element::Term) ? value : XTF::Search::Element::Term.new(value))
|
40
|
+
end
|
41
|
+
|
42
|
+
def content=(value)
|
43
|
+
value = [value] unless value.is_a?(Array)
|
44
|
+
@content = value
|
45
|
+
end
|
46
|
+
|
47
|
+
# TODO add section_type
|
48
|
+
def to_xml_node
|
49
|
+
xml = XTF::XML::Element.new self.tag_name.camelize(:lower)
|
50
|
+
self.attributes.each_pair { |key, value| xml.attributes[key.to_s.camelize(:lower)] = value if value}
|
51
|
+
self.content.each {|node| xml.add_element(node.to_xml_node)}
|
52
|
+
xml
|
53
|
+
end
|
54
|
+
def to_xml
|
55
|
+
to_xml_node.to_s
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class XTF::Search::Element::Facet < XTF::Search::Element::Base
|
2
|
+
attribute_keys :field, :select, :sort_groups_by, :sort_docs_by, :include_empty_groups
|
3
|
+
|
4
|
+
# can take a String or Symbol as first argument for required attribute #field
|
5
|
+
def initialize(*args)
|
6
|
+
@tag_name = "facet"
|
7
|
+
@field = args.shift.to_s if args[0].is_a?(String) or args[0].is_a?(Symbol)
|
8
|
+
params = args[0] || {}
|
9
|
+
raise ArgumentError, "supply field as first argument or as attribute of Hash, but not both!" if @field && params.key?(:field)
|
10
|
+
|
11
|
+
@field = params.delete(:field) unless @field
|
12
|
+
raise ArgumentError, "field is required." unless @field
|
13
|
+
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_xml_node
|
18
|
+
xml = XTF::XML::Element.new(self.tag_name)
|
19
|
+
self.attributes.each_pair { |key, value| xml.attributes[key.to_s.camelize(:lower)] = value if value}
|
20
|
+
xml
|
21
|
+
end
|
22
|
+
def to_xml
|
23
|
+
to_xml_node.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class XTF::Search::Element::Near < XTF::Search::Element::Clause
|
2
|
+
attribute_keys BASE_ATTRIBUTE_KEYS, :slop
|
3
|
+
|
4
|
+
# +slop+ is required. You can pass it in as the first argument or in the attributes +Hash+
|
5
|
+
# with the key +:slop+.
|
6
|
+
def initialize(*args)
|
7
|
+
@tag_name = "near"
|
8
|
+
@slop = args.shift.to_s if args[0].is_a?(String) || args[0].is_a?(Integer)
|
9
|
+
params = args[0] || {}
|
10
|
+
raise ArgumentError, "supply slop as first argument or as attribute of Hash, but not both!" if @slop && params.key?(:slop)
|
11
|
+
|
12
|
+
@slop = params.delete(:slop) unless @slop
|
13
|
+
raise ArgumentError, "slop is required." unless @slop
|
14
|
+
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class XTF::Search::Element::Or < XTF::Search::Element::Clause
|
2
|
+
attribute_keys BASE_ATTRIBUTE_KEYS, :fields, :slop
|
3
|
+
|
4
|
+
def initialize(*args)
|
5
|
+
@tag_name = "or"
|
6
|
+
params = args[0] || {}
|
7
|
+
raise ArgumentError, "Provide :field or :fields, but not both" if params.key?(:field) && params.key?(:fields)
|
8
|
+
raise ArgumentError, ":fields requires :slop" if params.key?(:fields) && !params.key?(:slop)
|
9
|
+
raise ArgumentError, ":slop requires :fields" if params.key?(:slop) && !params.key?(:fields)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Extends +Near+, as they are identical except for +tag_name+
|
2
|
+
class XTF::Search::Element::OrNear < XTF::Search::Element::Near
|
3
|
+
attribute_keys BASE_ATTRIBUTE_KEYS, :slop
|
4
|
+
|
5
|
+
# +slop+ is required. You can pass it in as the first argument or in the attributes +Hash+
|
6
|
+
# with the key +:slop+.
|
7
|
+
def initialize(*args)
|
8
|
+
@tag_name = "orNear"
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class XTF::Search::Element::Phrase < XTF::Search::Element::Clause
|
2
|
+
# Takes a +String+ and breaks it up into +Term+s:
|
3
|
+
attr_accessor :phrase
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
@tag_name = "phrase"
|
7
|
+
params = args[0] || {}
|
8
|
+
_phrase = params.delete(:phrase)
|
9
|
+
super(params)
|
10
|
+
self.phrase = _phrase if _phrase
|
11
|
+
end
|
12
|
+
|
13
|
+
# If the last term is a wildcard, then append it to the previous term.
|
14
|
+
def phrase=(terms)
|
15
|
+
raise ArgumentError unless terms.is_a?(String)
|
16
|
+
terms = terms.split(XTF::Search::Constants.phrase_delimiters)
|
17
|
+
terms.first.gsub!(/^"/, "")
|
18
|
+
terms.shift if terms.first == ""
|
19
|
+
terms.last.gsub!(/"$/,"")
|
20
|
+
terms.pop if terms.last == ""
|
21
|
+
if terms.last == "*"
|
22
|
+
terms.pop
|
23
|
+
terms.last << "*"
|
24
|
+
end
|
25
|
+
terms.each { |t| self.content << XTF::Search::Element::Term.new(t) }
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class XTF::Search::Element::Query < XTF::Search::Element::Base
|
2
|
+
|
3
|
+
STYLE_DEFAULT = "style/crossQuery/resultFormatter/default/resultFormatter.xsl"
|
4
|
+
INDEX_PATH_DEFAULT = "index"
|
5
|
+
|
6
|
+
attribute_keys :index_path, :style, :sort_docs_by, :start_doc, :max_docs, :term_limit, :work_limit,
|
7
|
+
:max_context, :max_snippets, :term_mode, :field, :normalize_scores, :explain_scores
|
8
|
+
|
9
|
+
attr_accessor :content # one Term or Clause, spellcheck, and any number of facet tags all in an array
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
@tag_name = 'query'
|
13
|
+
params = args[0] || {}
|
14
|
+
self.content = params.delete(:content) || []
|
15
|
+
super(params)
|
16
|
+
|
17
|
+
@style ||= STYLE_DEFAULT
|
18
|
+
@index_path ||= INDEX_PATH_DEFAULT
|
19
|
+
end
|
20
|
+
|
21
|
+
# Accepts a +Term+ or a +String+ which is converted to a +Term+ and adds it to the +content+.
|
22
|
+
def term=(value)
|
23
|
+
self.content << (value.is_a?(XTF::Search::Element::Term) ? value : XTF::Search::Element::Term.new(value))
|
24
|
+
end
|
25
|
+
|
26
|
+
def content=(value)
|
27
|
+
value = [value] unless value.is_a?(Array)
|
28
|
+
@content = value
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_xml_node
|
32
|
+
xml = XTF::XML::Element.new(self.tag_name)
|
33
|
+
self.attributes.each_pair { |key, value| xml.attributes[key.to_s.camelize(:lower)] = value if value}
|
34
|
+
# TODO validate only one term or clause present?
|
35
|
+
self.content.each {|node| xml.add_element(node.to_xml_node)}
|
36
|
+
xml
|
37
|
+
end
|
38
|
+
def to_xml
|
39
|
+
to_xml_node.to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
class XTF::Search::Element::Range < XTF::Search::Element::Clause
|
2
|
+
|
3
|
+
attribute_keys BASE_ATTRIBUTE_KEYS, :inclusive, :numeric
|
4
|
+
|
5
|
+
attr_accessor :lower, :upper
|
6
|
+
|
7
|
+
# +lower+ and +upper+ may be passed as the first and second arguments.
|
8
|
+
# If one is present, then both must be. Otherwise, they may be passed as
|
9
|
+
# part of the attributes +Hash+ with the keys +:lower+ and +:upper+.
|
10
|
+
# An +ArgumentError+ will be raised if they are not provided.
|
11
|
+
def initialize(*args)
|
12
|
+
@tag_name = "range"
|
13
|
+
|
14
|
+
# retrieve lower and upper if passed as first two arguments
|
15
|
+
@lower = args.shift.to_s if args[0].is_a?(String) || args[0].is_a?(Integer)
|
16
|
+
@upper = args.shift.to_s if args[0].is_a?(String) || args[0].is_a?(Integer)
|
17
|
+
raise ArgumentError, "If you provide --lower-- as first argument, you must provide --upper-- as second." if @lower && !@upper
|
18
|
+
|
19
|
+
params = args[0] || {}
|
20
|
+
raise ArgumentError, "You have provided --lower-- and --upper-- as both arguments and attributes! Pass them as one or the other, but not both." if @lower && (params[:lower] || params[:upper])
|
21
|
+
|
22
|
+
@lower = params[:lower] && params[:lower].to_s unless @lower
|
23
|
+
@upper = params[:upper] && params[:upper].to_s unless @upper
|
24
|
+
|
25
|
+
raise ArgumentError, "You must provide --lower-- and --upper-- as the first two arguments to new() or as members of the attributes Hash." unless @lower && @upper
|
26
|
+
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO add section_type
|
31
|
+
def to_xml_node
|
32
|
+
xml = XTF::XML::Element.new self.tag_name.camelize(:lower)
|
33
|
+
self.attributes.each_pair { |key, value| xml.attributes[key.to_s.camelize(:lower)] = value if value}
|
34
|
+
lnode = XTF::XML::Element.new("lower")
|
35
|
+
lnode.text = self.lower
|
36
|
+
unode = XTF::XML::Element.new("upper")
|
37
|
+
unode.text = self.upper
|
38
|
+
xml.add_element(lnode)
|
39
|
+
xml.add_element(unode)
|
40
|
+
xml
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class XTF::Search::Element::ResultData
|
2
|
+
attr_accessor :value
|
3
|
+
attr_reader :tag_name
|
4
|
+
|
5
|
+
def initialize(data=nil)
|
6
|
+
@tag_name = "resultData"
|
7
|
+
@value = data
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_xml_node
|
11
|
+
xml = XTF::XML::Element.new(self.tag_name)
|
12
|
+
xml.text = self.value
|
13
|
+
xml
|
14
|
+
end
|
15
|
+
def to_xml
|
16
|
+
to_xml_node.to_s
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class XTF::Search::Element::SectionType
|
2
|
+
|
3
|
+
attr_accessor :content # one Term or Clause
|
4
|
+
attr_reader :tag_name
|
5
|
+
|
6
|
+
def initialize(content = nil)
|
7
|
+
@tag_name = "sectionType"
|
8
|
+
@content = content
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_xml_node
|
12
|
+
xml = XTF::XML::Element.new("sectionType")
|
13
|
+
xml.add_element(self.content.to_xml_node) if self.content
|
14
|
+
xml
|
15
|
+
end
|
16
|
+
def to_xml
|
17
|
+
to_xml_node.to_s
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Models a single XTF Term. However, if the term is surrounded by double-quotes, then
|
2
|
+
# a Phrase will be emitted for to_xml_node().
|
3
|
+
|
4
|
+
class XTF::Search::Element::Term < XTF::Search::Element::Base
|
5
|
+
attr_accessor :value
|
6
|
+
attr_accessor :section_type
|
7
|
+
|
8
|
+
# Should a phrase be parsed? Defaults to +true+
|
9
|
+
attr_accessor :parse_phrase
|
10
|
+
|
11
|
+
# +new+ accepts an optional first argument for +value+ as well as an optional
|
12
|
+
# first or second argument +Hash+ of the +Term+'s +attributes+.
|
13
|
+
# +value+ may be passed as the first argument or in attributes +Hash+ with key +:value+.
|
14
|
+
# +section_type+ may be passed in the attributes +Hash+ with key +:section_type+.
|
15
|
+
def initialize(*args)
|
16
|
+
@tag_name = "term"
|
17
|
+
@value = args.shift if args[0].kind_of?(String)
|
18
|
+
params = args[0] || {}
|
19
|
+
@value = params.delete(:value) unless @value
|
20
|
+
@section_type = params.delete(:section_type)
|
21
|
+
@parse_phrase = params.key?(:parse_phrase) ? params.delete(:parse_phrase) : false
|
22
|
+
super
|
23
|
+
@value.strip! unless @value.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
# For convenience, if the Term's value matches /[\-\s\\\/.,;:]+/, it will be parsed as a Phrase.
|
27
|
+
# Double quotes on either end will be removed.
|
28
|
+
#
|
29
|
+
# "this phrase" woud yield:
|
30
|
+
#
|
31
|
+
# <phrase>
|
32
|
+
# <term>this</term
|
33
|
+
# <term>phrase</term
|
34
|
+
# </phrase>
|
35
|
+
#
|
36
|
+
def to_xml_node
|
37
|
+
if self.parse_phrase && self.value =~ XTF::Search::Constants.phrase_delimiters
|
38
|
+
phrase = XTF::Search::Element::Phrase.new(self.attributes)
|
39
|
+
phrase.phrase = self.value
|
40
|
+
phrase.to_xml_node
|
41
|
+
else
|
42
|
+
xml = XTF::XML::Element.new(self.tag_name)
|
43
|
+
self.attributes.each_pair { |key, value| xml.attributes[key.to_s.camelize(:lower)] = value if value}
|
44
|
+
xml.text = self.value
|
45
|
+
xml
|
46
|
+
end
|
47
|
+
end
|
48
|
+
def to_xml
|
49
|
+
to_xml_node.to_s
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module XTF
|
2
|
+
module Search
|
3
|
+
module Element
|
4
|
+
end
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
$:.unshift(File.dirname(__FILE__))
|
9
|
+
require 'element/base'
|
10
|
+
require 'element/section_type'
|
11
|
+
require 'element/result_data'
|
12
|
+
require 'element/clause'
|
13
|
+
require 'element/near'
|
14
|
+
require 'element/phrase'
|
15
|
+
Dir[File.dirname(__FILE__) + "/element/*.rb"].each { |file| require(file) }
|
data/lib/xtf/search.rb
ADDED
data/lib/xtf/xml.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# Code in this file is derived from Apache's Solr-Ruby project.
|
2
|
+
|
3
|
+
module XTF::XML
|
4
|
+
end
|
5
|
+
|
6
|
+
begin
|
7
|
+
|
8
|
+
# If we can load rubygems and libxml-ruby...
|
9
|
+
require 'rubygems'
|
10
|
+
require 'xml/libxml'
|
11
|
+
|
12
|
+
class XML::Attributes
|
13
|
+
def size
|
14
|
+
length
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Some Hpricot-like convenience methods for LibXml
|
19
|
+
class XML::Document
|
20
|
+
def self.parse_string(xml)
|
21
|
+
xml_parser = XML::Parser.string(xml)
|
22
|
+
xml_parser.parse
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# then make a few modifications to XML::Node so it can stand in for REXML::Element
|
27
|
+
class XML::Node
|
28
|
+
# element.add_element(another_element) should work
|
29
|
+
alias_method :add_element, :<<
|
30
|
+
|
31
|
+
# element.attributes['blah'] should work
|
32
|
+
# def attributes
|
33
|
+
# self
|
34
|
+
# end
|
35
|
+
|
36
|
+
def size
|
37
|
+
self.attributes.length
|
38
|
+
end
|
39
|
+
alias :length :size
|
40
|
+
|
41
|
+
# element.text = "blah" should work
|
42
|
+
def text=(x)
|
43
|
+
self << x.to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
def text
|
47
|
+
self.content
|
48
|
+
end
|
49
|
+
|
50
|
+
def at(xpath)
|
51
|
+
self.find_first(xpath)
|
52
|
+
end
|
53
|
+
|
54
|
+
# find the array of child nodes matching the given xpath
|
55
|
+
# TODO add these to Rexml?
|
56
|
+
def search(xpath)
|
57
|
+
results = self.find(xpath).to_a
|
58
|
+
if block_given?
|
59
|
+
results.each do |result|
|
60
|
+
yield result
|
61
|
+
end
|
62
|
+
end
|
63
|
+
return results
|
64
|
+
end
|
65
|
+
|
66
|
+
def inner_xml
|
67
|
+
child.to_s
|
68
|
+
end
|
69
|
+
alias inner_html inner_xml
|
70
|
+
|
71
|
+
def inner_text
|
72
|
+
self.content
|
73
|
+
end
|
74
|
+
end #XML::Node
|
75
|
+
|
76
|
+
# And use XML::Node for our XML generation
|
77
|
+
XTF::XML::Element = XML::Node
|
78
|
+
# raise LoadError
|
79
|
+
rescue LoadError => e # If we can't load either rubygems or libxml-ruby
|
80
|
+
|
81
|
+
# Just use REXML.
|
82
|
+
require 'rexml/document'
|
83
|
+
XTF::XML::Element = REXML::Element
|
84
|
+
|
85
|
+
end
|