fetch_util 0.4.0 → 0.5.1

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.
@@ -5,46 +5,40 @@ require "uri"
5
5
 
6
6
  module FetchUtil
7
7
  class Searcher
8
- SOURCES = {
9
- "duckduckgo" => "https://duckduckgo.com/?q=%<query>s&ia=web&kl=us-en",
10
- "google" => "https://www.google.com/search?hl=en&q=%<query>s",
11
- "bing" => "https://www.bing.com/search?setlang=en-US&q=%<query>s",
12
- "ecosia" => "https://www.ecosia.org/search?q=%<query>s",
13
- "brave" => "https://search.brave.com/search?q=%<query>s"
14
- }.freeze
15
-
16
- DEFAULT_SOURCES = %w[duckduckgo google].freeze
17
-
8
+ DEFAULT_SOURCES = %w[brave bing yahoo].freeze
9
+ STRUCTURED_QUERY_AUTHORITY = "yahoo"
10
+ STRUCTURED_QUERY = /"[^"]+"|'[^']+'|\(|\)|\[|\]|\{|\}|(?:\A|\s)[+-]?[[:alpha:]][[:alnum:]_-]*:/
18
11
  autoload :ResultFiltering, "fetch_util/searcher/result_filtering"
19
12
  include ResultFiltering
20
13
  private_constant :ResultFiltering
21
14
 
22
- def initialize(fetcher: nil, request_log: RequestLog.new, sources: nil, limit: nil, concurrency: 2, verbose: false, **fetch_options)
15
+ def initialize(transport: nil, request_log: RequestLog.new, sources: nil, limit: nil, verbose: false,
16
+ timeout: SearchTransport::DEFAULT_TIMEOUT)
23
17
  @request_log = request_log
24
- @sources = Array(sources || DEFAULT_SOURCES).map(&:to_s)
18
+ @sources_explicit = !sources.nil?
19
+ @sources = Array(sources || DEFAULT_SOURCES).map(&:to_s).uniq
20
+ unknown = @sources - SearchTransport::SOURCES.keys
21
+ raise ArgumentError, "unsupported search source: #{unknown.first}" if unknown.any?
22
+
23
+ validate_limit!(limit)
25
24
  @limit = limit
26
25
  @verbose = verbose
27
- @fetcher = fetcher || ParallelFetcher.new(concurrency: concurrency, request_log: request_log, **fetch_options)
26
+ @transport = transport || SearchTransport.new(sources: @sources, timeout: timeout)
28
27
  end
29
28
 
30
29
  def search(query)
31
30
  encoded_query = query.to_s.strip
32
31
  raise ArgumentError, "query must not be empty" if encoded_query.empty?
33
32
 
34
- urls = search_urls(encoded_query)
35
33
  @request_log.append(search_request_uri(encoded_query))
36
- fetched = begin
37
- @fetcher.fetch(urls.values)
38
- rescue ParallelFetcher::ParallelFetchError => e
39
- raise unless e.results&.compact&.any?
40
-
41
- e.results
42
- end
34
+ responses = @transport.search(encoded_query)
43
35
 
44
- {
36
+ payload = {
45
37
  query: encoded_query,
46
- results: formatted_results(apply_limit(aggregate(urls.keys, fetched)))
38
+ results: formatted_results(apply_limit(aggregate(responses, encoded_query)))
47
39
  }
40
+ payload[:diagnostics] = diagnostics(responses) if @verbose
41
+ payload
48
42
  end
49
43
 
50
44
  private
@@ -55,43 +49,37 @@ module FetchUtil
55
49
  limit.nil? ? results : results.first(limit)
56
50
  end
57
51
 
58
- def search_urls(query)
59
- urls = {}
60
-
61
- @sources.each do |source|
62
- template = SOURCES.fetch(source) do
63
- raise ArgumentError, "unsupported search source: #{source}"
64
- end
65
- urls[source] = format(template, query: CGI.escape(query))
66
- end
52
+ def validate_limit!(value)
53
+ return if value.nil?
54
+ return if value.is_a?(Integer) && value >= 0
67
55
 
68
- urls
56
+ raise ArgumentError, "limit must be a nonnegative integer"
69
57
  end
70
58
 
71
59
  def search_request_uri(query)
72
60
  "search://#{@sources.join(",")}?q=#{CGI.escape(query)}"
73
61
  end
74
62
 
75
- def aggregate(sources, fetched)
63
+ def aggregate(responses, query)
76
64
  parsed = {}
77
- max_size = 0
65
+ structured_query = query.match?(STRUCTURED_QUERY)
78
66
 
79
- sources.zip(fetched).each do |source, result|
80
- items = parse_markdown(result.markdown)
81
- parsed[source] = items
82
- max_size = [max_size, items.length].max
67
+ @sources.each do |source|
68
+ response = responses.find { |item| item.source == source }
69
+ parsed[source] = response ? response.candidates.filter_map { |candidate| normalized_candidate(candidate) } : []
83
70
  end
84
71
 
72
+ apply_structured_query_authority!(parsed, responses) if structured_query
73
+ max_size = parsed.values.map(&:length).max || 0
74
+
85
75
  items = []
86
76
  seen = {}
87
77
 
88
78
  max_size.times do |index|
89
- sources.each do |source|
79
+ @sources.each do |source|
90
80
  item = parsed.fetch(source)[index]
91
81
  next unless item
92
82
 
93
- item = item.merge(rank: index + 1)
94
-
95
83
  existing = seen[item[:url]]
96
84
  if existing
97
85
  merge_result!(existing, source, item)
@@ -107,6 +95,23 @@ module FetchUtil
107
95
  items
108
96
  end
109
97
 
98
+ def apply_structured_query_authority!(parsed, responses)
99
+ return if @sources_explicit
100
+ return unless @sources.include?(STRUCTURED_QUERY_AUTHORITY)
101
+
102
+ authority = responses.find { |response| response.source == STRUCTURED_QUERY_AUTHORITY }
103
+ return unless authority&.status == "ok" && authority.reason.nil?
104
+
105
+ authority_urls = parsed.fetch(STRUCTURED_QUERY_AUTHORITY).map { |item| item[:url] }
106
+ return if authority_urls.empty?
107
+
108
+ @sources.each do |source|
109
+ next if source == STRUCTURED_QUERY_AUTHORITY
110
+
111
+ parsed[source].select! { |item| authority_urls.include?(item[:url]) }
112
+ end
113
+ end
114
+
110
115
  def build_result(source, item)
111
116
  result = {
112
117
  title: item[:title],
@@ -121,7 +126,7 @@ module FetchUtil
121
126
  def merge_result!(result, source, item)
122
127
  result[:sources] << source unless result[:sources].include?(source)
123
128
  result[:ranks][source] ||= item[:rank]
124
- return if !item[:snippet] || (result[:snippet] && result[:snippet].length >= item[:snippet].length)
129
+ return if result[:snippet] || !item[:snippet]
125
130
 
126
131
  result[:snippet] = item[:snippet]
127
132
  end
@@ -141,65 +146,39 @@ module FetchUtil
141
146
  end
142
147
  end
143
148
 
144
- def parse_markdown(markdown)
145
- markdown.to_s.lines.filter_map do |line|
146
- parsed = parse_markdown_line(line)
147
- next unless parsed
148
-
149
- normalized_item(parsed[:title], parsed[:url], parsed[:snippet])
150
- end
151
- end
152
-
153
- def parse_markdown_line(line)
154
- stripped = line.to_s.strip
155
- return nil unless stripped.start_with?("- [")
156
-
157
- title_end = stripped.index("](")
158
- return nil unless title_end
159
-
160
- url_start = title_end + 2
161
- url_end = markdown_url_end_index(stripped, url_start)
162
- return nil unless url_end
163
-
164
- title = stripped[3...title_end]
165
- url = stripped[url_start...url_end]
166
- remainder = stripped[(url_end + 1)..].to_s
167
- snippet = remainder.start_with?(" - ") ? remainder[3..] : nil
168
-
169
- { title: title, url: url, snippet: snippet }
170
- end
171
-
172
- def markdown_url_end_index(line, url_start)
173
- depth = 0
174
-
175
- url_start.upto(line.length - 1) do |index|
176
- char = line[index]
177
- if char == "("
178
- depth += 1
179
- elsif char == ")"
180
- return index if depth.zero?
181
-
182
- depth -= 1
183
- end
149
+ def diagnostics(responses)
150
+ @sources.filter_map do |source|
151
+ response = responses.find { |item| item.source == source }
152
+ next unless response
153
+
154
+ diagnostic = {
155
+ source: response.source,
156
+ transport: response.transport,
157
+ status: response.status,
158
+ result_count: response.candidates.length,
159
+ elapsed_ms: response.elapsed_ms
160
+ }
161
+ diagnostic[:final_url] = response.final_url if response.final_url
162
+ diagnostic[:reason] = response.reason if response.reason
163
+ diagnostic
184
164
  end
185
-
186
- nil
187
165
  end
188
166
 
189
- def normalized_item(title, url, snippet)
190
- normalized_url = normalize_url(url)
167
+ def normalized_candidate(candidate)
168
+ normalized_url = normalize_url(candidate.url)
191
169
  return nil unless normalized_url
192
170
 
193
- normalized_title = normalize_title(title, normalized_url)
171
+ normalized_title = normalize_title(candidate.title, normalized_url)
194
172
  return nil if normalized_title.empty? || generic_title?(normalized_title, normalized_url)
195
173
 
196
- normalized_snippet = normalize_snippet(snippet, normalized_title, normalized_url)
174
+ normalized_snippet = normalize_snippet(candidate.snippet, normalized_title, normalized_url)
197
175
  return nil if search_engine_self_link?(normalized_title, normalized_url, normalized_snippet)
198
176
  return nil if low_value_result?(normalized_title, normalized_url, normalized_snippet)
199
177
 
200
178
  item = {
201
179
  title: normalized_title,
202
- url: normalized_url
180
+ url: normalized_url,
181
+ rank: candidate.source_rank
203
182
  }
204
183
 
205
184
  item[:snippet] = normalized_snippet if normalized_snippet
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FetchUtil
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.1"
5
5
  end
data/lib/fetch_util.rb CHANGED
@@ -64,6 +64,7 @@ module FetchUtil
64
64
  autoload :RequestLog, "fetch_util/request_log"
65
65
  autoload :Result, "fetch_util/result"
66
66
  autoload :Searcher, "fetch_util/searcher"
67
+ autoload :SearchTransport, "fetch_util/search_transport"
67
68
 
68
69
  module_function
69
70
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fetch_util
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - hmdne
@@ -86,6 +86,7 @@ files:
86
86
  - lib/fetch_util/browser/site_stabilization/social_platforms.rb
87
87
  - lib/fetch_util/browser/site_stabilization/travel_and_lodging.rb
88
88
  - lib/fetch_util/browser/stabilization.rb
89
+ - lib/fetch_util/browser/stabilization/anubis.rb
89
90
  - lib/fetch_util/browser/stabilization/page_flow.rb
90
91
  - lib/fetch_util/browser/stabilization/spa_hydration.rb
91
92
  - lib/fetch_util/cli.rb
@@ -114,6 +115,7 @@ files:
114
115
  - lib/fetch_util/regulatory/usage_preferences.rb
115
116
  - lib/fetch_util/request_log.rb
116
117
  - lib/fetch_util/result.rb
118
+ - lib/fetch_util/search_transport.rb
117
119
  - lib/fetch_util/searcher.rb
118
120
  - lib/fetch_util/searcher/result_filtering.rb
119
121
  - lib/fetch_util/version.rb