Skip to content

Commit fdf2582

Browse files
committed
Support retrieved_from: :doc_values for direct leaf fields
1 parent 8e9b12f commit fdf2582

36 files changed

Lines changed: 692 additions & 41 deletions

File tree

config/schema/artifacts/datastore_config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,9 @@ index_templates:
12451245
required: true
12461246
_size:
12471247
enabled: true
1248+
_source:
1249+
excludes:
1250+
- workspace_id2
12481251
settings:
12491252
index.mapping.ignore_malformed: false
12501253
index.mapping.coerce: false
@@ -1505,6 +1508,9 @@ indices:
15051508
dynamic: 'false'
15061509
_size:
15071510
enabled: true
1511+
_source:
1512+
excludes:
1513+
- full_address
15081514
settings:
15091515
index.mapping.ignore_malformed: false
15101516
index.mapping.coerce: false

config/schema/artifacts/runtime_metadata.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,7 @@ index_definitions_by_name:
14461446
__counts.shapes|type:
14471447
source: __self
14481448
full_address:
1449+
retrieved_from: doc_values
14491450
source: __self
14501451
geo_location.lat:
14511452
source: __self
@@ -2613,6 +2614,7 @@ index_definitions_by_name:
26132614
weight_in_ng_str:
26142615
source: __self
26152616
workspace_id2:
2617+
retrieved_from: doc_values
26162618
source: __self
26172619
workspace_name:
26182620
source: workspace

config/schema/artifacts_with_apollo/datastore_config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,9 @@ index_templates:
12451245
required: true
12461246
_size:
12471247
enabled: true
1248+
_source:
1249+
excludes:
1250+
- workspace_id2
12481251
settings:
12491252
index.mapping.ignore_malformed: false
12501253
index.mapping.coerce: false
@@ -1505,6 +1508,9 @@ indices:
15051508
dynamic: 'false'
15061509
_size:
15071510
enabled: true
1511+
_source:
1512+
excludes:
1513+
- full_address
15081514
settings:
15091515
index.mapping.ignore_malformed: false
15101516
index.mapping.coerce: false

config/schema/artifacts_with_apollo/runtime_metadata.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,6 +1475,7 @@ index_definitions_by_name:
14751475
__counts.shapes|type:
14761476
source: __self
14771477
full_address:
1478+
retrieved_from: doc_values
14781479
source: __self
14791480
geo_location.lat:
14801481
source: __self
@@ -2642,6 +2643,7 @@ index_definitions_by_name:
26422643
weight_in_ng_str:
26432644
source: __self
26442645
workspace_id2:
2646+
retrieved_from: doc_values
26452647
source: __self
26462648
workspace_name:
26472649
source: workspace

config/schema/widgets.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@
7575
t.field "id", "ID!"
7676

7777
# Here we use an alternate name for this field since it's the routing field and want to verify
78-
# that `name_in_index` works correctly on routing fields.
79-
t.field "workspace_id", "ID", name_in_index: "workspace_id2"
78+
# that `name_in_index` works correctly on routing fields, including when fetched from doc values.
79+
t.field "workspace_id", "ID", name_in_index: "workspace_id2", retrieved_from: :doc_values
8080

8181
# It's a bit funny we have both `amount_cents` and `cost` but it's nice to be able to test
8282
# aggregations on both a root numeric field and on a nested one, so we are keeping both here.
@@ -367,7 +367,7 @@
367367
# We use `indexing_only: true` here to verify that `id` can be an indexing-only field.
368368
t.field "id", "ID!", indexing_only: true
369369

370-
t.field "full_address", "String!"
370+
t.field "full_address", "String!", retrieved_from: :doc_values
371371
t.field "timestamps", "AddressTimestamps"
372372
t.field "geo_location", "GeoLocation"
373373

