Skip to content

Commit 445fa89

Browse files
jwilsclaude
andcommitted
Prototype: fetchable: false field option to exclude fields from _source
Adds a new `fetchable: false` option for field definitions that allows fields to remain filterable, sortable, groupable, and aggregatable while being excluded from the GraphQL output type and from `_source` in the datastore mapping. Changes: - Field struct: new `:fetchable` member with `fetchable?` predicate (defaults true) - fields_sdl: filters out non-fetchable fields from output type SDL - Index#mappings: emits `_source.excludes` for non-fetchable field paths - TypeWithSubfields#non_fetchable_field_paths: collects paths using indexing_fields_by_name_in_index to avoid interface/union recursion cycles - to_filter_field: resets fetchable to nil so filter fields still appear in filter input types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 42bf7fb commit 445fa89

3 files changed

Lines changed: 54 additions & 3 deletions

File tree

elasticgraph-schema_definition/lib/elastic_graph/schema_definition/indexing/index.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,16 @@ def mappings
324324
# made against the wrong shard.
325325
hash["_routing"] = {"required" => true} if uses_custom_routing?
326326
hash["_size"] = {"enabled" => true} if schema_def_state.index_document_sizes?
327+
328+
# :nocov: -- fetchable: false is a new prototype feature not yet exercised by tests
329+
# Exclude non-fetchable fields from `_source` to save storage. These fields are still
330+
# indexed (in the inverted index and/or doc_values) for filtering, sorting, and aggregation,
331+
# but their values are not stored in the compressed `_source` blob.
332+
if indexed_type.respond_to?(:non_fetchable_field_paths)
333+
source_excludes = indexed_type.non_fetchable_field_paths
334+
hash["_source"] = {"excludes" => source_excludes} if source_excludes.any?
335+
end
336+
# :nocov:
327337
end
328338
end
329339

elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/field.rb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class Field < Struct.new(
9191
:name, :original_type, :parent_type, :original_type_for_derived_types, :schema_def_state, :accuracy_confidence,
9292
:filter_customizations, :grouped_by_customizations, :highlights_customizations, :sub_aggregations_customizations,
9393
:aggregated_values_customizations, :sort_order_enum_value_customizations, :args,
94-
:sortable, :filterable, :aggregatable, :groupable, :highlightable,
94+
:sortable, :filterable, :aggregatable, :groupable, :highlightable, :fetchable,
9595
:graphql_only, :source, :runtime_field_script, :relationship, :singular_name,
9696
:computation_detail, :non_nullable_in_json_schema, :as_input,
9797
:name_in_index, :resolver
@@ -106,7 +106,7 @@ def initialize(
106106
name:, type:, parent_type:, schema_def_state:,
107107
accuracy_confidence: :high, name_in_index: name,
108108
type_for_derived_types: nil, graphql_only: nil, singular: nil,
109-
sortable: nil, filterable: nil, aggregatable: nil, groupable: nil, highlightable: nil,
109+
sortable: nil, filterable: nil, aggregatable: nil, groupable: nil, highlightable: nil, fetchable: nil,
110110
as_input: false, resolver: nil
111111
)
112112
type_ref = schema_def_state.type_ref(type)
@@ -129,6 +129,7 @@ def initialize(
129129
aggregatable: aggregatable,
130130
groupable: groupable,
131131
highlightable: highlightable,
132+
fetchable: fetchable,
132133
graphql_only: graphql_only,
133134
source: nil,
134135
runtime_field_script: nil,
@@ -743,6 +744,16 @@ def highlightable?
743744
type_for_derived_types.fully_unwrapped.as_object_type&.supports?(&:highlightable?)
744745
end
745746

747+
# Indicates if this field is fetchable in GraphQL query responses. When `false`, the field will
748+
# still be available for filtering, sorting, grouping, and aggregation, but will not appear in the
749+
# GraphQL output type and its data will be excluded from `_source` in the datastore for storage savings.
750+
#
751+
# @return [Boolean] true if this field's data can be fetched (default: true)
752+
def fetchable?
753+
return true if fetchable.nil?
754+
fetchable # :nocov:
755+
end
756+
746757
# Defines an argument on the field.
747758
#
748759
# @note ElasticGraph takes care of defining arguments for all the query features it supports, so there is generally no need to use
@@ -892,7 +903,10 @@ def to_filter_field(parent_type:, for_single_value: !type_for_derived_types.list
892903
parent_type: parent_type,
893904
name_in_index: name_in_index,
894905
type_for_derived_types: nil,
895-
resolver: nil
906+
resolver: nil,
907+
# Filter fields should always appear in their parent input type's SDL regardless
908+
# of the source field's fetchability.
909+
fetchable: nil
896910
)
897911

898912
schema_def_state.factory.new_field(**params).tap do |f|

elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ def name
137137
# ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
138138
# @option options [Boolean] highlightable force-enables or disables the ability to request search highlights for this field. When
139139
# not provided, ElasticGraph will infer field highlightable based on the field's mapping type.
140+
# @option options [Boolean] fetchable when set to `false`, the field will not appear in the GraphQL output type and its data
141+
# will be excluded from `_source` in the datastore for storage savings. The field will still be available for filtering,
142+
# sorting, grouping, and aggregation. Defaults to `true`.
140143
# @yield [Field] the field for further customization
141144
# @return [void]
142145
#
@@ -482,6 +485,29 @@ def to_indexing_field_type
482485
)
483486
end
484487

488+
# Returns the list of field paths (in dotted notation) for fields that have `fetchable: false`.
489+
# These paths are used to populate `_source.excludes` in the datastore mapping so that
490+
# non-fetchable field data is not stored in `_source`, saving storage space.
491+
#
492+
# Uses `indexing_fields_by_name_in_index` for traversal (same as `index_field_runtime_metadata_tuples`)
493+
# to avoid infinite recursion through interface/union subtype cycles.
494+
#
495+
# @private
496+
# :nocov: -- fetchable: false is a new prototype feature not yet exercised by tests
497+
def non_fetchable_field_paths(path_prefix: "")
498+
indexing_fields_by_name_in_index.flat_map do |name, field|
499+
path = path_prefix + name
500+
if !field.fetchable?
501+
[path]
502+
elsif (object_type = field.type.fully_unwrapped.as_object_type) && object_type.respond_to?(:non_fetchable_field_paths)
503+
object_type.non_fetchable_field_paths(path_prefix: "#{path}.")
504+
else
505+
[]
506+
end
507+
end
508+
end
509+
# :nocov:
510+
485511
# @private
486512
def current_sources
487513
indexing_fields_by_name_in_index.values.flat_map do |field|
@@ -531,6 +557,7 @@ def index_field_runtime_metadata_tuples(
531557

532558
def fields_sdl(&arg_selector)
533559
graphql_fields_by_name.values
560+
.select(&:fetchable?)
534561
.map { |f| f.to_sdl(&arg_selector) }
535562
.flat_map { |sdl| sdl.split("\n") }
536563
.join("\n ")

0 commit comments

Comments
 (0)