redi_search 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +1757 -0
- data/.travis.yml +23 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +220 -0
- data/Rakefile +12 -0
- data/bin/console +8 -0
- data/bin/publish +58 -0
- data/bin/setup +8 -0
- data/bin/test +7 -0
- data/lib/redi_search/client.rb +68 -0
- data/lib/redi_search/configuration.rb +17 -0
- data/lib/redi_search/document/converter.rb +26 -0
- data/lib/redi_search/document.rb +79 -0
- data/lib/redi_search/error.rb +6 -0
- data/lib/redi_search/index.rb +100 -0
- data/lib/redi_search/log_subscriber.rb +94 -0
- data/lib/redi_search/model.rb +57 -0
- data/lib/redi_search/result/collection.rb +22 -0
- data/lib/redi_search/schema/field.rb +21 -0
- data/lib/redi_search/schema/geo_field.rb +30 -0
- data/lib/redi_search/schema/numeric_field.rb +30 -0
- data/lib/redi_search/schema/tag_field.rb +32 -0
- data/lib/redi_search/schema/text_field.rb +36 -0
- data/lib/redi_search/schema.rb +34 -0
- data/lib/redi_search/search/and_clause.rb +15 -0
- data/lib/redi_search/search/boolean_clause.rb +72 -0
- data/lib/redi_search/search/clauses.rb +89 -0
- data/lib/redi_search/search/highlight_clause.rb +43 -0
- data/lib/redi_search/search/or_clause.rb +21 -0
- data/lib/redi_search/search/term.rb +72 -0
- data/lib/redi_search/search/where_clause.rb +66 -0
- data/lib/redi_search/search.rb +89 -0
- data/lib/redi_search/spellcheck.rb +53 -0
- data/lib/redi_search/version.rb +5 -0
- data/lib/redi_search.rb +40 -0
- data/redi_search.gemspec +48 -0
- metadata +141 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redi_search/schema"
|
4
|
+
require "redi_search/search"
|
5
|
+
require "redi_search/spellcheck"
|
6
|
+
|
7
|
+
module RediSearch
|
8
|
+
class Index
|
9
|
+
attr_reader :name, :schema, :model
|
10
|
+
|
11
|
+
def initialize(name, schema, model = nil)
|
12
|
+
@name = name
|
13
|
+
@schema = Schema.new(schema)
|
14
|
+
@model = model
|
15
|
+
end
|
16
|
+
|
17
|
+
def search(term = nil, **term_options)
|
18
|
+
Search.new(self, term, model, **term_options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def spellcheck(query, distance: 1)
|
22
|
+
Spellcheck.new(self, query, distance: distance)
|
23
|
+
end
|
24
|
+
|
25
|
+
def create
|
26
|
+
create!
|
27
|
+
rescue Redis::CommandError
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def create!
|
32
|
+
client.call!("CREATE", name, "SCHEMA", schema.to_a).ok?
|
33
|
+
end
|
34
|
+
|
35
|
+
def drop
|
36
|
+
drop!
|
37
|
+
rescue Redis::CommandError
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
def drop!
|
42
|
+
client.call!("DROP", name).ok?
|
43
|
+
end
|
44
|
+
|
45
|
+
def add(record, score = 1.0)
|
46
|
+
add!(record, score)
|
47
|
+
rescue Redis::CommandError
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def add!(record, score = 1.0)
|
52
|
+
client.call!(
|
53
|
+
"ADD", name, record.id, score, "REPLACE", "FIELDS",
|
54
|
+
Document::Converter.new(self, record).document.to_a
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_multiple!(records)
|
59
|
+
client.pipelined do
|
60
|
+
records.each do |record|
|
61
|
+
add!(record)
|
62
|
+
end
|
63
|
+
end.ok?
|
64
|
+
end
|
65
|
+
|
66
|
+
def del(record, delete_document: false)
|
67
|
+
client.call!("DEL", name, record.id, ("DD" if delete_document))
|
68
|
+
end
|
69
|
+
|
70
|
+
def exist?
|
71
|
+
!client.call!("INFO", name).empty?
|
72
|
+
rescue Redis::CommandError
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
def info
|
77
|
+
hash = Hash[*client.call!("INFO", name)]
|
78
|
+
info_struct = Struct.new(*hash.keys.map(&:to_sym))
|
79
|
+
info_struct.new(*hash.values)
|
80
|
+
rescue Redis::CommandError
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def fields
|
85
|
+
@fields ||= schema.fields.map(&:to_s)
|
86
|
+
end
|
87
|
+
|
88
|
+
def reindex(docs)
|
89
|
+
drop if exist?
|
90
|
+
create
|
91
|
+
add_multiple! docs
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def client
|
97
|
+
RediSearch.client
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RediSearch
|
4
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
5
|
+
def self.runtime=(value)
|
6
|
+
Thread.current[:searchkick_runtime] = value
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.runtime
|
10
|
+
Thread.current[:searchkick_runtime] ||= 0
|
11
|
+
end
|
12
|
+
|
13
|
+
#:nocov:
|
14
|
+
def self.reset_runtime
|
15
|
+
rt = runtime
|
16
|
+
self.runtime = 0
|
17
|
+
rt
|
18
|
+
end
|
19
|
+
#:nocov:
|
20
|
+
|
21
|
+
def search(event)
|
22
|
+
log_command(event, YELLOW)
|
23
|
+
end
|
24
|
+
|
25
|
+
def create(event)
|
26
|
+
log_command(event, GREEN)
|
27
|
+
end
|
28
|
+
|
29
|
+
def drop(event)
|
30
|
+
log_command(event, RED)
|
31
|
+
end
|
32
|
+
|
33
|
+
def add(event)
|
34
|
+
log_command(event, GREEN)
|
35
|
+
end
|
36
|
+
|
37
|
+
def info(event)
|
38
|
+
log_command(event, CYAN)
|
39
|
+
end
|
40
|
+
|
41
|
+
def pipeline(event)
|
42
|
+
log_command(event, MAGENTA)
|
43
|
+
end
|
44
|
+
|
45
|
+
def get(event)
|
46
|
+
log_command(event, CYAN)
|
47
|
+
end
|
48
|
+
|
49
|
+
def mget(event)
|
50
|
+
log_command(event, CYAN)
|
51
|
+
end
|
52
|
+
|
53
|
+
def del(event)
|
54
|
+
log_command(event, RED)
|
55
|
+
end
|
56
|
+
|
57
|
+
def spellcheck(event)
|
58
|
+
log_command(event, YELLOW)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def log_command(event, debug_color)
|
64
|
+
self.class.runtime += event.duration
|
65
|
+
return unless logger.debug?
|
66
|
+
|
67
|
+
payload = event.payload
|
68
|
+
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
69
|
+
command = command_string(payload)
|
70
|
+
|
71
|
+
debug " #{color(name, RED, true)} #{color(command, debug_color, true)}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def command_string(payload)
|
75
|
+
payload[:query].flatten.map.with_index do |arg, i|
|
76
|
+
arg = "FT.#{arg}" if prepend_ft?(arg, i)
|
77
|
+
arg = arg.inspect if inspect_arg?(payload, arg)
|
78
|
+
arg
|
79
|
+
end.join(" ")
|
80
|
+
end
|
81
|
+
|
82
|
+
def multiword?(string)
|
83
|
+
!string.to_s.starts_with?(/\(-?@/) && string.to_s.split(/\s|\|/).size > 1
|
84
|
+
end
|
85
|
+
|
86
|
+
def prepend_ft?(arg, index)
|
87
|
+
index.zero? && !multiword?(arg)
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspect_arg?(payload, arg)
|
91
|
+
multiword?(arg) && payload[:query].flatten.count > 1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redi_search/index"
|
4
|
+
require "redi_search/document/converter"
|
5
|
+
|
6
|
+
require "active_support/concern"
|
7
|
+
|
8
|
+
module RediSearch
|
9
|
+
module Model
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
attr_reader :redi_search_index
|
14
|
+
|
15
|
+
def redi_search(**options) # rubocop:disable Metrics/MethodLength
|
16
|
+
@redi_search_index = Index.new(
|
17
|
+
options[:index_name] || "#{name.underscore}_idx",
|
18
|
+
options[:schema],
|
19
|
+
self
|
20
|
+
)
|
21
|
+
|
22
|
+
if respond_to? :after_commit
|
23
|
+
after_commit :redi_search_add_document, on: [:create, :update]
|
24
|
+
end
|
25
|
+
if respond_to? :after_destroy_commit
|
26
|
+
after_destroy_commit :redi_search_delete_document
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def search(term = nil, **term_options)
|
31
|
+
redi_search_index.search(term, **term_options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def reindex
|
35
|
+
redi_search_index.reindex(all)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def redi_search_document
|
42
|
+
Document::Converter.new(self.class.redi_search_index, self).document
|
43
|
+
end
|
44
|
+
|
45
|
+
def redi_search_delete_document
|
46
|
+
return unless self.class.redi_search_index.exist?
|
47
|
+
|
48
|
+
self.class.redi_search_index.del(self, delete_document: true)
|
49
|
+
end
|
50
|
+
|
51
|
+
def redi_search_add_document
|
52
|
+
return unless self.class.redi_search_index.exist?
|
53
|
+
|
54
|
+
self.class.redi_search_index.add(self)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RediSearch
|
4
|
+
class Result
|
5
|
+
class Collection < Array
|
6
|
+
def initialize(index, count, records)
|
7
|
+
@count = count
|
8
|
+
super(Hash[*records].map do |doc_id, fields|
|
9
|
+
Document.new(index, doc_id, Hash[*fields])
|
10
|
+
end)
|
11
|
+
end
|
12
|
+
|
13
|
+
def count
|
14
|
+
@count || super
|
15
|
+
end
|
16
|
+
|
17
|
+
def size
|
18
|
+
@count || super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RediSearch
|
4
|
+
class Schema
|
5
|
+
class Field
|
6
|
+
private
|
7
|
+
|
8
|
+
FALSES = [
|
9
|
+
nil, "", false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
def boolean_options_string
|
13
|
+
boolean_options.map do |option|
|
14
|
+
unless FALSES.include?(send(option))
|
15
|
+
option.to_s.upcase.split("_").join
|
16
|
+
end
|
17
|
+
end.compact
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redi_search/schema/field"
|
4
|
+
|
5
|
+
module RediSearch
|
6
|
+
class Schema
|
7
|
+
class GeoField < Field
|
8
|
+
def initialize(name, sortable: false, no_index: false)
|
9
|
+
@name = name
|
10
|
+
@sortable = sortable
|
11
|
+
@no_index = no_index
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_a
|
15
|
+
query = [name.to_s, "GEO"]
|
16
|
+
query += boolean_options_string
|
17
|
+
|
18
|
+
query
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :name, :sortable, :no_index
|
24
|
+
|
25
|
+
def boolean_options
|
26
|
+
%i(sortable no_index)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redi_search/schema/field"
|
4
|
+
|
5
|
+
module RediSearch
|
6
|
+
class Schema
|
7
|
+
class NumericField < Field
|
8
|
+
def initialize(name, sortable: false, no_index: false)
|
9
|
+
@name = name
|
10
|
+
@sortable = sortable
|
11
|
+
@no_index = no_index
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_a
|
15
|
+
query = [name.to_s, "NUMERIC"]
|
16
|
+
query += boolean_options_string
|
17
|
+
|
18
|
+
query
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :name, :sortable, :no_index
|
24
|
+
|
25
|
+
def boolean_options
|
26
|
+
%i(sortable no_index)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redi_search/schema/field"
|
4
|
+
|
5
|
+
module RediSearch
|
6
|
+
class Schema
|
7
|
+
class TagField < Field
|
8
|
+
def initialize(name, separator: ",", sortable: false, no_index: false)
|
9
|
+
@name = name
|
10
|
+
@separator = separator
|
11
|
+
@sortable = sortable
|
12
|
+
@no_index = no_index
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_a
|
16
|
+
query = [name.to_s, "TAG"]
|
17
|
+
query += boolean_options_string
|
18
|
+
query += ["SEPARATOR", separator] if separator
|
19
|
+
|
20
|
+
query
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :name, :separator, :sortable, :no_index
|
26
|
+
|
27
|
+
def boolean_options
|
28
|
+
%i(sortable no_index)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record/type"
|
4
|
+
|
5
|
+
module RediSearch
|
6
|
+
class Schema
|
7
|
+
class TextField < Field
|
8
|
+
def initialize(name, weight: 1.0, phonetic: nil, sortable: false,
|
9
|
+
no_index: false, no_stem: false)
|
10
|
+
@name = name
|
11
|
+
@weight = weight
|
12
|
+
@phonetic = phonetic
|
13
|
+
@sortable = sortable
|
14
|
+
@no_index = no_index
|
15
|
+
@no_stem = no_stem
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_a
|
19
|
+
query = [name.to_s, "TEXT"]
|
20
|
+
query += boolean_options_string
|
21
|
+
query += ["WEIGHT", weight] if weight
|
22
|
+
query += ["PHONETIC", phonetic] if phonetic
|
23
|
+
|
24
|
+
query
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :name, :weight, :phonetic, :sortable, :no_index, :no_stem
|
30
|
+
|
31
|
+
def boolean_options
|
32
|
+
%i(sortable no_index no_stem)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redi_search/schema/geo_field"
|
4
|
+
require "redi_search/schema/numeric_field"
|
5
|
+
require "redi_search/schema/tag_field"
|
6
|
+
require "redi_search/schema/text_field"
|
7
|
+
|
8
|
+
module RediSearch
|
9
|
+
class Schema
|
10
|
+
def initialize(raw)
|
11
|
+
@raw = raw
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_a
|
15
|
+
raw.map do |field_name, options|
|
16
|
+
options = [options] if options.is_a? Symbol
|
17
|
+
schema, options = options.to_a.flatten
|
18
|
+
|
19
|
+
"RediSearch::Schema::#{schema.to_s.capitalize}Field".
|
20
|
+
constantize.
|
21
|
+
new(field_name, **options.to_h).
|
22
|
+
to_a
|
23
|
+
end.flatten
|
24
|
+
end
|
25
|
+
|
26
|
+
def fields
|
27
|
+
raw.keys
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :raw
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RediSearch
|
4
|
+
class Search
|
5
|
+
class BooleanClause
|
6
|
+
def initialize(search, term, prior_clause = nil, **term_options)
|
7
|
+
@search = search
|
8
|
+
@prior_clause = prior_clause
|
9
|
+
@not = false
|
10
|
+
|
11
|
+
initialize_term(term, **term_options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
raise ArgumentError, "missing query terms" if term.blank?
|
16
|
+
|
17
|
+
[
|
18
|
+
prior_clause.presence,
|
19
|
+
queryify_term.dup.prepend(not_operator)
|
20
|
+
].compact.join(operand)
|
21
|
+
end
|
22
|
+
|
23
|
+
def inspect
|
24
|
+
to_s.inspect
|
25
|
+
end
|
26
|
+
|
27
|
+
def not(term, **term_options)
|
28
|
+
@not = true
|
29
|
+
|
30
|
+
initialize_term(term, **term_options)
|
31
|
+
|
32
|
+
search
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_reader :prior_clause, :term, :search
|
38
|
+
|
39
|
+
def operand
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
def not_operator
|
44
|
+
return "" unless @not
|
45
|
+
|
46
|
+
"-"
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize_term(term, **term_options)
|
50
|
+
return if term.blank?
|
51
|
+
|
52
|
+
@term =
|
53
|
+
if term.is_a? RediSearch::Search
|
54
|
+
term
|
55
|
+
else
|
56
|
+
Term.new(term, term_options)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def queryify_term
|
61
|
+
if term.is_a?(RediSearch::Search) &&
|
62
|
+
!term.term_clause.is_a?(RediSearch::Search::WhereClause)
|
63
|
+
"(#{term.term_clause})"
|
64
|
+
elsif term.is_a?(RediSearch::Search)
|
65
|
+
term.term_clause
|
66
|
+
else
|
67
|
+
term
|
68
|
+
end.to_s
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redi_search/search/term"
|
4
|
+
require "redi_search/search/and_clause"
|
5
|
+
require "redi_search/search/or_clause"
|
6
|
+
require "redi_search/search/where_clause"
|
7
|
+
|
8
|
+
module RediSearch
|
9
|
+
class Search
|
10
|
+
module Clauses
|
11
|
+
def highlight(**options)
|
12
|
+
clauses.push(*HighlightClause.new(**options).clause)
|
13
|
+
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def slop(slop)
|
18
|
+
clauses.push("SLOP", slop)
|
19
|
+
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def in_order
|
24
|
+
clauses.push("INORDER")
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def no_content
|
30
|
+
@no_content = true
|
31
|
+
clauses.push("NOCONTENT")
|
32
|
+
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def language(language)
|
37
|
+
clauses.push("LANGUAGE", language)
|
38
|
+
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def sort_by(field, order: :asc)
|
43
|
+
raise ArgumentError unless %i(asc desc).include?(order.to_sym)
|
44
|
+
|
45
|
+
clauses.push("SORTBY", field, order)
|
46
|
+
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def limit(num, offset = 0)
|
51
|
+
clauses.push("LIMIT", offset, num)
|
52
|
+
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def where(**condition)
|
57
|
+
@term_clause = WhereClause.new(self, condition, @term_clause)
|
58
|
+
|
59
|
+
if condition.blank?
|
60
|
+
@term_clause
|
61
|
+
else
|
62
|
+
self
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def and(new_term = nil, **term_options)
|
67
|
+
@term_clause =
|
68
|
+
AndClause.new(self, new_term, @term_clause, **term_options)
|
69
|
+
|
70
|
+
if new_term.blank?
|
71
|
+
@term_clause
|
72
|
+
else
|
73
|
+
self
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def or(new_term = nil, **term_options)
|
78
|
+
@term_clause =
|
79
|
+
OrClause.new(self, new_term, @term_clause, **term_options)
|
80
|
+
|
81
|
+
if new_term.blank?
|
82
|
+
@term_clause
|
83
|
+
else
|
84
|
+
self
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RediSearch
|
4
|
+
class Search
|
5
|
+
class HighlightClause
|
6
|
+
def initialize(**options)
|
7
|
+
@options = options.to_h
|
8
|
+
|
9
|
+
parse_options
|
10
|
+
end
|
11
|
+
|
12
|
+
def clause
|
13
|
+
[
|
14
|
+
"HIGHLIGHT",
|
15
|
+
(fields_clause(**options[:fields]) if options.key?(:fields)),
|
16
|
+
(tags_clause(**options[:tags]) if options.key? :tags),
|
17
|
+
].compact.flatten(1)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :options, :tags_args, :fields_args
|
23
|
+
|
24
|
+
def parse_options
|
25
|
+
return if options.except(:fields, :tags).empty?
|
26
|
+
|
27
|
+
arg_error "Unsupported argument: #{options}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def tags_clause(open:, close:)
|
31
|
+
["TAGS", open, close]
|
32
|
+
end
|
33
|
+
|
34
|
+
def fields_clause(num:, field:)
|
35
|
+
["FIELDS", num, field]
|
36
|
+
end
|
37
|
+
|
38
|
+
def arg_error(msg)
|
39
|
+
raise ArgumentError, "Highlight: #{msg}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redi_search/search/boolean_clause"
|
4
|
+
|
5
|
+
module RediSearch
|
6
|
+
class Search
|
7
|
+
class OrClause < BooleanClause
|
8
|
+
def where(**condition)
|
9
|
+
@term = search.dup.where(condition)
|
10
|
+
|
11
|
+
search
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def operand
|
17
|
+
"|"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|