+
+
+
+
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+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
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+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
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+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
+408
+409
+410
+411
+412
+413
+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
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+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
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+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
+750
+751
+752
+753
+754
+755
+756
+757
+758
+759
+760
+761
+762
+763
+764
+765
+766
+767
+768
+769
+770
+771
+772
+773
+774
+775
+776
+777
+778
+779
+780
+781
+782
+783
+784
+785
+786
+787
+788
+789
+790
+791
+792
+793
+794
+795
+796
+797
+798
+799
+800
+801
+802
+803
+804
+805
+806
+807
+808
+809
+810
+811
+812
+813
+814
+815
+816
+817
+818
+819
+820
+821
+822
+823
+824
+825
+826
+827
+828
+829
+830
+831
+832
+833
+834
+835
+836
+837
+838
+839
+840
+841
+842
+843
+844
+845
+846
+847
+848
+849
+850
+851
+852
+853
+854
+855
+856
+857
+858
+859
+860
+861
+862
+863
+864
+865
+866
+867
+868
+869
+870
+871
+872
+873
+874
+875
+876
+877
+878
+879
+880
+881
+882
+883
+884
+885
+886
+887
+888
+889
+890
+891
+892
+893
+894
+895
+896
+897
+898
+899
+900
+901
+902
+903
+904
+905
+906
+907
+908
+909
+910
+911
+912
+913
+914
+915
+916
+917
+918
+919
+920
+921
+922
+923
+924
+925
+926
+927
+928
+929
+930
+931
+932
+933
+934
+935
+936
+937
+938
+939
+940
+941
+942
+943
+944
+945
+946
+947
+948
+949
+950
+951
+952
+953
+954
+955
+956
+957
+958
+959
+960
+961
+962
+963
+964
+965
+966
+967
+968
+969
+970
+971
+972
+973
+974
+975
+976
+977
+978
+979
+980
+981
+982
+983
+984
+985
+986
+987
+988
+989
+990
+991
+992
+993
+994
+995
+996
+997
+998
+999
+1000
+1001
+1002
+1003
+1004
+1005
+1006
+1007
+1008
+1009
+1010
+1011
+1012
+1013
+1014
+1015
+1016
+1017
+1018
+1019
+1020
+1021
+1022
+1023
+1024
+1025
+1026
+1027
+1028
+1029
+1030
+1031
+1032
+1033
+1034
+1035
+1036
+1037
+1038
+1039
+1040
+1041
+1042
+1043
+1044
+1045
+1046
+1047
+1048
+1049
+1050
+1051
+1052
+1053
+1054
+1055
+1056
+1057
+1058
+1059
+1060
+1061
+1062
+1063
+1064
+1065
+1066
+1067
+1068
+1069
+1070
+1071
+1072
+1073
+1074
+1075
+1076
+1077
+1078
+1079
+1080
+1081
+1082
+1083
+1084
+1085
+1086
+1087
+1088
+1089
+1090
+1091
+1092
+1093
+1094
+1095
+1096
+1097
+1098
+1099
+1100
+1101
+ |
+
+ # File 'elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/field.rb', line 89
+
+class Field < Struct.new(
+ :name, :original_type, :parent_type, :original_type_for_derived_types, :schema_def_state, :accuracy_confidence,
+ :filter_customizations, :grouped_by_customizations, :sub_aggregations_customizations,
+ :aggregated_values_customizations, :sort_order_enum_value_customizations,
+ :args, :sortable, :filterable, :aggregatable, :groupable, :graphql_only, :source, :runtime_field_script, :relationship, :singular_name,
+ :computation_detail, :non_nullable_in_json_schema, :backing_indexing_field, :as_input,
+ :legacy_grouping_schema, :name_in_index
+)
+ include Mixins::HasDocumentation
+ include Mixins::HasDirectives
+ include Mixins::HasTypeInfo
+ include Mixins::HasReadableToSAndInspect.new { |f| "#{f.parent_type.name}.#{f.name}: #{f.type}" }
+
+ def initialize(
+ name:, type:, parent_type:, schema_def_state:,
+ accuracy_confidence: :high, name_in_index: name,
+ runtime_metadata_graphql_field: SchemaArtifacts::RuntimeMetadata::GraphQLField::EMPTY,
+ type_for_derived_types: nil, graphql_only: nil, singular: nil,
+ sortable: nil, filterable: nil, aggregatable: nil, groupable: nil,
+ backing_indexing_field: nil, as_input: false, legacy_grouping_schema: false
+ )
+ type_ref = schema_def_state.type_ref(type)
+ super(
+ name: name,
+ original_type: type_ref,
+ parent_type: parent_type,
+ original_type_for_derived_types: type_for_derived_types ? schema_def_state.type_ref(type_for_derived_types) : type_ref,
+ schema_def_state: schema_def_state,
+ accuracy_confidence: accuracy_confidence,
+ filter_customizations: [],
+ grouped_by_customizations: [],
+ sub_aggregations_customizations: [],
+ aggregated_values_customizations: [],
+ sort_order_enum_value_customizations: [],
+ args: {},
+ sortable: sortable,
+ filterable: filterable,
+ aggregatable: aggregatable,
+ groupable: groupable,
+ graphql_only: graphql_only,
+ source: nil,
+ runtime_field_script: nil,
+ singular_name: singular,
+ name_in_index: name_in_index,
+ non_nullable_in_json_schema: false,
+ backing_indexing_field: backing_indexing_field,
+ as_input: as_input,
+ legacy_grouping_schema: legacy_grouping_schema
+ )
+
+ if name != name_in_index && name_in_index&.include?(".") && !graphql_only
+ raise Errors::SchemaError, "#{self} has an invalid `name_in_index`: #{name_in_index.inspect}. Only `graphql_only: true` fields can have a `name_in_index` that references a child field."
+ end
+
+ schema_def_state.register_user_defined_field(self)
+ yield self if block_given?
+ end
+
+ @@initialize_param_names = instance_method(:initialize).parameters.map(&:last).to_set
+
+ prepend Mixins::VerifiesGraphQLName
+
+ def type
+ original_type.to_final_form(as_input: as_input)
+ end
+
+ def type_for_derived_types
+ original_type_for_derived_types.to_final_form(as_input: as_input)
+ end
+
+ def customize_filter_field(&customization_block)
+ filter_customizations << customization_block
+ end
+
+ def customize_aggregated_values_field(&customization_block)
+ aggregated_values_customizations << customization_block
+ end
+
+ def customize_grouped_by_field(&customization_block)
+ grouped_by_customizations << customization_block
+ end
+
+ def customize_sub_aggregations_field(&customization_block)
+ sub_aggregations_customizations << customization_block
+ end
+
+ def customize_sort_order_enum_values(&customization_block)
+ sort_order_enum_value_customizations << customization_block
+ end
+
+ def on_each_generated_schema_element(&customization_block)
+ customization_block.call(self)
+ customize_filter_field(&customization_block)
+ customize_aggregated_values_field(&customization_block)
+ customize_grouped_by_field(&customization_block)
+ customize_sub_aggregations_field(&customization_block)
+ customize_sort_order_enum_values(&customization_block)
+ end
+
+ def json_schema(nullable: nil, **options)
+ if options.key?(:type)
+ raise Errors::SchemaError, "Cannot override JSON schema type of field `#{name}` with `#{options.fetch(:type)}`"
+ end
+
+ case nullable
+ when true
+ raise Errors::SchemaError, "`nullable: true` is not allowed on a field--just declare the GraphQL field as being nullable (no `!` suffix) instead."
+ when false
+ self.non_nullable_in_json_schema = true
+ end
+
+ super(**options)
+ end
+
+ def sourced_from(relationship, field_path)
+ self.source = schema_def_state.factory.new_field_source(
+ relationship_name: relationship,
+ field_path: field_path
+ )
+ end
+
+ def runtime_script(script)
+ self.runtime_field_script = script
+ end
+
+ def renamed_from(old_name)
+ schema_def_state.register_renamed_field(
+ parent_type.name,
+ from: old_name,
+ to: name,
+ defined_at: caller_locations(1, 1).first, defined_via: %(field.renamed_from "#{old_name}")
+ )
+ end
+
+ def to_sdl(type_structure_only: false, default_value_sdl: nil, &arg_selector)
+ if type_structure_only
+ "#{name}#{args_sdl(joiner: ", ", &arg_selector)}: #{type.name}"
+ else
+ args_sdl = args_sdl(joiner: "\n ", after_opening_paren: "\n ", &arg_selector)
+ "#{formatted_documentation}#{name}#{args_sdl}: #{type.name}#{default_value_sdl} #{directives_sdl}".strip
+ end
+ end
+
+ def sortable?
+ return sortable unless sortable.nil?
+
+ return false if type.list?
+
+ return false if type.unwrap_non_null.boolean?
+
+ return false if text?
+
+ return false if type.as_object_type&.has_custom_mapping_type?
+
+ true
+ end
+
+ def filterable?
+ return true if type.fully_unwrapped.name == "GeoLocation"
+
+ return false if relationship || type.fully_unwrapped.as_object_type&.does_not_support?(&:filterable?)
+ return true if filterable.nil?
+ filterable
+ end
+
+ def groupable?
+ return groupable unless groupable.nil?
+
+ return false if parent_type.indexed? && name == "id"
+
+ return false if relationship || type.fully_unwrapped.as_object_type&.does_not_support?(&:groupable?)
+
+ return list_field_groupable_by_single_values? if type.list? && type.fully_unwrapped.leaf?
+
+ return false if nested?
+
+ return false if text?
+
+ true
+ end
+
+ def aggregatable?
+ return aggregatable unless aggregatable.nil?
+ return false if relationship
+
+ return false if nested?
+
+ return false if text?
+
+ type_for_derived_types.fully_unwrapped.as_object_type&.supports?(&:aggregatable?) || index_leaf?
+ end
+
+ def sub_aggregatable?
+ return false if relationship
+
+ nested? || type_for_derived_types.fully_unwrapped.as_object_type&.supports?(&:sub_aggregatable?)
+ end
+
+ def argument(name, value_type, &block)
+ args[name] = schema_def_state.factory.new_argument(
+ self,
+ name,
+ schema_def_state.type_ref(value_type),
+ &block
+ )
+ end
+
+ def mapping_type
+ backing_indexing_field&.mapping_type || (resolve_mapping || {})["type"]
+ end
+
+ def list_field_groupable_by_single_values?
+ (type.list? || backing_indexing_field&.type&.list?) && !singular_name.nil?
+ end
+
+ def define_aggregated_values_field(parent_type)
+ return unless aggregatable?
+
+ unwrapped_type_for_derived_types = type_for_derived_types.fully_unwrapped
+ aggregated_values_type =
+ if index_leaf?
+ unwrapped_type_for_derived_types.resolved.aggregated_values_type
+ else
+ unwrapped_type_for_derived_types.as_aggregated_values
+ end
+
+ parent_type.field name, aggregated_values_type.name, name_in_index: name_in_index, graphql_only: true do |f|
+ f.documentation derived_documentation("Computed aggregate values for the `#{name}` field")
+ aggregated_values_customizations.each { |block| block.call(f) }
+ end
+ end
+
+ def define_grouped_by_field(parent_type)
+ return unless (field_name = grouped_by_field_name)
+
+ parent_type.field field_name, grouped_by_field_type_name, name_in_index: name_in_index, graphql_only: true do |f|
+ add_grouped_by_field_documentation(f)
+
+ define_legacy_timestamp_grouping_arguments_if_needed(f) if legacy_grouping_schema
+
+ grouped_by_customizations.each { |block| block.call(f) }
+ end
+ end
+
+ def grouped_by_field_type_name
+ unwrapped_type = type_for_derived_types.fully_unwrapped
+ if unwrapped_type.scalar_type_needing_grouped_by_object? && !legacy_grouping_schema
+ unwrapped_type.with_reverted_override.as_grouped_by.name
+ elsif unwrapped_type.leaf?
+ unwrapped_type.name
+ else
+ unwrapped_type.as_grouped_by.name
+ end
+ end
+
+ def add_grouped_by_field_documentation(field)
+ text = if list_field_groupable_by_single_values?
+ derived_documentation(
+ "The individual value from `#{name}` for this group",
+ list_field_grouped_by_doc_note("`#{name}`")
+ )
+ elsif type.list? && type.fully_unwrapped.object?
+ derived_documentation(
+ "The `#{name}` field value for this group",
+ list_field_grouped_by_doc_note("the selected subfields of `#{name}`")
+ )
+ elsif type_for_derived_types.fully_unwrapped.scalar_type_needing_grouped_by_object? && !legacy_grouping_schema
+ derived_documentation("Offers the different grouping options for the `#{name}` value within this group")
+ else
+ derived_documentation("The `#{name}` field value for this group")
+ end
+
+ field.documentation text
+ end
+
+ def grouped_by_field_name
+ return nil unless groupable?
+ list_field_groupable_by_single_values? ? singular_name : name
+ end
+
+ def define_sub_aggregations_field(parent_type:, type:)
+ parent_type.field name, type, name_in_index: name_in_index, graphql_only: true do |f|
+ f.documentation derived_documentation("Used to perform a sub-aggregation of `#{name}`")
+ sub_aggregations_customizations.each { |c| c.call(f) }
+
+ yield f if block_given?
+ end
+ end
+
+ def to_filter_field(parent_type:, for_single_value: !type_for_derived_types.list?)
+ type_prefix = text? ? "Text" : type_for_derived_types.fully_unwrapped.name
+ filter_type = schema_def_state
+ .type_ref(type_prefix)
+ .as_static_derived_type(filter_field_category(for_single_value))
+ .name
+
+ params = to_h
+ .slice(*@@initialize_param_names)
+ .merge(type: filter_type, parent_type: parent_type, name_in_index: name_in_index, type_for_derived_types: nil)
+
+ schema_def_state.factory.new_field(**params).tap do |f|
+ f.documentation derived_documentation(
+ "Used to filter on the `#{name}` field",
+ "Will be ignored if `null` or an empty object is passed"
+ )
+
+ filter_customizations.each { |c| c.call(f) }
+ end
+ end
+
+ def
+ argument schema_def_state.schema_elements.first.to_sym, "Int" do |a|
+ a.documentation <<~EOS
+ Used in conjunction with the `after` argument to forward-paginate through the `#{name}`.
+ When provided, limits the number of returned results to the first `n` after the provided
+ `after` cursor (or from the start of the `#{name}`, if no `after` cursor is provided).
+
+ See the [Relay GraphQL Cursor Connections
+ Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info.
+ EOS
+ end
+
+ argument schema_def_state.schema_elements.after.to_sym, "Cursor" do |a|
+ a.documentation <<~EOS
+ Used to forward-paginate through the `#{name}`. When provided, the next page after the
+ provided cursor will be returned.
+
+ See the [Relay GraphQL Cursor Connections
+ Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info.
+ EOS
+ end
+
+ argument schema_def_state.schema_elements.last.to_sym, "Int" do |a|
+ a.documentation <<~EOS
+ Used in conjunction with the `before` argument to backward-paginate through the `#{name}`.
+ When provided, limits the number of returned results to the last `n` before the provided
+ `before` cursor (or from the end of the `#{name}`, if no `before` cursor is provided).
+
+ See the [Relay GraphQL Cursor Connections
+ Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info.
+ EOS
+ end
+
+ argument schema_def_state.schema_elements.before.to_sym, "Cursor" do |a|
+ a.documentation <<~EOS
+ Used to backward-paginate through the `#{name}`. When provided, the previous page before the
+ provided cursor will be returned.
+
+ See the [Relay GraphQL Cursor Connections
+ Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info.
+ EOS
+ end
+ end
+
+ def to_indexing_field_reference
+ return nil if graphql_only
+
+ Indexing::FieldReference.new(
+ name: name,
+ name_in_index: name_in_index,
+ type: non_nullable_in_json_schema ? type.wrap_non_null : type,
+ mapping_options: mapping_options,
+ json_schema_options: json_schema_options,
+ accuracy_confidence: accuracy_confidence,
+ source: source,
+ runtime_field_script: runtime_field_script
+ )
+ end
+
+ def to_indexing_field
+ to_indexing_field_reference&.resolve
+ end
+
+ def resolve_mapping
+ to_indexing_field&.mapping
+ end
+
+ def paths_to_lists_for_count_indexing(has_list_ancestor: false)
+ self_path = (has_list_ancestor || type.list?) ? [name_in_index] : []
+
+ nested_paths =
+ if !nested? && (object_type = type.fully_unwrapped.as_object_type)
+ object_type.indexing_fields_by_name_in_index.values.flat_map do |sub_field|
+ sub_field.paths_to_lists_for_count_indexing(has_list_ancestor: has_list_ancestor || type.list?).map do |sub_path|
+ "#{name_in_index}#{LIST_COUNTS_FIELD_PATH_KEY_SEPARATOR}#{sub_path}"
+ end
+ end
+ else
+ []
+ end
+
+ self_path + nested_paths
+ end
+
+ def index_leaf?
+ type_for_derived_types.fully_unwrapped.leaf? || DATASTORE_PROPERTYLESS_OBJECT_TYPES.include?(mapping_type)
+ end
+
+ ACCURACY_SCORES = {
+ high: 3,
+
+ medium: 2,
+
+ low: 1
+ }
+
+ def self.pick_most_accurate_from(field1, field2, to_comparable: ->(it) { it })
+ return field1 if to_comparable.call(field1) == to_comparable.call(field2)
+ yield if field1.accuracy_confidence == field2.accuracy_confidence
+ _ = [field1, field2].max_by { |f| ACCURACY_SCORES.fetch(f.accuracy_confidence) }
+ end
+
+ def nested?
+ mapping_type == "nested"
+ end
+
+ def runtime_metadata_computation_detail(empty_bucket_value:, function:)
+ self.computation_detail = SchemaArtifacts::RuntimeMetadata::ComputationDetail.new(
+ empty_bucket_value: empty_bucket_value,
+ function: function
+ )
+ end
+
+ def runtime_metadata_graphql_field
+ SchemaArtifacts::RuntimeMetadata::GraphQLField.new(
+ name_in_index: name_in_index,
+ computation_detail: computation_detail,
+ relation: relationship&.runtime_metadata
+ )
+ end
+
+ private
+
+ def args_sdl(joiner:, after_opening_paren: "", &arg_selector)
+ selected_args = args.values.select(&arg_selector)
+ args_sdl = selected_args.map(&:to_sdl).flat_map { |s| s.split("\n") }.join(joiner)
+ return nil if args_sdl.empty?
+ "(#{after_opening_paren}#{args_sdl})"
+ end
+
+ def text?
+ mapping_type == "text"
+ end
+
+ def define_legacy_timestamp_grouping_arguments_if_needed(grouping_field)
+ case type.fully_unwrapped.name
+ when "Date"
+ grouping_field.argument schema_def_state.schema_elements.granularity, "DateGroupingGranularity!" do |a|
+ a.documentation "Determines the grouping granularity for this field."
+ end
+
+ grouping_field.argument schema_def_state.schema_elements.offset_days, "Int" do |a|
+ a.documentation <<~EOS
+ Number of days (positive or negative) to shift the `Date` boundaries of each date grouping bucket.
+
+ For example, when grouping by `YEAR`, this can be used to align the buckets with fiscal or school years instead of calendar years.
+ EOS
+ end
+ when "DateTime"
+ grouping_field.argument schema_def_state.schema_elements.granularity, "DateTimeGroupingGranularity!" do |a|
+ a.documentation "Determines the grouping granularity for this field."
+ end
+
+ grouping_field.argument schema_def_state.schema_elements.time_zone, "TimeZone" do |a|
+ a.documentation "The time zone to use when determining which grouping a `DateTime` value falls in."
+ a.default "UTC"
+ end
+
+ grouping_field.argument schema_def_state.schema_elements.offset, "DateTimeGroupingOffsetInput" do |a|
+ a.documentation <<~EOS
+ Amount of offset (positive or negative) to shift the `DateTime` boundaries of each grouping bucket.
+
+ For example, when grouping by `WEEK`, you can shift by 24 hours to change what day-of-week weeks are considered to start on.
+ EOS
+ end
+ end
+ end
+
+ def list_field_grouped_by_doc_note(individual_value_selection_description)
+ <<~EOS.strip
+ Note: `#{name}` is a collection field, but selecting this field will group on individual values of #{individual_value_selection_description}.
+ That means that a document may be grouped into multiple aggregation groupings (i.e. when its `#{name}`
+ field has multiple values) leading to some data duplication in the response. However, if a value shows
+ up in `#{name}` multiple times for a single document, that document will only be included in the group
+ once
+ EOS
+ end
+
+ def filter_field_category(for_single_value)
+ return :filter_input if for_single_value
+
+ return :list_filter_input if index_leaf?
+
+ return :fields_list_filter_input unless type_for_derived_types.list?
+
+ case mapping_type
+ when "nested" then :list_filter_input
+ when "object" then :fields_list_filter_input
+ else
+ raise Errors::SchemaError, <<~EOS
+ `#{parent_type.name}.#{name}` is a list-of-objects field, but the mapping type has not been explicitly specified. Elasticsearch and OpenSearch
+ offer two ways to index list-of-objects fields. It cannot be changed on an existing field without dropping the index and recreating it (losing
+ any existing indexed data!), and there are nuanced tradeoffs involved here, so ElasticGraph provides no default mapping in this situation.
+
+ If you're currently prototyping and don't want to spend time weighing this tradeoff, we recommend you do this:
+
+ ```
+ t.field "#{name}", "#{type.name}" do |f|
+ # Here we are opting for flexibility (nested) over pure performance (object).
+ # TODO: evaluate if we want to stick with `nested` before going to production.
+ f.mapping type: "nested"
+ end
+ ```
+
+ Read on for details of the tradeoff involved here.
+
+ -----------------------------------------------------------------------------------------------------------------------------
+
+ Here are the options:
+
+ 1) `f.mapping type: "object"` will cause each field path to be indexed as a separate "flattened" list.
+
+ For example, given a `Film` document like this:
+
+ ```
+ {
+ "name": "The Empire Strikes Back",
+ "characters": [
+ {"first": "Luke", "last": "Skywalker"},
+ {"first": "Han", "last": "Solo"}
+ ]
+ }
+ ```
+
+ ...the data will look like this in the inverted Lucene index:
+
+ ```
+ {
+ "name": "The Empire Strikes Back",
+ "characters.first": ["Luke", "Han"],
+ "characters.last": ["Skywalker", "Solo"]
+ }
+ ```
+
+ This is highly efficient, but there is no way to search on multiple fields of a character and be sure that the matching values came from the same character.
+ ElasticGraph models this in the filtering API it offers for this case:
+
+ ```
+ query {
+ films(filter: {
+ characters: {
+ first: {#{schema_def_state.schema_elements.any_satisfy}: {#{schema_def_state.schema_elements.equal_to_any_of}: ["Luke"]}}
+ last: {#{schema_def_state.schema_elements.any_satisfy}: {#{schema_def_state.schema_elements.equal_to_any_of}: ["Skywalker"]}}
+ }
+ }) {
+ # ...
+ }
+ }
+ ```
+
+ As suggested by this filtering API, this will match any film that has a character with a first name of "Luke" and a character
+ with the last name of "Skywalker", but this could be satisfied by two separate characters.
+
+ 2) `f.mapping type: "nested"` will cause each _object_ in the list to be indexed as a separate hidden document, preserving the independence of each.
+
+ Given a `Film` document like "The Empire Strikes Back" from above, the `nested` type will index separate hidden documents for each character. This
+ allows ElasticGraph to offer this filtering API instead:
+
+ ```
+ query {
+ films(filter: {
+ characters: {#{schema_def_state.schema_elements.any_satisfy}: {
+ first: {#{schema_def_state.schema_elements.equal_to_any_of}: ["Luke"]}
+ last: {#{schema_def_state.schema_elements.equal_to_any_of}: ["Skywalker"]}
+ }}
+ }) {
+ # ...
+ }
+ }
+ ```
+
+ As suggested by this filtering API, this will only match films that have a character named "Luke Skywalker". However, the Elasticsearch docs[^1][^2] warn
+ that the `nested` mapping type can lead to performance problems, and index sorting cannot be configured[^3] when the `nested` type is used.
+
+ [^1]: https://www.elastic.co/guide/en/elasticsearch/reference/8.10/nested.html
+ [^2]: https://www.elastic.co/guide/en/elasticsearch/reference/8.10/joining-queries.html
+ [^3]: https://www.elastic.co/guide/en/elasticsearch/reference/8.10/index-modules-index-sorting.html
+ EOS
+ end
+ end
+end
+ |
+
+