Module: ParadeDB::SearchMethods
- Defined in:
- lib/parade_db/search_methods.rb
Overview
SearchMethods extends ActiveRecord::Relation to add ParadeDB full-text search capabilities. This module is mixed into relations via .search() to provide chainable query methods.
Defined Under Namespace
Modules: AggregationRelation, FacetRelation, PredicateInspector Classes: AggregationQuery, FacetQuery
Constant Summary collapse
- AGGREGATE_SAFE_TEXT_TOKENIZERS =
%w[literal literal_normalized].freeze
- MLT_OPTION_ALIASES =
{ min_term_freq: :min_term_frequency, min_term_frequency: :min_term_frequency, max_query_terms: :max_query_terms, min_doc_freq: :min_doc_frequency, min_doc_frequency: :min_doc_frequency, max_doc_freq: :max_doc_frequency, max_doc_frequency: :max_doc_frequency, min_word_length: :min_word_length, max_word_length: :max_word_length, stopwords: :stopwords }.freeze
- MLT_INTEGER_OPTION_KEYS =
%i[ min_term_frequency max_query_terms min_doc_frequency max_doc_frequency min_word_length max_word_length ].freeze
- MLT_OPTION_ORDER =
%i[ min_term_frequency max_query_terms min_doc_frequency max_doc_frequency min_word_length max_word_length stopwords ].freeze
Instance Attribute Summary collapse
-
#_paradedb_current_field ⇒ Object
Internal state tracking.
-
#_paradedb_facet_fields ⇒ Object
Returns the value of attribute _paradedb_facet_fields.
Instance Method Summary collapse
-
#aggregate_by(*group_fields, exact: nil, **named_aggregations) ⇒ Object
Grouped ParadeDB aggregations: Product.search(:id).match_all.aggregate_by(:rating, agg: ParadeDB::Aggregations.value_count(:id)).
-
#build_facet_query(fields:, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil) ⇒ Object
Internal method to build facet query (for testing).
- #builder ⇒ Object
- #ensure_paradedb_predicate ⇒ Object
- #excluding(term) ⇒ Object
-
#exists ⇒ Object
Exists wrapper to match rows where the indexed field has a value.
-
#facets(*fields, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil) ⇒ Object
---- Facets ----.
- #facets_agg(exact: nil, **named_aggregations) ⇒ Object
- #has_paradedb_predicate? ⇒ Boolean
- #match_all(term = nil) ⇒ Object
- #match_any(term) ⇒ Object
- #more_like_this(key, fields: nil, **options) ⇒ Object
- #near(proximity) ⇒ Object
-
#nearest(column, vector, metric: nil) ⇒ Object
Orders by vector distance for Top-K pushdown inside the ParadeDB index.
-
#parse(query, lenient: nil, conjunction_mode: nil) ⇒ Object
Parse query-string syntax into ParadeDB query AST (e.g. "running AND shoes").
- #phrase(text) ⇒ Object
- #phrase_prefix(*terms, max_expansion: nil) ⇒ Object
- #primary_key ⇒ Object
-
#range(value = nil, gte: nil, gt: nil, lte: nil, lt: nil, type: nil) ⇒ Object
Range wrapper for numeric/date/timestamp fields in ParadeDB query context.
- #range_term(value, relation: nil, range_type: nil) ⇒ Object
- #regex(pattern) ⇒ Object
- #regex_phrase(*patterns, slop: nil, max_expansions: nil) ⇒ Object
-
#search(column) ⇒ Object
---- ParadeDB search entrypoints ----.
- #table_name ⇒ Object
- #term(value) ⇒ Object
- #term_set(*values) ⇒ Object
- #with_agg(exact: nil, **named_aggregations) ⇒ Object
- #with_facets(*fields, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil) ⇒ Object
-
#with_score ⇒ Object
---- Decorators ----.
- #with_snippet(column, start_tag: nil, end_tag: nil, max_chars: nil) ⇒ Object
- #with_snippet_positions(column, as: nil) ⇒ Object
- #with_snippets(column, start_tag: nil, end_tag: nil, max_chars: nil, limit: nil, offset: nil, sort_by: nil, as: nil) ⇒ Object
Instance Attribute Details
#_paradedb_current_field ⇒ Object
Internal state tracking
40 41 42 |
# File 'lib/parade_db/search_methods.rb', line 40 def _paradedb_current_field @_paradedb_current_field end |
#_paradedb_facet_fields ⇒ Object
Returns the value of attribute _paradedb_facet_fields.
41 42 43 |
# File 'lib/parade_db/search_methods.rb', line 41 def _paradedb_facet_fields @_paradedb_facet_fields end |
Instance Method Details
#aggregate_by(*group_fields, exact: nil, **named_aggregations) ⇒ Object
Grouped ParadeDB aggregations:
Product.search(:id).match_all.aggregate_by(:rating, agg: ParadeDB::Aggregations.value_count(:id))
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 |
# File 'lib/parade_db/search_methods.rb', line 401 def aggregate_by(*group_fields, exact: nil, **named_aggregations) ensure_paradedb_runtime! validate_exact_option!(exact) normalized_group_fields = normalize_group_fields(group_fields) agg_specs = normalize_named_aggregation_specs(named_aggregations) rel = self rel = rel.ensure_paradedb_predicate unless rel.has_paradedb_predicate? group_nodes = normalized_group_fields.map { |field| resolve_group_field_node(field) } aggregate_nodes = agg_specs.map do |alias_name, agg_spec| render_aggregation_node(agg_spec, exact: exact).as(alias_name.to_s) end rel.except(:select, :group).select(*group_nodes, *aggregate_nodes).group(*group_nodes) end |
#build_facet_query(fields:, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil) ⇒ Object
Internal method to build facet query (for testing)
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/parade_db/search_methods.rb', line 333 def build_facet_query(fields:, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil) ensure_paradedb_runtime! facet_args = normalize_facet_inputs(fields: fields, size: size, order: order, missing: missing, agg: agg) FacetQuery.build( relation: self, primary_key: paradedb_runtime_key_field, builder: builder, fields: facet_args[:fields], size: facet_args[:size], order: facet_args[:order], missing: facet_args[:missing], agg: facet_args[:agg], exact: exact, connection: connection ) end |
#builder ⇒ Object
89 90 91 92 93 94 |
# File 'lib/parade_db/search_methods.rb', line 89 def builder @_paradedb_builder ||= begin ensure_paradedb_runtime! QueryBuilder.new(table_name) end end |
#ensure_paradedb_predicate ⇒ Object
435 436 437 438 |
# File 'lib/parade_db/search_methods.rb', line 435 def ensure_paradedb_predicate # Add pdb.all() sentinel to force aggregate pushdown where(grouped(builder.match_all(paradedb_runtime_key_field))) end |
#excluding(term) ⇒ Object
137 138 139 140 141 142 |
# File 'lib/parade_db/search_methods.rb', line 137 def excluding(term) require_search_field! neg = builder.match(_paradedb_current_field, term) where(grouped(neg.not)) end |
#exists ⇒ Object
Exists wrapper to match rows where the indexed field has a value.
Use with .search(:id) (or another exists-compatible indexed field).
216 217 218 219 220 |
# File 'lib/parade_db/search_methods.rb', line 216 def exists require_search_field! where(grouped(builder.exists(_paradedb_current_field))) end |
#facets(*fields, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil) ⇒ Object
---- Facets ----
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/parade_db/search_methods.rb', line 309 def facets(*fields, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil) ensure_paradedb_runtime! validate_exact_option!(exact) if exact == false raise ArgumentError, "facets(exact: false) requires with_facets so aggregation runs as a window function" end build_facet_query( fields: fields, size: size, order: order, missing: missing, agg: agg, exact: exact ).execute end |
#facets_agg(exact: nil, **named_aggregations) ⇒ Object
326 327 328 329 330 |
# File 'lib/parade_db/search_methods.rb', line 326 def facets_agg(exact: nil, **named_aggregations) validate_exact_option!(exact) agg_specs = normalize_named_aggregation_specs(named_aggregations) build_aggregation_query(agg_specs, exact: exact).execute end |
#has_paradedb_predicate? ⇒ Boolean
431 432 433 |
# File 'lib/parade_db/search_methods.rb', line 431 def has_paradedb_predicate? PredicateInspector.relation_has_paradedb_predicate?(self) end |
#match_all(term = nil) ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/parade_db/search_methods.rb', line 118 def match_all(term = nil) require_search_field! node = if term.nil? builder.match_all(_paradedb_current_field) else builder.match(_paradedb_current_field, term) end where(grouped(node)) end |
#match_any(term) ⇒ Object
130 131 132 133 134 135 |
# File 'lib/parade_db/search_methods.rb', line 130 def match_any(term) require_search_field! node = builder.match_any(_paradedb_current_field, term) where(grouped(node)) end |
#more_like_this(key, fields: nil, **options) ⇒ Object
247 248 249 250 251 252 253 254 255 |
# File 'lib/parade_db/search_methods.rb', line 247 def more_like_this(key, fields: nil, **) ensure_paradedb_runtime! runtime_key_field = paradedb_runtime_key_field key_value = more_like_this_key_value(key, runtime_key_field) pk_node = builder[runtime_key_field] = () node = builder.more_like_this(pk_node, key_value, fields: fields, options: ) where(grouped(node)) end |
#near(proximity) ⇒ Object
184 185 186 187 188 189 |
# File 'lib/parade_db/search_methods.rb', line 184 def near(proximity) require_search_field! node = builder.near(_paradedb_current_field, proximity) where(grouped(node)) end |
#nearest(column, vector, metric: nil) ⇒ Object
Orders by vector distance for Top-K pushdown inside the ParadeDB index.
Adds key_field @@@ pdb.all() when the relation has no ParadeDB predicate,
since vector ordering requires a @@@ predicate to activate the index scan.
Callers must add .limit(k); the metric defaults to the index opclass metric.
422 423 424 425 426 427 428 429 |
# File 'lib/parade_db/search_methods.rb', line 422 def nearest(column, vector, metric: nil) ensure_paradedb_runtime! resolved_metric = metric || index_vector_metric(column) || ParadeDB::Vector::DEFAULT_METRIC node = builder.vector_distance(column, vector, metric: resolved_metric) rel = has_paradedb_predicate? ? self : ensure_paradedb_predicate rel.order(node.asc) end |
#parse(query, lenient: nil, conjunction_mode: nil) ⇒ Object
Parse query-string syntax into ParadeDB query AST (e.g. "running AND shoes").
203 204 205 206 207 208 209 210 211 212 |
# File 'lib/parade_db/search_methods.rb', line 203 def parse(query, lenient: nil, conjunction_mode: nil) require_search_field! node = builder.parse( _paradedb_current_field, query, lenient: lenient, conjunction_mode: conjunction_mode ) where(grouped(node)) end |
#phrase(text) ⇒ Object
144 145 146 147 148 149 |
# File 'lib/parade_db/search_methods.rb', line 144 def phrase(text) require_search_field! node = builder.phrase(_paradedb_current_field, text) where(grouped(node)) end |
#phrase_prefix(*terms, max_expansion: nil) ⇒ Object
191 192 193 194 195 196 197 198 199 200 |
# File 'lib/parade_db/search_methods.rb', line 191 def phrase_prefix(*terms, max_expansion: nil) require_search_field! node = builder.phrase_prefix( _paradedb_current_field, *terms, max_expansion: max_expansion ) where(grouped(node)) end |
#primary_key ⇒ Object
100 101 102 |
# File 'lib/parade_db/search_methods.rb', line 100 def primary_key klass.primary_key || :id end |
#range(value = nil, gte: nil, gt: nil, lte: nil, lt: nil, type: nil) ⇒ Object
Range wrapper for numeric/date/timestamp fields in ParadeDB query context. Examples:
Product.search(:rating).range(3..5)
Product.search(:rating).range(gte: 3, lt: 5)
226 227 228 229 230 231 232 |
# File 'lib/parade_db/search_methods.rb', line 226 def range(value = nil, gte: nil, gt: nil, lte: nil, lt: nil, type: nil) require_search_field! inferred_type = type || default_range_type_for_field(_paradedb_current_field) node = builder.range(_paradedb_current_field, value, gte: gte, gt: gt, lte: lte, lt: lt, type: inferred_type) where(grouped(node)) end |
#range_term(value, relation: nil, range_type: nil) ⇒ Object
234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/parade_db/search_methods.rb', line 234 def range_term(value, relation: nil, range_type: nil) require_search_field! inferred_range_type = range_type || (relation && infer_range_type_for_field(_paradedb_current_field)) node = builder.range_term( _paradedb_current_field, value, relation: relation, range_type: inferred_range_type ) where(grouped(node)) end |
#regex(pattern) ⇒ Object
151 152 153 154 155 156 |
# File 'lib/parade_db/search_methods.rb', line 151 def regex(pattern) require_search_field! node = builder.regex(_paradedb_current_field, pattern) where(grouped(node)) end |
#regex_phrase(*patterns, slop: nil, max_expansions: nil) ⇒ Object
158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/parade_db/search_methods.rb', line 158 def regex_phrase(*patterns, slop: nil, max_expansions: nil) require_search_field! node = builder.regex_phrase( _paradedb_current_field, *patterns, slop: slop, max_expansions: max_expansions ) where(grouped(node)) end |
#search(column) ⇒ Object
---- ParadeDB search entrypoints ----
106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/parade_db/search_methods.rb', line 106 def search(column) ensure_paradedb_runtime! search_column = if (column.is_a?(Symbol) || column.instance_of?(String)) && klass.respond_to?(:paradedb_normalize_search_column, true) klass.send(:paradedb_normalize_search_column, column) else column end extending(SearchMethods).tap { |rel| rel._paradedb_current_field = search_column } end |
#table_name ⇒ Object
96 97 98 |
# File 'lib/parade_db/search_methods.rb', line 96 def table_name klass.table_name end |
#term(value) ⇒ Object
170 171 172 173 174 175 |
# File 'lib/parade_db/search_methods.rb', line 170 def term(value) require_search_field! node = builder.term(_paradedb_current_field, value) where(grouped(node)) end |
#term_set(*values) ⇒ Object
177 178 179 180 181 182 |
# File 'lib/parade_db/search_methods.rb', line 177 def term_set(*values) require_search_field! node = builder.term_set(_paradedb_current_field, *values) where(grouped(node)) end |
#with_agg(exact: nil, **named_aggregations) ⇒ Object
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 |
# File 'lib/parade_db/search_methods.rb', line 380 def with_agg(exact: nil, **named_aggregations) ensure_paradedb_runtime! validate_exact_option!(exact) agg_specs = normalize_named_aggregation_specs(named_aggregations) rel = extending(FacetRelation, AggregationRelation) rel._paradedb_facet_fields = agg_specs.keys unless rel.has_paradedb_predicate? rel = rel.ensure_paradedb_predicate end facet_selects = agg_specs.map do |alias_name, agg_spec| render_aggregation_node(agg_spec, exact: exact).over.as("_#{alias_name}_facet") end rel = rel.select(klass.arel_table[::Arel.star]) if rel.select_values.empty? rel.select(*facet_selects) end |
#with_facets(*fields, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil) ⇒ Object
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 |
# File 'lib/parade_db/search_methods.rb', line 350 def with_facets(*fields, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil) ensure_paradedb_runtime! validate_exact_option!(exact) facet_args = normalize_facet_inputs(fields: fields, size: size, order: order, missing: missing, agg: agg) opts = { size: facet_args[:size], order: facet_args[:order], missing: facet_args[:missing], agg: facet_args[:agg] } facet_fields = facet_args[:agg].nil? ? facet_args[:fields] : [:agg] rel = extending(FacetRelation) rel._paradedb_facet_fields = facet_fields # Add pdb.all() if no ParadeDB predicates exist (for aggregate pushdown) unless rel.has_paradedb_predicate? rel = rel.ensure_paradedb_predicate end # Add window aggregates to SELECT using native Arel nodes. facet_selects = facet_fields.map do |field| json = facet_args[:agg] || facet_json(field, opts) builder.agg(json, exact: exact).over.as("_#{field}_facet") end rel = rel.select(klass.arel_table[::Arel.star]) if rel.select_values.empty? rel.select(*facet_selects) end |
#with_score ⇒ Object
---- Decorators ----
259 260 261 |
# File 'lib/parade_db/search_methods.rb', line 259 def with_score with_projection(builder.score(paradedb_runtime_key_field).as("search_score")) end |
#with_snippet(column, start_tag: nil, end_tag: nil, max_chars: nil) ⇒ Object
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
# File 'lib/parade_db/search_methods.rb', line 263 def with_snippet(column, start_tag: nil, end_tag: nil, max_chars: nil) formatted_args = [] formatted_args << start_tag unless start_tag.nil? formatted_args << end_tag unless end_tag.nil? formatted_args << Integer(max_chars) unless max_chars.nil? snippet = if formatted_args.empty? builder.snippet(column) else builder.snippet(column, *formatted_args) end with_projection(snippet.as("#{column}_snippet")) end |
#with_snippet_positions(column, as: nil) ⇒ Object
302 303 304 305 |
# File 'lib/parade_db/search_methods.rb', line 302 def with_snippet_positions(column, as: nil) positions = builder.snippet_positions(column) with_projection(positions.as(normalize_projection_alias(as, "#{column}_snippet_positions"))) end |
#with_snippets(column, start_tag: nil, end_tag: nil, max_chars: nil, limit: nil, offset: nil, sort_by: nil, as: nil) ⇒ Object
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 |
# File 'lib/parade_db/search_methods.rb', line 279 def with_snippets( column, start_tag: nil, end_tag: nil, max_chars: nil, limit: nil, offset: nil, sort_by: nil, as: nil ) snippets = builder.snippets( column, start_tag: start_tag, end_tag: end_tag, max_num_chars: normalize_integer_option!(max_chars, "max_chars"), limit: normalize_integer_option!(limit, "limit"), offset: normalize_integer_option!(offset, "offset"), sort_by: normalize_snippets_sort_by(sort_by) ) with_projection(snippets.as(normalize_projection_alias(as, "#{column}_snippets"))) end |