typesensual 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +293 -0
- data/.ruby-version +1 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +111 -0
- data/LICENSE.txt +21 -0
- data/README.md +160 -0
- data/Rakefile +6 -0
- data/lib/tasks/typesensual.rake +24 -0
- data/lib/typesensual/callbacks.rb +22 -0
- data/lib/typesensual/collection.rb +202 -0
- data/lib/typesensual/config.rb +26 -0
- data/lib/typesensual/field.rb +52 -0
- data/lib/typesensual/index.rb +161 -0
- data/lib/typesensual/railtie.rb +9 -0
- data/lib/typesensual/rake_helper.rb +103 -0
- data/lib/typesensual/schema.rb +49 -0
- data/lib/typesensual/search/hit.rb +43 -0
- data/lib/typesensual/search/results.rb +51 -0
- data/lib/typesensual/search.rb +137 -0
- data/lib/typesensual/state_helpers.rb +14 -0
- data/lib/typesensual/version.rb +5 -0
- data/lib/typesensual.rb +41 -0
- metadata +114 -0
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'typesensual/schema'
|
4
|
+
require 'typesensual/state_helpers'
|
5
|
+
require 'typesensual/callbacks' if defined?(ActiveRecord)
|
6
|
+
|
7
|
+
class Typesensual
|
8
|
+
class Collection
|
9
|
+
include StateHelpers
|
10
|
+
|
11
|
+
# The pattern we use for collection names, `name:env@version`, where `name`
|
12
|
+
# is the name of the index, `env` is the environment, and `version` is the
|
13
|
+
# timestamp of the collection's creation. If a name doesn't follow this
|
14
|
+
# pattern, `name` collects everything.
|
15
|
+
COLLECTION_NAME_PATTERN = /^
|
16
|
+
(?<name>.*?) # the name of the collection cannot include : or @
|
17
|
+
(?::(?<env>.*?))? # the env is optional, but also cannot include : or @
|
18
|
+
(?:@(?<version>\d+))? # the version is also optional but must be an integer
|
19
|
+
$/x.freeze
|
20
|
+
|
21
|
+
include StateHelpers
|
22
|
+
|
23
|
+
# @overload initialize(collection)
|
24
|
+
# Initialize a new collection from a Typesense collection hash
|
25
|
+
#
|
26
|
+
# @param collection [Hash] the Typesense collection hash
|
27
|
+
# * `created_at` [Integer] the timestamp of the collection's creation
|
28
|
+
# * `default_sorting_field` [String] the default sorting field
|
29
|
+
# * `enable_nested_fields` [Boolean] whether nested fields are enabled
|
30
|
+
# * `fields` [Array<Hash>] the fields in the collection
|
31
|
+
# * `name` [String] the name of the collection
|
32
|
+
# * `num_documents` [Integer] the number of documents in the collection
|
33
|
+
# * `symbols_to_index` [String] the symbols to index
|
34
|
+
# * `token_separators` [String] the token separators
|
35
|
+
# @overload initialize(name)
|
36
|
+
# Initialize a new collection, loading info from Typesense
|
37
|
+
#
|
38
|
+
# @param name [String] the name of the collection
|
39
|
+
# @raise [Typesense::Error::ObjectNotFound] if the collection doesn't exist
|
40
|
+
def initialize(collection_or_name)
|
41
|
+
@collection = if collection_or_name.is_a?(Hash)
|
42
|
+
collection_or_name.deep_stringify_keys
|
43
|
+
else
|
44
|
+
client.collections[collection_or_name].retrieve
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Reload the underlying collection data from Typesense
|
49
|
+
# @return [self]
|
50
|
+
def reload
|
51
|
+
@collection = client.collections[name].retrieve
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
# The time the collection was created, as tracked by Typesense
|
56
|
+
# @return [Time] the time the collection was created
|
57
|
+
def created_at
|
58
|
+
@created_at ||= Time.strptime(@collection['created_at'].to_s, '%s')
|
59
|
+
end
|
60
|
+
|
61
|
+
# The default sorting field for the collection
|
62
|
+
# @return [String] the default sorting field
|
63
|
+
def default_sorting_field
|
64
|
+
@collection['default_sorting_field']
|
65
|
+
end
|
66
|
+
|
67
|
+
# Whether the collection has nested fields enabled
|
68
|
+
# @return [Boolean] whether nested fields are enabled
|
69
|
+
def enable_nested_fields?
|
70
|
+
@collection['enable_nested_fields']
|
71
|
+
end
|
72
|
+
|
73
|
+
# The fields in the collection
|
74
|
+
# @return [Array<Field>] the field information
|
75
|
+
def fields
|
76
|
+
@collection['fields'].map do |field|
|
77
|
+
Field.new(field)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# The raw, underlying name of the collection
|
82
|
+
# @return [String] the name of the collection
|
83
|
+
def name
|
84
|
+
@collection['name']
|
85
|
+
end
|
86
|
+
|
87
|
+
# The number of documents in the collection
|
88
|
+
# @return [Integer] the number of documents in the collection
|
89
|
+
def num_documents
|
90
|
+
@collection['num_documents']
|
91
|
+
end
|
92
|
+
|
93
|
+
# Special characters in strings which should be indexed as text
|
94
|
+
# @return [Array<String>] the symbols to index
|
95
|
+
def symbols_to_index
|
96
|
+
@collection['symbols_to_index']
|
97
|
+
end
|
98
|
+
|
99
|
+
# Additional characters to be treated as separators when indexing text
|
100
|
+
# @return [Array<String>] the token separators
|
101
|
+
def token_separators
|
102
|
+
@collection['token_separators']
|
103
|
+
end
|
104
|
+
|
105
|
+
# The name of the index, parsed from the Typesensual collection naming scheme.
|
106
|
+
# @see COLLECTION_NAME_PATTERN
|
107
|
+
# @return [String] the name of the index
|
108
|
+
def index_name
|
109
|
+
parsed_name['name']
|
110
|
+
end
|
111
|
+
|
112
|
+
# The environment the collection is in, parsed from the Typesensual collection
|
113
|
+
# naming scheme.
|
114
|
+
# @see COLLECTION_NAME_PATTERN
|
115
|
+
# @return [String] the environment the collection is in
|
116
|
+
def env
|
117
|
+
parsed_name['env']
|
118
|
+
end
|
119
|
+
|
120
|
+
# The version of the collection, parsed from the Typesensual collection naming
|
121
|
+
# scheme.
|
122
|
+
# @see COLLECTION_NAME_PATTERN
|
123
|
+
# @return [String] the version of the collection
|
124
|
+
def version
|
125
|
+
parsed_name['version']
|
126
|
+
end
|
127
|
+
|
128
|
+
# Creates the collection in Typesense
|
129
|
+
# @return [self]
|
130
|
+
def create!
|
131
|
+
client.collections.create(@collection)
|
132
|
+
self
|
133
|
+
end
|
134
|
+
|
135
|
+
# Deletes the collection in Typesense
|
136
|
+
# @return [void]
|
137
|
+
def delete!
|
138
|
+
typesense_collection.delete
|
139
|
+
end
|
140
|
+
|
141
|
+
# Create a new collection using the given collection hash
|
142
|
+
#
|
143
|
+
# @param collection [Hash] the Typesense collection hash
|
144
|
+
# * `default_sorting_field` [String] the default sorting field
|
145
|
+
# * `enable_nested_fields` [Boolean] whether nested fields are enabled
|
146
|
+
# * `fields` [Array<Hash>] the fields in the collection
|
147
|
+
# * `name` [String] the name of the collection
|
148
|
+
# * `symbols_to_index` [String] the symbols to index
|
149
|
+
# * `token_separators` [String] the token separators
|
150
|
+
# @return [Collection] the created collection
|
151
|
+
def self.create!(collection)
|
152
|
+
new(collection).tap(&:create!)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Insert a single document into typesense
|
156
|
+
#
|
157
|
+
# @param doc [Hash] the document to insert
|
158
|
+
# @return [Boolean] if the document was inserted successfully
|
159
|
+
def insert_one!(doc)
|
160
|
+
typesense_collection.documents.create(doc)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Insert many documents into typesense. Notably, the input can be an enumerable
|
164
|
+
# or enumerator, which will be batched into groups of `batch_size` and inserted
|
165
|
+
# with the ID of any failed rows being provided in the response.
|
166
|
+
#
|
167
|
+
# @param docs [Enumerable<Hash>] the documents to insert
|
168
|
+
# @return [Array<Hash>] any failed insertions
|
169
|
+
def insert_many!(docs, batch_size: 100)
|
170
|
+
docs.lazy.each_slice(batch_size).with_object([]) do |slice, failures|
|
171
|
+
results = typesense_collection.documents.import(slice, return_id: true)
|
172
|
+
failures.push(*results.reject { |result| result['success'] })
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Remove a single document from typesense
|
177
|
+
#
|
178
|
+
# @param id [String] the ID of the document to remove
|
179
|
+
# @return [void]
|
180
|
+
def remove_one!(id)
|
181
|
+
typesense_collection.documents[id.to_s].delete
|
182
|
+
end
|
183
|
+
|
184
|
+
def search(query:, query_by:)
|
185
|
+
Search.new(
|
186
|
+
collection: typesense_collection,
|
187
|
+
query: query,
|
188
|
+
query_by: query_by
|
189
|
+
)
|
190
|
+
end
|
191
|
+
|
192
|
+
def typesense_collection
|
193
|
+
@typesense_collection ||= client.collections[name]
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
def parsed_name
|
199
|
+
@parsed_name ||= name.match(COLLECTION_NAME_PATTERN)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Typesensual
|
4
|
+
class Config
|
5
|
+
attr_accessor :nodes, :api_key
|
6
|
+
attr_writer :env, :client
|
7
|
+
|
8
|
+
def initialize(&block)
|
9
|
+
yield self if block
|
10
|
+
end
|
11
|
+
|
12
|
+
def env
|
13
|
+
@env ||= (defined?(Rails) ? Rails.env : nil)
|
14
|
+
end
|
15
|
+
|
16
|
+
def client
|
17
|
+
@client ||= Typesense::Client.new(connection_options)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def connection_options
|
23
|
+
{ nodes: nodes, api_key: api_key }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Typesensual
|
4
|
+
class Field
|
5
|
+
def initialize(hash)
|
6
|
+
@field = hash.stringify_keys
|
7
|
+
end
|
8
|
+
|
9
|
+
def facet?
|
10
|
+
@field['facet']
|
11
|
+
end
|
12
|
+
|
13
|
+
def index?
|
14
|
+
@field['index']
|
15
|
+
end
|
16
|
+
|
17
|
+
def infix?
|
18
|
+
@field['infix']
|
19
|
+
end
|
20
|
+
|
21
|
+
def locale
|
22
|
+
@field['locale'].presence
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
if @field['name'].is_a?(Regexp)
|
27
|
+
@field['name'].source
|
28
|
+
else
|
29
|
+
@field['name'].to_s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def optional?
|
34
|
+
@field['optional']
|
35
|
+
end
|
36
|
+
|
37
|
+
def sort?
|
38
|
+
@field['sort']
|
39
|
+
end
|
40
|
+
|
41
|
+
def type
|
42
|
+
@field['type']
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_h
|
46
|
+
@field.to_h.merge!(
|
47
|
+
'name' => name,
|
48
|
+
'locale' => locale
|
49
|
+
).compact!
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'typesensual/schema'
|
4
|
+
require 'typesensual/state_helpers'
|
5
|
+
|
6
|
+
class Typesensual
|
7
|
+
# Represents your index as a set of collections in Typesense. Manages
|
8
|
+
# Typesense aliases, but with some extra functionality for managing multiple
|
9
|
+
# environments and versions, and a nice DSL for defining your schema.
|
10
|
+
#
|
11
|
+
# @example Defining an index
|
12
|
+
# class PostsIndex < Typesensual::Index
|
13
|
+
# # Optional, default is inferred from the class name
|
14
|
+
# index_name 'user_posts'
|
15
|
+
#
|
16
|
+
# schema do
|
17
|
+
# field 'text', type: 'string[]'
|
18
|
+
# field 'user', type: 'string', facet: true
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
class Index
|
22
|
+
include StateHelpers
|
23
|
+
|
24
|
+
# Get or set the name for this index
|
25
|
+
#
|
26
|
+
# @overload index_name(value)
|
27
|
+
# Explicitly set the index name
|
28
|
+
#
|
29
|
+
# @param value [String] the name to identify this index
|
30
|
+
# @return [void]
|
31
|
+
#
|
32
|
+
# @overload index_name
|
33
|
+
# Get the index name (either explicitly set or inferred from the class name)
|
34
|
+
#
|
35
|
+
# @return [String] the name of this index
|
36
|
+
def self.index_name(value = nil)
|
37
|
+
if value
|
38
|
+
@index_name = value
|
39
|
+
else
|
40
|
+
@index_name ||= name.underscore.sub(/_index$/, '')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# The alias name for this index in the current environment
|
45
|
+
#
|
46
|
+
# @return [String] the alias name
|
47
|
+
def self.alias_name
|
48
|
+
[index_name, env].compact.join(':')
|
49
|
+
end
|
50
|
+
|
51
|
+
# Generate a new collection name for the given version
|
52
|
+
#
|
53
|
+
# @param version [String] the version to generate the collection name for
|
54
|
+
# @return [String] the generated collection name
|
55
|
+
def self.collection_name_for(version: Time.now.strftime('%s'))
|
56
|
+
"#{alias_name}@#{version}"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Create a new collection for this index
|
60
|
+
#
|
61
|
+
# @param version [String] the version to create the collection for
|
62
|
+
# @return [Collection] the newly created collection
|
63
|
+
def self.create!(version: Time.now.strftime('%s'))
|
64
|
+
generated_name = collection_name_for(version: version)
|
65
|
+
|
66
|
+
Collection.create!(@schema.to_h.merge('name' => generated_name))
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get the collections for this index
|
70
|
+
#
|
71
|
+
# @return [Array<Collection>] the collections that match the alias
|
72
|
+
def self.collections
|
73
|
+
Typesensual.collections.filter do |collection|
|
74
|
+
collection.index_name == index_name
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.collection_for(version:)
|
79
|
+
Typesensual.collections.find do |collection|
|
80
|
+
collection.version == version
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Get the collection that the alias points to
|
85
|
+
#
|
86
|
+
# @return [Collection] the collection that the alias points to
|
87
|
+
def self.collection
|
88
|
+
@collection ||= Collection.new(alias_name)
|
89
|
+
rescue Typesense::Error::ObjectNotFound
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
|
93
|
+
# Define the schema for the collection
|
94
|
+
#
|
95
|
+
# See {Schema} for more information
|
96
|
+
def self.schema(&block)
|
97
|
+
@schema = Typesensual::Schema.new(&block)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Updates the alias to point to the given collection name
|
101
|
+
#
|
102
|
+
# @param name [String, Collection] the collection to point the alias to
|
103
|
+
def self.update_alias!(name_or_collection)
|
104
|
+
name = if name_or_collection.is_a?(Collection)
|
105
|
+
name_or_collection.name
|
106
|
+
else
|
107
|
+
name_or_collection
|
108
|
+
end
|
109
|
+
|
110
|
+
client.aliases.upsert(alias_name, collection_name: name)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Indexes the given records into a collection, then updates the alias to
|
114
|
+
# point to it.
|
115
|
+
#
|
116
|
+
# @param records [Enumerable] the records to index
|
117
|
+
# @param collection [Collection] the collection to index into, defaults to a
|
118
|
+
# new collection
|
119
|
+
def self.reindex!(ids, collection: create!)
|
120
|
+
index_many(ids, collection: collection)
|
121
|
+
|
122
|
+
update_alias!(collection)
|
123
|
+
end
|
124
|
+
|
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
|
+
def self.index_one(id, collection: self.collection)
|
131
|
+
collection.insert_one!(new.index_one(id))
|
132
|
+
end
|
133
|
+
|
134
|
+
# The method to implement to index *many* records
|
135
|
+
# Unlike {#index_one}, this method should yield successive records to index
|
136
|
+
#
|
137
|
+
# @yield [Hash] a document to upsert in Typesense
|
138
|
+
def index_many(ids)
|
139
|
+
ids.each do |id|
|
140
|
+
yield index_one(id)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.index_many(ids, collection: self.collection, batch_size: 100)
|
145
|
+
collection.insert_many!(
|
146
|
+
new.enum_for(:index_many, ids),
|
147
|
+
batch_size: batch_size
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.remove_one(id, collection: self.collection)
|
152
|
+
collection.remove_one!(id)
|
153
|
+
end
|
154
|
+
|
155
|
+
if defined?(ActiveRecord)
|
156
|
+
def self.ar_callbacks
|
157
|
+
Typesensual::Callbacks.new(self)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'paint'
|
4
|
+
|
5
|
+
class Typesensual
|
6
|
+
class RakeHelper
|
7
|
+
HEADER_FORMAT = Paint["==> %s\n", :bold]
|
8
|
+
LIST_ROW_FORMAT = "%<prefix>4s %<version>-20s %<created_at>-20s %<documents>-20s\n"
|
9
|
+
LIST_HEADER = Paint[format(
|
10
|
+
LIST_ROW_FORMAT,
|
11
|
+
prefix: '', version: 'Version', created_at: 'Created At', documents: 'Documents'
|
12
|
+
), :bold, :underline]
|
13
|
+
|
14
|
+
class << self
|
15
|
+
# List the collections in all Index descendants
|
16
|
+
#
|
17
|
+
# @param output [IO] The output stream to write to
|
18
|
+
# @example
|
19
|
+
# rake typesensual:list
|
20
|
+
def list(output: $stdout)
|
21
|
+
# Build up a hash of indices and their (sorted) collections
|
22
|
+
indices = Index.descendants.to_h do |index|
|
23
|
+
[index, index.collections.sort_by(&:created_at).reverse]
|
24
|
+
end
|
25
|
+
|
26
|
+
indices.each do |index, collections|
|
27
|
+
alias_name = index.collection.name
|
28
|
+
|
29
|
+
output.printf(HEADER_FORMAT, index.name.titleize)
|
30
|
+
output.printf(LIST_HEADER)
|
31
|
+
|
32
|
+
collections.each do |collection|
|
33
|
+
output.printf(LIST_ROW_FORMAT,
|
34
|
+
prefix: collection.name == alias_name ? '->' : '',
|
35
|
+
version: collection.version,
|
36
|
+
created_at: collection.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
37
|
+
documents: collection.num_documents)
|
38
|
+
end
|
39
|
+
|
40
|
+
output.printf("\n")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Index all records from a model into an index
|
45
|
+
#
|
46
|
+
# @param index [String] The name of the index to index into
|
47
|
+
# @param model [String] The name of the model to index from
|
48
|
+
# @example
|
49
|
+
# rake typesensual:index[FooIndex,Foo]
|
50
|
+
def index(index:, model:, output: $stdout)
|
51
|
+
index = index.safe_constantize
|
52
|
+
model = model.safe_constantize
|
53
|
+
|
54
|
+
collection = index.create!
|
55
|
+
output.printf(
|
56
|
+
Paint["==> Indexing %<model>s into %<index>s (Version %<version>s)\n", :bold],
|
57
|
+
model: model.name,
|
58
|
+
index: index.name,
|
59
|
+
version: collection.version
|
60
|
+
)
|
61
|
+
failures = index.index_many(
|
62
|
+
model.ids,
|
63
|
+
collection: collection
|
64
|
+
)
|
65
|
+
|
66
|
+
failures.each do |failure|
|
67
|
+
output.puts(failure.to_json)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Update the alias for an index to point to a specific version
|
72
|
+
#
|
73
|
+
# @param index [String] The name of the index to update
|
74
|
+
# @param version [String] The version to update the alias to
|
75
|
+
# @example
|
76
|
+
# rake typesensual:update_alias[FooIndex,1]
|
77
|
+
def update_alias(index:, version:, output: $stdout)
|
78
|
+
index = index.safe_constantize
|
79
|
+
old_coll = index.collection
|
80
|
+
new_coll = index.collection_for(version: version)
|
81
|
+
|
82
|
+
unless new_coll
|
83
|
+
output.puts(Paint["--> No such version #{version} for #{index.name}", :bold])
|
84
|
+
return
|
85
|
+
end
|
86
|
+
|
87
|
+
output.puts(Paint["==> Alias for #{index.name}", :bold])
|
88
|
+
output.printf(
|
89
|
+
"Old: %<version>s (%<created_at>s)\n",
|
90
|
+
version: old_coll&.version || 'None',
|
91
|
+
created_at: old_coll&.created_at&.strftime('%Y-%m-%d %H:%M:%S') || 'N/A'
|
92
|
+
)
|
93
|
+
index.update_alias!(new_coll)
|
94
|
+
|
95
|
+
output.printf(
|
96
|
+
"New: %<version>s (%<created_at>s)\n",
|
97
|
+
version: new_coll.version,
|
98
|
+
created_at: new_coll.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
99
|
+
)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'typesensual/field'
|
4
|
+
|
5
|
+
class Typesensual
|
6
|
+
class Schema
|
7
|
+
def initialize(&block)
|
8
|
+
instance_eval(&block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def field(name, type: 'auto', locale: nil, facet: nil, index: nil, optional: nil)
|
12
|
+
@fields ||= []
|
13
|
+
@fields << Field.new(
|
14
|
+
name: name,
|
15
|
+
type: type,
|
16
|
+
locale: locale,
|
17
|
+
facet: facet,
|
18
|
+
index: index,
|
19
|
+
optional: optional
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def token_separators(*separators)
|
24
|
+
@token_separators = separators
|
25
|
+
end
|
26
|
+
|
27
|
+
def symbols_to_index(*symbols)
|
28
|
+
@symbols_to_index = symbols
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_sorting_field(field_name)
|
32
|
+
@default_sorting_field = field_name.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def enable_nested_fields(value = true) # rubocop:disable Style/OptionalBooleanParameter
|
36
|
+
@enable_nested_fields = value
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_h
|
40
|
+
{
|
41
|
+
'fields' => @fields&.map(&:to_h),
|
42
|
+
'token_separators' => @token_separators,
|
43
|
+
'symbols_to_index' => @symbols_to_index,
|
44
|
+
'default_sorting_field' => @default_sorting_field,
|
45
|
+
'enable_nested_fields' => @enable_nested_fields
|
46
|
+
}.compact!
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Typesensual
|
4
|
+
class Search
|
5
|
+
class Hit
|
6
|
+
# {
|
7
|
+
# "highlights": [
|
8
|
+
# {
|
9
|
+
# "field": "company_name",
|
10
|
+
# "matched_tokens": ["Stark"],
|
11
|
+
# "snippet": "<mark>Stark</mark> Industries"
|
12
|
+
# }
|
13
|
+
# ],
|
14
|
+
# "document": {
|
15
|
+
# "id": "124",
|
16
|
+
# "company_name": "Stark Industries",
|
17
|
+
# "num_employees": 5215,
|
18
|
+
# "country": "USA"
|
19
|
+
# },
|
20
|
+
# "text_match": 130916
|
21
|
+
# }
|
22
|
+
# @param collection [Hash] the Typesense hit hash
|
23
|
+
# * `highlights` [Array<Hash>] the highlights for the hit
|
24
|
+
# * `document` [Hash] the matching document
|
25
|
+
# * `text_match` [Integer] the text matching score
|
26
|
+
def initialize(hit)
|
27
|
+
@hit = hit
|
28
|
+
end
|
29
|
+
|
30
|
+
def highlights
|
31
|
+
@hit['highlights']
|
32
|
+
end
|
33
|
+
|
34
|
+
def document
|
35
|
+
@hit['document']
|
36
|
+
end
|
37
|
+
|
38
|
+
def score
|
39
|
+
@hit['text_match']
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Typesensual
|
4
|
+
class Search
|
5
|
+
class Results
|
6
|
+
def initialize(results)
|
7
|
+
@results = results
|
8
|
+
end
|
9
|
+
|
10
|
+
def hits
|
11
|
+
@results['hits'].map { |hit| Hit.new(hit) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def count
|
15
|
+
@results['found']
|
16
|
+
end
|
17
|
+
|
18
|
+
def out_of
|
19
|
+
@results['out_of']
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_page
|
23
|
+
@results['page']
|
24
|
+
end
|
25
|
+
|
26
|
+
def first_page?
|
27
|
+
current_page == 1
|
28
|
+
end
|
29
|
+
|
30
|
+
def last_page?
|
31
|
+
current_page == total_pages
|
32
|
+
end
|
33
|
+
|
34
|
+
def prev_page
|
35
|
+
current_page - 1 unless first_page?
|
36
|
+
end
|
37
|
+
|
38
|
+
def next_page
|
39
|
+
current_page + 1 unless last_page?
|
40
|
+
end
|
41
|
+
|
42
|
+
def search_time_ms
|
43
|
+
@results['search_time_ms']
|
44
|
+
end
|
45
|
+
|
46
|
+
def total_pages
|
47
|
+
(@results['found'] / @results['per_page'].to_f).ceil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|