redi_search 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rubocop.yml +1757 -0
  4. data/.travis.yml +23 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +17 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +220 -0
  9. data/Rakefile +12 -0
  10. data/bin/console +8 -0
  11. data/bin/publish +58 -0
  12. data/bin/setup +8 -0
  13. data/bin/test +7 -0
  14. data/lib/redi_search/client.rb +68 -0
  15. data/lib/redi_search/configuration.rb +17 -0
  16. data/lib/redi_search/document/converter.rb +26 -0
  17. data/lib/redi_search/document.rb +79 -0
  18. data/lib/redi_search/error.rb +6 -0
  19. data/lib/redi_search/index.rb +100 -0
  20. data/lib/redi_search/log_subscriber.rb +94 -0
  21. data/lib/redi_search/model.rb +57 -0
  22. data/lib/redi_search/result/collection.rb +22 -0
  23. data/lib/redi_search/schema/field.rb +21 -0
  24. data/lib/redi_search/schema/geo_field.rb +30 -0
  25. data/lib/redi_search/schema/numeric_field.rb +30 -0
  26. data/lib/redi_search/schema/tag_field.rb +32 -0
  27. data/lib/redi_search/schema/text_field.rb +36 -0
  28. data/lib/redi_search/schema.rb +34 -0
  29. data/lib/redi_search/search/and_clause.rb +15 -0
  30. data/lib/redi_search/search/boolean_clause.rb +72 -0
  31. data/lib/redi_search/search/clauses.rb +89 -0
  32. data/lib/redi_search/search/highlight_clause.rb +43 -0
  33. data/lib/redi_search/search/or_clause.rb +21 -0
  34. data/lib/redi_search/search/term.rb +72 -0
  35. data/lib/redi_search/search/where_clause.rb +66 -0
  36. data/lib/redi_search/search.rb +89 -0
  37. data/lib/redi_search/spellcheck.rb +53 -0
  38. data/lib/redi_search/version.rb +5 -0
  39. data/lib/redi_search.rb +40 -0
  40. data/redi_search.gemspec +48 -0
  41. metadata +141 -0
