typesensual 0.1.0 → 0.2.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/Gemfile.lock +7 -5
- data/README.md +24 -19
- data/lib/tasks/typesensual.rake +10 -0
- data/lib/typesensual/callbacks.rb +2 -3
- data/lib/typesensual/config.rb +13 -3
- data/lib/typesensual/field.rb +1 -1
- data/lib/typesensual/index.rb +16 -11
- data/lib/typesensual/rake_helper.rb +19 -0
- data/lib/typesensual/schema.rb +14 -1
- data/lib/typesensual/search.rb +48 -0
- data/lib/typesensual/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ea8c4aeb857be069ae139f07adf94182acb7cf65e4fc3d21efdaca0522227a8
|
4
|
+
data.tar.gz: de66e132cc8b81b58201fc517847e4b5c0bf3644abb1c79eafa56bc696fbb7ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a29f224543a07478a3fc61e23edd3c9d3bb52995740c266f43400dd33dcf96b7e6d0a9ee54f59e7d6500c1ce3a33be1d0cd4ef590e5f716a6ade44bc22eb6c66
|
7
|
+
data.tar.gz: e05a23f4d02ecc1242ba7b2ffdae7aca2a2f8c537a838186532e18b87a97b22c76385fe05a7b6c917d291f02461487abb378ab2f649c99cf0cd2c7587d0f6f7c
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
typesensual (0.
|
4
|
+
typesensual (0.2.0)
|
5
5
|
activesupport (>= 6.1.5)
|
6
6
|
paint (>= 2.0.0)
|
7
7
|
typesense (>= 0.13.0)
|
@@ -9,7 +9,7 @@ PATH
|
|
9
9
|
GEM
|
10
10
|
remote: https://rubygems.org/
|
11
11
|
specs:
|
12
|
-
activesupport (7.0.
|
12
|
+
activesupport (7.0.6)
|
13
13
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
14
14
|
i18n (>= 1.6, < 2)
|
15
15
|
minitest (>= 5.1)
|
@@ -22,11 +22,11 @@ GEM
|
|
22
22
|
ethon (0.16.0)
|
23
23
|
ffi (>= 1.15.0)
|
24
24
|
ffi (1.15.5)
|
25
|
-
i18n (1.
|
25
|
+
i18n (1.14.1)
|
26
26
|
concurrent-ruby (~> 1.0)
|
27
27
|
json (2.6.3)
|
28
|
-
minitest (5.
|
29
|
-
oj (3.
|
28
|
+
minitest (5.19.0)
|
29
|
+
oj (3.15.1)
|
30
30
|
paint (2.3.0)
|
31
31
|
parallel (1.23.0)
|
32
32
|
parser (3.2.2.1)
|
@@ -91,8 +91,10 @@ GEM
|
|
91
91
|
yard (0.9.34)
|
92
92
|
|
93
93
|
PLATFORMS
|
94
|
+
ruby
|
94
95
|
x86_64-darwin-19
|
95
96
|
x86_64-darwin-22
|
97
|
+
x86_64-linux
|
96
98
|
|
97
99
|
DEPENDENCIES
|
98
100
|
commonmarker
|
data/README.md
CHANGED
@@ -50,6 +50,14 @@ Typesensual.configure do |config|
|
|
50
50
|
end
|
51
51
|
```
|
52
52
|
|
53
|
+
Alternatively you can configure with env variables:
|
54
|
+
|
55
|
+
```env
|
56
|
+
TYPESENSUAL_NODES=http://node1:8108,http://node2:8108,http://node3:8108
|
57
|
+
TYPESENSUAL_API_KEY=xyz
|
58
|
+
TYPESENSUAL_ENV=test
|
59
|
+
```
|
60
|
+
|
53
61
|
### Creating your first index
|
54
62
|
|
55
63
|
Once the client is configured, you can create your first index. This is done by creating a subclass
|
@@ -57,7 +65,7 @@ of `Typesensual::Index` and defining your schema and how to load the data. For e
|
|
57
65
|
following index might be used to index movies from an ActiveRecord model:
|
58
66
|
|
59
67
|
```ruby
|
60
|
-
# app/indices/
|
68
|
+
# app/indices/movies_index.rb
|
61
69
|
class MoviesIndex < Typesensual::Index
|
62
70
|
# The schema of the collection
|
63
71
|
schema do
|
@@ -70,24 +78,7 @@ class MoviesIndex < Typesensual::Index
|
|
70
78
|
field 'genres', type: 'string[]', facet: true
|
71
79
|
end
|
72
80
|
|
73
|
-
def
|
74
|
-
movie = Movie.find(id).includes(:genres)
|
75
|
-
|
76
|
-
{
|
77
|
-
id: movie.id,
|
78
|
-
title: movie.title,
|
79
|
-
release_date: {
|
80
|
-
year: movie.release_date.year,
|
81
|
-
month: movie.release_date.month,
|
82
|
-
day: movie.release_date.day
|
83
|
-
},
|
84
|
-
average_rating: movie.average_rating,
|
85
|
-
user_count: movie.user_count,
|
86
|
-
genres: movie.genres.map(&:name)
|
87
|
-
}
|
88
|
-
end
|
89
|
-
|
90
|
-
def index_many(ids)
|
81
|
+
def index(ids)
|
91
82
|
Movies.where(id: ids).includes(:genres).find_each do |movie|
|
92
83
|
yield {
|
93
84
|
id: movie.id,
|
@@ -106,6 +97,20 @@ class MoviesIndex < Typesensual::Index
|
|
106
97
|
end
|
107
98
|
```
|
108
99
|
|
100
|
+
### Integrating with your model
|
101
|
+
|
102
|
+
If you use ActiveRecord, there's a set of premade callbacks you can use:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class Movie < ApplicationRecord
|
106
|
+
after_commit MoviesIndex.ar_callbacks, on: %i[create update destroy]
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
You're free to use these callbacks as-is, or you can use them as a starting point for your own
|
111
|
+
integration. They're just calling `MoviesIndex.index_one` and `MoviesIndex.remove_one` under the
|
112
|
+
hood, so you can do the same in your own callbacks or outside of ActiveRecord.
|
113
|
+
|
109
114
|
### Loading data into your index
|
110
115
|
|
111
116
|
Once you have defined your index, you can load data into it and update the alias to point to the
|
data/lib/tasks/typesensual.rake
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'typesensual/rake_helper'
|
4
|
+
|
3
5
|
namespace :typesensual do
|
4
6
|
desc 'List typesensual indices and their collections'
|
5
7
|
task list: :environment do
|
@@ -21,4 +23,12 @@ namespace :typesensual do
|
|
21
23
|
model: args[:model]
|
22
24
|
)
|
23
25
|
end
|
26
|
+
|
27
|
+
desc 'Delete a version of an index'
|
28
|
+
task :drop_version, %i[index version] => :environment do |_, args|
|
29
|
+
Typesensual::RakeHelper.drop_version(
|
30
|
+
index: args[:index],
|
31
|
+
version: args[:version]
|
32
|
+
)
|
33
|
+
end
|
24
34
|
end
|
@@ -2,9 +2,8 @@
|
|
2
2
|
|
3
3
|
class Typesensual
|
4
4
|
class Callbacks
|
5
|
-
def initialize(index
|
5
|
+
def initialize(index)
|
6
6
|
@index = index
|
7
|
-
@should_update = should_update
|
8
7
|
end
|
9
8
|
|
10
9
|
def after_create_commit(record)
|
@@ -12,7 +11,7 @@ class Typesensual
|
|
12
11
|
end
|
13
12
|
|
14
13
|
def after_update_commit(record)
|
15
|
-
@
|
14
|
+
@index.index_one(record.id)
|
16
15
|
end
|
17
16
|
|
18
17
|
def after_destroy_commit(record)
|
data/lib/typesensual/config.rb
CHANGED
@@ -2,21 +2,31 @@
|
|
2
2
|
|
3
3
|
class Typesensual
|
4
4
|
class Config
|
5
|
-
|
6
|
-
attr_writer :env, :client
|
5
|
+
attr_writer :env, :client, :nodes, :api_key
|
7
6
|
|
8
7
|
def initialize(&block)
|
9
8
|
yield self if block
|
10
9
|
end
|
11
10
|
|
12
11
|
def env
|
13
|
-
@env ||= (defined?(Rails) ? Rails.env : nil)
|
12
|
+
@env ||= ENV.fetch('TYPESENSUAL_ENV', (defined?(Rails) ? Rails.env : nil))
|
14
13
|
end
|
15
14
|
|
16
15
|
def client
|
17
16
|
@client ||= Typesense::Client.new(connection_options)
|
18
17
|
end
|
19
18
|
|
19
|
+
def nodes
|
20
|
+
@nodes ||= ENV['TYPESENSUAL_NODES']&.split(',')&.map do |node|
|
21
|
+
node_uri = URI.parse(node)
|
22
|
+
{ port: node_uri.port, host: node_uri.host, protocol: node_uri.scheme }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def api_key
|
27
|
+
@api_key ||= ENV.fetch('TYPESENSUAL_API_KEY', nil)
|
28
|
+
end
|
29
|
+
|
20
30
|
private
|
21
31
|
|
22
32
|
def connection_options
|
data/lib/typesensual/field.rb
CHANGED
data/lib/typesensual/index.rb
CHANGED
@@ -21,6 +21,12 @@ class Typesensual
|
|
21
21
|
class Index
|
22
22
|
include StateHelpers
|
23
23
|
|
24
|
+
def self.inherited(subclass)
|
25
|
+
super
|
26
|
+
# Copy the schema from the parent class to the subclass
|
27
|
+
subclass.instance_variable_set(:@schema, @schema&.dup)
|
28
|
+
end
|
29
|
+
|
24
30
|
# Get or set the name for this index
|
25
31
|
#
|
26
32
|
# @overload index_name(value)
|
@@ -94,7 +100,9 @@ class Typesensual
|
|
94
100
|
#
|
95
101
|
# See {Schema} for more information
|
96
102
|
def self.schema(&block)
|
97
|
-
@schema
|
103
|
+
@schema ||= Typesensual::Schema.new
|
104
|
+
@schema.instance_eval(&block) if block
|
105
|
+
@schema
|
98
106
|
end
|
99
107
|
|
100
108
|
# Updates the alias to point to the given collection name
|
@@ -122,28 +130,25 @@ class Typesensual
|
|
122
130
|
update_alias!(collection)
|
123
131
|
end
|
124
132
|
|
125
|
-
# The method to implement to index *one* record.
|
126
|
-
#
|
127
|
-
# @return [Hash] the document to upsert in Typesense
|
128
|
-
def index_one(_id); end
|
129
|
-
|
130
133
|
def self.index_one(id, collection: self.collection)
|
131
|
-
|
134
|
+
new.index([id]) do |record|
|
135
|
+
collection.insert_one!(record)
|
136
|
+
end
|
132
137
|
end
|
133
138
|
|
134
139
|
# The method to implement to index *many* records
|
135
|
-
#
|
140
|
+
# This method should yield successive records to index
|
136
141
|
#
|
137
142
|
# @yield [Hash] a document to upsert in Typesense
|
138
|
-
def
|
143
|
+
def index(ids)
|
139
144
|
ids.each do |id|
|
140
|
-
yield
|
145
|
+
yield({ id: id })
|
141
146
|
end
|
142
147
|
end
|
143
148
|
|
144
149
|
def self.index_many(ids, collection: self.collection, batch_size: 100)
|
145
150
|
collection.insert_many!(
|
146
|
-
new.enum_for(:
|
151
|
+
new.enum_for(:index, ids),
|
147
152
|
batch_size: batch_size
|
148
153
|
)
|
149
154
|
end
|
@@ -98,6 +98,25 @@ class Typesensual
|
|
98
98
|
created_at: new_coll.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
99
99
|
)
|
100
100
|
end
|
101
|
+
|
102
|
+
# Drop a version of an index
|
103
|
+
#
|
104
|
+
# @param index [String] The name of the index to remove a version from
|
105
|
+
# @param version [String] The version to remove
|
106
|
+
# @example
|
107
|
+
# rake typesensual:drop_version[FooIndex,1]
|
108
|
+
def drop_version(index:, version:, output: $stdout)
|
109
|
+
index = index.safe_constantize
|
110
|
+
collection = index.collection_for(version: version)
|
111
|
+
|
112
|
+
collection.delete!
|
113
|
+
|
114
|
+
output.printf(
|
115
|
+
"==> Dropped version %<version>s of %<index>s\n",
|
116
|
+
version: version,
|
117
|
+
index: index.name
|
118
|
+
)
|
119
|
+
end
|
101
120
|
end
|
102
121
|
end
|
103
122
|
end
|
data/lib/typesensual/schema.rb
CHANGED
@@ -4,8 +4,21 @@ require 'typesensual/field'
|
|
4
4
|
|
5
5
|
class Typesensual
|
6
6
|
class Schema
|
7
|
+
# Duplicate fields from the original
|
8
|
+
def initialize_copy(original)
|
9
|
+
%w[
|
10
|
+
@fields
|
11
|
+
@token_separators
|
12
|
+
@symbols_to_index
|
13
|
+
@default_sorting_field
|
14
|
+
@enable_nested_fields
|
15
|
+
].each do |var|
|
16
|
+
instance_variable_set(var, original.instance_variable_get(var).dup)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
7
20
|
def initialize(&block)
|
8
|
-
instance_eval(&block)
|
21
|
+
instance_eval(&block) unless block.nil?
|
9
22
|
end
|
10
23
|
|
11
24
|
def field(name, type: 'auto', locale: nil, facet: nil, index: nil, optional: nil)
|
data/lib/typesensual/search.rb
CHANGED
@@ -5,6 +5,8 @@ require 'typesensual/search/results'
|
|
5
5
|
|
6
6
|
class Typesensual
|
7
7
|
class Search
|
8
|
+
include StateHelpers
|
9
|
+
|
8
10
|
# Initialize a new search object for a collection
|
9
11
|
#
|
10
12
|
# @param collection [Typesensual::Collection] the Typesensual collection object
|
@@ -114,6 +116,7 @@ class Typesensual
|
|
114
116
|
end
|
115
117
|
|
116
118
|
# Generate the query document
|
119
|
+
# @return [Hash] the query document
|
117
120
|
def query
|
118
121
|
{
|
119
122
|
collection: @collection.name,
|
@@ -130,8 +133,53 @@ class Typesensual
|
|
130
133
|
end
|
131
134
|
|
132
135
|
# Load the results from the search query
|
136
|
+
# @return [Typesensual::Search::Results] the results of the search
|
133
137
|
def load
|
134
138
|
Results.new(@collection.typesense_collection.documents.search(query))
|
135
139
|
end
|
140
|
+
|
141
|
+
# Perform multiple searches in one request. There are two variants of this method, one which
|
142
|
+
# takes a list of anonymous queries and one which takes a hash of named queries. Named queries
|
143
|
+
# will probably be more readable for more than a couple of queries, but anonymous queries can be
|
144
|
+
# destructured directly.
|
145
|
+
#
|
146
|
+
# Both versions accept either a Search instance or a hash of search parameters.
|
147
|
+
#
|
148
|
+
# @overload multi(*searches)
|
149
|
+
# Perform an array of search queries in a single request. The return values are guaranteed to
|
150
|
+
# be in the same order as the provided searches.
|
151
|
+
#
|
152
|
+
# @param searches [<Typesensual::Search, Hash>] the searches to perform
|
153
|
+
# @return [<Typesensual::Search::Results>] the results of the searches
|
154
|
+
#
|
155
|
+
# @overload multi(searches)
|
156
|
+
# Perform multiple named search queries in a single request. The results will be keyed by the
|
157
|
+
# same names as the provided searches.
|
158
|
+
#
|
159
|
+
# @param searches [{Object => Typesensual::Search, Hash>] the searches to perform
|
160
|
+
# @return [{Object => Typesensual::Search::Results}] the results of the searches
|
161
|
+
def self.multi(*searches)
|
162
|
+
# If we have one argument and it's a hash, we're doing named searches
|
163
|
+
if searches.count == 1 && searches.first.is_a?(Hash)
|
164
|
+
keys = searches.first.keys
|
165
|
+
searches = searches.first.values
|
166
|
+
end
|
167
|
+
|
168
|
+
results = client.multi_search.perform({
|
169
|
+
searches: searches.flatten.map(&:query)
|
170
|
+
})
|
171
|
+
|
172
|
+
# Wrap our results in Result objects
|
173
|
+
wrapped_results = results['results'].map do |result|
|
174
|
+
Results.new(result)
|
175
|
+
end
|
176
|
+
|
177
|
+
# If we're doing named searches, re-key the results
|
178
|
+
if keys
|
179
|
+
keys.zip(wrapped_results).to_h
|
180
|
+
else
|
181
|
+
wrapped_results
|
182
|
+
end
|
183
|
+
end
|
136
184
|
end
|
137
185
|
end
|
data/lib/typesensual/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typesensual
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Emma Lejeck
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|