Class: Parse::AtlasSearch::SearchBuilder
- Inherits:
-
Object
- Object
- Parse::AtlasSearch::SearchBuilder
- Defined in:
- lib/parse/atlas_search/search_builder.rb
Overview
Builder for constructing $search aggregation pipeline stages. Supports fluent interface for complex queries.
Constant Summary collapse
- MAX_PATTERN_LENGTH =
Maximum length of a regex or wildcard query string. Atlas Search uses Lucene’s bounded regex evaluator; long patterns and full-string wildcards force a state-machine explosion or whole-index scan and can be used to DoS the search node.
256
Instance Attribute Summary collapse
-
#count_config ⇒ Object
readonly
Returns the value of attribute count_config.
-
#highlight_config ⇒ Object
readonly
Returns the value of attribute highlight_config.
-
#index_name ⇒ Object
readonly
Returns the value of attribute index_name.
-
#operators ⇒ Object
readonly
Returns the value of attribute operators.
Instance Method Summary collapse
-
#autocomplete(query:, path:, fuzzy: nil, token_order: nil) ⇒ self
Add an autocomplete operator (requires autocomplete index type).
-
#build ⇒ Hash
Build the $search aggregation stage.
-
#build_compound(must: nil, must_not: nil, should: nil, filter: nil, minimum_should_match: nil) ⇒ Hash
Build a compound query explicitly.
-
#exists(path:) ⇒ self
Add an exists operator to match documents where field exists.
-
#geo_shape(path:, relation:, geometry:, score: nil) ⇒ self
Atlas Search ‘geoShape` operator.
-
#geo_within(path:, box: nil, circle: nil, geometry: nil, score: nil) ⇒ self
Atlas Search ‘geoWithin` operator.
-
#initialize(index_name: nil) ⇒ SearchBuilder
constructor
A new instance of SearchBuilder.
-
#near(path:, origin:, pivot:, score: nil) ⇒ self
Atlas Search ‘near` operator on a geo path.
-
#phrase(query:, path:, slop: nil) ⇒ self
Add a phrase search operator.
-
#range(path:, gt: nil, gte: nil, lt: nil, lte: nil) ⇒ self
Add a range search operator for numeric/date fields.
-
#regex(query:, path:, allow_analyzed_field: nil) ⇒ self
Add a regex search operator.
-
#text(query:, path:, fuzzy: nil, score: nil, synonyms: nil) ⇒ self
Add a text search operator.
-
#wildcard(query:, path:, allow_analyzed_field: nil) ⇒ self
Add a wildcard search operator.
-
#with_count(type: "total") ⇒ self
Enable count metadata in results.
-
#with_fuzzy(max_edits: 2, prefix_length: 0, max_expansions: 50) ⇒ self
Add global fuzzy configuration for subsequent text operators.
-
#with_highlight(path: nil, max_chars_to_examine: nil, max_num_passages: nil) ⇒ self
Enable highlighting for search results.
Constructor Details
#initialize(index_name: nil) ⇒ SearchBuilder
Returns a new instance of SearchBuilder.
33 34 35 36 37 38 39 |
# File 'lib/parse/atlas_search/search_builder.rb', line 33 def initialize(index_name: nil) @index_name = index_name || Parse::AtlasSearch.default_index || "default" @operators = [] @highlight_config = nil @count_config = nil @fuzzy_config = nil end |
Instance Attribute Details
#count_config ⇒ Object (readonly)
Returns the value of attribute count_config.
31 32 33 |
# File 'lib/parse/atlas_search/search_builder.rb', line 31 def count_config @count_config end |
#highlight_config ⇒ Object (readonly)
Returns the value of attribute highlight_config.
31 32 33 |
# File 'lib/parse/atlas_search/search_builder.rb', line 31 def highlight_config @highlight_config end |
#index_name ⇒ Object (readonly)
Returns the value of attribute index_name.
31 32 33 |
# File 'lib/parse/atlas_search/search_builder.rb', line 31 def index_name @index_name end |
#operators ⇒ Object (readonly)
Returns the value of attribute operators.
31 32 33 |
# File 'lib/parse/atlas_search/search_builder.rb', line 31 def operators @operators end |
Instance Method Details
#autocomplete(query:, path:, fuzzy: nil, token_order: nil) ⇒ self
Add an autocomplete operator (requires autocomplete index type)
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/parse/atlas_search/search_builder.rb', line 94 def autocomplete(query:, path:, fuzzy: nil, token_order: nil) validate_query_length!(query, "autocomplete") operator = { "autocomplete" => { "query" => query, "path" => path.to_s, }, } if fuzzy operator["autocomplete"]["fuzzy"] = fuzzy.is_a?(Hash) ? fuzzy : { "maxEdits" => 1, "prefixLength" => 1, } end operator["autocomplete"]["tokenOrder"] = token_order if token_order @operators << operator self end |
#build ⇒ Hash
Build the $search aggregation stage
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/parse/atlas_search/search_builder.rb', line 341 def build if @operators.empty? raise InvalidSearchParameters, "At least one search operator must be specified" end search_stage = { "$search" => { "index" => @index_name } } # Single operator or compound if @operators.length == 1 search_stage["$search"].merge!(@operators.first) else # Multiple operators become a compound query with "must" clauses search_stage["$search"]["compound"] = { "must" => @operators } end # Add highlight config search_stage["$search"]["highlight"] = @highlight_config if @highlight_config # Add count config search_stage["$search"]["count"] = @count_config if @count_config search_stage end |
#build_compound(must: nil, must_not: nil, should: nil, filter: nil, minimum_should_match: nil) ⇒ Hash
Build a compound query explicitly
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 |
# File 'lib/parse/atlas_search/search_builder.rb', line 372 def build_compound(must: nil, must_not: nil, should: nil, filter: nil, minimum_should_match: nil) compound = {} compound["must"] = Array.wrap(must).map { |op| extract_operator(op) } if must compound["mustNot"] = Array.wrap(must_not).map { |op| extract_operator(op) } if must_not compound["should"] = Array.wrap(should).map { |op| extract_operator(op) } if should compound["filter"] = Array.wrap(filter).map { |op| extract_operator(op) } if filter compound["minimumShouldMatch"] = minimum_should_match if minimum_should_match search_stage = { "$search" => { "index" => @index_name, "compound" => compound, }, } search_stage["$search"]["highlight"] = @highlight_config if @highlight_config search_stage["$search"]["count"] = @count_config if @count_config search_stage end |
#exists(path:) ⇒ self
Add an exists operator to match documents where field exists
187 188 189 190 |
# File 'lib/parse/atlas_search/search_builder.rb', line 187 def exists(path:) @operators << { "exists" => { "path" => path.to_s } } self end |
#geo_shape(path:, relation:, geometry:, score: nil) ⇒ self
Atlas Search ‘geoShape` operator. Filters documents where the indexed geometry has the specified relation to a query geometry. Requires the indexed field to be mapped with `“geo”, indexShapes: true`.
Note: Atlas Search uses Cartesian (planar) distance, NOT the 2dsphere geodesic distance used by core MongoDB geo operators. For shapes spanning large areas the two engines can return different result sets.
211 212 213 214 215 216 217 218 219 220 |
# File 'lib/parse/atlas_search/search_builder.rb', line 211 def geo_shape(path:, relation:, geometry:, score: nil) op = { "path" => path.to_s, "relation" => relation.to_s, "geometry" => coerce_geojson_geometry(geometry), } op["score"] = score if score @operators << { "geoShape" => op } self end |
#geo_within(path:, box: nil, circle: nil, geometry: nil, score: nil) ⇒ self
Atlas Search ‘geoWithin` operator. Returns documents whose indexed point is inside the supplied region. Exactly one of `box:`, `circle:`, `geometry:` must be provided.
-
‘box`: `[bottom_left, top_right]` — each entry may be a GeoPoint or a GeoJSON Point Hash.
-
‘circle`: `<GeoPoint|Hash>, radius: <meters>`. Radius is measured in meters and must be non-negative.
-
‘geometry`: a GeoJSON Polygon or MultiPolygon (Hash, a Polygon, or GeoJSON::MultiPolygon).
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/parse/atlas_search/search_builder.rb', line 239 def geo_within(path:, box: nil, circle: nil, geometry: nil, score: nil) provided = [box, circle, geometry].count { |v| !v.nil? } if provided != 1 raise ArgumentError, "[Parse::AtlasSearch] geo_within requires exactly one of " \ "box:, circle:, or geometry: (got #{provided})." end op = { "path" => path.to_s } op["score"] = score if score if box unless box.is_a?(Array) && box.length == 2 raise ArgumentError, "[Parse::AtlasSearch] geo_within `box:` must be [bottom_left, top_right]." end op["box"] = { "bottomLeft" => coerce_geojson_point(box[0]), "topRight" => coerce_geojson_point(box[1]), } elsif circle unless circle.is_a?(Hash) raise ArgumentError, "[Parse::AtlasSearch] geo_within `circle:` must be a Hash." end center = circle[:center] || circle["center"] radius = circle[:radius] || circle["radius"] unless radius.is_a?(Numeric) && radius >= 0 raise ArgumentError, "[Parse::AtlasSearch] geo_within `circle: { radius: }` must be a non-negative number (meters)." end op["circle"] = { "center" => coerce_geojson_point(center), "radius" => radius.to_f } else op["geometry"] = coerce_geojson_geometry(geometry) end @operators << { "geoWithin" => op } self end |
#near(path:, origin:, pivot:, score: nil) ⇒ self
Atlas Search ‘near` operator on a geo path. SCORING operator —blends “distance from origin” into the document score; it does not strictly filter by distance. Combine with a `compound.must` text/exists clause to bound the result set.
‘pivot` is the distance (in meters) at which the score is halved: `score = pivot / (pivot + distance)`. Smaller pivot = steeper falloff, more weight on the closest hits.
289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/parse/atlas_search/search_builder.rb', line 289 def near(path:, origin:, pivot:, score: nil) unless pivot.is_a?(Numeric) && pivot > 0 raise ArgumentError, "[Parse::AtlasSearch] near `pivot:` must be a positive number (meters)." end op = { "path" => path.to_s, "origin" => coerce_geojson_point(origin), "pivot" => pivot.to_f, } op["score"] = score if score @operators << { "near" => op } self end |
#phrase(query:, path:, slop: nil) ⇒ self
Add a phrase search operator
73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/parse/atlas_search/search_builder.rb', line 73 def phrase(query:, path:, slop: nil) validate_query_length!(query, "phrase") operator = { "phrase" => { "query" => query, "path" => normalize_path(path), }, } operator["phrase"]["slop"] = slop if slop @operators << operator self end |
#range(path:, gt: nil, gte: nil, lt: nil, lte: nil) ⇒ self
Add a range search operator for numeric/date fields
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/parse/atlas_search/search_builder.rb', line 168 def range(path:, gt: nil, gte: nil, lt: nil, lte: nil) operator = { "range" => { "path" => path.to_s, }, } operator["range"]["gt"] = format_range_value(gt) if gt operator["range"]["gte"] = format_range_value(gte) if gte operator["range"]["lt"] = format_range_value(lt) if lt operator["range"]["lte"] = format_range_value(lte) if lte @operators << operator self end |
#regex(query:, path:, allow_analyzed_field: nil) ⇒ self
Add a regex search operator
146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/parse/atlas_search/search_builder.rb', line 146 def regex(query:, path:, allow_analyzed_field: nil) validate_pattern!(query, kind: "regex") operator = { "regex" => { "query" => query, "path" => normalize_path(path), }, } operator["regex"]["allowAnalyzedField"] = allow_analyzed_field unless allow_analyzed_field.nil? @operators << operator self end |
#text(query:, path:, fuzzy: nil, score: nil, synonyms: nil) ⇒ self
Add a text search operator
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/parse/atlas_search/search_builder.rb', line 48 def text(query:, path:, fuzzy: nil, score: nil, synonyms: nil) validate_query_length!(query, "text") operator = { "text" => { "query" => query, "path" => normalize_path(path), }, } if fuzzy operator["text"]["fuzzy"] = fuzzy.is_a?(Hash) ? fuzzy : { "maxEdits" => 2 } end operator["text"]["score"] = score if score operator["text"]["synonyms"] = synonyms if synonyms @operators << operator self end |
#wildcard(query:, path:, allow_analyzed_field: nil) ⇒ self
Add a wildcard search operator
123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/parse/atlas_search/search_builder.rb', line 123 def wildcard(query:, path:, allow_analyzed_field: nil) validate_pattern!(query, kind: "wildcard") operator = { "wildcard" => { "query" => query, "path" => normalize_path(path), }, } operator["wildcard"]["allowAnalyzedField"] = allow_analyzed_field unless allow_analyzed_field.nil? @operators << operator self end |
#with_count(type: "total") ⇒ self
Enable count metadata in results
333 334 335 336 |
# File 'lib/parse/atlas_search/search_builder.rb', line 333 def with_count(type: "total") @count_config = { "type" => type } self end |
#with_fuzzy(max_edits: 2, prefix_length: 0, max_expansions: 50) ⇒ self
Add global fuzzy configuration for subsequent text operators
308 309 310 311 312 313 314 315 |
# File 'lib/parse/atlas_search/search_builder.rb', line 308 def with_fuzzy(max_edits: 2, prefix_length: 0, max_expansions: 50) @fuzzy_config = { "maxEdits" => max_edits, "prefixLength" => prefix_length, "maxExpansions" => max_expansions, } self end |
#with_highlight(path: nil, max_chars_to_examine: nil, max_num_passages: nil) ⇒ self
Enable highlighting for search results
322 323 324 325 326 327 328 |
# File 'lib/parse/atlas_search/search_builder.rb', line 322 def with_highlight(path: nil, max_chars_to_examine: nil, max_num_passages: nil) @highlight_config = {} @highlight_config["path"] = normalize_path(path) if path @highlight_config["maxCharsToExamine"] = max_chars_to_examine if max_chars_to_examine @highlight_config["maxNumPassages"] = max_num_passages if max_num_passages self end |