typesensual 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9231d4598d045c7089f290474867722966f4c4e720cc34a8de0ef960b5ac650a
4
- data.tar.gz: 6b9400c0f282f3df2b3bec0f310f453b3383ddd05846894a1390176db60b206e
3
+ metadata.gz: 4ea8c4aeb857be069ae139f07adf94182acb7cf65e4fc3d21efdaca0522227a8
4
+ data.tar.gz: de66e132cc8b81b58201fc517847e4b5c0bf3644abb1c79eafa56bc696fbb7ce
5
5
  SHA512:
6
- metadata.gz: 7ebec05f05e05762734582d0b5080844a567002313aff70b4009642da0aeb20874fbe7a12e9b523dcc2f702bf0b98c24342f3f23dcf751582b42399f21542114
7
- data.tar.gz: 709890ec4af5c9da8b680910c865d4900d0eae090cd9cc471bfb50c0860e486854cbcb01ee934aac1bfa124da6722771b2154d2a80b7a911416876ff9598257e
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.1.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.5)
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.13.0)
25
+ i18n (1.14.1)
26
26
  concurrent-ruby (~> 1.0)
27
27
  json (2.6.3)
28
- minitest (5.18.0)
29
- oj (3.14.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/movie_index.rb
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 index_one(id)
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
@@ -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, should_update: ->(_record) { true })
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
- @should_update.call(record) && @index.index_one(record.id)
14
+ @index.index_one(record.id)
16
15
  end
17
16
 
18
17
  def after_destroy_commit(record)
@@ -2,21 +2,31 @@
2
2
 
3
3
  class Typesensual
4
4
  class Config
5
- attr_accessor :nodes, :api_key
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
@@ -43,7 +43,7 @@ class Typesensual
43
43
  end
44
44
 
45
45
  def to_h
46
- @field.to_h.merge!(
46
+ @field.to_h.merge(
47
47
  'name' => name,
48
48
  'locale' => locale
49
49
  ).compact!
@@ -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 = Typesensual::Schema.new(&block)
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
- collection.insert_one!(new.index_one(id))
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
- # Unlike {#index_one}, this method should yield successive records to index
140
+ # This method should yield successive records to index
136
141
  #
137
142
  # @yield [Hash] a document to upsert in Typesense
138
- def index_many(ids)
143
+ def index(ids)
139
144
  ids.each do |id|
140
- yield index_one(id)
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(:index_many, ids),
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
@@ -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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Typesensual
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
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.1.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-07-23 00:00:00.000000000 Z
11
+ date: 2023-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport