redi_search 0.1.0 → 1.0.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/README.md +516 -112
- data/lib/redi_search.rb +5 -2
- data/lib/redi_search/add.rb +70 -0
- data/lib/redi_search/alter.rb +30 -0
- data/lib/redi_search/create.rb +53 -0
- data/lib/redi_search/document.rb +71 -16
- data/lib/redi_search/index.rb +31 -26
- data/lib/redi_search/lazily_load.rb +65 -0
- data/lib/redi_search/log_subscriber.rb +4 -0
- data/lib/redi_search/model.rb +41 -18
- data/lib/redi_search/schema.rb +17 -8
- data/lib/redi_search/schema/text_field.rb +0 -2
- data/lib/redi_search/search.rb +22 -44
- data/lib/redi_search/search/clauses.rb +60 -31
- data/lib/redi_search/search/clauses/and.rb +17 -0
- data/lib/redi_search/search/clauses/application_clause.rb +18 -0
- data/lib/redi_search/search/clauses/boolean.rb +72 -0
- data/lib/redi_search/search/clauses/highlight.rb +47 -0
- data/lib/redi_search/search/clauses/in_order.rb +17 -0
- data/lib/redi_search/search/clauses/language.rb +23 -0
- data/lib/redi_search/search/clauses/limit.rb +27 -0
- data/lib/redi_search/search/clauses/no_content.rb +17 -0
- data/lib/redi_search/search/clauses/no_stop_words.rb +17 -0
- data/lib/redi_search/search/clauses/or.rb +23 -0
- data/lib/redi_search/search/clauses/return.rb +23 -0
- data/lib/redi_search/search/clauses/slop.rb +23 -0
- data/lib/redi_search/search/clauses/sort_by.rb +25 -0
- data/lib/redi_search/search/clauses/verbatim.rb +17 -0
- data/lib/redi_search/search/clauses/where.rb +66 -0
- data/lib/redi_search/search/clauses/with_scores.rb +17 -0
- data/lib/redi_search/search/result.rb +46 -0
- data/lib/redi_search/search/term.rb +4 -4
- data/lib/redi_search/spellcheck.rb +30 -29
- data/lib/redi_search/spellcheck/result.rb +44 -0
- data/lib/redi_search/version.rb +1 -1
- metadata +101 -31
- data/.gitignore +0 -11
- data/.rubocop.yml +0 -1757
- data/.travis.yml +0 -23
- data/Gemfile +0 -17
- data/Rakefile +0 -12
- data/bin/console +0 -8
- data/bin/publish +0 -58
- data/bin/setup +0 -8
- data/bin/test +0 -7
- data/lib/redi_search/document/converter.rb +0 -26
- data/lib/redi_search/error.rb +0 -6
- data/lib/redi_search/result/collection.rb +0 -22
- data/lib/redi_search/search/and_clause.rb +0 -15
- data/lib/redi_search/search/boolean_clause.rb +0 -72
- data/lib/redi_search/search/highlight_clause.rb +0 -43
- data/lib/redi_search/search/or_clause.rb +0 -21
- data/lib/redi_search/search/where_clause.rb +0 -66
- data/redi_search.gemspec +0 -48
data/lib/redi_search.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "redis"
|
4
4
|
require "active_support"
|
5
|
+
require "active_model"
|
5
6
|
require "active_support/core_ext/object"
|
6
7
|
require "active_support/core_ext/module/delegation"
|
7
8
|
|
@@ -28,8 +29,10 @@ module RediSearch
|
|
28
29
|
yield(configuration)
|
29
30
|
end
|
30
31
|
|
31
|
-
|
32
|
-
|
32
|
+
delegate :client, to: :configuration
|
33
|
+
|
34
|
+
def env
|
35
|
+
@env ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
33
36
|
end
|
34
37
|
end
|
35
38
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RediSearch
|
4
|
+
class Add
|
5
|
+
include ActiveModel::Validations
|
6
|
+
|
7
|
+
validates :score, numericality: {
|
8
|
+
greater_than_or_equal_to: 0.0, less_than_or_equal_to: 1.0
|
9
|
+
}
|
10
|
+
|
11
|
+
def initialize(index, document, score: 1.0, replace: {}, language: nil,
|
12
|
+
no_save: false)
|
13
|
+
@index = index
|
14
|
+
@document = document
|
15
|
+
@score = score || 1.0
|
16
|
+
@replace = replace
|
17
|
+
@language = language
|
18
|
+
@no_save = no_save
|
19
|
+
end
|
20
|
+
|
21
|
+
def call!
|
22
|
+
validate!
|
23
|
+
|
24
|
+
RediSearch.client.call!(*command).ok?
|
25
|
+
end
|
26
|
+
|
27
|
+
def call
|
28
|
+
call!
|
29
|
+
rescue Redis::CommandError
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :index, :document, :score, :replace, :language, :no_save
|
36
|
+
|
37
|
+
def command
|
38
|
+
[
|
39
|
+
"ADD",
|
40
|
+
index.name,
|
41
|
+
document.document_id,
|
42
|
+
score,
|
43
|
+
*extract_options,
|
44
|
+
"FIELDS",
|
45
|
+
document.redis_attributes
|
46
|
+
].compact
|
47
|
+
end
|
48
|
+
|
49
|
+
def extract_options
|
50
|
+
opts = []
|
51
|
+
opts << ["LANGUAGE", language] if language
|
52
|
+
opts << "NOSAVE" if no_save
|
53
|
+
opts << replace_options if replace?
|
54
|
+
opts
|
55
|
+
end
|
56
|
+
|
57
|
+
def replace?
|
58
|
+
replace.present?
|
59
|
+
end
|
60
|
+
|
61
|
+
def replace_options
|
62
|
+
["REPLACE"].tap do |replace_option|
|
63
|
+
if replace.is_a?(Hash)
|
64
|
+
replace_option << "PARTIAL" if replace[:partial]
|
65
|
+
# replace_option << "NOCREATE" if replace[:no_create]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RediSearch
|
4
|
+
class Alter
|
5
|
+
def initialize(index, field_name, schema)
|
6
|
+
@index = index
|
7
|
+
@field_name = field_name
|
8
|
+
@raw_schema = schema
|
9
|
+
end
|
10
|
+
|
11
|
+
def call!
|
12
|
+
index.schema.alter(field_name, raw_schema)
|
13
|
+
RediSearch.client.call!(
|
14
|
+
"ALTER",
|
15
|
+
index.name,
|
16
|
+
"SCHEMA",
|
17
|
+
"ADD",
|
18
|
+
*field_schema
|
19
|
+
).ok?
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :index, :field_name, :raw_schema
|
25
|
+
|
26
|
+
def field_schema
|
27
|
+
@field_schema ||= Schema.make_field(field_name, raw_schema)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RediSearch
|
4
|
+
class Create
|
5
|
+
OPTION_MAPPER = {
|
6
|
+
max_text_fields: "MAXTEXTFIELDS",
|
7
|
+
no_offsets: "NOOFFSETS",
|
8
|
+
no_highlight: "NOHL",
|
9
|
+
no_fields: "NOFIELDS",
|
10
|
+
no_frequencies: "NOFREQS"
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
def initialize(index, schema, options)
|
14
|
+
@index = index
|
15
|
+
@schema = schema
|
16
|
+
@options = options
|
17
|
+
end
|
18
|
+
|
19
|
+
def call!
|
20
|
+
RediSearch.client.call!(
|
21
|
+
"CREATE",
|
22
|
+
index.name,
|
23
|
+
*extract_options.compact,
|
24
|
+
"SCHEMA",
|
25
|
+
schema.to_a
|
26
|
+
).ok?
|
27
|
+
end
|
28
|
+
|
29
|
+
def call
|
30
|
+
call!
|
31
|
+
rescue Redis::CommandError
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_reader :index, :schema, :options
|
38
|
+
|
39
|
+
def extract_options
|
40
|
+
options.map do |clause, switch|
|
41
|
+
next unless OPTION_MAPPER.key?(clause.to_sym) && switch
|
42
|
+
|
43
|
+
OPTION_MAPPER[clause.to_sym]
|
44
|
+
end << temporary_option
|
45
|
+
end
|
46
|
+
|
47
|
+
def temporary_option
|
48
|
+
return [] unless options[:temporary]
|
49
|
+
|
50
|
+
["TEMPORARY", options[:temporary]]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/redi_search/document.rb
CHANGED
@@ -3,8 +3,22 @@
|
|
3
3
|
module RediSearch
|
4
4
|
class Document
|
5
5
|
class << self
|
6
|
+
def for_object(index, record, serializer: nil, only: [])
|
7
|
+
object_to_serialize = serializer&.new(record) || record
|
8
|
+
|
9
|
+
field_values = index.schema.fields.map do |field|
|
10
|
+
next if only.present? && !only.include?(field.to_sym)
|
11
|
+
|
12
|
+
[field.to_s, object_to_serialize.public_send(field)]
|
13
|
+
end.compact.to_h
|
14
|
+
|
15
|
+
new(index, object_to_serialize.id, field_values)
|
16
|
+
end
|
17
|
+
|
6
18
|
def get(index, document_id)
|
7
|
-
response = RediSearch.client.call!(
|
19
|
+
response = RediSearch.client.call!(
|
20
|
+
"GET", index.name, prepend_document_id(index, document_id)
|
21
|
+
)
|
8
22
|
|
9
23
|
return if response.blank?
|
10
24
|
|
@@ -12,41 +26,62 @@ module RediSearch
|
|
12
26
|
end
|
13
27
|
|
14
28
|
def mget(index, *document_ids)
|
29
|
+
unique_document_ids = document_ids.map do |id|
|
30
|
+
prepend_document_id(index, id)
|
31
|
+
end
|
15
32
|
document_ids.zip(
|
16
|
-
RediSearch.client.call!("MGET", index.name, *
|
33
|
+
RediSearch.client.call!("MGET", index.name, *unique_document_ids)
|
17
34
|
).map do |document|
|
18
35
|
next if document[1].blank?
|
19
36
|
|
20
37
|
new(index, document[0], Hash[*document[1]])
|
21
38
|
end.compact
|
22
39
|
end
|
40
|
+
|
41
|
+
def prepend_document_id(index, document_id)
|
42
|
+
if document_id.to_s.starts_with? index.name
|
43
|
+
document_id
|
44
|
+
else
|
45
|
+
"#{index.name}#{document_id}"
|
46
|
+
end
|
47
|
+
end
|
23
48
|
end
|
24
49
|
|
25
|
-
attr_reader :
|
50
|
+
attr_reader :attributes, :score
|
26
51
|
|
27
|
-
def initialize(index, document_id, fields)
|
52
|
+
def initialize(index, document_id, fields, score = nil)
|
28
53
|
@index = index
|
29
54
|
@document_id = document_id
|
30
|
-
@
|
55
|
+
@attributes = fields
|
56
|
+
@score = score
|
31
57
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
end
|
58
|
+
attributes.each do |field, value|
|
59
|
+
next unless schema_fields.include? field
|
60
|
+
|
61
|
+
instance_variable_set(:"@#{field}", value)
|
62
|
+
define_singleton_method(field) { value }
|
38
63
|
end
|
39
64
|
end
|
40
65
|
|
41
|
-
def del
|
42
|
-
client.call!(
|
66
|
+
def del(delete_document: false)
|
67
|
+
client.call!(
|
68
|
+
"DEL", index.name, document_id, ("DD" if delete_document)
|
69
|
+
) == 1
|
43
70
|
end
|
44
71
|
|
45
72
|
#:nocov:
|
73
|
+
def inspect
|
74
|
+
inspection = pretty_print_attributes.map do |field_name|
|
75
|
+
"#{field_name}: #{public_send(field_name)}"
|
76
|
+
end.compact.join(", ")
|
77
|
+
|
78
|
+
"#<#{self.class} #{inspection}>"
|
79
|
+
end
|
80
|
+
|
46
81
|
def pretty_print(printer) # rubocop:disable Metrics/MethodLength
|
47
82
|
printer.object_address_group(self) do
|
48
83
|
printer.seplist(
|
49
|
-
|
84
|
+
pretty_print_attributes , proc { printer.text "," }
|
50
85
|
) do |field_name|
|
51
86
|
printer.breakable " "
|
52
87
|
printer.group(1) do
|
@@ -58,14 +93,34 @@ module RediSearch
|
|
58
93
|
end
|
59
94
|
end
|
60
95
|
end
|
96
|
+
|
97
|
+
def pretty_print_attributes
|
98
|
+
pp_attrs = attributes.keys.dup
|
99
|
+
pp_attrs.push("document_id")
|
100
|
+
pp_attrs.push("score") if score.present?
|
101
|
+
|
102
|
+
pp_attrs.compact
|
103
|
+
end
|
61
104
|
#:nocov:
|
62
105
|
|
63
106
|
def schema_fields
|
64
107
|
@schema_fields ||= index.schema.fields.map(&:to_s)
|
65
108
|
end
|
66
109
|
|
67
|
-
def
|
68
|
-
|
110
|
+
def redis_attributes
|
111
|
+
attributes.to_a.flatten
|
112
|
+
end
|
113
|
+
|
114
|
+
def document_id
|
115
|
+
self.class.prepend_document_id(index, @document_id)
|
116
|
+
end
|
117
|
+
|
118
|
+
def document_id_without_index
|
119
|
+
if @document_id.to_s.starts_with? index.name
|
120
|
+
@document_id.gsub(index.name, "")
|
121
|
+
else
|
122
|
+
@document_id
|
123
|
+
end
|
69
124
|
end
|
70
125
|
|
71
126
|
private
|
data/lib/redi_search/index.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "redi_search/add"
|
4
|
+
require "redi_search/create"
|
3
5
|
require "redi_search/schema"
|
4
6
|
require "redi_search/search"
|
5
7
|
require "redi_search/spellcheck"
|
8
|
+
require "redi_search/alter"
|
6
9
|
|
7
10
|
module RediSearch
|
8
11
|
class Index
|
@@ -15,21 +18,19 @@ module RediSearch
|
|
15
18
|
end
|
16
19
|
|
17
20
|
def search(term = nil, **term_options)
|
18
|
-
Search.new(self, term,
|
21
|
+
Search.new(self, term, **term_options)
|
19
22
|
end
|
20
23
|
|
21
24
|
def spellcheck(query, distance: 1)
|
22
25
|
Spellcheck.new(self, query, distance: distance)
|
23
26
|
end
|
24
27
|
|
25
|
-
def create
|
26
|
-
|
27
|
-
rescue Redis::CommandError
|
28
|
-
false
|
28
|
+
def create(**options)
|
29
|
+
Create.new(self, schema, options).call
|
29
30
|
end
|
30
31
|
|
31
|
-
def create!
|
32
|
-
|
32
|
+
def create!(**options)
|
33
|
+
Create.new(self, schema, options).call!
|
33
34
|
end
|
34
35
|
|
35
36
|
def drop
|
@@ -42,29 +43,24 @@ module RediSearch
|
|
42
43
|
client.call!("DROP", name).ok?
|
43
44
|
end
|
44
45
|
|
45
|
-
def add(
|
46
|
-
|
47
|
-
rescue Redis::CommandError
|
48
|
-
false
|
46
|
+
def add(document, **options)
|
47
|
+
Add.new(self, document, **options).call
|
49
48
|
end
|
50
49
|
|
51
|
-
def add!(
|
52
|
-
|
53
|
-
"ADD", name, record.id, score, "REPLACE", "FIELDS",
|
54
|
-
Document::Converter.new(self, record).document.to_a
|
55
|
-
)
|
50
|
+
def add!(document, **options)
|
51
|
+
Add.new(self, document, **options).call!
|
56
52
|
end
|
57
53
|
|
58
|
-
def add_multiple!(
|
54
|
+
def add_multiple!(documents, **options)
|
59
55
|
client.pipelined do
|
60
|
-
|
61
|
-
add!(
|
56
|
+
documents.each do |document|
|
57
|
+
add!(document, **options)
|
62
58
|
end
|
63
59
|
end.ok?
|
64
60
|
end
|
65
61
|
|
66
|
-
def del(
|
67
|
-
|
62
|
+
def del(document, delete_document: false)
|
63
|
+
document.del(delete_document: delete_document)
|
68
64
|
end
|
69
65
|
|
70
66
|
def exist?
|
@@ -82,13 +78,22 @@ module RediSearch
|
|
82
78
|
end
|
83
79
|
|
84
80
|
def fields
|
85
|
-
|
81
|
+
schema.fields.map(&:to_s)
|
82
|
+
end
|
83
|
+
|
84
|
+
def reindex(documents, recreate: false, **options)
|
85
|
+
drop if recreate
|
86
|
+
create unless exist?
|
87
|
+
|
88
|
+
add_multiple! documents, **options
|
89
|
+
end
|
90
|
+
|
91
|
+
def document_count
|
92
|
+
info["num_docs"].to_i
|
86
93
|
end
|
87
94
|
|
88
|
-
def
|
89
|
-
|
90
|
-
create
|
91
|
-
add_multiple! docs
|
95
|
+
def alter(field_name, schema)
|
96
|
+
Alter.new(self, field_name, schema).call!
|
92
97
|
end
|
93
98
|
|
94
99
|
private
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RediSearch
|
4
|
+
module LazilyLoad
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
included do
|
10
|
+
delegate :size, :each, to: :to_a
|
11
|
+
end
|
12
|
+
|
13
|
+
def loaded?
|
14
|
+
@loaded = false unless defined? @loaded
|
15
|
+
|
16
|
+
@loaded
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_a
|
20
|
+
execute unless loaded?
|
21
|
+
|
22
|
+
@documents
|
23
|
+
end
|
24
|
+
|
25
|
+
alias load to_a
|
26
|
+
|
27
|
+
#:nocov:
|
28
|
+
def inspect
|
29
|
+
execute unless loaded?
|
30
|
+
|
31
|
+
to_a
|
32
|
+
end
|
33
|
+
|
34
|
+
def pretty_print(printer)
|
35
|
+
execute unless loaded?
|
36
|
+
|
37
|
+
printer.pp(documents)
|
38
|
+
rescue Redis::CommandError => e
|
39
|
+
printer.pp(e.message)
|
40
|
+
end
|
41
|
+
#:nocov:
|
42
|
+
|
43
|
+
def count
|
44
|
+
to_a.size
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def command
|
50
|
+
raise NotImplementedError, "included class did not define #{__method__}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def execute
|
54
|
+
@loaded = true
|
55
|
+
|
56
|
+
RediSearch.client.call!(*command).yield_self do |response|
|
57
|
+
parse_response(response)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_response(_response)
|
62
|
+
raise NotImplementedError, "included class did not define #{__method__}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|