redi_search 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|