querylane 0.1.0 → 0.3.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 +92 -1
- data/CONTRIBUTING.md +1 -1
- data/README.md +2 -0
- data/app/assets/stylesheets/querylane.css +28 -6
- data/app/javascript/querylane/controllers/box_controller.js +1 -1
- data/app/javascript/querylane/controllers/results_controller.js +12 -1
- data/app/views/querylane/_chips.html.erb +2 -1
- data/app/views/querylane/_denied.html.erb +2 -1
- data/app/views/querylane/_empty.html.erb +10 -4
- data/app/views/querylane/_facets.html.erb +3 -2
- data/app/views/querylane/_first_load.html.erb +2 -1
- data/app/views/querylane/_form.html.erb +19 -9
- data/app/views/querylane/_group.html.erb +2 -1
- data/app/views/querylane/_help.html.erb +3 -2
- data/app/views/querylane/_options.html.erb +5 -3
- data/app/views/querylane/_options_row.html.erb +18 -7
- data/app/views/querylane/_outcome.html.erb +5 -3
- data/app/views/querylane/_pager.html.erb +3 -2
- data/app/views/querylane/_preview.html.erb +2 -1
- data/app/views/querylane/_record.html.erb +2 -1
- data/app/views/querylane/_related.html.erb +2 -1
- data/app/views/querylane/_results.html.erb +2 -2
- data/app/views/querylane/_rows.html.erb +2 -1
- data/app/views/querylane/_saved.html.erb +15 -6
- data/app/views/querylane/_suggest_box.html.erb +10 -7
- data/app/views/querylane/_suggestions.html.erb +2 -2
- data/app/views/querylane/_toolbar.html.erb +2 -1
- data/app/views/querylane/_unavailable.html.erb +2 -1
- data/app/views/querylane/_writes.html.erb +4 -3
- data/config/locales/querylane.en.yml +32 -0
- data/lib/querylane/chips.rb +24 -7
- data/lib/querylane/configuration.rb +9 -0
- data/lib/querylane/controller.rb +5 -1
- data/lib/querylane/copy.rb +28 -0
- data/lib/querylane/definition.rb +44 -10
- data/lib/querylane/parser.rb +16 -13
- data/lib/querylane/presenter.rb +19 -7
- data/lib/querylane/query.rb +59 -17
- data/lib/querylane/request.rb +32 -13
- data/lib/querylane/rewriter.rb +44 -2
- data/lib/querylane/search.rb +12 -0
- data/lib/querylane/searchers/meilisearch.rb +19 -2
- data/lib/querylane/version.rb +1 -1
- data/lib/querylane.rb +1 -0
- metadata +3 -2
data/lib/querylane/query.rb
CHANGED
|
@@ -14,6 +14,9 @@ module Querylane
|
|
|
14
14
|
|
|
15
15
|
attr_reader :definition, :source, :nodes, :error
|
|
16
16
|
|
|
17
|
+
# @return [Querylane::Configuration] the limits the query was read with
|
|
18
|
+
attr_reader :config
|
|
19
|
+
|
|
17
20
|
# @param definition [Querylane::Definition]
|
|
18
21
|
# @param source [String]
|
|
19
22
|
# @return [Querylane::Query]
|
|
@@ -21,6 +24,41 @@ module Querylane
|
|
|
21
24
|
new(definition, source.to_s, config)
|
|
22
25
|
end
|
|
23
26
|
|
|
27
|
+
# English defaults of `querylane.errors.*` (the locale file repeats them).
|
|
28
|
+
MESSAGES = {
|
|
29
|
+
parked: "%{key}: is not available yet.",
|
|
30
|
+
unknown_operator: "%{key}: is not an operator.",
|
|
31
|
+
did_you_mean: "%{key}: is not an operator. Did you mean %{suggestion}:?",
|
|
32
|
+
filter_value: "\"%{value}\" is not a valid %{key}: value.",
|
|
33
|
+
filter_value_hidden: "That value is not a valid %{key}: value.",
|
|
34
|
+
flag_value: "%{value} is not a value of %{key}:. Try %{values}.",
|
|
35
|
+
text_in_or: "OR joins operators, not names. Search one name at a time.",
|
|
36
|
+
text_in_not: "- and NOT apply to operators, not names.",
|
|
37
|
+
text_in_group: "Parentheses group operators, not names.",
|
|
38
|
+
one_text_operator: "Search %{operators} one at a time, or combine with %{filters}.",
|
|
39
|
+
one_text_operator_alone: "Search %{operators} one at a time.",
|
|
40
|
+
list_or: " or ",
|
|
41
|
+
too_long: "Search is too long (%{max} characters at most).",
|
|
42
|
+
close_quote: "Close the quote.",
|
|
43
|
+
needs_value: "%{key}: needs a value.",
|
|
44
|
+
unexpected: "Unexpected %{text}.",
|
|
45
|
+
or_needs_terms: "OR needs a term on both sides.",
|
|
46
|
+
needs_term: "%{operator} needs a term.",
|
|
47
|
+
ended_early: "Search ended early.",
|
|
48
|
+
too_deep: "Too many nested parentheses (%{max} at most).",
|
|
49
|
+
close_parenthesis: "Add the closing ).",
|
|
50
|
+
empty_parentheses: "Empty parentheses.",
|
|
51
|
+
too_many_terms: "Too many terms (%{max} at most)."
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# A query error message in the current locale.
|
|
55
|
+
#
|
|
56
|
+
# @param name [Symbol] a key of MESSAGES (`querylane.errors.<name>`)
|
|
57
|
+
# @return [String]
|
|
58
|
+
def self.message(name, **values)
|
|
59
|
+
I18n.t("querylane.errors.#{name}", **values, default: MESSAGES.fetch(name))
|
|
60
|
+
end
|
|
61
|
+
|
|
24
62
|
def valid? = error.nil?
|
|
25
63
|
|
|
26
64
|
def blank? = source.strip.empty?
|
|
@@ -67,6 +105,7 @@ module Querylane
|
|
|
67
105
|
def initialize(definition, source, config)
|
|
68
106
|
@definition = definition
|
|
69
107
|
@source = source
|
|
108
|
+
@config = config
|
|
70
109
|
@nodes = []
|
|
71
110
|
@error = nil
|
|
72
111
|
root = Parser.parse(source, config: config)
|
|
@@ -106,7 +145,7 @@ module Querylane
|
|
|
106
145
|
end
|
|
107
146
|
return text_term(definition.default_text, value, term, context, interpreted) if key.nil?
|
|
108
147
|
|
|
109
|
-
raise Invalid.new(
|
|
148
|
+
raise Invalid.new(message(:parked, key: key), term.range) if definition.parked.include?(key.to_s.downcase)
|
|
110
149
|
|
|
111
150
|
operator = definition.operator(key) || raise(unknown_key(key, term))
|
|
112
151
|
resolve_operator(operator, value, term, context, interpreted)
|
|
@@ -121,22 +160,18 @@ module Querylane
|
|
|
121
160
|
end
|
|
122
161
|
|
|
123
162
|
def text_term(operator, value, term, context, interpreted)
|
|
124
|
-
if context
|
|
125
|
-
|
|
126
|
-
or: "OR joins operators, not names. Search one name at a time.",
|
|
127
|
-
not: "- and NOT apply to operators, not names.",
|
|
128
|
-
group: "Parentheses group operators, not names."
|
|
129
|
-
}.fetch(context)
|
|
130
|
-
raise Invalid.new(message, term.range)
|
|
131
|
-
end
|
|
163
|
+
raise Invalid.new(message(:"text_in_#{context}"), term.range) if context
|
|
164
|
+
|
|
132
165
|
Term.new(:text, operator, value, term.quoted, term.range, interpreted, term.value)
|
|
133
166
|
end
|
|
134
167
|
|
|
135
168
|
def filter_term(operator, value, term, interpreted)
|
|
136
|
-
|
|
169
|
+
# A copy: a normalizer that edits its argument must not change the typed value.
|
|
170
|
+
normalized = operator.normalize ? operator.normalize.call(value.dup) : value
|
|
137
171
|
if operator.format && !operator.format.match?(normalized)
|
|
138
|
-
|
|
139
|
-
|
|
172
|
+
# A sensitive value is never repeated, not even to the host's copy.
|
|
173
|
+
text = operator.sensitive ? message(:filter_value_hidden, key: operator.key) : message(:filter_value, key: operator.key, value: value)
|
|
174
|
+
raise Invalid.new([text, Copy.text(operator.invalid)].compact.join(" "), term.range)
|
|
140
175
|
end
|
|
141
176
|
Term.new(:filter, operator, normalized, term.quoted, term.range, interpreted, term.value)
|
|
142
177
|
end
|
|
@@ -144,7 +179,7 @@ module Querylane
|
|
|
144
179
|
def flag_term(operator, value, term, interpreted)
|
|
145
180
|
symbol = value.to_s.downcase.to_sym
|
|
146
181
|
unless operator.values.key?(symbol)
|
|
147
|
-
raise Invalid.new(
|
|
182
|
+
raise Invalid.new(message(:flag_value, key: operator.key, value: value, values: operator.values.keys.join(", ")), term.range)
|
|
148
183
|
end
|
|
149
184
|
Term.new(:flag, operator, symbol, term.quoted, term.range, interpreted, term.value)
|
|
150
185
|
end
|
|
@@ -154,19 +189,26 @@ module Querylane
|
|
|
154
189
|
return if operators.size <= 1
|
|
155
190
|
|
|
156
191
|
second = text_terms.find { |term| term.operator == operators.last }
|
|
157
|
-
|
|
158
|
-
|
|
192
|
+
joiner = message(:list_or)
|
|
193
|
+
keys = operators.map(&:key).join(joiner)
|
|
194
|
+
# The first two filters, in declaration order (a host declares its best companions first).
|
|
195
|
+
others = definition.filters.first(2).map { |filter| "#{filter.key}:" }.join(joiner)
|
|
196
|
+
text = others.empty? ? message(:one_text_operator_alone, operators: keys) : message(:one_text_operator, operators: keys, filters: others)
|
|
197
|
+
raise Invalid.new(text, second.range)
|
|
159
198
|
end
|
|
160
199
|
|
|
161
200
|
# "stauts: is not an operator. Did you mean status:?", with the corrected term as the fix.
|
|
162
201
|
def unknown_key(key, term)
|
|
163
202
|
suggestion = definition.operator_keys.min_by { |candidate| distance(candidate, key) }
|
|
164
|
-
return Invalid.new(
|
|
203
|
+
return Invalid.new(message(:unknown_operator, key: key), term.range) unless suggestion && distance(suggestion, key) <= 2
|
|
165
204
|
|
|
166
205
|
fixed = source[term.range].sub(/\A[^:]+/, suggestion)
|
|
167
|
-
Invalid.new(
|
|
206
|
+
Invalid.new(message(:did_you_mean, key: key, suggestion: suggestion), term.range, fixed)
|
|
168
207
|
end
|
|
169
208
|
|
|
209
|
+
# A query error from `querylane.errors.*`, English by default.
|
|
210
|
+
def message(name, **values) = Query.message(name, **values)
|
|
211
|
+
|
|
170
212
|
# Levenshtein distance (short strings only).
|
|
171
213
|
def distance(a, b)
|
|
172
214
|
previous = (0..b.length).to_a
|
data/lib/querylane/request.rb
CHANGED
|
@@ -4,30 +4,47 @@ module Querylane
|
|
|
4
4
|
# One search request, bounded and allowlisted against the definition.
|
|
5
5
|
#
|
|
6
6
|
# Params: query (Configuration#param_name), toggle ("facet:value"),
|
|
7
|
-
# remove (chip index), clear, instead, widen, relax, compose (with
|
|
7
|
+
# remove (chip index), as_words (chip index), clear, instead, widen, relax, compose (with
|
|
8
8
|
# compose_submit), options, sort, page, per, revision.
|
|
9
9
|
class Request
|
|
10
|
-
attr_reader :fix, :saved, :save_search, :saved_name, :group, :query, :toggle, :remove, :clear, :instead, :widen, :relax, :compose, :options, :sort, :page, :per, :revision
|
|
10
|
+
attr_reader :fix, :saved, :delete_saved, :save_search, :saved_name, :group, :query, :toggle, :remove, :as_words, :clear, :instead, :widen, :relax, :compose, :options, :sort, :page, :per, :revision
|
|
11
11
|
|
|
12
12
|
# @param params [ActionController::Parameters, Hash]
|
|
13
13
|
# @param definition [Querylane::Definition]
|
|
14
14
|
def self.from_params(params, definition:, config: Querylane.configuration)
|
|
15
|
-
values = params
|
|
16
|
-
values = values.
|
|
17
|
-
new(definition: definition, query: values[config.param_name.to_s], toggle: values["toggle"], remove: values["remove"],
|
|
15
|
+
values = normalized_params(params)
|
|
16
|
+
values["page"] = values["delete_saved_page"] if values["delete_saved"].present? && values["delete_saved_page"].present?
|
|
17
|
+
new(definition: definition, query: values[config.param_name.to_s], toggle: values["toggle"], remove: values["remove"],
|
|
18
|
+
as_words: values["as_words"], clear: values["clear"],
|
|
18
19
|
instead: values["instead"], widen: values["widen"], relax: values["relax"],
|
|
19
20
|
compose: (values["compose"] if values["compose_submit"].present?), options: values["options"],
|
|
20
|
-
fix: values["fix"], group: values["group"], saved: values["saved"], save_search: values["save_search"], saved_name: values["saved_name"], sort: values["sort"], page: values["page"], per: values["per"], revision: values["revision"])
|
|
21
|
+
fix: values["fix"], group: values["group"], saved: values["saved"], delete_saved: values["delete_saved"], save_search: values["save_search"], saved_name: values["saved_name"], sort: values["sort"], page: values["page"], per: values["per"], revision: values["revision"])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The normalized, bounded deletion value used by both the request parser and POST guard.
|
|
25
|
+
#
|
|
26
|
+
# @param params [ActionController::Parameters, Hash]
|
|
27
|
+
# @return [String, nil]
|
|
28
|
+
def self.delete_saved_param(params)
|
|
29
|
+
normalized_params(params)["delete_saved"].to_s.presence&.first(64)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.normalized_params(params)
|
|
33
|
+
values = params.respond_to?(:to_unsafe_h) ? params.to_unsafe_h : params.to_h
|
|
34
|
+
values.transform_keys(&:to_s)
|
|
21
35
|
end
|
|
36
|
+
private_class_method :normalized_params
|
|
22
37
|
|
|
23
38
|
# instead: "chip.alternative" (use another reading of an interpreted term);
|
|
24
|
-
# widen: a facet key (drop that facet's terms); relax: match fewer words
|
|
39
|
+
# widen: a facet key (drop that facet's terms); relax: match fewer words;
|
|
40
|
+
# as_words: a chip index (search that filter's typed value as plain words).
|
|
25
41
|
# compose: options-panel fields {"name" => "…", "policy" => "1 2", "is" => [...], "options" => [...]}
|
|
26
42
|
# (applied only with compose_submit); options: open the options panel;
|
|
27
43
|
# group: a GroupToken (all records of one group, see Search#related);
|
|
28
|
-
# saved: a saved search id to run; save_search with saved_name: save the query
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
# saved: a saved search id to run; save_search with saved_name: save the query;
|
|
45
|
+
# delete_saved: an owned saved search id to delete without rewriting the query.
|
|
46
|
+
def initialize(definition:, query: nil, toggle: nil, remove: nil, as_words: nil, clear: nil, instead: nil, widen: nil, relax: nil,
|
|
47
|
+
compose: nil, options: nil, fix: nil, group: nil, saved: nil, delete_saved: nil, save_search: nil, saved_name: nil, sort: nil, page: nil, per: nil, revision: nil)
|
|
31
48
|
@query = query.to_s
|
|
32
49
|
@clear = clear.present?
|
|
33
50
|
@instead = instead.to_s.match(/\A(\d+)\.(\d+)\z/)&.captures&.map(&:to_i)
|
|
@@ -36,12 +53,14 @@ module Querylane
|
|
|
36
53
|
@options = options.present?
|
|
37
54
|
@group = group.presence
|
|
38
55
|
@fix = fix.present?
|
|
39
|
-
@
|
|
40
|
-
@
|
|
56
|
+
@delete_saved = delete_saved.to_s.presence&.first(64)
|
|
57
|
+
@saved = saved.to_s.presence&.first(64) unless @delete_saved
|
|
58
|
+
@save_search = save_search.present? && !@delete_saved
|
|
41
59
|
@saved_name = saved_name.to_s.squish.first(80)
|
|
42
60
|
@compose = compose.is_a?(Hash) ? compose.to_h.transform_keys(&:to_s) : nil
|
|
43
61
|
@toggle = parse_toggle(definition, toggle)
|
|
44
62
|
@remove = (Integer(remove.to_s, exception: false) if remove.present?)
|
|
63
|
+
@as_words = (Integer(as_words.to_s, exception: false) if as_words.present?)
|
|
45
64
|
@sort = definition.sort(sort)
|
|
46
65
|
@per = definition.per_options.include?(per.to_i) ? per.to_i : definition.per_options.first
|
|
47
66
|
@page = rewriting? ? 1 : [page.to_i, 1].max
|
|
@@ -52,7 +71,7 @@ module Querylane
|
|
|
52
71
|
def offset = (page - 1) * per
|
|
53
72
|
|
|
54
73
|
# Whether this request asks the server to rewrite the query text.
|
|
55
|
-
def rewriting? = (@toggle || @remove || @clear || @instead || @widen || @compose || @saved) ? true : false
|
|
74
|
+
def rewriting? = (@toggle || @remove || @as_words || @clear || @instead || @widen || @compose || @saved) ? true : false
|
|
56
75
|
|
|
57
76
|
private
|
|
58
77
|
|
data/lib/querylane/rewriter.rb
CHANGED
|
@@ -50,6 +50,40 @@ module Querylane
|
|
|
50
50
|
text.strip
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# The query with one filter chip's term replaced by its typed value as
|
|
54
|
+
# plain words ("smith last:smiht" → "smith smiht"), for a chip that offers
|
|
55
|
+
# it (Chip#as_words); otherwise the query unchanged.
|
|
56
|
+
#
|
|
57
|
+
# @param query [Querylane::Query] valid
|
|
58
|
+
# @param chip [Querylane::Chips::Chip, nil]
|
|
59
|
+
# @return [String]
|
|
60
|
+
def as_words(query, chip)
|
|
61
|
+
return query.source unless chip&.as_words
|
|
62
|
+
|
|
63
|
+
words_in_place(query, chip.ranges.first, chip.as_words)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The query text with `range` replaced by `typed` as plain words.
|
|
67
|
+
#
|
|
68
|
+
# @return [String]
|
|
69
|
+
def words_in_place(query, range, typed)
|
|
70
|
+
words = typed.split.map { |word| bare_word(query.definition, word) }.join(" ")
|
|
71
|
+
# Keep the words apart from a neighbour typed without a space (`last:"x"is:active`).
|
|
72
|
+
before = (range.begin.positive? && !query.source[range.begin - 1].match?(/\s/)) ? " " : ""
|
|
73
|
+
after = (range.end < query.source.length && !query.source[range.end].match?(/\s/)) ? " " : ""
|
|
74
|
+
replace(query.source, range, "#{before}#{words}#{after}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A word that reads back as a plain word: anything the parser would take
|
|
78
|
+
# as syntax (an operator key, a leading "-", OR, NOT, brackets, quotes) or
|
|
79
|
+
# the interpreter would read as an operator ("1234" as a last four) is
|
|
80
|
+
# quoted, since quoted words are never interpreted.
|
|
81
|
+
def bare_word(definition, word)
|
|
82
|
+
plain = word.match?(/\A[^\s()"-][^\s()"]*\z/) && !word.match?(/\A#{Parser::KEY}/o) && !%w[OR NOT].include?(word) &&
|
|
83
|
+
definition.interpret(word).nil?
|
|
84
|
+
plain ? word : quoted(word)
|
|
85
|
+
end
|
|
86
|
+
|
|
53
87
|
# The options panel's view of a query: each field's value (keyed by the
|
|
54
88
|
# compose field name), the parts no field can show (kept as typed), and,
|
|
55
89
|
# per masked field, the masked terms it leaves in the box (masked values
|
|
@@ -139,7 +173,7 @@ module Querylane
|
|
|
139
173
|
written = found.map { |token| token.quoted ? quoted(token.text) : quote(token.text) }
|
|
140
174
|
operator.default ? written.join(" ") : written.map { |text| "#{operator.key}:#{text}" }.join(" ")
|
|
141
175
|
else
|
|
142
|
-
found = found.map { |token| operator.normalize ? token.with(text: operator.normalize.call(token.text)) : token }
|
|
176
|
+
found = found.map { |token| (operator.normalize && !typed?(operator)) ? token.with(text: operator.normalize.call(token.text)) : token }
|
|
143
177
|
join_or(found.uniq.map { |token| "#{operator.key}:#{token.quoted ? quoted(token.text) : quote(token.text)}" })
|
|
144
178
|
end
|
|
145
179
|
end
|
|
@@ -169,7 +203,15 @@ module Querylane
|
|
|
169
203
|
end
|
|
170
204
|
end
|
|
171
205
|
|
|
172
|
-
|
|
206
|
+
# A field's text for a term: quoted as typed; a `display_value: :typed` filter
|
|
207
|
+
# shows the value as typed, others (and interpreted terms, whose typed
|
|
208
|
+
# token would read differently once prefixed) the normalized value.
|
|
209
|
+
def display(term)
|
|
210
|
+
value = (typed?(term.operator) && !term.interpreted) ? term.raw.to_s : term.value.to_s
|
|
211
|
+
term.quoted ? quoted(value) : value
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def typed?(operator) = operator.is_a?(Definition::FilterOperator) && operator.display_value == :typed
|
|
173
215
|
|
|
174
216
|
# Always quoted, with " and \ escaped.
|
|
175
217
|
def quoted(value) = %("#{value.to_s.gsub(/["\\]/) { |char| "\\#{char}" }}")
|
data/lib/querylane/search.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Querylane
|
|
|
33
33
|
query = Query.analyze(@definition, @request.query, config: @config)
|
|
34
34
|
query, rewritten = rewrite(query)
|
|
35
35
|
@notice = save(query) if @request.save_search && !@scope.denied?
|
|
36
|
+
@notice = delete_saved(@request.delete_saved) if @request.delete_saved && !@scope.denied?
|
|
36
37
|
return result(:first_load, query, rewritten) if query.valid? && query.blank?
|
|
37
38
|
return result(:invalid, query, rewritten, error: query.error) unless query.valid?
|
|
38
39
|
return result(:short, query, rewritten, chips: Chips.for(query)) if query.too_short?(@definition.min_text_length)
|
|
@@ -211,6 +212,8 @@ module Querylane
|
|
|
211
212
|
chip = Chips.for(query).find { |candidate| candidate.index == chip_index }
|
|
212
213
|
alternative = chip&.alternatives&.find { |candidate| candidate.index == alternative_index }
|
|
213
214
|
alternative ? Rewriter.replace(query.source, chip.ranges.first, alternative.text) : query.source
|
|
215
|
+
elsif @request.as_words
|
|
216
|
+
Rewriter.as_words(query, Chips.for(query).find { |candidate| candidate.index == @request.as_words })
|
|
214
217
|
else
|
|
215
218
|
chip = Chips.for(query).find { |candidate| candidate.index == @request.remove }
|
|
216
219
|
chip ? Rewriter.remove(query.source, chip.ranges) : query.source
|
|
@@ -309,6 +312,15 @@ module Querylane
|
|
|
309
312
|
nil
|
|
310
313
|
end
|
|
311
314
|
|
|
315
|
+
def delete_saved(id)
|
|
316
|
+
store = @definition.saved_searches
|
|
317
|
+
return :not_available unless store&.respond_to?(:delete)
|
|
318
|
+
|
|
319
|
+
store.delete(context: @context, id: id) ? :deleted : :not_available
|
|
320
|
+
rescue => error
|
|
321
|
+
saved_store_failed(error)
|
|
322
|
+
end
|
|
323
|
+
|
|
312
324
|
def saved_list
|
|
313
325
|
store = @definition.saved_searches
|
|
314
326
|
store && Array(store.all(context: @context))
|
|
@@ -67,10 +67,27 @@ module Querylane
|
|
|
67
67
|
|
|
68
68
|
def client = @client.respond_to?(:call) ? @client.call : @client
|
|
69
69
|
|
|
70
|
+
# Turns a backend failure into SearchUnavailable and logs why. An API
|
|
71
|
+
# error is logged by code, type and status only: its message can echo
|
|
72
|
+
# the filter, and filters carry what the user searched for.
|
|
70
73
|
def call
|
|
71
74
|
yield
|
|
72
|
-
rescue ::Meilisearch::ApiError
|
|
73
|
-
|
|
75
|
+
rescue ::Meilisearch::ApiError => error
|
|
76
|
+
unavailable(error, "code=#{error.code} type=#{error.type} http=#{error.http_code}", error.code)
|
|
77
|
+
rescue ::Meilisearch::CommunicationError, ::Meilisearch::TimeoutError => error
|
|
78
|
+
unavailable(error, without_url_secrets(error.message))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A connection error can quote the client URL; keep its host and port,
|
|
82
|
+
# drop any user:password@ and query string.
|
|
83
|
+
def without_url_secrets(message)
|
|
84
|
+
message.gsub(%r{(\w+://)[^\s/@]+@}, '\1').gsub(/\?[^\s)]*/, "")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def unavailable(error, detail, code = nil)
|
|
88
|
+
name = "Meilisearch #{error.class.name.demodulize}"
|
|
89
|
+
Querylane.configuration.logger.warn("[querylane] search unavailable: #{name} #{detail}")
|
|
90
|
+
raise SearchUnavailable, [name, code].compact.join(" ")
|
|
74
91
|
end
|
|
75
92
|
|
|
76
93
|
# Indexes whose scope and query can match something; the others are
|
data/lib/querylane/version.rb
CHANGED
data/lib/querylane.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: querylane
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ajaya Agrawalla
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -79,6 +79,7 @@ files:
|
|
|
79
79
|
- lib/querylane/configuration.rb
|
|
80
80
|
- lib/querylane/context.rb
|
|
81
81
|
- lib/querylane/controller.rb
|
|
82
|
+
- lib/querylane/copy.rb
|
|
82
83
|
- lib/querylane/definition.rb
|
|
83
84
|
- lib/querylane/doctor.rb
|
|
84
85
|
- lib/querylane/engine.rb
|