Module: Parse::AtlasSearch

Defined in:
lib/parse/atlas_search.rb,
lib/parse/atlas_search/result.rb,
lib/parse/atlas_search/session.rb,
lib/parse/atlas_search/index_manager.rb,
lib/parse/atlas_search/search_builder.rb

Overview

Note:

Requires the 'mongo' gem and a MongoDB Atlas cluster with Search enabled. Also works with local Atlas deployments created via atlas deployments setup --type local.

Atlas Search module for MongoDB Atlas full-text search capabilities. Provides direct access to Atlas Search features bypassing Parse Server.

Examples:

Enable Atlas Search

Parse::MongoDB.configure(uri: "mongodb+srv://...", enabled: true)
Parse::AtlasSearch.configure(enabled: true, default_index: "default")

Full-text search

result = Parse::AtlasSearch.search("Song", "love", index: "song_search")
result.results.each { |song| puts song.title }

Autocomplete

result = Parse::AtlasSearch.autocomplete("Song", "lov", field: :title)
result.suggestions # => ["Love Story", "Lovely Day", "Love Me Do"]

Defined Under Namespace

Modules: IndexManager, Session Classes: ACLRequired, AutocompleteResult, FacetedResult, FacetedSearchNotACLSafe, IndexNotFound, InvalidSearchParameters, NotAvailable, SearchBuilder, SearchResult

Constant Summary collapse

IndexCatalog =

Alias for IndexManager exposing the type-aware lookup surface (Parse::AtlasSearch::IndexManager.list_search_indexes, Parse::AtlasSearch::IndexManager.list_vector_indexes, Parse::AtlasSearch::IndexManager.find_vector_index). Backed by the same module, so the cache, mutex, and TTL are shared across both names — calling IndexCatalog.clear_cache and IndexManager.clear_cache invalidates the same store.

Examples:

Discover the vector index covering Movie.embedding

idx = Parse::AtlasSearch::IndexCatalog.find_vector_index(
  "Movie", field: "embedding"
)
idx&.dig("name")
# => "Movie_embedding_openai_text_embedding_ada_002_1536_idx"
IndexManager

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.allow_rawBoolean

Whether raw: true is honored on search, autocomplete, and faceted_search. When false (the default), raw: is ignored and callers receive converted Parse-format documents. Even when true, internal-fields denylist (cf. PipelineSecurity::INTERNAL_FIELDS_DENYLIST) is ALWAYS stripped — there is no path that returns _hashed_password, _session_token, etc., regardless of raw:.

Returns:

  • (Boolean)


79
80
81
# File 'lib/parse/atlas_search.rb', line 79

def allow_raw
  @allow_raw
end

.default_indexString

Default search index name to use when none specified.

Returns:



67
68
69
# File 'lib/parse/atlas_search.rb', line 67

def default_index
  @default_index
end

.enabledBoolean

Feature flag to enable/disable Atlas Search.

Returns:

  • (Boolean)


62
63
64
# File 'lib/parse/atlas_search.rb', line 62

def enabled
  @enabled
end

.require_session_tokenBoolean

When true, search, autocomplete, and faceted_search raise ACLRequired unless the caller passes either session_token: or master: true. Default: false, matching the pre-ACL behavior — a one-time [Parse::AtlasSearch:SECURITY] banner is emitted instead for missing-token calls, the same pattern used by Parse::Agent for master-key construction.

New deployments are strongly encouraged to flip this to true at startup. The next major release will flip the default.

Returns:

  • (Boolean)


94
95
96
# File 'lib/parse/atlas_search.rb', line 94

def require_session_token
  @require_session_token
end

Class Method Details

.authorizationParse::Authorization::Context?

The default client's authorization context, or nil when no client has been configured yet.

Nil-tolerant on purpose. These delegators exist only for backward compatibility, and the module-level API they replace worked in a process that had never called Parse.setup (it kept its own process-local caches). Raising ConnectionError from what used to be a plain attribute reader would break configuration code that touches these before setting up a client, and reset! is a test helper that must not require a live client to run.



