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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +19 -3
- data/SKILL.md +10 -4
- data/lib/fetch_util/assets/extract.js +1 -1
- data/lib/fetch_util/cli.rb +1 -2
- data/lib/fetch_util/search_transport.rb +496 -0
- data/lib/fetch_util/searcher.rb +45 -86
- data/lib/fetch_util/version.rb +1 -1
- data/lib/fetch_util.rb +1 -0
- metadata +2 -1
data/lib/fetch_util/searcher.rb
CHANGED
|
@@ -5,46 +5,38 @@ require "uri"
|
|
|
5
5
|
|
|
6
6
|
module FetchUtil
|
|
7
7
|
class Searcher
|
|
8
|
-
|
|
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(
|
|
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
|
-
@
|
|
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
|
-
|
|
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(
|
|
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
|
|
59
|
-
|
|
50
|
+
def validate_limit!(value)
|
|
51
|
+
return if value.nil?
|
|
52
|
+
return if value.is_a?(Integer) && value >= 0
|
|
60
53
|
|
|
61
|
-
|
|
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(
|
|
61
|
+
def aggregate(responses)
|
|
76
62
|
parsed = {}
|
|
77
63
|
max_size = 0
|
|
78
64
|
|
|
79
|
-
sources.
|
|
80
|
-
|
|
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
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
next unless
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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
|
data/lib/fetch_util/version.rb
CHANGED
data/lib/fetch_util.rb
CHANGED
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
|
+
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
|