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 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