fetch_util 0.4.0 → 0.5.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.
@@ -5,46 +5,38 @@ 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
8
+ DEFAULT_SOURCES = %w[brave bing].freeze
17
9
 
18
10
  autoload :ResultFiltering, "fetch_util/searcher/result_filtering"
19
11
  include ResultFiltering
20
12
  private_constant :ResultFiltering
21
13
 
22
- def initialize(fetcher: nil, request_log: RequestLog.new, sources: nil, limit: nil, concurrency: 2, verbose: false, **fetch_options)
14
+ def initialize(transport: nil, request_log: RequestLog.new, sources: nil, limit: nil, verbose: false,
15
+ timeout: SearchTransport::DEFAULT_TIMEOUT)
23
16
  @request_log = request_log
24
- @sources = Array(sources || DEFAULT_SOURCES).map(&:to_s)
17
+ @sources = Array(sources || DEFAULT_SOURCES).map(&:to_s).uniq
18
+ unknown = @sources - SearchTransport::SOURCES.keys
19
+ raise ArgumentError, "unsupported search source: #{unknown.first}" if unknown.any?
20
+
21
+ validate_limit!(limit)
25
22
  @limit = limit
26
23
  @verbose = verbose
27
- @fetcher = fetcher || ParallelFetcher.new(concurrency: concurrency, request_log: request_log, **fetch_options)
24
+ @transport = transport || SearchTransport.new(sources: @sources, timeout: timeout)
28
25
  end
29
26
 
30
27
  def search(query)
31
28
  encoded_query = query.to_s.strip
32
29
  raise ArgumentError, "query must not be empty" if encoded_query.empty?
33
30
 
34
- urls = search_urls(encoded_query)
35
31
  @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
32
+ responses = @transport.search(encoded_query)
43
33
 
44
- {
34
+ payload = {
45
35
  query: encoded_query,
46
- results: formatted_results(apply_limit(aggregate(urls.keys, fetched)))
36
+ results: formatted_results(apply_limit(aggregate(responses)))
47
37
  }
38
+ payload[:diagnostics] = diagnostics(responses) if @verbose
39
+ payload
48
40
  end
49
41
 
50
42
  private
@@ -55,29 +47,24 @@ module FetchUtil
55
47
  limit.nil? ? results : results.first(limit)
56
48
  end
57
49
 
58
- def search_urls(query)
59
- urls = {}
50
+ def validate_limit!(value)
51
+ return if value.nil?
52
+ return if value.is_a?(Integer) && value >= 0
60
53
 
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
67
-
68
- urls
54
+ raise ArgumentError, "limit must be a nonnegative integer"
69
55
  end
70
56
 
71
57
  def search_request_uri(query)
72
58
  "search://#{@sources.join(",")}?q=#{CGI.escape(query)}"
73
59
  end
74
60
 
75
- def aggregate(sources, fetched)
61
+ def aggregate(responses)
76
62
  parsed = {}
77
63
  max_size = 0
78
64
 
79
- sources.zip(fetched).each do |source, result|
80
- items = parse_markdown(result.markdown)
65
+ @sources.each do |source|
66
+ response = responses.find { |item| item.source == source }
67
+ items = response ? response.candidates.filter_map { |candidate| normalized_candidate(candidate) } : []
81
68
  parsed[source] = items
82
69
  max_size = [max_size, items.length].max
83
70
  end
@@ -86,12 +73,10 @@ module FetchUtil
86
73
  seen = {}
87
74
 
88
75
  max_size.times do |index|
89
- sources.each do |source|
76
+ @sources.each do |source|
90
77
  item = parsed.fetch(source)[index]
91
78
  next unless item
92
79
 
93
- item = item.merge(rank: index + 1)
94
-
95
80
  existing = seen[item[:url]]
96
81
  if existing
97
82
  merge_result!(existing, source, item)
@@ -121,7 +106,7 @@ module FetchUtil
121
106
  def merge_result!(result, source, item)
122
107
  result[:sources] << source unless result[:sources].include?(source)
123
108
  result[:ranks][source] ||= item[:rank]
124
- return if !item[:snippet] || (result[:snippet] && result[:snippet].length >= item[:snippet].length)
109
+ return if result[:snippet] || !item[:snippet]
125
110
 
126
111
  result[:snippet] = item[:snippet]
127
112
  end
@@ -141,65 +126,39 @@ module FetchUtil
141
126
  end
142
127
  end
143
128
 
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
129
+ def diagnostics(responses)
130
+ @sources.filter_map do |source|
131
+ response = responses.find { |item| item.source == source }
132
+ next unless response
133
+
134
+ diagnostic = {
135
+ source: response.source,
136
+ transport: response.transport,
137
+ status: response.status,
138
+ result_count: response.candidates.length,
139
+ elapsed_ms: response.elapsed_ms
140
+ }
141
+ diagnostic[:final_url] = response.final_url if response.final_url
142
+ diagnostic[:reason] = response.reason if response.reason
143
+ diagnostic
184
144
  end
185
-
186
- nil
187
145
  end
188
146
 
189
- def normalized_item(title, url, snippet)
190
- normalized_url = normalize_url(url)
147
+ def normalized_candidate(candidate)
148
+ normalized_url = normalize_url(candidate.url)
191
149
  return nil unless normalized_url
192
150
 
193
- normalized_title = normalize_title(title, normalized_url)
151
+ normalized_title = normalize_title(candidate.title, normalized_url)
194
152
  return nil if normalized_title.empty? || generic_title?(normalized_title, normalized_url)
195
153
 
196
- normalized_snippet = normalize_snippet(snippet, normalized_title, normalized_url)
154
+ normalized_snippet = normalize_snippet(candidate.snippet, normalized_title, normalized_url)
197
155
  return nil if search_engine_self_link?(normalized_title, normalized_url, normalized_snippet)
198
156
  return nil if low_value_result?(normalized_title, normalized_url, normalized_snippet)
199
157
 
200
158
  item = {
201
159
  title: normalized_title,
202
- url: normalized_url
160
+ url: normalized_url,
161
+ rank: candidate.source_rank
203
162
  }
204
163
 
205
164
  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.0"
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hmdne
@@ -114,6 +114,7 @@ files:
114
114
  - lib/fetch_util/regulatory/usage_preferences.rb
115
115
  - lib/fetch_util/request_log.rb
116
116
  - lib/fetch_util/result.rb
117
+ - lib/fetch_util/search_transport.rb
117
118
  - lib/fetch_util/searcher.rb
118
119
  - lib/fetch_util/searcher/result_filtering.rb
119
120
  - lib/fetch_util/version.rb