redi_search 2.0.0 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/lint.yml +22 -0
- data/.github/workflows/tests.yml +49 -0
- data/.rubocop.yml +137 -37
- data/Appraisals +4 -4
- data/Gemfile +3 -2
- data/README.md +21 -35
- data/Rakefile +1 -0
- data/bin/console +3 -6
- data/gemfiles/{activerecord_51.gemfile → activerecord_60.gemfile} +4 -2
- data/gemfiles/{activerecord_52.gemfile → activerecord_61.gemfile} +4 -2
- data/lib/redi_search.rb +2 -0
- data/lib/redi_search/add_field.rb +1 -1
- data/lib/redi_search/client.rb +16 -6
- data/lib/redi_search/client/response.rb +1 -1
- data/lib/redi_search/create.rb +2 -1
- data/lib/redi_search/document.rb +10 -15
- data/lib/redi_search/document/finder.rb +12 -50
- data/lib/redi_search/hset.rb +28 -0
- data/lib/redi_search/index.rb +19 -17
- data/lib/redi_search/log_subscriber.rb +7 -6
- data/lib/redi_search/model.rb +4 -10
- data/lib/redi_search/schema.rb +10 -5
- data/lib/redi_search/schema/field.rb +10 -3
- data/lib/redi_search/schema/geo_field.rb +1 -1
- data/lib/redi_search/schema/numeric_field.rb +1 -1
- data/lib/redi_search/schema/tag_field.rb +5 -1
- data/lib/redi_search/schema/text_field.rb +1 -1
- data/lib/redi_search/search.rb +3 -0
- data/lib/redi_search/search/clauses/boolean.rb +4 -4
- data/lib/redi_search/search/clauses/or.rb +1 -1
- data/lib/redi_search/version.rb +1 -1
- data/redi_search.gemspec +3 -3
- metadata +17 -16
- data/.travis.yml +0 -63
- data/lib/redi_search/add.rb +0 -67
data/Gemfile
CHANGED
@@ -9,10 +9,11 @@ gemspec
|
|
9
9
|
gem "appraisal", "~> 2.2"
|
10
10
|
gem "faker"
|
11
11
|
gem "mocha"
|
12
|
-
gem "pry"
|
13
12
|
gem "rubocop"
|
13
|
+
gem "rubocop-minitest"
|
14
14
|
gem "rubocop-performance"
|
15
|
+
gem "rubocop-rake"
|
15
16
|
gem "simplecov"
|
16
17
|
gem "sqlite3"
|
17
18
|
|
18
|
-
gem "activerecord", "6.
|
19
|
+
gem "activerecord", "~> 6.1"
|
data/README.md
CHANGED
@@ -6,8 +6,7 @@
|
|
6
6
|
|
7
7
|
# RediSearch
|
8
8
|
|
9
|
-
|
10
|
-
[![Test Coverage](https://api.codeclimate.com/v1/badges/c6437acac5684de2549d/test_coverage)](https://codeclimate.com/github/npezza93/redi_search/test_coverage)
|
9
|
+
![Build Status](https://github.com/npezza93/redi_search/workflows/tests/badge.svg)
|
11
10
|
[![Maintainability](https://api.codeclimate.com/v1/badges/c6437acac5684de2549d/maintainability)](https://codeclimate.com/github/npezza93/redi_search/maintainability)
|
12
11
|
|
13
12
|
A simple, but powerful, Ruby wrapper around RediSearch, a search engine on top of
|
@@ -244,13 +243,11 @@ With no options: `{ place: :geo }`
|
|
244
243
|
|
245
244
|
## Document
|
246
245
|
|
247
|
-
A `Document` is the Ruby representation of a
|
246
|
+
A `Document` is the Ruby representation of a Redis hash.
|
248
247
|
|
249
|
-
You can fetch a `Document` using `.get`
|
248
|
+
You can fetch a `Document` using `.get` class methods.
|
250
249
|
- `get(index, document_id)` fetches a single `Document` in an `Index` for a
|
251
250
|
given `document_id`.
|
252
|
-
- `mget(index, *document_ids)` fetches a collection of `Document`s
|
253
|
-
in an `Index` for the given `document_ids`.
|
254
251
|
|
255
252
|
You can also make a `Document` instance using the
|
256
253
|
`.for_object(index, record, serializer: nil, only: [])` class method. It takes
|
@@ -269,10 +266,7 @@ to override each other. There is also a `#document_id_without_index` method
|
|
269
266
|
which removes the prepended index name.
|
270
267
|
|
271
268
|
Finally there is a `#del` method that will remove the `Document` from the
|
272
|
-
`Index`.
|
273
|
-
whether the `Document` should be completely removed from the Redis instance vs
|
274
|
-
just the `Index`.
|
275
|
-
|
269
|
+
`Index`.
|
276
270
|
|
277
271
|
## Index
|
278
272
|
|
@@ -314,42 +308,32 @@ RediSearch::Index.new(name_of_index, schema)
|
|
314
308
|
- If set, we avoid saving the term frequencies in the index. This saves
|
315
309
|
memory but does not allow sorting based on the frequencies of a given
|
316
310
|
term within the document.
|
317
|
-
- `drop`
|
311
|
+
- `drop(keep_docs: false)`
|
318
312
|
- Drops the `Index` from the Redis instance, returns a boolean. Has an
|
319
313
|
accompanying bang method that will raise an exception upon failure. Will
|
320
|
-
return `false` if the `Index` has already been dropped.
|
314
|
+
return `false` if the `Index` has already been dropped. Takes an option
|
315
|
+
keyword arg, `keep_docs`, that will by default remove all the document
|
316
|
+
hashes in Redis.
|
321
317
|
- `exist?`
|
322
318
|
- Returns a boolean signifying `Index` existence.
|
323
319
|
- `info`
|
324
320
|
- Returns a struct object with all the information about the `Index`.
|
325
321
|
- `fields`
|
326
322
|
- Returns an array of the field names in the `Index`.
|
327
|
-
- `add(document
|
328
|
-
- Takes a `Document` object
|
323
|
+
- `add(document)`
|
324
|
+
- Takes a `Document` object. Has an
|
329
325
|
accompanying bang method that will raise an exception upon failure.
|
330
|
-
|
331
|
-
- `language` -> Use a stemmer for the supplied language during indexing.
|
332
|
-
- `no_save` -> Don't save the actual `Document` in the database and only index it.
|
333
|
-
- `replace` -> Accepts a boolean or a hash. If a truthy value is passed, we
|
334
|
-
will do an UPSERT style insertion - and delete an older version of the
|
335
|
-
`Document` if it exists.
|
336
|
-
- `replace: { partial: true }` -> Allows you to not have to specify all
|
337
|
-
fields for reindexing. Fields not given to the command will be loaded from
|
338
|
-
the current version of the `Document`.
|
339
|
-
- `add_multiple(documents, score: 1.0, replace: {}, language: nil, no_save: false)`
|
326
|
+
- `add_multiple(documents)`
|
340
327
|
- Takes an array of `Document` objects. This provides a more performant way to
|
341
328
|
add multiple documents to the `Index`. Accepts the same options as `add`.
|
342
|
-
- `del(document
|
343
|
-
- Removes a `Document` from the `Index`.
|
344
|
-
`Document` should be completely removed from the Redis instance vs just the
|
345
|
-
`Index`.
|
329
|
+
- `del(document)`
|
330
|
+
- Removes a `Document` from the `Index`.
|
346
331
|
- `document_count`
|
347
332
|
- Returns the number of `Document`s in the `Index`
|
348
333
|
- `add_field(field_name, schema)`
|
349
334
|
- Adds a new field to the `Index`. Ex: `index.add_field(:first_name, text: { phonetic: "dm:en" })`
|
350
|
-
- `reindex(documents, recreate: false
|
335
|
+
- `reindex(documents, recreate: false)`
|
351
336
|
- If `recreate` is `true` the `Index` will be dropped and recreated
|
352
|
-
- `options` accepts the same options as `add`
|
353
337
|
|
354
338
|
|
355
339
|
## Searching
|
@@ -521,11 +505,9 @@ end
|
|
521
505
|
This will automatically add `User.search` and `User.spellcheck`
|
522
506
|
methods which behave the same as if you called them on an `Index` instance.
|
523
507
|
|
524
|
-
`User.reindex(only: []
|
525
|
-
`RediSearch::Index#reindex`. Some of the differences include:
|
526
|
-
-
|
527
|
-
option `replace: { partial: true }`.
|
528
|
-
- `Document`s do not to be passed as the first parameter. The `search_import`
|
508
|
+
`User.reindex(recreate: false, only: [])` is also added and behaves
|
509
|
+
similarly to `RediSearch::Index#reindex`. Some of the differences include:
|
510
|
+
- `Document`s do not need to be passed as the first parameter. The `search_import`
|
529
511
|
scope is automatically called and all the records are converted
|
530
512
|
to `Document`s.
|
531
513
|
- Accepts an optional `only` parameter where you can specify a limited number
|
@@ -561,6 +543,10 @@ class User < ApplicationRecord
|
|
561
543
|
end
|
562
544
|
```
|
563
545
|
|
546
|
+
When searching, by default a collection of `Document`s is returned. Calling
|
547
|
+
`#results` on the search query will execute the search, and then look up all the
|
548
|
+
found records in the database and return an ActiveRecord relation.
|
549
|
+
|
564
550
|
The default `Index` name for model `Index`s is
|
565
551
|
`#{model_name.plural}_#{RediSearch.env}`. The `redi_search` method takes an
|
566
552
|
optional `index_prefix` argument which gets prepended to the index name:
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
@@ -5,7 +5,7 @@ require "bundler/setup"
|
|
5
5
|
require "redi_search"
|
6
6
|
|
7
7
|
require "faker"
|
8
|
-
require "
|
8
|
+
require "irb"
|
9
9
|
require "active_support/logger"
|
10
10
|
require "active_record"
|
11
11
|
|
@@ -35,10 +35,7 @@ def seed_users(count = 10_000)
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def reload!
|
38
|
-
|
39
|
-
files = $LOADED_FEATURES.select { |feat| feat =~ /\/redi_search\// }
|
40
|
-
files.each { |file| load file }
|
41
|
-
true
|
38
|
+
exec($0)
|
42
39
|
end
|
43
40
|
|
44
|
-
|
41
|
+
IRB.start
|
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
+
gem "appraisal", "~> 2.2"
|
6
|
+
gem "faker"
|
5
7
|
gem "mocha"
|
6
|
-
gem "pry"
|
7
8
|
gem "rubocop"
|
9
|
+
gem "rubocop-minitest"
|
8
10
|
gem "rubocop-performance"
|
9
11
|
gem "simplecov"
|
10
12
|
gem "sqlite3"
|
11
|
-
gem "activerecord", "<
|
13
|
+
gem "activerecord", "< 6.1", ">= 6.0"
|
12
14
|
|
13
15
|
gemspec path: "../"
|
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
+
gem "appraisal", "~> 2.2"
|
6
|
+
gem "faker"
|
5
7
|
gem "mocha"
|
6
|
-
gem "pry"
|
7
8
|
gem "rubocop"
|
9
|
+
gem "rubocop-minitest"
|
8
10
|
gem "rubocop-performance"
|
9
11
|
gem "simplecov"
|
10
12
|
gem "sqlite3"
|
11
|
-
gem "activerecord", "< 6.
|
13
|
+
gem "activerecord", "< 6.2", ">= 6.1"
|
12
14
|
|
13
15
|
gemspec path: "../"
|
data/lib/redi_search.rb
CHANGED
data/lib/redi_search/client.rb
CHANGED
@@ -9,10 +9,12 @@ module RediSearch
|
|
9
9
|
class Client
|
10
10
|
def initialize(redis = Redis.new)
|
11
11
|
@redis = redis
|
12
|
+
@pipeline = false
|
12
13
|
end
|
13
14
|
|
14
|
-
def call!(command, *params)
|
15
|
+
def call!(command, *params, skip_ft: false)
|
15
16
|
instrument(command.downcase, query: [command, params]) do
|
17
|
+
command = "FT.#{command}" unless skip_ft
|
16
18
|
send_command(command, *params)
|
17
19
|
end
|
18
20
|
end
|
@@ -20,24 +22,32 @@ module RediSearch
|
|
20
22
|
def multi
|
21
23
|
Response.new(redis.multi do
|
22
24
|
instrument("pipeline", query: ["begin pipeline"])
|
23
|
-
yield
|
25
|
+
capture_pipeline { yield }
|
24
26
|
instrument("pipeline", query: ["finish pipeline"])
|
25
27
|
end)
|
26
28
|
end
|
27
29
|
|
28
30
|
private
|
29
31
|
|
30
|
-
attr_reader
|
32
|
+
attr_reader :redis
|
33
|
+
attr_accessor :pipeline
|
34
|
+
|
35
|
+
def capture_pipeline
|
36
|
+
self.pipeline = true
|
37
|
+
yield
|
38
|
+
self.pipeline = false
|
39
|
+
end
|
31
40
|
|
32
41
|
def send_command(command, *params)
|
33
|
-
Response.new(redis.call(
|
42
|
+
Response.new(redis.call(command, *params))
|
34
43
|
end
|
35
44
|
|
36
45
|
def instrument(action, payload, &block)
|
37
46
|
ActiveSupport::Notifications.instrument(
|
38
47
|
"action.redi_search",
|
39
|
-
{ name: "RediSearch", action: action }.
|
40
|
-
|
48
|
+
{ name: "RediSearch", action: action, inside_pipeline: pipeline }.
|
49
|
+
merge(payload),
|
50
|
+
&Proc.new(&(block || proc {})) # rubocop:disable Lint/EmptyBlock
|
41
51
|
)
|
42
52
|
end
|
43
53
|
end
|
data/lib/redi_search/create.rb
CHANGED
@@ -31,7 +31,8 @@ module RediSearch
|
|
31
31
|
attr_reader :index, :schema, :options
|
32
32
|
|
33
33
|
def command
|
34
|
-
["CREATE", index.name,
|
34
|
+
["CREATE", index.name, "ON", "HASH", "PREFIX", 1, index.name,
|
35
|
+
*extract_options.compact, "SCHEMA", schema.to_a]
|
35
36
|
end
|
36
37
|
|
37
38
|
def extract_options
|
data/lib/redi_search/document.rb
CHANGED
@@ -11,8 +11,8 @@ module RediSearch
|
|
11
11
|
def for_object(index, record, serializer: nil, only: [])
|
12
12
|
object_to_serialize = serializer&.new(record) || record
|
13
13
|
|
14
|
-
field_values = index.schema.fields.map do |field|
|
15
|
-
next unless only.empty? || only.include?(field
|
14
|
+
field_values = index.schema.fields.map(&:name).map do |field|
|
15
|
+
next unless only.empty? || only.include?(field)
|
16
16
|
|
17
17
|
[field.to_s, object_to_serialize.public_send(field)]
|
18
18
|
end.compact.to_h
|
@@ -23,10 +23,6 @@ module RediSearch
|
|
23
23
|
def get(index, document_id)
|
24
24
|
Finder.new(index, document_id).find
|
25
25
|
end
|
26
|
-
|
27
|
-
def mget(index, *document_ids)
|
28
|
-
Finder.new(index, *document_ids).find
|
29
|
-
end
|
30
26
|
end
|
31
27
|
|
32
28
|
attr_reader :attributes, :score
|
@@ -40,17 +36,20 @@ module RediSearch
|
|
40
36
|
load_attributes
|
41
37
|
end
|
42
38
|
|
43
|
-
def del
|
44
|
-
|
45
|
-
call!(*command.compact).ok?
|
39
|
+
def del
|
40
|
+
RediSearch.client.call!("DEL", document_id, skip_ft: true).ok?
|
46
41
|
end
|
47
42
|
|
48
43
|
def schema_fields
|
49
|
-
@schema_fields ||= index.schema.fields.map
|
44
|
+
@schema_fields ||= index.schema.fields.map do |field|
|
45
|
+
field.name.to_s
|
46
|
+
end
|
50
47
|
end
|
51
48
|
|
52
49
|
def redis_attributes
|
53
|
-
attributes.
|
50
|
+
attributes.flat_map do |field, value|
|
51
|
+
[field, index.schema[field.to_sym].serialize(value)]
|
52
|
+
end
|
54
53
|
end
|
55
54
|
|
56
55
|
def document_id
|
@@ -73,10 +72,6 @@ module RediSearch
|
|
73
72
|
|
74
73
|
attr_reader :index
|
75
74
|
|
76
|
-
def call!(*command)
|
77
|
-
RediSearch.client.call!(*command)
|
78
|
-
end
|
79
|
-
|
80
75
|
def load_attributes
|
81
76
|
attributes.each do |field, value|
|
82
77
|
next unless schema_fields.include? field.to_s
|
@@ -3,75 +3,37 @@
|
|
3
3
|
module RediSearch
|
4
4
|
class Document
|
5
5
|
class Finder
|
6
|
-
def initialize(index,
|
6
|
+
def initialize(index, document_id)
|
7
7
|
@index = index
|
8
|
-
@
|
8
|
+
@document_id = document_id
|
9
9
|
end
|
10
10
|
|
11
11
|
def find
|
12
|
-
if
|
13
|
-
parse_multi_documents
|
14
|
-
else
|
15
|
-
parse_document(document_ids.first, response)
|
16
|
-
end
|
12
|
+
Document.new(index, document_id, Hash[*response]) if response?
|
17
13
|
end
|
18
14
|
|
19
15
|
private
|
20
16
|
|
21
|
-
attr_reader :index, :
|
17
|
+
attr_reader :index, :document_id
|
22
18
|
|
23
19
|
def response
|
24
|
-
@response ||= call!(
|
20
|
+
@response ||= call!("HGETALL", prepended_document_id)
|
25
21
|
end
|
26
22
|
|
27
23
|
def call!(*command)
|
28
|
-
RediSearch.client.call!(*command)
|
29
|
-
end
|
30
|
-
|
31
|
-
def get_command
|
32
|
-
if multi?
|
33
|
-
"MGET"
|
34
|
-
else
|
35
|
-
"GET"
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def multi?
|
40
|
-
document_ids.size > 1
|
41
|
-
end
|
42
|
-
|
43
|
-
def prepended_document_ids
|
44
|
-
document_ids.map do |document_id|
|
45
|
-
prepend_document_id(document_id)
|
46
|
-
end
|
24
|
+
RediSearch.client.call!(*command, skip_ft: true)
|
47
25
|
end
|
48
26
|
|
49
|
-
def
|
50
|
-
if
|
51
|
-
|
27
|
+
def prepended_document_id
|
28
|
+
if document_id.to_s.start_with? index.name
|
29
|
+
document_id
|
52
30
|
else
|
53
|
-
"#{index.name}#{
|
31
|
+
"#{index.name}#{document_id}"
|
54
32
|
end
|
55
33
|
end
|
56
34
|
|
57
|
-
def
|
58
|
-
|
59
|
-
parse_document(document_id, response[index])
|
60
|
-
end.compact
|
61
|
-
end
|
62
|
-
|
63
|
-
def parse_document(document_id, document_response)
|
64
|
-
return unless document_response?(document_response)
|
65
|
-
|
66
|
-
Document.new(index, document_id, Hash[*document_response])
|
67
|
-
end
|
68
|
-
|
69
|
-
def document_response?(document_response)
|
70
|
-
if document_response.respond_to?(:empty?)
|
71
|
-
!document_response.empty?
|
72
|
-
else
|
73
|
-
!document_response.nil?
|
74
|
-
end
|
35
|
+
def response?
|
36
|
+
!response.to_a.empty?
|
75
37
|
end
|
76
38
|
end
|
77
39
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RediSearch
|
4
|
+
class Hset
|
5
|
+
def initialize(index, document)
|
6
|
+
@index = index
|
7
|
+
@document = document
|
8
|
+
end
|
9
|
+
|
10
|
+
def call!
|
11
|
+
RediSearch.client.call!(*command, skip_ft: true)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
call!
|
16
|
+
rescue Redis::CommandError
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :index, :document
|
23
|
+
|
24
|
+
def command
|
25
|
+
["HSET", document.document_id, document.redis_attributes].compact
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|