170
171
172
173
174
# File 'lib/parse/atlas_search.rb', line 170

def authorization
  Parse.client&.authorization
rescue Parse::Error::ConnectionError
  nil
end

.autocomplete(collection_name, query, field:, **options) ⇒ Parse::AtlasSearch::AutocompleteResult

Perform an autocomplete search for search-as-you-type functionality.

Examples:

Basic autocomplete

result = Parse::AtlasSearch.autocomplete("Song", "lov", field: :title)
result.suggestions # => ["Love Story", "Lovely Day", "Love Me Do"]

Parameters:

  • collection_name (String)

    the Parse collection name

  • query (String)

    the partial search query (prefix)

  • field (Symbol, String)

    the field configured for autocomplete

  • options (Hash)

    autocomplete options

Options Hash (**options):

  • :index (String)

    search index name (default: configured default_index)

  • :fuzzy (Boolean)

    enable fuzzy matching (default: false)

  • :fuzzy_max_edits (Integer)

    max edit distance (1 or 2, default: 1)

  • :token_order (String)

    "any" or "sequential" (default: "any")

  • :limit (Integer)

    max suggestions to return (default: 10)

  • :filter (Hash)

    additional constraints

  • :raw (Boolean)

    return raw documents (default: false)

  • :class_name (String)

    Parse class name for object conversion.

  • :session_token (String)

    Parse session token used to scope ACL/CLP enforcement to the owning user.

  • :master (Boolean)

    run with master-key semantics and bypass ACL/CLP enforcement (default: false).

  • :acl_user (Parse::User, Parse::Pointer)

    act as the given user pointer for ACL evaluation (no REST equivalent; mongo-direct only).

  • :acl_role (String, Parse::Role)

    act as the given role for ACL evaluation (no REST equivalent; mongo-direct only).

  • :read_preference (Symbol)

    MongoDB read preference applied to the underlying collection (e.g. :secondary).

  • :max_time_ms (Integer)

    maximum server-side execution time in milliseconds for the aggregate command.

Returns:

Raises:



489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
# File 'lib/parse/atlas_search.rb', line 489

