xtf-ruby 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|