data/.travis.yml ADDED
@@ -0,0 +1,23 @@
1
+ ---
2
+ env:
3
+ global:
4
+ - CC_TEST_REPORTER_ID=fec34310f03fd2cc767a85fa23d5102f3dca67b9cc967a48d9940027731394e8
5
+ sudo: false
6
+ language: ruby
7
+ cache: bundler
8
+ rvm: 2.6.3
9
+ before_install: gem install bundler -v 1.17.3
10
+ services: docker
11
+ before_script:
12
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
13
+ - chmod +x ./cc-test-reporter
14
+ - ./cc-test-reporter before-build
15
+ before_install:
16
+ - docker run -d -p 6379:6379 redislabs/redisearch:latest --protected-mode no --loadmodule /usr/lib/redis/modules/redisearch.so
17
+ after_script:
18
+ - if [[ "$TRAVIS_TEST_RESULT" == 0 ]]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi
19
+ script: bundle exec $COMMAND
20
+ matrix:
21
+ include:
22
+ - env: COMMAND='rake test'
23
+ - env: COMMAND=rubocop
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at npezza93@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ gemspec
8
+
9
+ gem "faker"
10
+ gem "minitest", "~> 5.0"
11
+ gem "pry"
12
+ gem "pry-rails"
13
+ gem "rails", "~> 6.0.0.rc1"
14
+ gem "rubocop"
15
+ gem "rubocop-performance"
16
+ gem "simplecov"
17
+ gem "sqlite3"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Nick Pezza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # RediSearch
2
+
3
+ [![Build Status](https://travis-ci.com/npezza93/redi_search.svg?branch=master)](https://travis-ci.com/npezza93/redi_search)
4
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/c6437acac5684de2549d/test_coverage)](https://codeclimate.com/github/npezza93/redi_search/test_coverage)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/c6437acac5684de2549d/maintainability)](https://codeclimate.com/github/npezza93/redi_search/maintainability)
6
+
7
+ A simple, but powerful Ruby wrapper around RediSearch,
8
+ a search engine on top of Redis.
9
+
10
+ ## Installation
11
+
12
+ Firstly, Redis and RediSearch need to be installed.
13
+
14
+ You can download Redis from https://redis.io/download, and check out installation instructions [here](https://github.com/antirez/redis#installing-redis). Alternatively, on macOS or Linux you can install via Homebrew.
15
+
16
+ To install RediSearch:
17
+ 1. `git clone https://github.com/RedisLabsModules/RediSearch.git`
18
+ 1. `cd RediSearch`
19
+ 1. `mkdir build`
20
+ 1. `cd build`
21
+ 1. `cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo`
22
+ 1. `make`
23
+ 1. `redis-server --loadmodule ./redisearch.so or load the module in your redis.conf`
24
+
25
+ You can also checkout [here](https://oss.redislabs.com/redisearch/Quick_Start.html) for more detailed installation instructions. If you already have a redis-server running you can also update your redis.conf file to always load the redisearch module. (On macOS the redis.conf file can be found `/usr/local/etc/redis.conf`)
26
+
27
+
28
+ After Redis and RediSearch are up and running, add this line to your application's Gemfile:
29
+
30
+ ```ruby
31
+ gem 'redi_search'
32
+ ```
33
+
34
+ And then execute:
35
+ ```bash
36
+ ❯ bundle
37
+ ````
38
+
39
+ Or install it yourself as:
40
+ ```bash
41
+ ❯ gem install redi_search
42
+ ```
43
+
44
+ and require it:
45
+ ```ruby
46
+ require 'redi_search'
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ### Configuration
52
+ ```ruby
53
+ RediSearch.configure do |config|
54
+ config.redis_config = {
55
+ host: "127.0.0.1",
56
+ port: "6379"
57
+ }
58
+ end
59
+ ```
60
+
61
+ ### Index
62
+
63
+ All actions revolve around indexes. To instantiate one:
64
+ ```ruby
65
+ RediSearch::Index.new(name_of_index, schema)
66
+ ```
67
+ The name is a string identifying the index and the schema is the argument is a hash that defines all the fields in an index. Each field can be one of four types: geo, numeric, tag, or text.
68
+
69
+ #### Text field options
70
+ - *weight* (default: 1.0)
71
+ - Declares the importance of this field when calculating result accuracy. This is a multiplication factor.
72
+ - Ex: `{ name: { text: { weight: 2 } } }`
73
+ - *phonetic*
74
+ - Will perform phonetic matching on field in searches by default. The obligatory {matcher} argument specifies the phonetic algorithm and language used. The following matchers are supported:
75
+ - dm:en - Double Metaphone for English
76
+ - dm:fr - Double Metaphone for French
77
+ - dm:pt - Double Metaphone for Portuguese
78
+ - dm:es - Double Metaphone for Spanish
79
+ - Ex: `{ name: { text: { phonetic: 'dm:en' } } }`
80
+ - *sortable* (default: false)
81
+ - Allows the user to later sort the results by the value of this field (this adds memory overhead so do not declare it on large text fields).
82
+ - Ex: `{ name: { text: { sortable: true } } }`
83
+ - *no_index* (default: false)
84
+ - Field will not be indexed. This is useful in conjunction with `sortable`, to create fields whose update using PARTIAL will not cause full reindexing of the document. If a field has `no_index` and doesn't have `sortable`, it will just be ignored by the index.
85
+ - Ex: `{ name: { text: { no_index: true } } }`
86
+ - *no_stem* (default: false)
87
+ - Disable stemming when indexing its values. This may be ideal for things like proper names.
88
+ - Ex: `{ name: { text: { no_stem: true } } }`
89
+
90
+ #### Numeric field options
91
+ - *sortable* (default: false)
92
+ - Allows the user to later sort the results by the value of this field (this adds memory overhead so do not declare it on large text fields).
93
+ - Ex: `{ id: { numeric: { sortable: true } } }`
94
+ - *no_index* (default: false)
95
+ - Field will not be indexed. This is useful in conjunction with `sortable`, to create fields whose update using PARTIAL will not cause full reindexing of the document. If a field has `no_index` and doesn't have `sortable`, it will just be ignored by the index.
96
+ - Ex: `{ id: { numeric: { no_index: true } } }`
97
+
98
+ #### Tag field options
99
+ - *sortable* (default: false)
100
+ - Allows the user to later sort the results by the value of this field (this adds memory overhead so do not declare it on large text fields).
101
+ - Ex: `{ tag: { tag: { sortable: true } } }`
102
+ - *no_index* (default: false)
103
+ - Field will not be indexed. This is useful in conjunction with `sortable`, to create fields whose update using PARTIAL will not cause full reindexing of the document. If a field has `no_index` and doesn't have `sortable`, it will just be ignored by the index.
104
+ - Ex: `{ tag: { tag: { no_index: true } } }`
105
+ - *separator* (default: ",")
106
+ - Indicates how the text contained in the field is to be split into individual tags. The default is ,. The value must be a single character.
107
+ - Ex: `{ tag: { tag: { separator: ',' } } }`
108
+
109
+ #### Geo field options
110
+ - *sortable* (default: false)
111
+ - Allows the user to later sort the results by the value of this field (this adds memory overhead so do not declare it on large text fields).
112
+ - Ex: `{ place: { geo: { sortable: true } } }`
113
+ - *no_index* (default: false)
114
+ - Field will not be indexed. This is useful in conjunction with `sortable`, to create fields whose update using PARTIAL will not cause full reindexing of the document. If a field has `no_index` and doesn't have `sortable`, it will just be ignored by the index.
115
+ - Ex: `{ place: { geo: { no_index: true } } }`
116
+
117
+ Some of the commands that are available on an index are as follows:
118
+ - *create*
119
+ - creates the index in the Redis instance, returns a boolean. Has an accompanying bang method that will raise an exception upon failure.
120
+ - *drop*
121
+ - drops the index from the Redis instance, returns a boolean. Has an accompanying bang method that will raise an exception upon failure.
122
+ - *exist?*
123
+ - Returns a boolean signifying indexes existence.
124
+ - *info*
125
+ - Returns a hash with all the information for the index
126
+ - *fields*
127
+ - Returns an array of field names in the index
128
+ - *add*
129
+ - Takes an object as the first argument and a second argument that is a score (a value between 0.0 and 1.0). The object passed must respond to all the fields in the schema. Has an accompanying bang method that will raise an exception upon failure.
130
+ - *add_multiple!*
131
+ - Takes an array of objects that respond to all the fields in the schema. This provides a more performant way to add multiple documents to the index.
132
+
133
+ ### Searching
134
+
135
+ Searching is initiated off an `RediSearch::Index` object.
136
+ ```ruby
137
+ main ❯ index = RediSearch::Index.new("user_idx", name: { text: { phonetic: "dm:en" } })
138
+ main ❯ index.search("john")
139
+ RediSearch (1.1ms) FT.SEARCH user_idx `john`
140
+ => [#<RediSearch::Document:0x00007f862e241b78 first: "Gene", last: "Volkman", document_id: "10039">,
141
+ #<RediSearch::Document:0x00007f862e2417b8 first: "Jeannie", last: "Ledner", document_id: "9998">]
142
+ ```
143
+ - Simple phrase query - hello AND world
144
+ ```ruby
145
+ index.search("hello").and("world")
146
+ ```
147
+ - Exact phrase query - hello FOLLOWED BY world
148
+ ```ruby
149
+ index.search("hello world")
150
+ ```
151
+ - Union: documents containing either hello OR world
152
+ ```ruby
153
+ index.search("hello").or("world")
154
+ ```
155
+ - Not: documents containing hello but not world
156
+ ```ruby
157
+ index.search("hello").and.not("world")
158
+ ```
159
+
160
+ All terms support a few options that can be applied.
161
+
162
+ - Prefix Queries: match all terms starting with a prefix
163
+ ```ruby
164
+ index.search("hel", prefix: true)
165
+ index.search("hello worl", prefix: true)
166
+ index.search("hel", prefix: true).and("worl", prefix: true)
167
+ index.search("hello").and.not("worl", prefix: true)
168
+ ```
169
+
170
+ - Optional terms with higher priority to ones containing more matches
171
+ ```ruby
172
+ index.search("foo").and("bar", optional: true).and("baz", optional: true)
173
+ ```
174
+
175
+ - Fuzzy matches are performed based on Levenshtein distance (LD). The maximum Levenshtein distance supported is 3.
176
+ ```ruby
177
+ index.search("zuchini", fuzziness: 1)
178
+ ```
179
+
180
+ - Complex intersections and unions
181
+ ```ruby
182
+ # Intersection of unions
183
+ index.search(index.search("hello").or("halo")).and(index.search("world").or("werld"))
184
+ # Negation of union
185
+ index.search("hello").and.not(index.search("world").or("werld"))
186
+ # Union inside phrase
187
+ index.search("hello").and(index.search("world").or("werld"))
188
+ ```
189
+
190
+ ### Rails Integration
191
+
192
+ Integration with Rails is on by default! All you have to do is add the following to the model you want to search:
193
+ ```ruby
194
+ class User < ApplicationRecord
195
+ redi_search schema: {
196
+ first: { text: { phonetic: "dm:en" } },
197
+ last: { text: { phonetic: "dm:en" } }
198
+ }
199
+ end
200
+ ```
201
+
202
+ This will automatically add `User.search` and `User.reindex` methods. You can also use `User.redi_search_index` to get the `RediSearch::Index` instance. `User.reindex` will first `drop` the index if it exists, create the index with the given schema, and then add all the records to the index.
203
+
204
+ ## Development
205
+
206
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. You can also start a rails console if you `cd` into `test/dummy`.
207
+
208
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, execute `bin/publish (major|minor|patch)` which will update the version number in `version.rb`, create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
209
+
210
+ ## Contributing
211
+
212
+ Bug reports and pull requests are welcome on GitHub at https://github.com/npezza93/redi_search. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
213
+
214
+ ## License
215
+
216
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
217
+
218
+ ## Code of Conduct
219
+
220
+ Everyone interacting in the RediSearch project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/npezza93/redi_search/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "redi_search"
6
+
7
+ require "pry"
8
+ Pry.start
data/bin/publish ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "pathname"
5
+ require "fileutils"
6
+ require_relative "../lib/redi_search/version"
7
+
8
+ # path to your application root.
9
+ APP_ROOT = Pathname.new File.expand_path("..", __dir__)
10
+ MASTER_CHECK = <<~MASTER_CHECK
11
+ if [ $(git symbolic-ref --short -q HEAD) != 'master' ];
12
+ then exit 1;
13
+ fi
14
+ MASTER_CHECK
15
+ VERSION_TYPES = %w(major minor patch).freeze
16
+
17
+ def system!(*args)
18
+ system(*args) || abort("\n== Command #{args} failed ==")
19
+ end
20
+
21
+ abort("\n== Version Type incorrect ==") unless VERSION_TYPES.include?(ARGV[0])
22
+
23
+ abort("\n== Not on master") unless system(MASTER_CHECK)
24
+
25
+ current_version = RediSearch::VERSION.split(".").map(&:to_i)
26
+
27
+ case ARGV[0]
28
+ when "major"
29
+ current_version[0] += 1
30
+ current_version[1] = 0
31
+ current_version[2] = 0
32
+ when "minor"
33
+ current_version[1] += 1
34
+ current_version[2] = 0
35
+ when "patch"
36
+ current_version[2] += 1
37
+ end
38
+
39
+ FileUtils.chdir APP_ROOT do
40
+ contents = <<~FILE
41
+ # frozen_string_literal: true
42
+
43
+ module RediSearch
44
+ VERSION = "#{current_version.join('.')}"
45
+ end
46
+ FILE
47
+
48
+ puts "== Updating version to #{current_version.join('.')} =="
49
+ File.write("lib/redi_search/version.rb", contents)
50
+
51
+ system! "git add lib/redi_search/version.rb"
52
+
53
+ puts "== Committing updated files =="
54
+ system! "git commit -m 'Version bump to #{current_version.join('.')}'"
55
+
56
+ puts "== Tagging release =="
57
+ system! "bundle exec rake release"
58
+ end
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/bin/test ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $: << File.expand_path("../test", __dir__)
5
+
6
+ require "bundler/setup"
7
+ require "rails/plugin/test"
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis"
4
+
5
+ module RediSearch
6
+ class Client
7
+ class Response < SimpleDelegator
8
+ def initialize(response)
9
+ @response = response
10
+
11
+ super(response)
12
+ end
13
+
14
+ def ok?
15
+ if response.is_a? String
16
+ response == "OK"
17
+ elsif response.is_a? Array
18
+ response.all? { |pipeline_response| pipeline_response == "OK" }
19
+ else
20
+ response
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :response
27
+ end
28
+
29
+ def initialize(redis_config)
30
+ @redis = Redis.new(redis_config)
31
+ end
32
+
33
+ def call!(command, *params)
34
+ instrument(command.downcase, query: [command, params]) do
35
+ send_command(command, *params)
36
+ end
37
+ end
38
+
39
+ def pipelined
40
+ Response.new(redis.pipelined do
41
+ instrument("pipeline", query: ["begin pipeline"])
42
+ yield
43
+ instrument("pipeline", query: ["finish pipeline"])
44
+ end)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :redis
50
+
51
+ def send_command(command, *params)
52
+ Response.new(redis.call("FT.#{command}", *params))
53
+ end
54
+
55
+ def instrument(action, payload)
56
+ block =
57
+ if block_given?
58
+ Proc.new
59
+ else
60
+ proc {}
61
+ end
62
+
63
+ ActiveSupport::Notifications.instrument(
64
+ "#{action}.redi_search", { name: "RediSearch" }.merge(payload), &block
65
+ )
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redi_search/client"
4
+
5
+ module RediSearch
6
+ class Configuration
7
+ attr_writer :redis_config
8
+
9
+ def client
10
+ @client ||= Client.new(redis_config)
11
+ end
12
+
13
+ def redis_config
14
+ @redis_config ||= { host: "127.0.0.1", port: "6379" }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RediSearch
4
+ class Document
5
+ class Converter
6
+ def initialize(index, record)
7
+ @index = index
8
+ @record = record
9
+ end
10
+
11
+ def document
12
+ Document.new(
13
+ index,
14
+ record.id,
15
+ index.schema.fields.map do |field|
16
+ [field.to_s, record.public_send(field)]
17
+ end.to_h
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :index, :record
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RediSearch
4
+ class Document
5
+ class << self
6
+ def get(index, document_id)
7
+ response = RediSearch.client.call!("GET", index.name, document_id)
8
+
9
+ return if response.blank?
10
+
11
+ new(index, document_id, Hash[*response])
12
+ end
13
+
14
+ def mget(index, *document_ids)
15
+ document_ids.zip(
16
+ RediSearch.client.call!("MGET", index.name, *document_ids)
17
+ ).map do |document|
18
+ next if document[1].blank?
19
+
20
+ new(index, document[0], Hash[*document[1]])
21
+ end.compact
22
+ end
23
+ end
24
+
25
+ attr_reader :document_id
26
+
27
+ def initialize(index, document_id, fields)
28
+ @index = index
29
+ @document_id = document_id
30
+ @to_a = []
31
+
32
+ schema_fields.each do |field|
33
+ @to_a.push([field, fields[field]])
34
+ instance_variable_set(:"@#{field}", fields[field])
35
+ define_singleton_method field do
36
+ fields[field]
37
+ end
38
+ end
39
+ end
40
+
41
+ def del
42
+ client.call!("DEL", index.name, document_id).ok?
43
+ end
44
+
45
+ #:nocov:
46
+ def pretty_print(printer) # rubocop:disable Metrics/MethodLength
47
+ printer.object_address_group(self) do
48
+ printer.seplist(
49
+ schema_fields.append("document_id"), proc { printer.text "," }
50
+ ) do |field_name|
51
+ printer.breakable " "
52
+ printer.group(1) do
53
+ printer.text field_name
54
+ printer.text ":"
55
+ printer.breakable
56
+ printer.pp public_send(field_name)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ #:nocov:
62
+
63
+ def schema_fields
64
+ @schema_fields ||= index.schema.fields.map(&:to_s)
65
+ end
66
+
67
+ def to_a
68
+ @to_a.flatten
69
+ end
70
+
71
+ private
72
+
73
+ attr_reader :index
74
+
75
+ def client
76
+ RediSearch.client
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RediSearch
4
+ class Error < StandardError
5
+ end
6
+ end