def autocomplete(collection_name, query, field:, **options)
  require_available!

  raise InvalidSearchParameters, "field is required for autocomplete" if field.nil?
  raise InvalidSearchParameters, "query must be a non-empty string" if query.nil? || query.to_s.strip.empty?

  # Wave-3b READPREF-4: see #search for rationale.
  read_preference = options.delete(:read_preference)
  resolution = resolve_scope!(options, method_name: :autocomplete)

  # Enforce CLP `find` (and pointerFields requirement) on the same
  # collection autocomplete is about to scan. Without this an
  # autocomplete UI on a protected class would silently surface
  # the protected field's leading characters to any caller.
  assert_clp_find!(collection_name, resolution)
  pointer_fields = resolve_pointer_fields!(collection_name, resolution)

  # ATLAS-4: refuse autocomplete on a protected field. The
  # autocomplete operator returns the leading characters of the
  # indexed field value verbatim — running autocomplete on, say,
  # `email` when CLP marks `email` protected would defeat the
  # protectedFields contract.
  protected_fields = Parse::CLPScope.protected_fields_for(
    collection_name, resolution.permission_strings,
  )
  field_str = field.to_s
  if !resolution.master? && protected_fields.include?(field_str)
    raise Parse::CLPScope::Denied.new(
      collection_name, :find,
      "Parse::AtlasSearch.autocomplete refused: field '#{field_str}' is in " \
      "protectedFields for the current scope; autocompleting on it would " \
      "leak the protected field's value.",
    )
  end

  index_name = options[:index] || @default_index
  limit = options[:limit] || 10

  # Build autocomplete search stage
  builder = SearchBuilder.new(index_name: index_name)
  builder.autocomplete(
    query: query.to_s,
    path: field_str,
    fuzzy: options[:fuzzy],
    token_order: options[:token_order],
  )

  # CRITICAL: $search MUST be stage 0 of the pipeline (see
  # comments in #search). Build manually; do NOT route through
  # Parse::MongoDB.aggregate (which would prepend an ACL $match
  # at position 0 and break Atlas's invariant).
  pipeline = [builder.build]

  # Add score
  pipeline << { "$addFields" => { "_score" => { "$meta" => "searchScore" } } }

  # Inject ACL $match AFTER $search/$addFields and BEFORE the
  # caller-supplied filter; see {.search} for the rationale.
  unless resolution.master?
    acl_match = Parse::ACLScope.match_stage_for(resolution)
    pipeline << acl_match if acl_match
  end

  # Add filter if provided
  if options[:filter]
    mongo_filter = convert_filter_for_mongodb(options[:filter], collection_name, resolution: resolution)
    pipeline << { "$match" => mongo_filter }
  end

  # Sort by score and limit
  pipeline << { "$sort" => { "_score" => -1 } }
  pipeline << { "$limit" => limit }

  raw_results = run_atlas_pipeline!(
    collection_name, pipeline, options[:max_time_ms],
    read_preference: read_preference,
    authorizing_client: Parse::ACLScope.client_of(resolution),
  )

  unless resolution.master?
    Parse::ACLScope.redact_results!(raw_results, resolution)
    Parse::CLPScope.redact_protected_fields!(raw_results, protected_fields) if protected_fields.any?
    if pointer_fields
      raw_results = Parse::CLPScope.filter_by_pointer_fields(
        raw_results, pointer_fields, resolution.user_id,
      )
    end
  end

  # Extract suggestions (the field values). Run after the
  # protectedFields strip / pointerFields filter so a redacted
  # row can't surface its field value through the suggestion list.
  suggestions = raw_results.map { |doc| doc[field_str] }.compact.uniq

  # Convert to full objects if needed
  class_name = options[:class_name] || collection_name
  results = if raw_mode?(options[:raw])
      sanitize_raw_results(raw_results)
    else
      parse_results = Parse::MongoDB.convert_documents_to_parse(raw_results, class_name)
      parse_results.map { |doc| build_parse_object(doc, class_name) }.compact
    end

  AutocompleteResult.new(suggestions: suggestions, results: results)
end

.available?Boolean

Check if Atlas Search is available and enabled

Returns:

  • (Boolean)


238
239
240
241
# File 'lib/parse/atlas_search.rb', line 238

def available?
  return false unless defined?(Parse::MongoDB)
  Parse::MongoDB.available? && enabled?
end

.compare_upstream_rolesObject

Deprecated.

Use client.authorization.compare_upstream_roles.



152
# File 'lib/parse/atlas_search.rb', line 152

def compare_upstream_roles = authorization&.compare_upstream_roles || false

.compare_upstream_roles=(value) ⇒ Object



154
155
156
# File 'lib/parse/atlas_search.rb', line 154

def compare_upstream_roles=(value)
  authorization&.compare_upstream_roles = value
end

.configure(enabled: true, default_index: "default", allow_raw: nil, require_session_token: nil, session_cache_ttl: nil, role_cache_ttl: nil, upstream_role_reader: nil, compare_upstream_roles: nil) ⇒ Object

Configure Atlas Search (uses Parse::MongoDB connection)

Examples:

Parse::AtlasSearch.configure(enabled: true, default_index: "default")

