typesense-rails 1.0.0.rc1
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 +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +42 -0
- data/Gemfile.lock +260 -0
- data/LICENSE +22 -0
- data/README.md +239 -0
- data/Rakefile +18 -0
- data/lib/typesense/config.rb +30 -0
- data/lib/typesense/import_job.rb +21 -0
- data/lib/typesense/pagination/kaminari.rb +39 -0
- data/lib/typesense/pagination/pagy.rb +29 -0
- data/lib/typesense/pagination/will_paginate.rb +17 -0
- data/lib/typesense/pagination.rb +20 -0
- data/lib/typesense/railtie.rb +12 -0
- data/lib/typesense/tasks/typesense.rake +17 -0
- data/lib/typesense/typesense_job.rb +9 -0
- data/lib/typesense/utilities.rb +47 -0
- data/lib/typesense/version.rb +3 -0
- data/lib/typesense-rails.rb +996 -0
- data/spec/integration_spec.rb +1178 -0
- data/spec/spec_helper.rb +54 -0
- data/typesense-rails.gemspec +69 -0
- metadata +164 -0
@@ -0,0 +1,1178 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "spec_helper"))
|
2
|
+
# require "debug"
|
3
|
+
# DEBUGGER__::CONFIG.set_config(
|
4
|
+
# port: 12345,
|
5
|
+
# nonstop: false,
|
6
|
+
# )
|
7
|
+
# debugger
|
8
|
+
NEW_RAILS = Gem.loaded_specs["rails"].version >= Gem::Version.new("6.0")
|
9
|
+
|
10
|
+
require "active_record"
|
11
|
+
unless NEW_RAILS
|
12
|
+
require "active_job/test_helper"
|
13
|
+
ActiveJob::Base.queue_adapter = :test
|
14
|
+
end
|
15
|
+
require "sqlite3" unless defined?(JRUBY_VERSION)
|
16
|
+
require "logger"
|
17
|
+
require "sequel"
|
18
|
+
require "active_model_serializers"
|
19
|
+
|
20
|
+
Typesense.configuration = {
|
21
|
+
nodes: [{
|
22
|
+
host: "localhost", # For Typesense Cloud use xxx.a1.typesense.net
|
23
|
+
port: 8108, # For Typesense Cloud use 443
|
24
|
+
protocol: "http", # For Typesense Cloud use https
|
25
|
+
}],
|
26
|
+
api_key: "xyz",
|
27
|
+
connection_timeout_seconds: 2,
|
28
|
+
}
|
29
|
+
|
30
|
+
begin
|
31
|
+
FileUtils.rm("data.sqlite3")
|
32
|
+
rescue StandardError
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
36
|
+
ActiveRecord::Base.logger.level = Logger::WARN
|
37
|
+
ActiveRecord::Base.establish_connection(
|
38
|
+
"adapter" => defined?(JRUBY_VERSION) ? "jdbcsqlite3" : "sqlite3",
|
39
|
+
"database" => "data.sqlite3",
|
40
|
+
"pool" => 5,
|
41
|
+
"timeout" => 5000,
|
42
|
+
)
|
43
|
+
|
44
|
+
ActiveRecord::Base.raise_in_transactional_callbacks = true if ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks)
|
45
|
+
|
46
|
+
SEQUEL_DB = Sequel.connect(if defined?(JRUBY_VERSION)
|
47
|
+
"jdbc:sqlite:sequel_data.sqlite3"
|
48
|
+
else
|
49
|
+
{ "adapter" => "sqlite",
|
50
|
+
"database" => "sequel_data.sqlite3" }
|
51
|
+
end)
|
52
|
+
|
53
|
+
unless SEQUEL_DB.table_exists?(:sequel_books)
|
54
|
+
SEQUEL_DB.create_table(:sequel_books) do
|
55
|
+
primary_key :id
|
56
|
+
String :name
|
57
|
+
String :author
|
58
|
+
FalseClass :released
|
59
|
+
FalseClass :premium
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
ActiveRecord::Schema.define do
|
64
|
+
create_table :products do |t|
|
65
|
+
t.string :name
|
66
|
+
t.string :href
|
67
|
+
t.string :type
|
68
|
+
t.text :description
|
69
|
+
t.datetime :release_date
|
70
|
+
end
|
71
|
+
create_table :colors do |t|
|
72
|
+
t.string :name
|
73
|
+
t.string :short_name
|
74
|
+
t.integer :hex
|
75
|
+
end
|
76
|
+
create_table :namespaced_models do |t|
|
77
|
+
t.string :name
|
78
|
+
t.integer :another_private_value
|
79
|
+
end
|
80
|
+
create_table :uniq_users, id: false do |t|
|
81
|
+
t.string :name
|
82
|
+
end
|
83
|
+
create_table :nullable_ids do |t|
|
84
|
+
end
|
85
|
+
create_table :nested_items do |t|
|
86
|
+
t.integer :parent_id
|
87
|
+
t.boolean :hidden
|
88
|
+
end
|
89
|
+
create_table :cities do |t|
|
90
|
+
t.string :name
|
91
|
+
t.string :country
|
92
|
+
t.float :lat
|
93
|
+
t.float :lng
|
94
|
+
t.string :gl_array
|
95
|
+
end
|
96
|
+
create_table :with_slaves do |t|
|
97
|
+
end
|
98
|
+
create_table :mongo_objects do |t|
|
99
|
+
t.string :name
|
100
|
+
end
|
101
|
+
create_table :books do |t|
|
102
|
+
t.string :name
|
103
|
+
t.string :author
|
104
|
+
t.boolean :premium
|
105
|
+
t.boolean :released
|
106
|
+
end
|
107
|
+
create_table :ebooks do |t|
|
108
|
+
t.string :name
|
109
|
+
t.string :author
|
110
|
+
t.boolean :premium
|
111
|
+
t.boolean :released
|
112
|
+
end
|
113
|
+
create_table :disabled_booleans do |t|
|
114
|
+
t.string :name
|
115
|
+
end
|
116
|
+
create_table :disabled_procs do |t|
|
117
|
+
t.string :name
|
118
|
+
end
|
119
|
+
create_table :disabled_symbols do |t|
|
120
|
+
t.string :name
|
121
|
+
end
|
122
|
+
create_table :encoded_strings do |t|
|
123
|
+
end
|
124
|
+
create_table :forward_to_replicas do |t|
|
125
|
+
t.string :name
|
126
|
+
end
|
127
|
+
create_table :forward_to_replicas_twos do |t|
|
128
|
+
t.string :name
|
129
|
+
end
|
130
|
+
create_table :sub_replicas do |t|
|
131
|
+
t.string :name
|
132
|
+
end
|
133
|
+
create_table :enqueued_objects do |t|
|
134
|
+
t.string :name
|
135
|
+
end
|
136
|
+
create_table :disabled_enqueued_objects do |t|
|
137
|
+
t.string :name
|
138
|
+
end
|
139
|
+
create_table :misconfigured_blocks do |t|
|
140
|
+
t.string :name
|
141
|
+
end
|
142
|
+
if defined?(ActiveModel::Serializer)
|
143
|
+
create_table :serialized_objects do |t|
|
144
|
+
t.string :name
|
145
|
+
t.string :skip
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class Product < ActiveRecord::Base
|
151
|
+
include Typesense
|
152
|
+
|
153
|
+
typesense auto_index: false,
|
154
|
+
if: :published?, unless: ->(o) { o.href.blank? },
|
155
|
+
index_name: safe_index_name("my_products_index") do
|
156
|
+
attribute :href, :name
|
157
|
+
|
158
|
+
multi_way_synonyms [
|
159
|
+
{ "phone-synonym" => %w[galaxy samsung samsung_electronics] },
|
160
|
+
]
|
161
|
+
|
162
|
+
one_way_synonyms [
|
163
|
+
{ "smart-phone-synonym" => { "root" => "smartphone",
|
164
|
+
"synonyms" => %w[nokia samsung motorola android] } },
|
165
|
+
]
|
166
|
+
end
|
167
|
+
|
168
|
+
def published?
|
169
|
+
release_date.blank? || release_date <= Time.now
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class Camera < Product
|
174
|
+
end
|
175
|
+
|
176
|
+
class Color < ActiveRecord::Base
|
177
|
+
include Typesense
|
178
|
+
attr_accessor :not_indexed
|
179
|
+
|
180
|
+
typesense collection_name: safe_index_name("Color"), per_environment: true do
|
181
|
+
fields [
|
182
|
+
{ "name" => "name", "type" => "string", "facet" => true },
|
183
|
+
{ "name" => "short_name", "type" => "string", "index" => false, "optional" => true },
|
184
|
+
{ "name" => "hex", "type" => "int32" },
|
185
|
+
]
|
186
|
+
|
187
|
+
default_sorting_field "hex"
|
188
|
+
|
189
|
+
# we're using all attributes of the Color class + the _tag "extra" attribute
|
190
|
+
end
|
191
|
+
|
192
|
+
def hex_changed?
|
193
|
+
false
|
194
|
+
end
|
195
|
+
|
196
|
+
def will_save_change_to_short_name?
|
197
|
+
false
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
class DisabledBoolean < ActiveRecord::Base
|
202
|
+
include Typesense
|
203
|
+
|
204
|
+
typesense disable_indexing: true, index_name: safe_index_name("DisabledBoolean") do
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
class DisabledProc < ActiveRecord::Base
|
209
|
+
include Typesense
|
210
|
+
|
211
|
+
typesense disable_indexing: proc { true }, index_name: safe_index_name("DisabledProc") do
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
class DisabledSymbol < ActiveRecord::Base
|
216
|
+
include Typesense
|
217
|
+
|
218
|
+
typesense disable_indexing: :truth, index_name: safe_index_name("DisabledSymbol") do
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.truth
|
222
|
+
true
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
module Namespaced
|
227
|
+
def self.table_name_prefix
|
228
|
+
"namespaced_"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
class Namespaced::Model < ActiveRecord::Base
|
233
|
+
include Typesense
|
234
|
+
|
235
|
+
typesense index_name: safe_index_name(typesense_collection_name({})) do
|
236
|
+
attribute :customAttr do
|
237
|
+
40 + another_private_value
|
238
|
+
end
|
239
|
+
attribute :myid do
|
240
|
+
id
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
class UniqUser < ActiveRecord::Base
|
246
|
+
include Typesense
|
247
|
+
|
248
|
+
typesense index_name: safe_index_name("UniqUser"), per_environment: true, id: :name do
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
class NullableId < ActiveRecord::Base
|
253
|
+
include Typesense
|
254
|
+
|
255
|
+
typesense index_name: safe_index_name("NullableId"), per_environment: true, id: :custom_id,
|
256
|
+
if: :never do
|
257
|
+
end
|
258
|
+
|
259
|
+
def custom_id
|
260
|
+
nil
|
261
|
+
end
|
262
|
+
|
263
|
+
def never
|
264
|
+
false
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
class NestedItem < ActiveRecord::Base
|
269
|
+
has_many :children, class_name: "NestedItem", foreign_key: "parent_id"
|
270
|
+
|
271
|
+
include Typesense
|
272
|
+
|
273
|
+
typesense index_name: safe_index_name("NestedItem"), per_environment: true, unless: :hidden do
|
274
|
+
attribute :nb_children
|
275
|
+
end
|
276
|
+
|
277
|
+
def nb_children
|
278
|
+
children.count
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
class City < ActiveRecord::Base
|
283
|
+
include Typesense
|
284
|
+
|
285
|
+
serialize :gl_array
|
286
|
+
|
287
|
+
def location
|
288
|
+
lat.present? && lng.present? ? [lat, lng] : gl_array
|
289
|
+
end
|
290
|
+
|
291
|
+
typesense index_name: safe_index_name("City"), per_environment: true do
|
292
|
+
add_attribute :a_null_lat, :a_lng, :location
|
293
|
+
|
294
|
+
predefined_fields [{ "name" => "location", "type" => "geopoint" }]
|
295
|
+
end
|
296
|
+
|
297
|
+
def a_null_lat
|
298
|
+
nil
|
299
|
+
end
|
300
|
+
|
301
|
+
def a_lng
|
302
|
+
1.2345678
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
class SequelBook < Sequel::Model(SEQUEL_DB)
|
307
|
+
plugin :active_model
|
308
|
+
|
309
|
+
include Typesense
|
310
|
+
|
311
|
+
typesense index_name: safe_index_name("SequelBook"), per_environment: true, sanitize: true do
|
312
|
+
add_attribute :test
|
313
|
+
add_attribute :test2
|
314
|
+
end
|
315
|
+
|
316
|
+
def after_create
|
317
|
+
SequelBook.new
|
318
|
+
end
|
319
|
+
|
320
|
+
def test
|
321
|
+
"test"
|
322
|
+
end
|
323
|
+
|
324
|
+
def test2
|
325
|
+
"test2"
|
326
|
+
end
|
327
|
+
|
328
|
+
private
|
329
|
+
|
330
|
+
def public?
|
331
|
+
released && !premium
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
describe "SequelBook" do
|
336
|
+
before(:all) do
|
337
|
+
SequelBook.clear_index!
|
338
|
+
rescue StandardError
|
339
|
+
ArgumentError
|
340
|
+
end
|
341
|
+
|
342
|
+
it "should index the book" do
|
343
|
+
@steve_jobs = SequelBook.create name: "Steve Jobs", author: "Walter Isaacson", premium: true,
|
344
|
+
released: true
|
345
|
+
results = SequelBook.search("steve", "name")
|
346
|
+
expect(results.size).to eq(1)
|
347
|
+
expect(results[0].id).to eq(@steve_jobs.id)
|
348
|
+
end
|
349
|
+
|
350
|
+
it "should not override after hooks" do
|
351
|
+
expect(SequelBook).to receive(:new).twice.and_call_original
|
352
|
+
SequelBook.create name: "Steve Jobs", author: "Walter Isaacson", premium: true, released: true
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
class MongoObject < ActiveRecord::Base
|
357
|
+
include Typesense
|
358
|
+
|
359
|
+
typesense index_name: safe_index_name("MongoObject") do
|
360
|
+
end
|
361
|
+
|
362
|
+
def self.reindex!
|
363
|
+
raise NameError, "never reached"
|
364
|
+
end
|
365
|
+
|
366
|
+
def index!
|
367
|
+
raise NameError, "never reached"
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
class Book < ActiveRecord::Base
|
372
|
+
include Typesense
|
373
|
+
|
374
|
+
typesense index_name: safe_index_name("SecuredBook"), per_environment: true, sanitize: true do
|
375
|
+
end
|
376
|
+
|
377
|
+
private
|
378
|
+
|
379
|
+
def public?
|
380
|
+
released && !premium
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
class Ebook < ActiveRecord::Base
|
385
|
+
include Typesense
|
386
|
+
attr_accessor :current_time, :published_at
|
387
|
+
|
388
|
+
typesense index_name: safe_index_name("eBooks") do
|
389
|
+
end
|
390
|
+
|
391
|
+
def typesense_dirty?
|
392
|
+
return true if published_at.nil? || current_time.nil?
|
393
|
+
|
394
|
+
# Consider dirty if published date is in the past
|
395
|
+
# This doesn't make so much business sense but it's easy to test.
|
396
|
+
published_at < current_time
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
class EncodedString < ActiveRecord::Base
|
401
|
+
include Typesense
|
402
|
+
|
403
|
+
typesense force_utf8_encoding: true, index_name: safe_index_name("EncodedString") do
|
404
|
+
attribute :value do
|
405
|
+
"\xC2\xA0\xE2\x80\xA2\xC2\xA0".force_encoding("ascii-8bit")
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
class SubReplicas < ActiveRecord::Base
|
411
|
+
include Typesense
|
412
|
+
|
413
|
+
typesense force_utf8_encoding: true, index_name: safe_index_name("SubReplicas") do
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
class EnqueuedObject < ActiveRecord::Base
|
418
|
+
include Typesense
|
419
|
+
|
420
|
+
include GlobalID::Identification
|
421
|
+
|
422
|
+
def id
|
423
|
+
read_attribute(:id)
|
424
|
+
end
|
425
|
+
|
426
|
+
def self.find(_id)
|
427
|
+
EnqueuedObject.first
|
428
|
+
end
|
429
|
+
|
430
|
+
typesense enqueue: proc { |record| raise "enqueued #{record.id}" },
|
431
|
+
index_name: safe_index_name("EnqueuedObject") do
|
432
|
+
attributes ["name"]
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
class DisabledEnqueuedObject < ActiveRecord::Base
|
437
|
+
include Typesense
|
438
|
+
|
439
|
+
typesense(enqueue: proc { |_record| raise "enqueued" },
|
440
|
+
index_name: safe_index_name("EnqueuedObject"),
|
441
|
+
disable_indexing: true) do
|
442
|
+
attributes ["name"]
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
class MisconfiguredBlock < ActiveRecord::Base
|
447
|
+
include Typesense
|
448
|
+
end
|
449
|
+
|
450
|
+
if defined?(ActiveModel::Serializer)
|
451
|
+
class SerializedObjectSerializer < ActiveModel::Serializer
|
452
|
+
attributes :name
|
453
|
+
end
|
454
|
+
|
455
|
+
class SerializedObject < ActiveRecord::Base
|
456
|
+
include Typesense
|
457
|
+
|
458
|
+
typesense index_name: safe_index_name("SerializedObject") do
|
459
|
+
use_serializer SerializedObjectSerializer
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
if defined?(ActiveModel::Serializer)
|
465
|
+
describe "SerializedObject" do
|
466
|
+
before(:all) do
|
467
|
+
SerializedObject.clear_index!
|
468
|
+
rescue StandardError
|
469
|
+
ArgumentError
|
470
|
+
end
|
471
|
+
|
472
|
+
it "should push the name but not the other attribute" do
|
473
|
+
o = SerializedObject.new name: "test", skip: "skip me"
|
474
|
+
attributes = SerializedObject.typesense_settings.get_attributes(o)
|
475
|
+
expect(attributes).to eq({ name: "test" })
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
describe "Encoding" do
|
481
|
+
before(:all) do
|
482
|
+
EncodedString.clear_index!
|
483
|
+
rescue StandardError
|
484
|
+
ArgumentError
|
485
|
+
end
|
486
|
+
|
487
|
+
if Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f > 1.8
|
488
|
+
it "should convert to utf-8" do
|
489
|
+
EncodedString.create!
|
490
|
+
results = EncodedString.raw_search("", "value")
|
491
|
+
expect(results["hits"].size).to eq(1)
|
492
|
+
expect(results["hits"].first["document"]["value"]).to eq("\xC2\xA0\xE2\x80\xA2\xC2\xA0".force_encoding("utf-8"))
|
493
|
+
end
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
describe "Settings" do
|
498
|
+
it "should detect settings changes" do
|
499
|
+
expect(Color.send(:typesense_settings_changed?, nil, {})).to eq(true)
|
500
|
+
|
501
|
+
expect(Color.send(:typesense_settings_changed?, {}, { "searchableAttributes" => ["name"] })).to eq(true)
|
502
|
+
|
503
|
+
expect(Color.send(:typesense_settings_changed?,
|
504
|
+
{ "searchableAttributes" => ["name"] },
|
505
|
+
{ "searchableAttributes" => %w[name hex] })).to eq(true)
|
506
|
+
|
507
|
+
expect(Color.send(:typesense_settings_changed?,
|
508
|
+
{ "searchableAttributes" => ["name"] },
|
509
|
+
{ "customRanking" => ["asc(hex)"] })).to eq(true)
|
510
|
+
end
|
511
|
+
|
512
|
+
it "should not detect settings changes" do
|
513
|
+
expect(Color.send(:typesense_settings_changed?, {}, {})).to eq(false)
|
514
|
+
|
515
|
+
expect(Color.send(:typesense_settings_changed?,
|
516
|
+
{ "searchableAttributes" => ["name"] },
|
517
|
+
{ searchableAttributes: ["name"] })).to eq(false)
|
518
|
+
|
519
|
+
expect(Color.send(:typesense_settings_changed?,
|
520
|
+
{ "searchableAttributes" => ["name"], "customRanking" => ["asc(hex)"] },
|
521
|
+
{ "customRanking" => ["asc(hex)"] })).to eq(false)
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
describe "Change detection" do
|
526
|
+
it "should detect attribute changes" do
|
527
|
+
color = Color.new name: "dark-blue", short_name: "blue", hex: 123
|
528
|
+
|
529
|
+
expect(Color.typesense_must_reindex?(color)).to eq(true)
|
530
|
+
color.save
|
531
|
+
expect(Color.typesense_must_reindex?(color)).to eq(false)
|
532
|
+
|
533
|
+
color.hex = 123_456
|
534
|
+
expect(Color.typesense_must_reindex?(color)).to eq(false)
|
535
|
+
|
536
|
+
color.not_indexed = "strstr"
|
537
|
+
expect(Color.typesense_must_reindex?(color)).to eq(false)
|
538
|
+
|
539
|
+
color.name = "red"
|
540
|
+
expect(Color.typesense_must_reindex?(color)).to eq(true)
|
541
|
+
end
|
542
|
+
it "should detect attribute changes even in a transaction" do
|
543
|
+
color = Color.new name: "dark-blue", short_name: "blue", hex: 123
|
544
|
+
color.save
|
545
|
+
|
546
|
+
expect(color.instance_variable_get("@typesense_must_reindex")).to be_nil
|
547
|
+
Color.transaction do
|
548
|
+
color.name = "red"
|
549
|
+
color.save
|
550
|
+
color.not_indexed = "strstr"
|
551
|
+
color.save
|
552
|
+
expect(color.instance_variable_get("@typesense_must_reindex")).to eq(true)
|
553
|
+
end
|
554
|
+
expect(color.instance_variable_get("@typesense_must_reindex")).to be_nil
|
555
|
+
|
556
|
+
color.delete
|
557
|
+
end
|
558
|
+
|
559
|
+
it "should detect change with typesense_dirty? method" do
|
560
|
+
ebook = Ebook.new name: "My life", author: "Myself", premium: false, released: true
|
561
|
+
|
562
|
+
# Check initial state - should need reindexing due to typesense_dirty? method
|
563
|
+
expect(Ebook.typesense_must_reindex?(ebook)).to eq(true)
|
564
|
+
|
565
|
+
# Set current time and published time where published_at < current_time
|
566
|
+
ebook.current_time = 10
|
567
|
+
ebook.published_at = 8
|
568
|
+
expect(Ebook.typesense_must_reindex?(ebook)).to eq(true)
|
569
|
+
|
570
|
+
# Change published_at to be after current_time
|
571
|
+
ebook.published_at = 12
|
572
|
+
expect(Ebook.typesense_must_reindex?(ebook)).to eq(false)
|
573
|
+
end
|
574
|
+
|
575
|
+
it "should know if the _changed? method is user-defined",
|
576
|
+
skip: Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f < 1.9 do
|
577
|
+
color = Color.new name: "dark-blue", short_name: "blue", hex: 123
|
578
|
+
|
579
|
+
expect do
|
580
|
+
Color.send(:automatic_changed_method?, color, :something_that_doesnt_exist)
|
581
|
+
end.to raise_error(ArgumentError)
|
582
|
+
|
583
|
+
expect(Color.send(:automatic_changed_method?, color, :name_changed?)).to eq(true)
|
584
|
+
expect(Color.send(:automatic_changed_method?, color, :hex_changed?)).to eq(false)
|
585
|
+
expect(Color.send(:automatic_changed_method?, color, :will_save_change_to_short_name?)).to eq(false)
|
586
|
+
|
587
|
+
if Color.send(:automatic_changed_method_deprecated?)
|
588
|
+
expect(Color.send(:automatic_changed_method?, color, :will_save_change_to_name?)).to eq(true)
|
589
|
+
expect(Color.send(:automatic_changed_method?, color, :will_save_change_to_hex?)).to eq(true)
|
590
|
+
end
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
describe "Namespaced::Model" do
|
595
|
+
before(:all) do
|
596
|
+
Namespaced::Model.clear_index!
|
597
|
+
rescue StandardError
|
598
|
+
ArgumentError
|
599
|
+
end
|
600
|
+
|
601
|
+
it "should have an index name without :: hierarchy" do
|
602
|
+
expect(Namespaced::Model.index_name.end_with?("Namespaced_Model")).to eq(true)
|
603
|
+
end
|
604
|
+
|
605
|
+
it "should use the block to determine attribute's value" do
|
606
|
+
m = Namespaced::Model.new(another_private_value: 2)
|
607
|
+
attributes = Namespaced::Model.typesense_settings.get_attributes(m)
|
608
|
+
expect(attributes["customAttr"]).to eq(42)
|
609
|
+
expect(attributes["myid"]).to eq(m.id)
|
610
|
+
end
|
611
|
+
|
612
|
+
it "should always update when there is no custom _changed? function" do
|
613
|
+
m = Namespaced::Model.new(another_private_value: 2)
|
614
|
+
m.save
|
615
|
+
results = Namespaced::Model.search("*", "", { "filter_by" => "customAttr:42" })
|
616
|
+
expect(results.size).to eq(1)
|
617
|
+
expect(results[0].id).to eq(m.id)
|
618
|
+
|
619
|
+
m.another_private_value = 5
|
620
|
+
m.save
|
621
|
+
|
622
|
+
results = Namespaced::Model.search("*", "", { "filter_by" => "customAttr:42" })
|
623
|
+
expect(results.size).to eq(0)
|
624
|
+
|
625
|
+
results = Namespaced::Model.search("*", "", { "filter_by" => "customAttr:45" })
|
626
|
+
expect(results.size).to eq(1)
|
627
|
+
expect(results[0].id).to eq(m.id)
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
describe "UniqUsers" do
|
632
|
+
before(:all) do
|
633
|
+
UniqUser.clear_index!
|
634
|
+
rescue StandardError
|
635
|
+
ArgumentError
|
636
|
+
end
|
637
|
+
|
638
|
+
it "should not use the id field" do
|
639
|
+
UniqUser.create name: "fooBar"
|
640
|
+
results = UniqUser.search("foo", "name")
|
641
|
+
expect(results.size).to eq(1)
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
describe "NestedItem" do
|
646
|
+
before(:all) do
|
647
|
+
NestedItem.clear_index!
|
648
|
+
rescue StandardError
|
649
|
+
ArgumentError
|
650
|
+
end
|
651
|
+
|
652
|
+
it "should fetch attributes unscoped" do
|
653
|
+
@i1 = NestedItem.create hidden: false
|
654
|
+
@i2 = NestedItem.create hidden: true
|
655
|
+
|
656
|
+
@i1.children << NestedItem.create(hidden: true) << NestedItem.create(hidden: true)
|
657
|
+
NestedItem.where(id: [@i1.id, @i2.id]).reindex!(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
658
|
+
|
659
|
+
result = NestedItem.retrieve_document(@i1.id)
|
660
|
+
expect(result["nb_children"]).to eq(2)
|
661
|
+
|
662
|
+
result = NestedItem.raw_search("*", "")
|
663
|
+
expect(result["found"]).to eq(1)
|
664
|
+
|
665
|
+
if @i2.respond_to? :update_attributes
|
666
|
+
@i2.update_attributes hidden: false
|
667
|
+
else
|
668
|
+
@i2.update hidden: false
|
669
|
+
end
|
670
|
+
|
671
|
+
result = NestedItem.raw_search("*", "")
|
672
|
+
expect(result["found"]).to eq(2)
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
describe "Colors" do
|
677
|
+
before(:all) do
|
678
|
+
Color.clear_index!
|
679
|
+
end
|
680
|
+
|
681
|
+
it "should detect predefined_fields" do
|
682
|
+
color = Color.create name: "dark-blue", hex: 123
|
683
|
+
expect(color.short_name).to be_nil
|
684
|
+
end
|
685
|
+
|
686
|
+
it "should auto index" do
|
687
|
+
@blue = Color.create!(name: "blue", short_name: "b", hex: 0xFF0000)
|
688
|
+
results = Color.search("blue", "name")
|
689
|
+
expect(results.size).to eq(1)
|
690
|
+
expect(results).to include(@blue)
|
691
|
+
end
|
692
|
+
|
693
|
+
it "should facet as well" do
|
694
|
+
results = Color.search("*", "", { "facet_by" => "name" })
|
695
|
+
|
696
|
+
expect(results.raw_answer).not_to be_nil
|
697
|
+
expect(results.raw_answer["facet_counts"]).not_to be_nil
|
698
|
+
expect(results.raw_answer["facet_counts"].size).to eq(1)
|
699
|
+
expect(results.raw_answer["facet_counts"][0]["counts"][0]["count"]).to eq(1)
|
700
|
+
end
|
701
|
+
|
702
|
+
it "should be raw searchable" do
|
703
|
+
results = Color.raw_search("blue", "name")
|
704
|
+
expect(results["hits"].size).to eq(1)
|
705
|
+
expect(results["found"]).to eq(1)
|
706
|
+
end
|
707
|
+
|
708
|
+
it "should not auto index if scoped" do
|
709
|
+
Color.without_auto_index do
|
710
|
+
Color.create!(name: "blue", short_name: "b", hex: 0xFF0000)
|
711
|
+
end
|
712
|
+
expect(Color.search("blue", "name").size).to eq(1)
|
713
|
+
Color.reindex!(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
714
|
+
expect(Color.search("blue", "name").size).to eq(2)
|
715
|
+
end
|
716
|
+
|
717
|
+
it "should not be searchable with non-indexed fields" do
|
718
|
+
@blue = Color.create!(name: "blue", short_name: "x", hex: 0xFF0000)
|
719
|
+
expect { Color.search("x", "short_name") }.to raise_error(Typesense::Error)
|
720
|
+
# expect(results.size).to eq(0)
|
721
|
+
end
|
722
|
+
|
723
|
+
it "should rank with default_sorting_field hex" do
|
724
|
+
@blue = Color.create!(name: "red", short_name: "r3", hex: 3)
|
725
|
+
@blue2 = Color.create!(name: "red", short_name: "r1", hex: 1)
|
726
|
+
@blue3 = Color.create!(name: "red", short_name: "r2", hex: 2)
|
727
|
+
results = Color.search("red", "name")
|
728
|
+
expect(results.size).to eq(3)
|
729
|
+
expect(results[0].hex).to eq(3)
|
730
|
+
expect(results[1].hex).to eq(2)
|
731
|
+
expect(results[2].hex).to eq(1)
|
732
|
+
end
|
733
|
+
|
734
|
+
it "should update the index if the attribute changed" do
|
735
|
+
@purple = Color.create!(name: "purple", short_name: "p", hex: 123)
|
736
|
+
expect(Color.search("purple", "name").size).to eq(1)
|
737
|
+
expect(Color.search("pink", "name").size).to eq(0)
|
738
|
+
@purple.name = "pink"
|
739
|
+
@purple.save
|
740
|
+
expect(Color.search("purple", "name").size).to eq(0)
|
741
|
+
expect(Color.search("pink", "name").size).to eq(1)
|
742
|
+
end
|
743
|
+
|
744
|
+
it "should use the specified scope" do
|
745
|
+
Color.clear_index!
|
746
|
+
Color.where(name: "red").reindex!(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
747
|
+
expect(Color.search("*", "").size).to eq(3)
|
748
|
+
Color.clear_index!
|
749
|
+
Color.where(id: Color.first.id).reindex!(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
750
|
+
expect(Color.search("*", "").size).to eq(1)
|
751
|
+
end
|
752
|
+
|
753
|
+
it "should have a Rails env-based index name" do
|
754
|
+
expect(Color.index_name).to eq(safe_index_name("Color") + "_#{Rails.env}")
|
755
|
+
end
|
756
|
+
|
757
|
+
it "should include the _highlightResult and _snippetResults" do
|
758
|
+
@green = Color.create!(name: "green", short_name: "gre", hex: 0x00FF00)
|
759
|
+
results = Color.search("green", "name", { "highlight_fields" => ["short_name"] })
|
760
|
+
expect(results.size).to eq(1)
|
761
|
+
expect(results[0].highlight_result).to_not be_nil
|
762
|
+
expect(results[0].snippet_result).to_not be_nil
|
763
|
+
end
|
764
|
+
|
765
|
+
it "should index an array of objects" do
|
766
|
+
json = Color.raw_search("*", "")
|
767
|
+
Color.index_objects Color.limit(1)
|
768
|
+
expect(json["found"]).to eq(Color.raw_search("*", "")["found"])
|
769
|
+
end
|
770
|
+
|
771
|
+
it "should not index non-saved object" do
|
772
|
+
expect { Color.new(name: "purple").index!(true) }.to raise_error(ArgumentError)
|
773
|
+
expect { Color.new(name: "purple").remove_from_index!(true) }.to raise_error(ArgumentError)
|
774
|
+
end
|
775
|
+
|
776
|
+
it "should reindex with a temporary index name based on custom index name & per_environment" do
|
777
|
+
Color.reindex
|
778
|
+
end
|
779
|
+
it "should process objects async" do
|
780
|
+
Color.clear_index!
|
781
|
+
|
782
|
+
colors = []
|
783
|
+
Color.without_auto_index do
|
784
|
+
colors = [
|
785
|
+
Color.create!(name: "red", short_name: "r", hex: 0xFF0000),
|
786
|
+
Color.create!(name: "green", short_name: "g", hex: 0x00FF00),
|
787
|
+
Color.create!(name: "blue", short_name: "b", hex: 0x0000FF),
|
788
|
+
]
|
789
|
+
end
|
790
|
+
|
791
|
+
expect(Typesense::ImportJob).to receive(:perform).exactly(1).times.and_call_original do |jsonl, collection_name, batch_size|
|
792
|
+
documents = jsonl.split("\n").map { |d| JSON.parse(d) }
|
793
|
+
expect(documents.length).to eq(3)
|
794
|
+
expect(documents.map { |d| d["name"] }).to contain_exactly("red", "green", "blue")
|
795
|
+
end
|
796
|
+
|
797
|
+
Color.typesense_index_objects_async(colors)
|
798
|
+
|
799
|
+
# Allow time for indexing to complete
|
800
|
+
sleep(1)
|
801
|
+
|
802
|
+
results = Color.search("*", "")
|
803
|
+
expect(results.size).to eq(3)
|
804
|
+
expect(results.map(&:name)).to contain_exactly("red", "green", "blue")
|
805
|
+
expect(results.map(&:hex)).to contain_exactly(0xFF0000, 0x00FF00, 0x0000FF)
|
806
|
+
end
|
807
|
+
end
|
808
|
+
|
809
|
+
describe "An imaginary store" do
|
810
|
+
before(:all) do
|
811
|
+
begin
|
812
|
+
Product.clear_index!
|
813
|
+
rescue StandardError
|
814
|
+
ArgumentError
|
815
|
+
end
|
816
|
+
# Google products
|
817
|
+
@blackberry = Product.create!(name: "blackberry", href: "google")
|
818
|
+
@nokia = Product.create!(name: "nokia", href: "google")
|
819
|
+
|
820
|
+
# Amazon products
|
821
|
+
@android = Product.create!(name: "android", href: "amazon")
|
822
|
+
@samsung = Product.create!(name: "samsung", href: "amazon")
|
823
|
+
@motorola = Product.create!(name: "motorola", href: "amazon",
|
824
|
+
description: "Not sure about features since I've never owned one.")
|
825
|
+
|
826
|
+
# Ebay products
|
827
|
+
@palmpre = Product.create!(name: "palmpre", href: "ebay")
|
828
|
+
@palm_pixi_plus = Product.create!(name: "palm pixi plus", href: "ebay")
|
829
|
+
@lg_vortex = Product.create!(name: "lg vortex", href: "ebay")
|
830
|
+
@t_mobile = Product.create!(name: "t mobile", href: "ebay")
|
831
|
+
|
832
|
+
# Yahoo products
|
833
|
+
@htc = Product.create!(name: "htc", href: "yahoo")
|
834
|
+
@htc_evo = Product.create!(name: "htc evo", href: "yahoo")
|
835
|
+
@ericson = Product.create!(name: "ericson", href: "yahoo")
|
836
|
+
|
837
|
+
# Apple products
|
838
|
+
@iphone = Product.create!(name: "iphone", href: "apple",
|
839
|
+
description: "Puts even more features at your fingertips")
|
840
|
+
|
841
|
+
# Unindexed products
|
842
|
+
@sekrit = Product.create!(name: "super sekrit", href: "amazon", release_date: Time.now + 1.day)
|
843
|
+
@no_href = Product.create!(name: "super sekrit too; missing href")
|
844
|
+
|
845
|
+
# Subproducts
|
846
|
+
@camera = Camera.create!(name: "canon eos rebel t3", href: "canon")
|
847
|
+
|
848
|
+
100.times do
|
849
|
+
Product.create!(name: "crapoola", href: "crappy")
|
850
|
+
end
|
851
|
+
|
852
|
+
@products_in_database = Product.all
|
853
|
+
|
854
|
+
Product.reindex(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
855
|
+
sleep 5
|
856
|
+
end
|
857
|
+
|
858
|
+
describe "pagination" do
|
859
|
+
it "should display total results correctly" do
|
860
|
+
results = Product.search("crapoola", "name", { "per_page" => Typesense::IndexSettings::DEFAULT_BATCH_SIZE })
|
861
|
+
expect(results.length).to eq(Product.where(name: "crapoola").count)
|
862
|
+
end
|
863
|
+
end
|
864
|
+
|
865
|
+
describe "basic searching" do
|
866
|
+
it "should find the iphone" do
|
867
|
+
results = Product.search("iphone", "name")
|
868
|
+
expect(results.size).to eq(1)
|
869
|
+
expect(results).to include(@iphone)
|
870
|
+
end
|
871
|
+
|
872
|
+
it "should search case insensitively" do
|
873
|
+
results = Product.search("IPHONE", "name")
|
874
|
+
expect(results.size).to eq(1)
|
875
|
+
expect(results).to include(@iphone)
|
876
|
+
end
|
877
|
+
|
878
|
+
it "should find all amazon products" do
|
879
|
+
results = Product.search("amazon", "href")
|
880
|
+
expect(results.size).to eq(3)
|
881
|
+
expect(results).to include(@android, @samsung, @motorola)
|
882
|
+
end
|
883
|
+
|
884
|
+
it 'should find all "palm" phones with wildcard word search' do
|
885
|
+
results = Product.search("pal", "name")
|
886
|
+
|
887
|
+
expect(results).to include(@palmpre, @palm_pixi_plus)
|
888
|
+
end
|
889
|
+
|
890
|
+
it "should search multiple words from the same field" do
|
891
|
+
results = Product.search("palm pixi plus", "name")
|
892
|
+
expect(results).to include(@palm_pixi_plus)
|
893
|
+
end
|
894
|
+
|
895
|
+
it "should narrow the results by searching across multiple fields" do
|
896
|
+
results = Product.search("apple iphone", "href,name")
|
897
|
+
expect(results.size).to eq(1)
|
898
|
+
expect(results).to include(@iphone)
|
899
|
+
end
|
900
|
+
|
901
|
+
it "should not search on non-indexed fields" do
|
902
|
+
expect { Product.search("features", "description") }.to raise_error(Typesense::Error)
|
903
|
+
end
|
904
|
+
|
905
|
+
it "should delete the associated record" do
|
906
|
+
@iphone.destroy
|
907
|
+
results = Product.search("iphone", "name")
|
908
|
+
expect(results.size).to eq(0)
|
909
|
+
end
|
910
|
+
|
911
|
+
it "should not throw an exception if a search result isn't found locally" do
|
912
|
+
Product.without_auto_index { @palmpre.destroy }
|
913
|
+
expect { Product.search("pal", "name").to_json }.to_not raise_error
|
914
|
+
end
|
915
|
+
|
916
|
+
it "should return the other results if those are still available locally" do
|
917
|
+
Product.without_auto_index { @palmpre.destroy }
|
918
|
+
results = Product.search("pal", "name")
|
919
|
+
expect(results).to include(@palm_pixi_plus)
|
920
|
+
end
|
921
|
+
|
922
|
+
it "should not duplicate an already indexed record" do
|
923
|
+
expect(Product.search("nokia", "name").size).to eq(1)
|
924
|
+
@nokia.index!
|
925
|
+
expect(Product.search("nokia", "name").size).to eq(1)
|
926
|
+
@nokia.index!
|
927
|
+
@nokia.index!
|
928
|
+
expect(Product.search("nokia", "name").size).to eq(1)
|
929
|
+
end
|
930
|
+
|
931
|
+
it "should not duplicate while reindexing" do
|
932
|
+
n = Product.search("*", "", { "per_page" => Typesense::IndexSettings::DEFAULT_BATCH_SIZE }).length
|
933
|
+
Product.reindex!(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
934
|
+
expect(Product.search("*", "", { "per_page" => Typesense::IndexSettings::DEFAULT_BATCH_SIZE }).size).to eq(n)
|
935
|
+
Product.reindex!(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
936
|
+
Product.reindex!(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
937
|
+
expect(Product.search("*", "", { "per_page" => Typesense::IndexSettings::DEFAULT_BATCH_SIZE }).size).to eq(n)
|
938
|
+
end
|
939
|
+
|
940
|
+
it "should not return products that are not indexable" do
|
941
|
+
@sekrit.index!
|
942
|
+
@no_href.index!
|
943
|
+
results = Product.search("sekrit", "name")
|
944
|
+
expect(results.size).to eq(0)
|
945
|
+
end
|
946
|
+
|
947
|
+
it "should include items belong to subclasses" do
|
948
|
+
@camera.index!
|
949
|
+
results = Product.search("eos rebel", "name")
|
950
|
+
expect(results).to include(@camera)
|
951
|
+
end
|
952
|
+
|
953
|
+
it "should delete a not-anymore-indexable product" do
|
954
|
+
results = Product.search("sekrit", "name")
|
955
|
+
expect(results.size).to eq(0)
|
956
|
+
|
957
|
+
@sekrit.release_date = Time.now - 1.day
|
958
|
+
@sekrit.save!
|
959
|
+
@sekrit.index!
|
960
|
+
results = Product.search("sekrit", "name")
|
961
|
+
expect(results.size).to eq(1)
|
962
|
+
|
963
|
+
@sekrit.release_date = Time.now + 1.day
|
964
|
+
@sekrit.save!
|
965
|
+
@sekrit.index!
|
966
|
+
results = Product.search("sekrit", "name")
|
967
|
+
expect(results.size).to eq(0)
|
968
|
+
end
|
969
|
+
|
970
|
+
it "should delete not-anymore-indexable product while reindexing" do
|
971
|
+
n = Product.search("*", "", { "per_page" => Typesense::IndexSettings::DEFAULT_BATCH_SIZE }).size
|
972
|
+
Product.where(release_date: nil).first.update_attribute :release_date, Time.now + 1.day
|
973
|
+
Product.reindex!(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
974
|
+
expect(Product.search("*", "",
|
975
|
+
{ "per_page" => Typesense::IndexSettings::DEFAULT_BATCH_SIZE }).size).to eq(n - 1)
|
976
|
+
end
|
977
|
+
|
978
|
+
it "should find using multi-way synonyms" do
|
979
|
+
expect(Product.search("galaxy", "name").size).to eq(Product.search("samsung", "name").size)
|
980
|
+
end
|
981
|
+
|
982
|
+
it "should find using one-way synonyms" do
|
983
|
+
expect(Product.search("smartphone", "name").size).to eq(4)
|
984
|
+
end
|
985
|
+
end
|
986
|
+
end
|
987
|
+
|
988
|
+
describe "Cities" do
|
989
|
+
before(:all) do
|
990
|
+
City.clear_index!
|
991
|
+
rescue StandardError
|
992
|
+
ArgumentError
|
993
|
+
end
|
994
|
+
|
995
|
+
it "should index geo" do
|
996
|
+
sf = City.create name: "San Francisco", country: "USA", lat: 37.75, lng: -122.68
|
997
|
+
mv = City.create name: "Mountain View", country: 'No man\'s land', lat: 37.38, lng: -122.08
|
998
|
+
sf_and_mv = City.create name: "San Francisco & Mountain View", country: "Hybrid", gl_array: [37.75, -122.08]
|
999
|
+
results = City.search("*", "", { "filter_by" => "location:(37.33, -121.89,50 km)" })
|
1000
|
+
expect(results.size).to eq(2)
|
1001
|
+
expect(results).to include(mv, sf_and_mv)
|
1002
|
+
|
1003
|
+
results = City.search("*", "", { "filter_by" => "location:(37.33, -121.89, 500 km)" })
|
1004
|
+
expect(results.size).to eq(3)
|
1005
|
+
expect(results).to include(mv)
|
1006
|
+
expect(results).to include(sf)
|
1007
|
+
expect(results).to include(sf_and_mv)
|
1008
|
+
end
|
1009
|
+
end
|
1010
|
+
|
1011
|
+
describe "MongoObject" do
|
1012
|
+
it "should not have method conflicts" do
|
1013
|
+
expect { MongoObject.reindex! }.to raise_error(NameError)
|
1014
|
+
expect { MongoObject.new.index! }.to raise_error(NameError)
|
1015
|
+
MongoObject.typesense_reindex!
|
1016
|
+
MongoObject.create(name: "mongo").typesense_index!
|
1017
|
+
end
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
describe "Book" do
|
1021
|
+
before(:all) do
|
1022
|
+
Book.clear_index!
|
1023
|
+
rescue StandardError
|
1024
|
+
ArgumentError
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
it "should sanitize attributes" do
|
1028
|
+
@hack = Book.create! name: '"><img src=x onerror=alert(1)> hack0r',
|
1029
|
+
author: '<script type="text/javascript">alert(1)</script>', premium: true, released: true
|
1030
|
+
b = Book.raw_search("hack", "name")
|
1031
|
+
expect(b["hits"].length).to eq(1)
|
1032
|
+
begin
|
1033
|
+
expect(b["hits"][0]["document"]["name"]).to eq('"> hack0r')
|
1034
|
+
expect(b["hits"][0]["document"]["author"]).to eq("")
|
1035
|
+
expect(b["hits"][0]["highlights"][0]["snippet"]).to eq('"> <mark>hack</mark>0r')
|
1036
|
+
rescue StandardError
|
1037
|
+
# rails 4.2's sanitizer
|
1038
|
+
begin
|
1039
|
+
expect(b["hits"][0]["document"]["name"]).to eq(""> hack0r")
|
1040
|
+
expect(b["hits"][0]["document"]["author"]).to eq("")
|
1041
|
+
expect(b["hits"][0]["highlights"][0]["snippet"]).to eq(""> <mark>hack0r</mark>")
|
1042
|
+
rescue StandardError
|
1043
|
+
# jruby
|
1044
|
+
expect(b["hits"][0]["document"]["name"]).to eq('"> hack0r')
|
1045
|
+
expect(b["hits"][0]["document"]["author"]).to eq("")
|
1046
|
+
expect(b["hits"][0]["highlights"][0]["snippet"]).to eq('"> <mark>hack0r</mark>')
|
1047
|
+
end
|
1048
|
+
end
|
1049
|
+
end
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
describe "Kaminari" do
|
1053
|
+
before(:all) do
|
1054
|
+
require "kaminari"
|
1055
|
+
Typesense.configuration = {
|
1056
|
+
nodes: [{
|
1057
|
+
host: "localhost", # For Typesense Cloud use xxx.a1.typesense.net
|
1058
|
+
port: 8108, # For Typesense Cloud use 443
|
1059
|
+
protocol: "http", # For Typesense Cloud use https
|
1060
|
+
}],
|
1061
|
+
api_key: "xyz",
|
1062
|
+
connection_timeout_seconds: 2,
|
1063
|
+
pagination_backend: :kaminari,
|
1064
|
+
}
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
it "should paginate" do
|
1068
|
+
pagination = City.search("*", "")
|
1069
|
+
expect(pagination.total_count).to eq(City.raw_search("*", "")["found"])
|
1070
|
+
p1 = City.search("*", "", { page: 1, per_page: 1 })
|
1071
|
+
expect(p1.size).to eq(1)
|
1072
|
+
expect(p1[0]).to eq(pagination[0])
|
1073
|
+
expect(p1.total_count).to eq(City.raw_search("*", "")["found"])
|
1074
|
+
p2 = City.search("*", "", { page: 2, per_page: 1 })
|
1075
|
+
expect(p2.size).to eq(1)
|
1076
|
+
expect(p2[0]).to eq(pagination[1])
|
1077
|
+
expect(p2.total_count).to eq(City.raw_search("*", "")["found"])
|
1078
|
+
end
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
describe "Will_paginate" do
|
1082
|
+
before(:all) do
|
1083
|
+
require "will_paginate"
|
1084
|
+
Typesense.configuration = {
|
1085
|
+
nodes: [{
|
1086
|
+
host: "localhost", # For Typesense Cloud use xxx.a1.typesense.net
|
1087
|
+
port: 8108, # For Typesense Cloud use 443
|
1088
|
+
protocol: "http", # For Typesense Cloud use https
|
1089
|
+
}],
|
1090
|
+
api_key: "xyz",
|
1091
|
+
connection_timeout_seconds: 2,
|
1092
|
+
pagination_backend: :will_paginate,
|
1093
|
+
}
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
it "should paginate" do
|
1097
|
+
p1 = City.search("*", "", { "per_page" => 2 })
|
1098
|
+
|
1099
|
+
expect(p1.length).to eq(2)
|
1100
|
+
expect(p1.per_page).to eq(2)
|
1101
|
+
expect(p1.total_entries).to eq(City.raw_search("*", "")["found"])
|
1102
|
+
end
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
describe "Disabled" do
|
1106
|
+
before(:all) do
|
1107
|
+
begin
|
1108
|
+
DisabledBoolean.clear_index!
|
1109
|
+
rescue StandardError
|
1110
|
+
ArgumentError
|
1111
|
+
end
|
1112
|
+
begin
|
1113
|
+
DisabledProc.clear_index!
|
1114
|
+
rescue StandardError
|
1115
|
+
ArgumentError
|
1116
|
+
end
|
1117
|
+
begin
|
1118
|
+
DisabledSymbol.clear_index!
|
1119
|
+
rescue StandardError
|
1120
|
+
ArgumentError
|
1121
|
+
end
|
1122
|
+
end
|
1123
|
+
|
1124
|
+
it "should disable the indexing using a boolean" do
|
1125
|
+
DisabledBoolean.create name: "foo"
|
1126
|
+
expect(DisabledBoolean.search("*", "").size).to eq(0)
|
1127
|
+
end
|
1128
|
+
|
1129
|
+
it "should disable the indexing using a proc" do
|
1130
|
+
DisabledProc.create name: "foo"
|
1131
|
+
expect(DisabledProc.search("*", "").size).to eq(0)
|
1132
|
+
end
|
1133
|
+
|
1134
|
+
it "should disable the indexing using a symbol" do
|
1135
|
+
DisabledSymbol.create name: "foo"
|
1136
|
+
expect(DisabledSymbol.search("*", "").size).to eq(0)
|
1137
|
+
end
|
1138
|
+
end
|
1139
|
+
|
1140
|
+
describe "NullableId" do
|
1141
|
+
before(:all) do
|
1142
|
+
end
|
1143
|
+
it "should not delete a null objectID" do
|
1144
|
+
NullableId.create!
|
1145
|
+
end
|
1146
|
+
end
|
1147
|
+
|
1148
|
+
describe "EnqueuedObject" do
|
1149
|
+
it "should enqueue a job" do
|
1150
|
+
expect do
|
1151
|
+
EnqueuedObject.create! name: "test"
|
1152
|
+
end.to raise_error("enqueued 1")
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
it "should not enqueue a job inside no index block" do
|
1156
|
+
expect do
|
1157
|
+
EnqueuedObject.without_auto_index do
|
1158
|
+
EnqueuedObject.create! name: "test"
|
1159
|
+
end
|
1160
|
+
end.not_to raise_error
|
1161
|
+
end
|
1162
|
+
end
|
1163
|
+
|
1164
|
+
describe "DisabledEnqueuedObject" do
|
1165
|
+
it "should not try to enqueue a job" do
|
1166
|
+
expect do
|
1167
|
+
DisabledEnqueuedObject.create! name: "test"
|
1168
|
+
end.not_to raise_error
|
1169
|
+
end
|
1170
|
+
end
|
1171
|
+
|
1172
|
+
describe "Misconfigured Block" do
|
1173
|
+
it "should force the typesense block" do
|
1174
|
+
expect do
|
1175
|
+
MisconfiguredBlock.reindex
|
1176
|
+
end.to raise_error(ArgumentError)
|
1177
|
+
end
|
1178
|
+
end
|