config/site/src/guides/ai-tools.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ The [elasticgraph-mcp-server](https://pypi.org/project/elasticgraph-mcp-server/)
4343

4444
### Installation
4545

46-
Install and run the MCP server, for example as a [Goose extension](https://block.github.io/goose/docs/getting-started/using-extensions), using:
46+
Install and run the MCP server, for example as a [Goose extension](https://goose-docs.ai/docs/getting-started/using-extensions/), using:
4747

4848
{% include copyable_code_snippet.html language="shell" code="uvx elasticgraph-mcp-server" %}
4949

elasticgraph-graphql/lib/elastic_graph/graphql/datastore_query.rb

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def ignored_values_for_routing
303303
def to_datastore_body
304304
@to_datastore_body ||= aggregations_datastore_body
305305
.merge(document_paginator.to_datastore_body)
306-
.merge({highlight: highlight, query: filter_interpreter.build_query(all_filters), _source: source}.compact)
306+
.merge({docvalue_fields: docvalue_fields, highlight: highlight, query: filter_interpreter.build_query(all_filters), _source: source}.compact)
307307
end
308308

309309
def aggregations_datastore_body
@@ -323,13 +323,28 @@ def aggregations_datastore_body
323323
# we only ask for the fields we need to return.
324324
def source
325325
return true if request_all_fields
326-
requested_source_fields = requested_fields - ["id"]
326+
requested_source_fields = requested_fields_for_source - ["id"]
327327
return false if requested_source_fields.empty?
328328
# Merging in requested_fields as _source:{includes:} based on Elasticsearch documentation:
329329
# https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html#include-exclude
330330
{includes: requested_source_fields.to_a}
331331
end
332332

333+
def docvalue_fields
334+
requested_docvalue_fields =
335+
if request_all_fields
336+
all_docvalue_fields
337+
else
338+
requested_fields.select do |field_path|
339+
requested_via_doc_values?(field_path)
340+
end
341+
end
342+
343+
return nil if requested_docvalue_fields.empty?
344+
345+
requested_docvalue_fields.to_a
346+
end
347+
333348
def highlight
334349
return nil if !request_all_highlights && requested_highlights.empty?
335350

@@ -343,6 +358,30 @@ def highlight
343358
{fields:, highlight_query:}.compact
344359
end
345360

361+
def requested_fields_for_source
362+
@requested_fields_for_source ||= requested_fields.reject do |field_path|
363+
requested_via_doc_values?(field_path)
364+
end
365+
end
366+
367+
def all_docvalue_fields
368+
@all_docvalue_fields ||= search_index_definitions.flat_map do |index_def|
369+
index_def.fields_by_path.filter_map do |field_path, field|
370+
field_path if field.retrieved_from_doc_values?
371+
end
372+
end.to_set
373+
end
374+
375+
def requested_via_doc_values?(field_path)
376+
return false if field_path == "id"
377+
378+
field_definitions = search_index_definitions.filter_map do |index_def|
379+
index_def.fields_by_path[field_path]
380+
end
381+
382+
field_definitions.any? && field_definitions.all?(&:retrieved_from_doc_values?)
383+
end
384+
346385
# Encapsulates dependencies of `Query`, giving us something we can expose off of `application`
347386
# to build queries when desired.
348387
class Builder < Support::MemoizableData.define(:runtime_metadata, :logger, :filter_interpreter, :filter_node_interpreter, :default_page_size, :max_page_size)

elasticgraph-graphql/lib/elastic_graph/graphql/datastore_response/document.rb

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,21 @@
88

99
require "elastic_graph/graphql/decoded_cursor"
1010
require "elastic_graph/support/memoizable_data"
11+
require "elastic_graph/support/hash_util"
1112
require "forwardable"
1213

1314
module ElasticGraph
1415
class GraphQL
1516
module DatastoreResponse
17+
DOCUMENT_MISSING_VALUE = ::Object.new.freeze
18+
1619
# Represents a document fetched from the datastore. Exposes both the raw metadata
1720
# provided by the datastore and the doc payload itself. In addition, you can treat
1821
# it just like a document hash using `#[]` or `#fetch`.
1922
Document = Support::MemoizableData.define(:raw_data, :payload, :decoded_cursor_factory) do
2023
# @implements Document
2124
extend Forwardable
2225

23-
def_delegators :payload, :[], :fetch
24-
2526
def self.build(raw_data, decoded_cursor_factory: DecodedCursor::Factory::Null)
2627
source = raw_data.fetch("_source") do
2728
{} # : ::Hash[::String, untyped]
@@ -51,6 +52,33 @@ def id
5152
raw_data["_id"]
5253
end
5354

55+
def [](key)
56+
fetch(key) { nil }
57+
end
58+
59+
def fetch(key, default_value = DOCUMENT_MISSING_VALUE)
60+
value = lookup_value_at([key], list: false, missing_value: DOCUMENT_MISSING_VALUE)
61+
return value unless value.equal?(DOCUMENT_MISSING_VALUE)
62+
return yield(key) if block_given?
63+
return default_value unless default_value.equal?(DOCUMENT_MISSING_VALUE)
64+
65+
raise KeyError, "key not found: #{key}"
66+
end
67+
68+
def fetch_value_at(path, list:, default_value: DOCUMENT_MISSING_VALUE)
69+
value = lookup_value_at(path, list: list, missing_value: DOCUMENT_MISSING_VALUE)
70+
return value unless value.equal?(DOCUMENT_MISSING_VALUE)
71+
return yield(path) if block_given?
72+
return default_value unless default_value.equal?(DOCUMENT_MISSING_VALUE)
73+
74+
raise KeyError, "path not found: #{path.join(".")}"
75+
end
76+
77+
def value_at(path, list:)
78+
value = lookup_value_at(path, list: list, missing_value: DOCUMENT_MISSING_VALUE)
79+
value.equal?(DOCUMENT_MISSING_VALUE) ? nil : value
80+
end
81+
5482
def sort
5583
raw_data["sort"]
5684
end
@@ -77,6 +105,18 @@ def to_s
77105
"#<#{self.class.name} #{datastore_path}>"
78106
end
79107
alias_method :inspect, :to_s
108+
109+
private
110+
111+
def lookup_value_at(path, list:, missing_value:)
112+
value = Support::HashUtil.fetch_value_at_path(payload, path) { missing_value }
113+
return value unless value.equal?(missing_value)
114+
115+
field_values = raw_data.fetch("fields", {}).fetch(path.join("."), missing_value)
116+
return missing_value if field_values.equal?(missing_value)
117+
118+
list ? field_values : field_values.first
119+
end
80120
end
81121
end
82122
end

elasticgraph-graphql/lib/elastic_graph/graphql/datastore_response/search_response.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def filter_results(field_path, values, size)
114114
# `id` within `_source`, given it's available as `_id`.
115115
->(hit) { values.include?(hit.fetch("_id")) }
116116
else
117-
->(hit) { values.intersect?(Support::HashUtil.fetch_leaf_values_at_path(hit.fetch("_source"), field_path).to_set) }
117+
->(hit) { values.intersect?(hit_values_at_path(hit, field_path).to_set) }
118118
end
119119

120120
hits = raw_data.fetch("hits").fetch("hits").select(&filter).first(size)
@@ -131,6 +131,15 @@ def docs_description
131131
(documents.size < 3) ? documents.inspect : "[#{documents.first}, ..., #{documents.last}]"
132132
end
133133

134+
def hit_values_at_path(hit, field_path)
135+
Support::HashUtil.fetch_leaf_values_at_path(hit.fetch("_source"), field_path)
136+
rescue KeyError => error
137+
fields = hit.fetch("fields", {})
138+
return fields.fetch(field_path.join(".")) if fields.key?(field_path.join("."))
139+
140+
raise error
141+
end
142+
134143
def total_document_count(default: nil)
135144
super() || default || raise(Errors::CountUnavailableError, "#{__method__} is unavailable; set `query.total_document_count_needed = true` to make it available")
136145
end

elasticgraph-graphql/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ def initialize(elasticgraph_graphql:, config:)
1919
end
2020

2121
def resolve(field:, object:, args:, context:)
22-
data =
22+
value =
2323
case object
2424
when DatastoreResponse::Document
25-
object.payload
25+
object.value_at(field.path_in_index, list: field.type.list?)
2626
else
27-
object
27+
data = object
28+
Support::HashUtil.fetch_value_at_path(data, field.path_in_index) { nil }
2829
end
2930

30-
value = Support::HashUtil.fetch_value_at_path(data, field.path_in_index) { nil }
3131
value = [] if value.nil? && field.type.list?
3232

3333
if field.type.relay_connection?

0 commit comments

Comments
 (0)