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(boost: nil, constant_score: nil) ⇒ 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, tokenizer: nil, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil, constant_score: nil) ⇒ Object
- #match_any(term, tokenizer: nil, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil, constant_score: nil) ⇒ Object
- #more_like_this(key, fields: nil, **options) ⇒ Object
- #near(proximity, boost: nil, const: nil) ⇒ Object
-
#parse(query, lenient: nil, conjunction_mode: nil, boost: nil, constant_score: nil) ⇒ Object
Parse query-string syntax into ParadeDB query AST (e.g. "running AND shoes").
- #phrase(text, slop: nil, tokenizer: nil, boost: nil, constant_score: nil) ⇒ Object
- #phrase_prefix(*terms, max_expansion: nil, boost: nil, constant_score: nil) ⇒ Object
- #primary_key ⇒ Object
-
#range(value = nil, gte: nil, gt: nil, lte: nil, lt: nil, type: nil, boost: nil, constant_score: nil) ⇒ Object
Range wrapper for numeric/date/timestamp fields in ParadeDB query context.
- #range_term(value, relation: nil, range_type: nil, boost: nil, constant_score: nil) ⇒ Object
- #regex(pattern, boost: nil, constant_score: nil) ⇒ Object
- #regex_phrase(*patterns, slop: nil, max_expansions: nil, boost: nil, constant_score: nil) ⇒ Object
-
#search(column) ⇒ Object
---- ParadeDB search entrypoints ----.
- #table_name ⇒ Object
- #term(value, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil, constant_score: nil) ⇒ Object
- #term_set(*values, boost: nil, constant_score: nil) ⇒ 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))
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 |
# File 'lib/parade_db/search_methods.rb', line 465 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)
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 |
# File 'lib/parade_db/search_methods.rb', line 397 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! ParadeDB::Arel::Builder.new(table_name) end end |
#ensure_paradedb_predicate ⇒ Object
486 487 488 489 |
# File 'lib/parade_db/search_methods.rb', line 486 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
171 172 173 174 175 176 |
# File 'lib/parade_db/search_methods.rb', line 171 def excluding(term) require_search_field! neg = builder.match(_paradedb_current_field, term) where(grouped(neg.not)) end |
#exists(boost: nil, constant_score: nil) ⇒ Object
Exists wrapper to match rows where the indexed field has a value.
Use with .search(:id) (or another exists-compatible indexed field).
278 279 280 281 282 |
# File 'lib/parade_db/search_methods.rb', line 278 def exists(boost: nil, constant_score: nil) require_search_field! where(grouped(builder.exists(_paradedb_current_field, boost: boost, constant_score: constant_score))) end |
#facets(*fields, size: 10, order: :count_desc, missing: nil, agg: nil, exact: nil) ⇒ Object
---- Facets ----
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 |
# File 'lib/parade_db/search_methods.rb', line 373 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
390 391 392 393 394 |
# File 'lib/parade_db/search_methods.rb', line 390 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
482 483 484 |
# File 'lib/parade_db/search_methods.rb', line 482 def has_paradedb_predicate? PredicateInspector.relation_has_paradedb_predicate?(self) end |
#match_all(term = nil, tokenizer: nil, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil, constant_score: nil) ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/parade_db/search_methods.rb', line 118 def match_all( term = nil, tokenizer: nil, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil, constant_score: nil ) require_search_field! node = if term.nil? && tokenizer.nil? && distance.nil? && prefix.nil? && transposition_cost_one.nil? builder.match_all(_paradedb_current_field, boost: boost, constant_score: constant_score) else builder.match( _paradedb_current_field, term, tokenizer: tokenizer, distance: distance, prefix: prefix, transposition_cost_one: transposition_cost_one, boost: boost, constant_score: constant_score ) end where(grouped(node)) end |
#match_any(term, tokenizer: nil, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil, constant_score: nil) ⇒ Object
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/parade_db/search_methods.rb', line 147 def match_any( term, tokenizer: nil, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil, constant_score: nil ) require_search_field! node = builder.match_any( _paradedb_current_field, term, tokenizer: tokenizer, distance: distance, prefix: prefix, transposition_cost_one: transposition_cost_one, boost: boost, constant_score: constant_score ) where(grouped(node)) end |
#more_like_this(key, fields: nil, **options) ⇒ Object
311 312 313 314 315 316 317 318 319 |
# File 'lib/parade_db/search_methods.rb', line 311 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, boost: nil, const: nil) ⇒ Object
242 243 244 245 246 247 |
# File 'lib/parade_db/search_methods.rb', line 242 def near(proximity, boost: nil, const: nil) require_search_field! node = builder.near(_paradedb_current_field, proximity, boost: boost, const: const) where(grouped(node)) end |
#parse(query, lenient: nil, conjunction_mode: nil, boost: nil, constant_score: nil) ⇒ Object
Parse query-string syntax into ParadeDB query AST (e.g. "running AND shoes").
263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/parade_db/search_methods.rb', line 263 def parse(query, lenient: nil, conjunction_mode: nil, boost: nil, constant_score: nil) require_search_field! node = builder.parse( _paradedb_current_field, query, lenient: lenient, conjunction_mode: conjunction_mode, boost: boost, constant_score: constant_score ) where(grouped(node)) end |
#phrase(text, slop: nil, tokenizer: nil, boost: nil, constant_score: nil) ⇒ Object
178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/parade_db/search_methods.rb', line 178 def phrase(text, slop: nil, tokenizer: nil, boost: nil, constant_score: nil) require_search_field! node = builder.phrase( _paradedb_current_field, text, slop: slop, tokenizer: tokenizer, boost: boost, constant_score: constant_score ) where(grouped(node)) end |
#phrase_prefix(*terms, max_expansion: nil, boost: nil, constant_score: nil) ⇒ Object
249 250 251 252 253 254 255 256 257 258 259 260 |
# File 'lib/parade_db/search_methods.rb', line 249 def phrase_prefix(*terms, max_expansion: nil, boost: nil, constant_score: nil) require_search_field! node = builder.phrase_prefix( _paradedb_current_field, *terms, max_expansion: max_expansion, boost: boost, constant_score: constant_score ) 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, boost: nil, constant_score: 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)
288 289 290 291 292 293 294 |
# File 'lib/parade_db/search_methods.rb', line 288 def range(value = nil, gte: nil, gt: nil, lte: nil, lt: nil, type: nil, boost: nil, constant_score: 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, boost: boost, constant_score: constant_score) where(grouped(node)) end |
#range_term(value, relation: nil, range_type: nil, boost: nil, constant_score: nil) ⇒ Object
296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
# File 'lib/parade_db/search_methods.rb', line 296 def range_term(value, relation: nil, range_type: nil, boost: nil, constant_score: 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, boost: boost, constant_score: constant_score ) where(grouped(node)) end |
#regex(pattern, boost: nil, constant_score: nil) ⇒ Object
192 193 194 195 196 197 |
# File 'lib/parade_db/search_methods.rb', line 192 def regex(pattern, boost: nil, constant_score: nil) require_search_field! node = builder.regex(_paradedb_current_field, pattern, boost: boost, constant_score: constant_score) where(grouped(node)) end |
#regex_phrase(*patterns, slop: nil, max_expansions: nil, boost: nil, constant_score: nil) ⇒ Object
199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/parade_db/search_methods.rb', line 199 def regex_phrase(*patterns, slop: nil, max_expansions: nil, boost: nil, constant_score: nil) require_search_field! node = builder.regex_phrase( _paradedb_current_field, *patterns, slop: slop, max_expansions: max_expansions, boost: boost, constant_score: constant_score ) 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, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil, constant_score: nil) ⇒ Object
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/parade_db/search_methods.rb', line 213 def term( value, distance: nil, prefix: nil, transposition_cost_one: nil, boost: nil, constant_score: nil ) require_search_field! node = builder.term( _paradedb_current_field, value, distance: distance, prefix: prefix, transposition_cost_one: transposition_cost_one, boost: boost, constant_score: constant_score ) where(grouped(node)) end |
#term_set(*values, boost: nil, constant_score: nil) ⇒ Object
235 236 237 238 239 240 |
# File 'lib/parade_db/search_methods.rb', line 235 def term_set(*values, boost: nil, constant_score: nil) require_search_field! node = builder.term_set(_paradedb_current_field, *values, boost: boost, constant_score: constant_score) where(grouped(node)) end |
#with_agg(exact: nil, **named_aggregations) ⇒ Object
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 |
# File 'lib/parade_db/search_methods.rb', line 444 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
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 |
# File 'lib/parade_db/search_methods.rb', line 414 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 ----
323 324 325 |
# File 'lib/parade_db/search_methods.rb', line 323 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
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/parade_db/search_methods.rb', line 327 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
366 367 368 369 |
# File 'lib/parade_db/search_methods.rb', line 366 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
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 |
# File 'lib/parade_db/search_methods.rb', line 343 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 |