querylane 0.1.0 → 0.2.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 +73 -1
- data/CONTRIBUTING.md +1 -1
- data/README.md +2 -0
- data/app/assets/stylesheets/querylane.css +12 -1
- data/app/javascript/querylane/controllers/box_controller.js +1 -1
- data/app/javascript/querylane/controllers/results_controller.js +8 -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 +2 -1
- 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 +27 -0
- data/lib/querylane/chips.rb +24 -7
- data/lib/querylane/configuration.rb +9 -0
- 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 +9 -6
- data/lib/querylane/rewriter.rb +44 -2
- data/lib/querylane/search.rb +2 -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/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
|
@@ -211,6 +211,8 @@ module Querylane
|
|
|
211
211
|
chip = Chips.for(query).find { |candidate| candidate.index == chip_index }
|
|
212
212
|
alternative = chip&.alternatives&.find { |candidate| candidate.index == alternative_index }
|
|
213
213
|
alternative ? Rewriter.replace(query.source, chip.ranges.first, alternative.text) : query.source
|
|
214
|
+
elsif @request.as_words
|
|
215
|
+
Rewriter.as_words(query, Chips.for(query).find { |candidate| candidate.index == @request.as_words })
|
|
214
216
|
else
|
|
215
217
|
chip = Chips.for(query).find { |candidate| candidate.index == @request.remove }
|
|
216
218
|
chip ? Rewriter.remove(query.source, chip.ranges) : query.source
|
|
@@ -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.2.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-10
|
|
11
|
+
date: 2026-11-10 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
|