noiseless 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +54 -0
- data/lib/noiseless/adapter.rb +1 -6
- data/lib/noiseless/adapters/execution_modules/postgresql_execution.rb +2 -3
- data/lib/noiseless/adapters/postgresql.rb +1 -11
- data/lib/noiseless/bulk_importer.rb +1 -5
- data/lib/noiseless/document_manager.rb +1 -5
- data/lib/noiseless/model.rb +19 -71
- data/lib/noiseless/model_registry.rb +1 -7
- data/lib/noiseless/multi_search.rb +23 -111
- data/lib/noiseless/pagination.rb +11 -37
- data/lib/noiseless/query_builder.rb +11 -12
- data/lib/noiseless/query_delegation.rb +15 -0
- data/lib/noiseless/record_class_resolver.rb +15 -0
- data/lib/noiseless/response/records.rb +7 -2
- data/lib/noiseless/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 195219b6de1210cb1f888a5a322e41c624403b7cab4f07cbd700ca9f5f573196
|
|
4
|
+
data.tar.gz: b92048b781e491ae6c8f0d9114dbf4a4094ba04b7e06bf8394c3a6164e18521a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: afc592e3f9e505ff2272845eb4b8cddaae23543400fe83b1c11c70895fd0b8a537e670c1cc78103df93999f72f51cc0cdb8c008772a59e1e881df024e5a9cf24
|
|
7
|
+
data.tar.gz: 7b4a4113ea30314fbed156161dfa031344881ba17f422ac233cd5a92b91f2b63ff4f7e07282e3a993e98bf42d48d87813b8c4e3aea23ece3365695a1779b382e
|
data/README.md
CHANGED
|
@@ -114,6 +114,51 @@ end
|
|
|
114
114
|
|
|
115
115
|
For best performance, run independent searches concurrently within a single `Async` block rather than creating separate `Sync` blocks per search.
|
|
116
116
|
|
|
117
|
+
### Loading Database Records
|
|
118
|
+
|
|
119
|
+
Search classes return search results by default. To load database records, configure
|
|
120
|
+
the record model explicitly and request `response_type: :records`:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
class Company::Search < Noiseless::Model
|
|
124
|
+
index_name 'companies'
|
|
125
|
+
record_class 'Company' # A Class is also accepted; strings resolve lazily.
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
records = Company::Search.search_sync(response_type: :records) do |s|
|
|
129
|
+
s.match(:name, 'tech')
|
|
130
|
+
end
|
|
131
|
+
records.to_a # Company records in search relevance order
|
|
132
|
+
|
|
133
|
+
# The instance API supports the same option.
|
|
134
|
+
Company::Search.new.match(:name, 'tech').execute_sync(response_type: :records)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`record_class` is inherited and does not change the default response type. Index
|
|
138
|
+
names are not used to infer record classes: aliases and versioned indexes need
|
|
139
|
+
not match table names. Requesting `:records` without a model supporting `.where`
|
|
140
|
+
raises `ArgumentError`, even when the search has no hits. Records missing from
|
|
141
|
+
the database are omitted; search totals still describe backend hits.
|
|
142
|
+
|
|
143
|
+
### Pagination and Multi-model Search
|
|
144
|
+
|
|
145
|
+
The paginator executes lazily and waits for its response when results or totals
|
|
146
|
+
are accessed. Multi-search also waits and returns a `MultiSearchResponse`, not
|
|
147
|
+
an `Async::Task`. Both work inside an existing async context.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
page = Company::Search.page(2).per(10)
|
|
151
|
+
page.to_a
|
|
152
|
+
page.total_count
|
|
153
|
+
|
|
154
|
+
response = Noiseless.multi_search(models: [Company::Search, Product::Search]) do |s|
|
|
155
|
+
s.match(:name, 'tech')
|
|
156
|
+
s.limit(10)
|
|
157
|
+
end
|
|
158
|
+
response.results_for_model(Company::Search)
|
|
159
|
+
response.records_for_model(Company::Search) # Uses its configured record_class.
|
|
160
|
+
```
|
|
161
|
+
|
|
117
162
|
### Advanced Queries
|
|
118
163
|
|
|
119
164
|
```ruby
|
|
@@ -179,6 +224,15 @@ end
|
|
|
179
224
|
|
|
180
225
|
### Running Tests Locally
|
|
181
226
|
|
|
227
|
+
Run the pagination, multi-search, and record-loading unit tests without services:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
bundle exec ruby -Itest -Ilib -e 'ARGV.each { require_relative it }' \
|
|
231
|
+
test/{search_pagination,multi_search,record_response,active_record_response,postgresql_search_contract,query_contract}_test.rb
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
For the full integration suite:
|
|
235
|
+
|
|
182
236
|
```bash
|
|
183
237
|
docker compose up -d postgres elasticsearch opensearch typesense
|
|
184
238
|
bin/test
|
data/lib/noiseless/adapter.rb
CHANGED
|
@@ -227,12 +227,7 @@ module Noiseless
|
|
|
227
227
|
end
|
|
228
228
|
|
|
229
229
|
def build_pagination_hash(paginate_node)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
{
|
|
233
|
-
from: (paginate_node.page - 1) * paginate_node.per_page,
|
|
234
|
-
size: paginate_node.per_page
|
|
235
|
-
}
|
|
230
|
+
Pagination.query_options(paginate_node)
|
|
236
231
|
end
|
|
237
232
|
|
|
238
233
|
# Parses a backend HTTP response, raising when the backend reported an error.
|
|
@@ -189,9 +189,8 @@ module Noiseless
|
|
|
189
189
|
# Helper methods
|
|
190
190
|
|
|
191
191
|
def resolve_model(indexes, model_class = nil)
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
# fall through to index-name resolution for anything else.
|
|
192
|
+
# Respect explicit search-class mappings, including aliased indexes.
|
|
193
|
+
model_class = RecordClassResolver.resolve(model_class)
|
|
195
194
|
return model_class if active_record_model?(model_class)
|
|
196
195
|
|
|
197
196
|
index_name = indexes&.first
|
|
@@ -61,7 +61,7 @@ module Noiseless
|
|
|
61
61
|
raw_response,
|
|
62
62
|
model_class: model_class,
|
|
63
63
|
response_type: response_type,
|
|
64
|
-
query_hash:
|
|
64
|
+
query_hash: build_pagination_hash(ast_node.paginate)
|
|
65
65
|
)
|
|
66
66
|
end
|
|
67
67
|
end
|
|
@@ -156,16 +156,6 @@ module Noiseless
|
|
|
156
156
|
Rails.logger.error("Failed to detect PostgreSQL extensions: #{e.message}")
|
|
157
157
|
[]
|
|
158
158
|
end
|
|
159
|
-
|
|
160
|
-
def build_pagination_from_ast(ast_node)
|
|
161
|
-
paginate = ast_node.paginate
|
|
162
|
-
return { from: 0, size: 20 } unless paginate
|
|
163
|
-
|
|
164
|
-
{
|
|
165
|
-
from: (paginate.page - 1) * paginate.per_page,
|
|
166
|
-
size: paginate.per_page
|
|
167
|
-
}
|
|
168
|
-
end
|
|
169
159
|
end
|
|
170
160
|
end
|
|
171
161
|
end
|
|
@@ -157,11 +157,7 @@ module Noiseless
|
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
def index_name
|
|
160
|
-
@index_name ||=
|
|
161
|
-
Array(model_class.search_index).first
|
|
162
|
-
else
|
|
163
|
-
model_class.name.demodulize.underscore.pluralize
|
|
164
|
-
end
|
|
160
|
+
@index_name ||= QueryBuilder.indexes_for(model_class).first
|
|
165
161
|
end
|
|
166
162
|
|
|
167
163
|
def recreate_index
|
|
@@ -70,11 +70,7 @@ module Noiseless
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def index_name
|
|
73
|
-
@index_name ||=
|
|
74
|
-
Array(model_instance.class.search_index).first
|
|
75
|
-
else
|
|
76
|
-
model_instance.class.name.demodulize.underscore.pluralize
|
|
77
|
-
end
|
|
73
|
+
@index_name ||= QueryBuilder.indexes_for(model_instance.class).first
|
|
78
74
|
end
|
|
79
75
|
|
|
80
76
|
def supports_dirty_tracking?
|
data/lib/noiseless/model.rb
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
module Noiseless
|
|
4
4
|
class Model
|
|
5
5
|
extend DSL::ClassMethods
|
|
6
|
+
extend QueryDelegation
|
|
7
|
+
|
|
8
|
+
delegate_query_methods :match, :multi_match, :filter, :range, :sort, :limit, :offset, :paginate,
|
|
9
|
+
:aggregation, :geo_distance, :indexes, :remove_duplicates, :facet_sample_slope, :pinned_hits,
|
|
10
|
+
to: :@builder
|
|
11
|
+
|
|
12
|
+
# Explicit mapping: index names may be aliases, not record class names.
|
|
13
|
+
# String names are resolved on read so Rails can reload the record class.
|
|
14
|
+
def self.record_class(value = nil)
|
|
15
|
+
unless value.nil?
|
|
16
|
+
raise ArgumentError, "record_class must be a Class or a class name String" unless value.is_a?(Class) || value.is_a?(String)
|
|
17
|
+
|
|
18
|
+
return @record_class = value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
configured = @record_class
|
|
22
|
+
configured ||= superclass.record_class unless self == Model
|
|
23
|
+
configured.is_a?(String) ? configured.constantize : configured
|
|
24
|
+
end
|
|
6
25
|
|
|
7
26
|
def initialize
|
|
8
27
|
@builder = QueryBuilder.new(self.class)
|
|
@@ -23,77 +42,6 @@ module Noiseless
|
|
|
23
42
|
end
|
|
24
43
|
end
|
|
25
44
|
|
|
26
|
-
# Instance methods that delegate to the query builder
|
|
27
|
-
def match(field, value, **)
|
|
28
|
-
@builder.match(field, value, **)
|
|
29
|
-
self
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def multi_match(query, fields, **)
|
|
33
|
-
@builder.multi_match(query, fields, **)
|
|
34
|
-
self
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def filter(field, value, **)
|
|
38
|
-
@builder.filter(field, value, **)
|
|
39
|
-
self
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def range(field, gte: nil, lte: nil, gt: nil, lt: nil)
|
|
43
|
-
@builder.range(field, gte: gte, lte: lte, gt: gt, lt: lt)
|
|
44
|
-
self
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def sort(field, direction = :asc, **)
|
|
48
|
-
@builder.sort(field, direction, **)
|
|
49
|
-
self
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def limit(size)
|
|
53
|
-
@builder.limit(size)
|
|
54
|
-
self
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def offset(from)
|
|
58
|
-
@builder.offset(from)
|
|
59
|
-
self
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def paginate(**)
|
|
63
|
-
@builder.paginate(**)
|
|
64
|
-
self
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def aggregation(name, type, **)
|
|
68
|
-
@builder.aggregation(name, type, **)
|
|
69
|
-
self
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def geo_distance(field, lat:, lon:, distance:, **)
|
|
73
|
-
@builder.geo_distance(field, lat: lat, lon: lon, distance: distance, **)
|
|
74
|
-
self
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def indexes(names)
|
|
78
|
-
@builder.indexes(names)
|
|
79
|
-
self
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def remove_duplicates(value: true)
|
|
83
|
-
@builder.remove_duplicates(value: value)
|
|
84
|
-
self
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def facet_sample_slope(value)
|
|
88
|
-
@builder.facet_sample_slope(value)
|
|
89
|
-
self
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def pinned_hits(value)
|
|
93
|
-
@builder.pinned_hits(value)
|
|
94
|
-
self
|
|
95
|
-
end
|
|
96
|
-
|
|
97
45
|
def execute(connection: nil, response_type: nil)
|
|
98
46
|
client = Noiseless.connections.client(connection || self.class.connection)
|
|
99
47
|
ast = @builder.to_ast
|
|
@@ -17,7 +17,7 @@ module Noiseless
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
# Index models by their search index names
|
|
20
|
-
index_names =
|
|
20
|
+
index_names = QueryBuilder.indexes_for(model_class)
|
|
21
21
|
index_names.each do |index_name|
|
|
22
22
|
@models_by_index[index_name.to_sym] ||= []
|
|
23
23
|
@models_by_index[index_name.to_sym] << model_class
|
|
@@ -67,11 +67,5 @@ module Noiseless
|
|
|
67
67
|
@models.clear
|
|
68
68
|
@models_by_index.clear
|
|
69
69
|
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
def default_index_name(model_class)
|
|
74
|
-
model_class.name.demodulize.underscore.pluralize
|
|
75
|
-
end
|
|
76
70
|
end
|
|
77
71
|
end
|
|
@@ -2,72 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Noiseless
|
|
4
4
|
class MultiSearch
|
|
5
|
+
extend QueryDelegation
|
|
6
|
+
|
|
7
|
+
delegate_query_methods :match, :multi_match, :filter, :sort, :limit, :offset, :aggregation,
|
|
8
|
+
:geo_distance, :remove_duplicates, :facet_sample_slope, :pinned_hits,
|
|
9
|
+
to: :@builder
|
|
10
|
+
|
|
5
11
|
def initialize(models: nil, indexes: nil, connection: nil)
|
|
6
12
|
@models = resolve_models(models)
|
|
7
13
|
@indexes = resolve_indexes(indexes)
|
|
8
14
|
@connection = connection || Noiseless.config.default_connection
|
|
9
|
-
@builder = QueryBuilder.new(nil)
|
|
15
|
+
@builder = QueryBuilder.new(nil).indexes(@indexes)
|
|
10
16
|
end
|
|
11
17
|
|
|
12
18
|
def search(&block)
|
|
13
19
|
yield(@builder) if block
|
|
14
20
|
|
|
15
21
|
client = Noiseless.connections.client(@connection)
|
|
16
|
-
ast = @builder.to_ast
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
bool: ast.bool,
|
|
22
|
-
sort: ast.sort,
|
|
23
|
-
paginate: ast.paginate,
|
|
24
|
-
vector: ast.vector,
|
|
25
|
-
collapse: ast.collapse,
|
|
26
|
-
search_after: ast.search_after,
|
|
27
|
-
aggregations: ast.aggregations,
|
|
28
|
-
hybrid: ast.hybrid,
|
|
29
|
-
pipeline: ast.pipeline,
|
|
30
|
-
image_query: ast.image_query,
|
|
31
|
-
conversation: ast.conversation,
|
|
32
|
-
joins: ast.joins,
|
|
33
|
-
remove_duplicates: ast.remove_duplicates,
|
|
34
|
-
facet_sample_slope: ast.facet_sample_slope,
|
|
35
|
-
pinned_hits: ast.pinned_hits
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
raw_response = client.search(ast_with_indexes)
|
|
39
|
-
MultiSearchResponse.new(raw_response, @models, @indexes)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Delegate query building methods to the internal builder
|
|
43
|
-
def match(field, value, **)
|
|
44
|
-
@builder.match(field, value, **)
|
|
45
|
-
self
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def multi_match(query, fields, **)
|
|
49
|
-
@builder.multi_match(query, fields, **)
|
|
50
|
-
self
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def filter(field, value, **)
|
|
54
|
-
@builder.filter(field, value, **)
|
|
55
|
-
self
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def sort(field, direction = :asc, **)
|
|
59
|
-
@builder.sort(field, direction, **)
|
|
60
|
-
self
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def limit(size)
|
|
64
|
-
@builder.limit(size)
|
|
65
|
-
self
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def offset(from)
|
|
69
|
-
@builder.offset(from)
|
|
70
|
-
self
|
|
22
|
+
ast = @builder.indexes(@indexes).to_ast
|
|
23
|
+
response = Sync { client.search(ast, response_type: :results).wait }
|
|
24
|
+
MultiSearchResponse.new(response.response, @models, @indexes).tap do |result|
|
|
25
|
+
result.include_pagination_info(Pagination.query_options(ast.paginate))
|
|
26
|
+
end
|
|
71
27
|
end
|
|
72
28
|
|
|
73
29
|
def paginate(page, per_page)
|
|
@@ -75,31 +31,6 @@ module Noiseless
|
|
|
75
31
|
self
|
|
76
32
|
end
|
|
77
33
|
|
|
78
|
-
def aggregation(name, type, **)
|
|
79
|
-
@builder.aggregation(name, type, **)
|
|
80
|
-
self
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def geo_distance(field, lat:, lon:, distance:, **)
|
|
84
|
-
@builder.geo_distance(field, lat: lat, lon: lon, distance: distance, **)
|
|
85
|
-
self
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def remove_duplicates(value: true)
|
|
89
|
-
@builder.remove_duplicates(value: value)
|
|
90
|
-
self
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def facet_sample_slope(value)
|
|
94
|
-
@builder.facet_sample_slope(value)
|
|
95
|
-
self
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def pinned_hits(value)
|
|
99
|
-
@builder.pinned_hits(value)
|
|
100
|
-
self
|
|
101
|
-
end
|
|
102
|
-
|
|
103
34
|
private
|
|
104
35
|
|
|
105
36
|
def resolve_models(models)
|
|
@@ -125,9 +56,7 @@ module Noiseless
|
|
|
125
56
|
def resolve_indexes(indexes)
|
|
126
57
|
case indexes
|
|
127
58
|
when nil
|
|
128
|
-
@models.flat_map
|
|
129
|
-
Array(model.search_index || default_index_name(model))
|
|
130
|
-
end.uniq
|
|
59
|
+
@models.flat_map { |model| QueryBuilder.indexes_for(model) }.uniq
|
|
131
60
|
when Array
|
|
132
61
|
indexes.map(&:to_s)
|
|
133
62
|
when String, Symbol
|
|
@@ -136,10 +65,6 @@ module Noiseless
|
|
|
136
65
|
[]
|
|
137
66
|
end
|
|
138
67
|
end
|
|
139
|
-
|
|
140
|
-
def default_index_name(model_class)
|
|
141
|
-
model_class.name.demodulize.underscore.pluralize
|
|
142
|
-
end
|
|
143
68
|
end
|
|
144
69
|
|
|
145
70
|
# Multi-search response class
|
|
@@ -149,6 +74,10 @@ module Noiseless
|
|
|
149
74
|
@models = models
|
|
150
75
|
@indexes = indexes
|
|
151
76
|
@results_by_model = nil
|
|
77
|
+
@models_by_index = {}
|
|
78
|
+
models.each do |model|
|
|
79
|
+
QueryBuilder.indexes_for(model).each { |index| @models_by_index[index] ||= model }
|
|
80
|
+
end
|
|
152
81
|
end
|
|
153
82
|
|
|
154
83
|
def each(&)
|
|
@@ -203,17 +132,8 @@ module Noiseless
|
|
|
203
132
|
end
|
|
204
133
|
|
|
205
134
|
def determine_model_class(hit)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
# First, try to find models registered for this specific index
|
|
209
|
-
models_for_index = Noiseless.registry.models_for_index(index_name)
|
|
210
|
-
return models_for_index.first if models_for_index.size == 1
|
|
211
|
-
|
|
212
|
-
# If multiple models or none found, try to infer from index name
|
|
213
|
-
@models.find do |model|
|
|
214
|
-
model_indexes = Array(model.search_index || default_index_name(model))
|
|
215
|
-
model_indexes.include?(index_name)
|
|
216
|
-
end
|
|
135
|
+
# Only group into requested models, using the same index rules as search.
|
|
136
|
+
@models_by_index[hit["_index"]]
|
|
217
137
|
end
|
|
218
138
|
|
|
219
139
|
def load_records_by_model
|
|
@@ -221,22 +141,14 @@ module Noiseless
|
|
|
221
141
|
|
|
222
142
|
results_by_model.each do |model_key, model_hits|
|
|
223
143
|
model_class = @models.find { |m| m.name.to_sym == model_key }
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
ids = model_hits.map { |hit| hit["_id"] }
|
|
227
|
-
records_by_id = model_class.where(id: ids).index_by { it.id.to_s }
|
|
228
|
-
|
|
229
|
-
# Sort by search relevance
|
|
230
|
-
sorted_records = model_hits.filter_map { |hit| records_by_id[hit["_id"].to_s] }
|
|
144
|
+
record_class = RecordClassResolver.resolve(model_class)
|
|
145
|
+
next unless record_class.respond_to?(:where)
|
|
231
146
|
|
|
232
|
-
|
|
147
|
+
raw = { "hits" => { "hits" => model_hits, "total" => model_hits.size } }
|
|
148
|
+
records[model_key] = Response::Records.new(raw, record_class).to_a
|
|
233
149
|
end
|
|
234
150
|
|
|
235
151
|
records
|
|
236
152
|
end
|
|
237
|
-
|
|
238
|
-
def default_index_name(model_class)
|
|
239
|
-
model_class.name.demodulize.underscore.pluralize
|
|
240
|
-
end
|
|
241
153
|
end
|
|
242
154
|
end
|
data/lib/noiseless/pagination.rb
CHANGED
|
@@ -5,6 +5,12 @@ module Noiseless
|
|
|
5
5
|
DEFAULT_PER_PAGE = 20
|
|
6
6
|
MAX_PER_PAGE = 100
|
|
7
7
|
|
|
8
|
+
def self.query_options(paginate)
|
|
9
|
+
return { from: 0, size: DEFAULT_PER_PAGE } unless paginate
|
|
10
|
+
|
|
11
|
+
{ from: (paginate.page - 1) * paginate.per_page, size: paginate.per_page }
|
|
12
|
+
end
|
|
13
|
+
|
|
8
14
|
# Simple paginated array wrapper - no external dependencies
|
|
9
15
|
class PaginatedArray < Array
|
|
10
16
|
attr_accessor :current_page, :per_page, :total_count
|
|
@@ -131,6 +137,10 @@ module Noiseless
|
|
|
131
137
|
# Search paginator - builds and executes paginated queries
|
|
132
138
|
class SearchPaginator
|
|
133
139
|
include Enumerable
|
|
140
|
+
extend QueryDelegation
|
|
141
|
+
|
|
142
|
+
delegate_query_methods :match, :multi_match, :filter, :sort, :aggregation, :geo_distance, :vector,
|
|
143
|
+
to: :@query_builder
|
|
134
144
|
|
|
135
145
|
def initialize(model_class, page: 1, per_page: nil)
|
|
136
146
|
@model_class = model_class
|
|
@@ -213,42 +223,6 @@ module Noiseless
|
|
|
213
223
|
@results.to_a
|
|
214
224
|
end
|
|
215
225
|
|
|
216
|
-
# Query building delegation
|
|
217
|
-
def match(field, value, **)
|
|
218
|
-
@query_builder.match(field, value, **)
|
|
219
|
-
self
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def multi_match(query, fields, **)
|
|
223
|
-
@query_builder.multi_match(query, fields, **)
|
|
224
|
-
self
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def filter(field, value, **)
|
|
228
|
-
@query_builder.filter(field, value, **)
|
|
229
|
-
self
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def sort(field, direction = :asc, **)
|
|
233
|
-
@query_builder.sort(field, direction, **)
|
|
234
|
-
self
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def aggregation(name, type, **)
|
|
238
|
-
@query_builder.aggregation(name, type, **)
|
|
239
|
-
self
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def geo_distance(field, lat:, lon:, distance:, **)
|
|
243
|
-
@query_builder.geo_distance(field, lat: lat, lon: lon, distance: distance, **)
|
|
244
|
-
self
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def vector(field, embedding, **)
|
|
248
|
-
@query_builder.vector(field, embedding, **)
|
|
249
|
-
self
|
|
250
|
-
end
|
|
251
|
-
|
|
252
226
|
# Response access
|
|
253
227
|
def results
|
|
254
228
|
execute_search unless @executed
|
|
@@ -312,7 +286,7 @@ module Noiseless
|
|
|
312
286
|
|
|
313
287
|
client = Noiseless.connections.client(@model_class.connection)
|
|
314
288
|
ast = @query_builder.to_ast
|
|
315
|
-
@results = client.search(ast, model_class: @model_class)
|
|
289
|
+
@results = Sync { client.search(ast, model_class: @model_class).wait }
|
|
316
290
|
@total_count = @results.total
|
|
317
291
|
@executed = true
|
|
318
292
|
end
|
|
@@ -2,9 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Noiseless
|
|
4
4
|
class QueryBuilder
|
|
5
|
+
def self.indexes_for(model)
|
|
6
|
+
# Multi-search starts without a model and supplies its own indexes.
|
|
7
|
+
return [] if model.nil?
|
|
8
|
+
|
|
9
|
+
return Array(model.search_index) if model.respond_to?(:search_index) && model.search_index&.any?
|
|
10
|
+
return [model.index_name] if model.respond_to?(:index_name) && model.index_name
|
|
11
|
+
|
|
12
|
+
[model.name.demodulize.underscore.pluralize]
|
|
13
|
+
end
|
|
14
|
+
|
|
5
15
|
def initialize(model)
|
|
6
16
|
@model = model
|
|
7
|
-
@indexes =
|
|
17
|
+
@indexes = self.class.indexes_for(model)
|
|
8
18
|
@nodes = []
|
|
9
19
|
@aggregations = []
|
|
10
20
|
@collapse = nil
|
|
@@ -270,16 +280,5 @@ module Noiseless
|
|
|
270
280
|
raise ArgumentError, "pinned_hits must be a String, Hash, or Array of [id, position]"
|
|
271
281
|
end
|
|
272
282
|
end
|
|
273
|
-
|
|
274
|
-
def determine_indexes(model)
|
|
275
|
-
# Check for search_index (plural array) first
|
|
276
|
-
return Array(model.search_index) if model.respond_to?(:search_index) && model.search_index&.any?
|
|
277
|
-
|
|
278
|
-
# Check for index_name (singular string) next
|
|
279
|
-
return [model.index_name] if model.respond_to?(:index_name) && model.index_name
|
|
280
|
-
|
|
281
|
-
# Fallback to pluralized model name
|
|
282
|
-
[model.name.demodulize.underscore.pluralize]
|
|
283
|
-
end
|
|
284
283
|
end
|
|
285
284
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module QueryDelegation
|
|
5
|
+
# Unlike ordinary delegation, fluent query methods return their wrapper.
|
|
6
|
+
def delegate_query_methods(*names, to:)
|
|
7
|
+
names.each do |name|
|
|
8
|
+
define_method(name) do |*args, **options, &block|
|
|
9
|
+
instance_variable_get(to).public_send(name, *args, **options, &block)
|
|
10
|
+
self
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module RecordClassResolver
|
|
5
|
+
# Search definitions and database models are different kinds of classes.
|
|
6
|
+
# Only a Noiseless::Model uses the explicit record_class mapping.
|
|
7
|
+
def self.resolve(model_class)
|
|
8
|
+
if model_class.is_a?(Class) && model_class <= Model
|
|
9
|
+
model_class.record_class
|
|
10
|
+
else
|
|
11
|
+
model_class
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -4,7 +4,12 @@ module Noiseless
|
|
|
4
4
|
module Response
|
|
5
5
|
class Records < Base
|
|
6
6
|
def initialize(raw_response, model_class)
|
|
7
|
-
|
|
7
|
+
record_class = RecordClassResolver.resolve(model_class)
|
|
8
|
+
unless record_class.respond_to?(:where)
|
|
9
|
+
raise ArgumentError, "#{model_class || 'nil'} cannot load records; configure record_class on your Noiseless::Model search class"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
super(raw_response, record_class)
|
|
8
13
|
@records = nil
|
|
9
14
|
@record_hit_map = nil
|
|
10
15
|
end
|
|
@@ -62,7 +67,7 @@ module Noiseless
|
|
|
62
67
|
end
|
|
63
68
|
|
|
64
69
|
def load_records
|
|
65
|
-
return [] if hits.empty?
|
|
70
|
+
return [] if hits.empty?
|
|
66
71
|
|
|
67
72
|
# Extract IDs from hits
|
|
68
73
|
ids = hits.map { |hit| hit["_id"] }
|
data/lib/noiseless/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: noiseless
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -291,7 +291,9 @@ files:
|
|
|
291
291
|
- lib/noiseless/multi_search.rb
|
|
292
292
|
- lib/noiseless/pagination.rb
|
|
293
293
|
- lib/noiseless/query_builder.rb
|
|
294
|
+
- lib/noiseless/query_delegation.rb
|
|
294
295
|
- lib/noiseless/railtie.rb
|
|
296
|
+
- lib/noiseless/record_class_resolver.rb
|
|
295
297
|
- lib/noiseless/response.rb
|
|
296
298
|
- lib/noiseless/response/aggregations.rb
|
|
297
299
|
- lib/noiseless/response/empty.rb
|