Parameters:

  • enabled (Boolean) (defaults to: true)

    whether to enable Atlas Search (default: true)

  • default_index (String) (defaults to: "default")

    default search index name (default: "default")

  • allow_raw (Boolean) (defaults to: nil)

    whether raw: true is honored on search/autocomplete/faceted_search. Defaults to false (raw flag ignored) in production-like environments and true when RACK_ENV/RAILS_ENV is development or test. Internal-field stripping runs regardless.

  • require_session_token (Boolean) (defaults to: nil)

    when true, library calls without session_token: or master: true raise ACLRequired. See #require_session_token. Default: false.

  • session_cache_ttl (Integer) (defaults to: nil)

    session-token cache TTL (seconds). Default: 3600.

  • role_cache_ttl (Integer) (defaults to: nil)

    role-name cache TTL (seconds). Default: 30.

  • upstream_role_reader (#roles_for, nil) (defaults to: nil)

    see #upstream_role_reader. Default: unchanged (nil unless set separately).

  • compare_upstream_roles (Boolean) (defaults to: nil)

    see #compare_upstream_roles. Default: unchanged (false unless set separately).



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/parse/atlas_search.rb', line 199

def configure(enabled: true,
              default_index: "default",
              allow_raw: nil,
              require_session_token: nil,
              session_cache_ttl: nil,
              role_cache_ttl: nil,
              upstream_role_reader: nil,
              compare_upstream_roles: nil)
  Parse::MongoDB.require_gem!
  @enabled = enabled
  @default_index = default_index
  @allow_raw = allow_raw.nil? ? default_allow_raw : allow_raw
  @require_session_token = require_session_token unless require_session_token.nil?
  # Authorization settings belong to the client, not to this module.
  # Forwarded rather than stored so there is exactly one copy.
  authorization&.configure(
    identity_cache_ttl: session_cache_ttl,
    role_cache_ttl: role_cache_ttl,
    upstream_role_reader: upstream_role_reader,
    compare_upstream_roles: compare_upstream_roles,
  )
  IndexManager.clear_cache
end

.enabled?Boolean

Check if Atlas Search is enabled

Returns:

  • (Boolean)


245
246
247
# File 'lib/parse/atlas_search.rb', line 245

def enabled?
  @enabled == true
end

.faceted_search(collection_name, query, facets, **options) ⇒ Parse::AtlasSearch::FacetedResult

Perform a faceted search with category counts.

Examples:

Faceted search by genre and year

facets = {
  genre: { type: :string, path: :genre },
  decade: { type: :number, path: :year, boundaries: [1970, 1980, 1990, 2000, 2010] }
}
result = Parse::AtlasSearch.faceted_search("Song", "rock", facets)
result.facets[:genre] # => [{ value: "Rock", count: 150 }, ...]

Parameters:

  • collection_name (String)

    the Parse collection name

  • query (String, nil)

    the search query text (nil for match-all)

  • facets (Hash)

    facet definitions

  • options (Hash)

    search options (same as #search; see that method for the full list of accepted @option entries including :index, :fields, :fuzzy, :limit, :filter, :read_preference, :max_time_ms, and the scoping kwargs :master, :session_token, :acl_user, :acl_role). Note: scoped identity kwargs require master: true to be passed explicitly — $searchMeta bucket counts cannot be filtered by ACL after the fact, so the method refuses to silently downgrade.

Returns:



618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
# File 'lib/parse/atlas_search.rb', line 618

def faceted_search(collection_name, query, facets, **options)
  require_available!

  # Faceted search uses $searchMeta, which outputs a single
  # metadata document — bucket counts can't be retroactively
  # filtered by a post-$searchMeta $match because the matched
  # documents are not in the output stream. ACL-aware faceting
  # requires either tokenizing _rperm in the search index and
  # injecting a compound.filter inside $searchMeta, or running
  # two passes with manual aggregation. Both are deferred.
  #
  # Library-layer defense: refuse ANY scoped identity kwarg
  # (session_token:, acl_user:, acl_role:) unless the caller
  # explicitly accepts master-key semantics by also passing
  # `master: true`. The original code only checked
  # `session_token:`, leaving `acl_user:` / `acl_role:` callers
  # (ATLAS-10) silently downgraded to the unauthenticated/
  # public-mode banner branch — which on $searchMeta produces
  # bucket counts that include rows the caller cannot read,
  # exfiltrating restricted document counts and category
  # values. Checking the raw options BEFORE resolve_scope!
  # pops them so the error path can name what the caller
  # actually passed.
  scoped_kwargs = %i[session_token acl_user acl_role]
  offending = scoped_kwargs.select { |k| !options[k].nil? }
  if offending.any? && options[:master] != true
    raise FacetedSearchNotACLSafe,
          "Parse::AtlasSearch.faceted_search cannot enforce per-row " \
          "ACL on $searchMeta bucket counts (got #{offending.first}:). " \
          "Pass `master: true` to run with master-key semantics and " \
          "accept that bucket counts include all rows, or use " \
          "#search for ACL-scoped results without facets."
  end
  # Wave-3b READPREF-4: see #search for rationale. Captured
  # before resolve_scope! pops the auth kwargs so the recursive
  # search() call below can re-thread it explicitly (resolve!
  # also strips it during that recursion).
  read_preference = options.delete(:read_preference)
  resolution = resolve_scope!(options, method_name: :faceted_search)
  acl = { master: resolution.master? }

  index_name = options[:index] || @default_index
  limit = options[:limit] || 100
  skip_val = options[:skip] || 0

  # Build facet definitions for $searchMeta
  facet_definitions = build_facet_definitions(facets)

  search_meta_stage = {
    "$searchMeta" => {
      "index" => index_name,
      "facet" => {
        "facets" => facet_definitions,
      },
    },
  }

  # Add operator for the search query if present
  if query.present?
    fields = normalize_fields(options[:fields])
    if fields.present?
      should_clauses = fields.map do |field|
        { "text" => { "query" => query, "path" => field } }
      end
      search_meta_stage["$searchMeta"]["facet"]["operator"] = {
        "compound" => { "should" => should_clauses, "minimumShouldMatch" => 1 },
      }
    else
      search_meta_stage["$searchMeta"]["facet"]["operator"] = {
        "text" => { "query" => query, "path" => { "wildcard" => "*" } },
      }
    end
  end

  # Execute facet query. $searchMeta MUST be the only / first
  # stage of its pipeline — Atlas rejects anything prepended.
  # Bypass Parse::MongoDB.aggregate (which would prepend a
  # public-mode ACL $match at position 0 under the no-auth-kwargs
  # fallthrough) and call the collection directly. At this point
  # the call is master-only by construction (the offending-kwargs
  # check above ensures any scoped caller bailed out), so no
  # ACL/CLP enforcement runs here either.
  facet_pipeline = [search_meta_stage]
  facet_results_raw = run_atlas_pipeline!(
    collection_name, facet_pipeline, options[:max_time_ms],
    read_preference: read_preference,
    authorizing_client: Parse::ACLScope.client_of(resolution),
  )

  # Extract facet results
  facet_data = {}
  total_count = 0

  if facet_results_raw.first
    raw = facet_results_raw.first
    total_count = raw.dig("count", "total") || 0

    if raw["facet"]
      facets.keys.each do |facet_name|
        bucket_key = facet_name.to_s
        if raw["facet"][bucket_key]
          facet_data[facet_name] = raw["facet"][bucket_key]["buckets"].map do |bucket|
            { value: bucket["_id"], count: bucket["count"] }
          end
        end
      end
    end
  end

  # Get actual results with regular $search. Forward master:
  # explicitly because resolve_acl_options! popped it from the
  # options hash; without re-adding it the recursive call would
  # take the unauthenticated path and emit the banner a second
  # time (or raise ACLRequired under strict mode). Re-thread
  # read_preference: the same way for the same reason — the
  # outer faceted_search popped it before delegating.
  results = if limit > 0 && query.present?
      search_opts = options.merge(limit: limit, skip: skip_val)
      search_opts[:master] = true if acl[:master]
      search_opts[:read_preference] = read_preference if read_preference
      # Re-thread the client for the same reason as the auth kwargs
      # above: the outer faceted_search popped it in resolve_scope!, so
      # this inner search would otherwise resolve and read as the
      # default application.
      search_opts[:client] = Parse::ACLScope.client_of(resolution) if Parse::ACLScope.client_of(resolution)
      search(collection_name, query, **search_opts).results
    else
      []
    end

  FacetedResult.new(results: results, facets: facet_data, total_count: total_count)
end

.index_ready?(collection_name, index_name = nil) ⇒ Boolean

Check if a search index exists and is ready

Parameters:

  • collection_name (String)

    the Parse collection name

  • index_name (String) (defaults to: nil)

    the index name to check (default: default_index)

Returns:

  • (Boolean)

    true if index exists and is queryable



281
282
283
# File 'lib/parse/atlas_search.rb', line 281

def index_ready?(collection_name, index_name = nil)
  IndexManager.index_ready?(collection_name, index_name || @default_index)
end

.indexes(collection_name) ⇒ Array<Hash>

List search indexes for a collection (cached)

Parameters:

  • collection_name (String)

    the Parse collection name

Returns:

  • (Array<Hash>)

    array of index definitions



273
274
275
# File 'lib/parse/atlas_search.rb', line 273

def indexes(collection_name)
  IndexManager.list_indexes(collection_name)
end

.refresh_indexes(collection_name = nil) ⇒ Object

Force refresh the index cache for a collection

Parameters:

  • collection_name (String) (defaults to: nil)

    the Parse collection name (nil to clear all)



287
288
289
# File 'lib/parse/atlas_search.rb', line 287

def refresh_indexes(collection_name = nil)
  IndexManager.clear_cache(collection_name)
end

.reset!Object

Reset Atlas Search configuration to first-load defaults. Clears the session/role caches as well; this is primarily a test helper.



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/parse/atlas_search.rb', line 252

def reset!
  @enabled = false
  @default_index = "default"
  @allow_raw = default_allow_raw
  @require_session_token = false
  authorization&.configure(
    identity_cache_ttl: Parse::Authorization::Context::DEFAULT_IDENTITY_TTL,
    role_cache_ttl: Parse::Authorization::Context::DEFAULT_ROLE_TTL,
    identity_cache: Parse::Authorization::MemoryCache.new,
    role_cache: Parse::Authorization::MemoryCache.new,
    upstream_role_reader: false,
    compare_upstream_roles: false,
  )
  authorization&.upstream_role_reader = nil
  @master_warned = false
  IndexManager.clear_cache
end

.role_cacheObject

Deprecated.

Use client.authorization.role_cache.



138
# File 'lib/parse/atlas_search.rb', line 138

def role_cache = authorization&.role_cache

.role_cache=(value) ⇒ Object



140
141
142
# File 'lib/parse/atlas_search.rb', line 140

def role_cache=(value)
  authorization&.role_cache = value
end

.role_cache_ttlObject

Deprecated.

Use client.authorization.role_cache_ttl.



124
# File 'lib/parse/atlas_search.rb', line 124

def role_cache_ttl = authorization&.role_cache_ttl || Parse::Authorization::Context::DEFAULT_ROLE_TTL

.role_cache_ttl=(value) ⇒ Object



126
127
128
# File 'lib/parse/atlas_search.rb', line 126

def role_cache_ttl=(value)
  authorization&.role_cache_ttl = value
end

.search(collection_name, query, **options) ⇒ Parse::AtlasSearch::SearchResult

Perform a full-text search using Atlas Search.

Examples:

Basic search

result = Parse::AtlasSearch.search("Song", "love ballad")
result.results.each { |song| puts song.title }

Search with fuzzy matching and field restriction

result = Parse::AtlasSearch.search("Song", "lvoe",
  fields: [:title, :lyrics],
  fuzzy: true,
  limit: 20
)

Parameters:

  • collection_name (String)

    the Parse collection name (e.g., "Song")

  • query (String)

    the search query text

  • options (Hash)

    search options

Options Hash (**options):

  • :index (String)

    search index name (default: configured default_index)

  • :fields (Array<String>, String, Symbol)

    fields to search (default: all indexed fields)

  • :fuzzy (Boolean)

    enable fuzzy matching (default: false)

  • :fuzzy_max_edits (Integer)

    max edit distance for fuzzy (1 or 2, default: 2)

  • :highlight_field (Symbol, String)

    field to return highlights for

  • :limit (Integer)

    max results to return (default: 100)

  • :skip (Integer)

    number of results to skip (default: 0)

  • :filter (Hash)

    additional constraints to apply

  • :sort (Hash)

    sort specification (default: by relevance score)

  • :raw (Boolean)

    return raw MongoDB documents (default: false)

  • :class_name (String)

    Parse class name for object conversion

  • :session_token (String)

    Parse session token used to scope ACL/CLP enforcement to the owning user.

  • :master (Boolean)

    run with master-key semantics and bypass ACL/CLP enforcement (default: false).

  • :acl_user (Parse::User, Parse::Pointer)

    act as the given user pointer for ACL evaluation (no REST equivalent; mongo-direct only).

  • :acl_role (String, Parse::Role)

    act as the given role for ACL evaluation (no REST equivalent; mongo-direct only).

  • :read_preference (Symbol)

    MongoDB read preference applied to the underlying collection (e.g. :secondary).

  • :max_time_ms (Integer)

    maximum server-side execution time in milliseconds for the aggregate command.

Returns:



336
337
338
339
340
341
342
343
344
345
346
347
348
349
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
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/parse/atlas_search.rb', line 336

def search(collection_name, query, **options)
  require_available!
  validate_search_params!(query)

  # Wave-3b READPREF-4: read-preference is consumed at the
  # collection-with-read-preference step inside run_atlas_pipeline!.
  # Pop it here so it doesn't surface in `options` for any
  # downstream consumer (SearchBuilder, recursive search()
  # call from faceted_search) that iterates the hash.
  read_preference = options.delete(:read_preference)
  resolution = resolve_scope!(options, method_name: :search)

  # Enforce CLP `find` (and pointerFields requirement) BEFORE
  # we build / execute the pipeline. Without this, a scoped
  # caller can issue $search against a collection whose CLP
  # would refuse them on the equivalent REST find.
  assert_clp_find!(collection_name, resolution)
  pointer_fields = resolve_pointer_fields!(collection_name, resolution)

  # Compute the protectedFields strip set early so we can
  # refuse a highlight_field that's in it (ATLAS-4). Avoids
  # the awkward "we return objects but secretly drop their
  # highlights" state — fail loudly instead.
  protected_fields = Parse::CLPScope.protected_fields_for(
    collection_name, resolution.permission_strings,
  )
  assert_highlight_field_allowed!(options[:highlight_field], protected_fields, resolution)

  index_name = options[:index] || @default_index
  fields = normalize_fields(options[:fields])
  limit = options[:limit] || 100
  skip_val = options[:skip] || 0

  # Build the $search stage
  builder = SearchBuilder.new(index_name: index_name)

  if fields.present?
    fields.each do |field|
      builder.text(query: query, path: field, fuzzy: options[:fuzzy])
    end
  else
    builder.text(query: query, path: { "wildcard" => "*" }, fuzzy: options[:fuzzy])
  end

  if options[:highlight_field]
    builder.with_highlight(path: options[:highlight_field])
  end

  # $search MUST be stage 0 of an Atlas Search pipeline (MongoDB
  # rejects any other first stage), so we must NOT route through
  # Parse::MongoDB.aggregate — that helper prepends the ACL $match
  # to position 0. Execution plus the full SDK-side enforcement
  # chain (ACL $match placed AFTER $search, protectedFields strip,
  # pointerFields filter, sub-doc redaction) lives in
  # `search_pipeline!`, shared with the builder-block path
  # (`search_with_stage` / Parse::Query#atlas_search).
  search_pipeline!(
    collection_name, builder.build,
    resolution: resolution,
    protected_fields: protected_fields,
    pointer_fields: pointer_fields,
    highlight_field: options[:highlight_field],
    filter: options[:filter],
    sort: options[:sort],
    skip: skip_val,
    limit: limit,
    max_time_ms: options[:max_time_ms],
    read_preference: read_preference,
    class_name: options[:class_name] || collection_name,
    raw: options[:raw],
  )
end

.search_with_stage(collection_name, search_stage, **options) ⇒ Parse::AtlasSearch::SearchResult

Execute a caller-supplied $search stage (built via a SearchBuilder) with the same ACL/CLP/protectedFields/ pointerFields enforcement as search, keeping $search at stage 0. This is the entry point for Parse::Query#atlas_search's builder-block mode, which constructs its own $search stage instead of going through the query/fields/fuzzy options.

Auth is resolved from options exactly like search: session_token: / master: true / acl_user: / acl_role: (mutually exclusive). Without one, behavior follows require_session_token (public-only fallback, or ACLRequired).

Parameters:

  • collection_name (String)

    the Parse collection name

  • search_stage (Hash)

    a built { "$search" => {...} } stage

  • options (Hash)

    :filter, :sort, :skip, :limit, :highlight_field, :max_time_ms, :read_preference, :class_name, :raw, plus the auth kwargs above.

Returns:



427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/parse/atlas_search.rb', line 427

def search_with_stage(collection_name, search_stage, **options)
  require_available!
  assert_search_stage_safe!(search_stage)

  read_preference = options.delete(:read_preference)
  resolution = resolve_scope!(options, method_name: :search)
  assert_clp_find!(collection_name, resolution)
  pointer_fields = resolve_pointer_fields!(collection_name, resolution)
  protected_fields = Parse::CLPScope.protected_fields_for(
    collection_name, resolution.permission_strings,
  )
  assert_highlight_field_allowed!(options[:highlight_field], protected_fields, resolution)

  search_pipeline!(
    collection_name, search_stage,
    resolution: resolution,
    protected_fields: protected_fields,
    pointer_fields: pointer_fields,
    highlight_field: options[:highlight_field],
    filter: options[:filter],
    sort: options[:sort],
    skip: options[:skip] || 0,
    limit: options[:limit] || 100,
    max_time_ms: options[:max_time_ms],
    read_preference: read_preference,
    class_name: options[:class_name] || collection_name,
    raw: options[:raw],
  )
end

.session_cacheObject

Deprecated.

Use client.authorization.identity_cache.



131
# File 'lib/parse/atlas_search.rb', line 131

def session_cache = authorization&.identity_cache

.session_cache=(value) ⇒ Object



133
134
135
# File 'lib/parse/atlas_search.rb', line 133

def session_cache=(value)
  authorization&.identity_cache = value
end

.session_cache_ttlObject

Deprecated.

Use client.authorization.identity_cache_ttl. Renamed because it never stored sessions: it stores one user id per token, and the old name led readers to reason about _Session semantics that were never involved.



117
# File 'lib/parse/atlas_search.rb', line 117

def session_cache_ttl = authorization&.identity_cache_ttl || Parse::Authorization::Context::DEFAULT_IDENTITY_TTL

.session_cache_ttl=(value) ⇒ Object



119
120
121
# File 'lib/parse/atlas_search.rb', line 119

def session_cache_ttl=(value)
  authorization&.identity_cache_ttl = value
end

.upstream_role_readerObject

Deprecated.

Use client.authorization.upstream_role_reader.



145
# File 'lib/parse/atlas_search.rb', line 145

def upstream_role_reader = authorization&.upstream_role_reader

.upstream_role_reader=(value) ⇒ Object



147
148
149
# File 'lib/parse/atlas_search.rb', line 147

def upstream_role_reader=(value)
  authorization&.upstream_role_reader = value
end