weaviate_record 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeaviateRecord
4
+ module Queries
5
+ # This class contains functions to perform count operation on Weaviate Relations
6
+ module Count
7
+ # Return the count of records matching the given conditions or search filters.
8
+ #
9
+ # +:bm25+ will not work here because it is not supported in aggregation queries
10
+ # +:limit+ and +:offset+ does not work with aggregation queries too
11
+ #
12
+ # ==== Example:
13
+ # Article.where(title: 'movie').count #=> 1
14
+ def count
15
+ query = to_query.slice(:class_name, :near_text, :near_vector, :near_object, :where)
16
+ query[:fields] = 'meta { count }'
17
+ @connection.client.query.aggs(**query).dig(0, 'meta', 'count')
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeaviateRecord
4
+ module Queries
5
+ # This module contains functions to perform limit query
6
+ module Limit
7
+ # Limit the number of records to be fetched from the database
8
+ #
9
+ # ==== Example:
10
+ # articles = Article.limit(5)
11
+ # articles.size #=> 5
12
+ def limit(limit_value)
13
+ raise TypeError, 'Limit should be as integer' unless limit_value.to_i.to_s == limit_value.to_s
14
+
15
+ @limit = limit_value
16
+ @loaded = false
17
+ self
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeaviateRecord
4
+ module Queries
5
+ # This module includes methods to perform 'near object' queries.
6
+ module NearObject
7
+ # Performs a similarity search based on the given object.
8
+ # This method takes either id of the object or the object itself and returns the list of objects
9
+ # that are nearer to it in terms of vector distance. You can also limit the distance by passing
10
+ # the distance parameter.
11
+ #
12
+ # ==== Example:
13
+ # Article.create(content: 'This is a movie about friendship, action and adventure')
14
+ # # => #<Article:0x00000001052091e8 id: "983c0970-2c65-4c38-a93f-2ca9272d784b"... >
15
+ # obj = Article.create(content: 'This is a review about a movie')
16
+ # # => #<Article:0x00000001052091e8 id: "0476e426-7e7f-4010-bfad-20c57a65c5c7"... >
17
+ #
18
+ # Article.near_object(obj)
19
+ # # => [... #<Article:0x00000001052091e8 id: "983c0970-2c65-4c38-a93f-2ca9272d784b"... > ]
20
+ def near_object(object, distance: WeaviateRecord.config.similarity_search_threshold)
21
+ unless object.is_a?(WeaviateRecord::Base) || object.is_a?(String)
22
+ raise TypeError, "Invalid type #{object.class} for near object query"
23
+ end
24
+
25
+ raise TypeError, 'Invalid uuid' if object.is_a?(String) && !Constants::UUID_REGEX.match?(object)
26
+ raise TypeError, 'Invalid value for distance' unless distance.is_a?(Numeric)
27
+
28
+ @near_object = "{ id: #{(object.is_a?(WeaviateRecord::Base) ? object.id : object).inspect}, " \
29
+ "distance: #{distance} }"
30
+ @loaded = false
31
+ self
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeaviateRecord
4
+ module Queries
5
+ # This module contains functions to perform near_text query (Context Based Search)
6
+ module NearText
7
+ # Perform a similarity search on Weaviate collection.
8
+ # This method also takes an optional distance parameter to specify the threshold of similarity.
9
+ # You can also pass multiple texts to search in the collection.
10
+ #
11
+ # ==== Example:
12
+ # Article.create(content: 'This is a movie about friendship, action and adventure')
13
+ # # => #<Article:0x00000001052091e8 id: "983c0970-2c65-4c38-a93f-2ca9272d784b"... >
14
+ #
15
+ # Article.near_text('review about a movie')
16
+ # # => [#<Article:0x00000001052091e8 id: "983c0970-2c65-4c38-a93f-2ca9272d784b"... >]
17
+ def near_text(*texts, distance: WeaviateRecord.config.similarity_search_threshold)
18
+ raise TypeError, 'invalid value for text' unless texts.all? { |text| text.is_a?(String) }
19
+ raise TypeError, 'Invalid value for distance' unless distance.is_a?(Numeric)
20
+
21
+ @near_text_options[:distance] = distance
22
+ @near_text_options[:concepts] += texts.map! { |text| text.gsub('"', "'") }
23
+ @loaded = false
24
+ self
25
+ end
26
+
27
+ private
28
+
29
+ def formatted_near_text_value
30
+ texts = @near_text_options[:concepts].map(&:inspect).join(', ')
31
+
32
+ "{ concepts: [#{texts}], distance: #{@near_text_options[:distance]} }"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeaviateRecord
4
+ module Queries
5
+ # This module provides method for near vector query
6
+ module NearVector
7
+ # Performs a similarity search based on the given vector.
8
+ # This method takes a vector (Array of float values) and
9
+ # returns the list of objects that are nearer to it in terms of vector distance.
10
+ # You can also limit the distance by passing the distance parameter.
11
+ #
12
+ # ==== Example:
13
+ # Article.create(content: 'This is a movie about friendship, action and adventure')
14
+ # # => #<Article:0x00000001052091e8 id: "983c0970-2c65-4c38-a93f-2ca9272d784b"... >
15
+ #
16
+ # vector = Article.select(_additional: :vector).where(id: "983c0970-2c65-4c38-a93f-2ca9272d784b").vector
17
+ # # => [-0.37226558, 0.10700592, -0.3906307, 0.1064298 ... ]
18
+ #
19
+ # Article.near_vector(vector)
20
+ # # => [... #<Article:0x00000001052091e8 id: "983c0970-2c65-4c38-a93f-2ca9272d784b"... > ]
21
+ def near_vector(vector, distance: WeaviateRecord.config.similarity_search_threshold)
22
+ raise TypeError, "Invalid type #{vector.class} for near vector query" unless vector.is_a?(Array)
23
+ raise TypeError, 'Invalid vector' unless vector.all? { |v| v.is_a?(Float) }
24
+ raise TypeError, 'Invalid value for distance' unless distance.is_a?(Numeric)
25
+
26
+ @near_vector = "{ vector: #{vector}, distance: #{distance} }"
27
+ @loaded = false
28
+ self
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeaviateRecord
4
+ module Queries
5
+ # This module contains function to offset Weaviate records
6
+ module Offset
7
+ # Offset the number of records to be fetched from the database
8
+ #
9
+ # ==== Example:
10
+ # Article.count #=> 10
11
+ # articles = Article.offset(5)
12
+ # articles.size #=> 5
13
+ def offset(offset_value)
14
+ raise TypeError, 'Offset should be an integer' unless offset_value.to_i.to_s == offset_value.to_s
15
+
16
+ @offset = offset_value
17
+ @loaded = false
18
+ self
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeaviateRecord
4
+ module Queries
5
+ # This module contains function to sort Weaviate records
6
+ module Order
7
+ # Sort the records based on the given attributes.
8
+ # You can pass multiple attributes to sort the records.
9
+ # This sorting specification will be ignored if you are performing keyword (bm25) search.
10
+ #
11
+ # ==== Example:
12
+ # Article.order(:title)
13
+ # # Sorts the records based on title in ascending order
14
+ #
15
+ # Article.order(:title, created_at: :desc)
16
+ # # Sorts the records based on title in ascending order and created_at in descending order
17
+ def order(*args, **kw_args)
18
+ raise ArgumentError, 'expected at least one argument' if args.empty? && kw_args.empty?
19
+
20
+ sorting_specifiers = combine_arguments(args, kw_args)
21
+ assign_sort_options(sorting_specifiers)
22
+ @loaded = false
23
+ self
24
+ end
25
+
26
+ private
27
+
28
+ def validate_attribute_and_order(attribute, sorting_order)
29
+ unless attribute.is_a?(Symbol) || attribute.is_a?(String)
30
+ raise TypeError, 'Invalid type for sorting attribute, should be either type or symbol'
31
+ end
32
+
33
+ return if %i[asc desc].include? sorting_order
34
+
35
+ raise WeaviateRecord::Errors::SortingOptionError, 'Invalid sorting order'
36
+ end
37
+
38
+ def combine_arguments(args, kw_args)
39
+ [*args.map! { |attribute| convert_to_sorting_specifier(attribute) },
40
+ *kw_args.map { |attribute, sorting_order| convert_to_sorting_specifier(attribute, sorting_order) }]
41
+ end
42
+
43
+ def convert_to_sorting_specifier(attribute, sorting_order = :asc)
44
+ validate_attribute_and_order(attribute, sorting_order)
45
+ attribute = map_to_weaviate_attribute(attribute)
46
+
47
+ "{ path: [#{attribute.to_s.inspect}], order: #{sorting_order} }"
48
+ end
49
+
50
+ def map_to_weaviate_attribute(attribute)
51
+ return '_id' if attribute.to_s == 'id'
52
+ return attribute unless WeaviateRecord::Constants::SPECIAL_ATTRIBUTE_MAPPINGS.key?(attribute.to_s)
53
+
54
+ "_#{WeaviateRecord::Constants::SPECIAL_ATTRIBUTE_MAPPINGS[attribute.to_s]}"
55
+ end
56
+
57
+ def assign_sort_options(sorting_specifiers)
58
+ @sort_options = if @sort_options.nil?
59
+ merge_sorting_specifiers(*sorting_specifiers)
60
+ elsif @sort_options.start_with?('[')
61
+ merge_sorting_specifiers(@sort_options[2...-2], *sorting_specifiers)
62
+ else
63
+ merge_sorting_specifiers(@sort_options, *sorting_specifiers)
64
+ end
65
+ end
66
+
67
+ def merge_sorting_specifiers(*sorting_specifiers)
68
+ return sorting_specifiers[0] if sorting_specifiers.size == 1
69
+
70
+ "[ #{sorting_specifiers.join(', ')} ]"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeaviateRecord
4
+ module Queries
5
+ # This module contains function to perform select query on Weaviate
6
+ module Select
7
+ # Select the attributes to be fetched from the database.
8
+ # You can also pass nested attributes to be fetched.
9
+ # Meta attributes that needs to be fetched should be passed aa a value for key '_additional'.
10
+ # In Weaviate, +id+ is also a meta attribute.
11
+ # If select is not called on a Weaviate query, by default
12
+ # it will fetch all normak attributes with id and timestamps.
13
+ #
14
+ # ==== Example:
15
+ # Article.select(:content, :title)
16
+ # Article.select(_additional: :vector)
17
+ # Article.select( _additional: [:id, :created_at, :updated_at])
18
+ # Article.select(_additional: { answer: :result })
19
+ #
20
+ # Article.all #=> fetches id, content, title, created_at, updated_at
21
+ #
22
+ # There is one more special scenario where you can pass the graphql query directly.
23
+ # It will be used for summarization offered by summarizer module.
24
+ #
25
+ # ==== Example:
26
+ # Article.select(_additional: 'summary(properties: ["content"]) { result }')
27
+ def select(*args)
28
+ args.each do |arg|
29
+ if arg.is_a? Hash
30
+ @select_options[:nested_attributes].merge! arg
31
+ else
32
+ @select_options[:attributes] << arg.to_s unless @select_options[:attributes].include?(arg.to_s)
33
+ end
34
+ end
35
+ @loaded = false
36
+ self
37
+ end
38
+
39
+ private
40
+
41
+ def combined_select_attributes
42
+ attributes = format_array_attribute(@select_options[:attributes])
43
+ return attributes if @select_options[:nested_attributes].empty?
44
+
45
+ "#{attributes} #{format_nested_attribute(@select_options[:nested_attributes])}"
46
+ end
47
+
48
+ def create_or_process_select_attributes(custom_selected, attributes)
49
+ if custom_selected
50
+ attributes.gsub(/(?<=\s)(#{WeaviateRecord::Constants::SPECIAL_ATTRIBUTE_MAPPINGS.keys.join('|')})(?=\s)/,
51
+ WeaviateRecord::Constants::SPECIAL_ATTRIBUTE_MAPPINGS)
52
+ else
53
+ [
54
+ *WeaviateRecord::Schema.find_collection(@klass).attributes_list,
55
+ '_additional { id creationTimeUnix lastUpdateTimeUnix }'
56
+ ].join(' ')
57
+ end
58
+ end
59
+
60
+ def format_array_attribute(array)
61
+ array.map do |attribute|
62
+ case attribute
63
+ when String, Symbol then attribute.to_s
64
+ when Array then format_array_attribute(attribute)
65
+ when Hash then format_nested_attribute(attribute)
66
+ else raise TypeError
67
+ end
68
+ end.join(' ')
69
+ end
70
+
71
+ def format_nested_attribute(hash)
72
+ return_string = String.new
73
+ hash.each do |key, value|
74
+ return_string << key.to_s << ' { '
75
+ case value
76
+ when String, Symbol then return_string << value.to_s << ' } '
77
+ when Array then return_string << format_array_attribute(value) << ' } '
78
+ when Hash then return_string << format_nested_attribute(value) << ' } ' else raise TypeError
79
+ end
80
+ end
81
+ return_string.rstrip
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module WeaviateRecord
6
+ module Queries
7
+ # This module contains function to perform where query on Weaviate
8
+ module Where
9
+ # Perform a where query on the collection. You can pass a string or keyword arguments as a query.
10
+ # It follows the same syntax as ActiveRecord where query. Chaining of where queries is also supported.
11
+ #
12
+ # ==== Example:
13
+ # Article.where('title = ?', 'Hello World')
14
+ # Article.where(title: 'Hello World')
15
+ # Article.where('title = ? AND content = ?', 'Hello World', 'This is a content')
16
+ # Article.where(title: 'Hello World').where(content: 'This is a content')
17
+ def where(query = '', *values, **kw_args)
18
+ validate_arguments(query, values, kw_args)
19
+ keyword_query = process_keyword_conditions(kw_args)
20
+ string_query = process_string_conditions(query, *values)
21
+ combined_query = combine_queries(keyword_query, string_query)
22
+ @where_query = @where_query ? create_logical_condition(@where_query, 'And', combined_query) : combined_query
23
+ @loaded = false
24
+ self
25
+ end
26
+
27
+ # :enddoc:
28
+
29
+ def self.to_ruby_hash(string_condition)
30
+ pattern = /(?<=\s)\w+:|(?<=operator:\s)\w+/
31
+ keys_and_operator = string_condition.scan(pattern).uniq
32
+ json_equivalent = keys_and_operator.map { |i| i.end_with?(':') ? "#{i[0...-1].inspect}:" : i.inspect }
33
+ JSON.parse string_condition.gsub(pattern, keys_and_operator.zip(json_equivalent).to_h)
34
+ rescue StandardError
35
+ raise WeaviateRecord::Errors::WhereQueryConversionError, 'invalid where query format'
36
+ end
37
+
38
+ private
39
+
40
+ def validate_arguments(query, values, kw_args)
41
+ if values.empty? && kw_args.empty?
42
+ raise WeaviateRecord::Errors::InvalidWhereQueryError, 'invalid argument for where query'
43
+ end
44
+
45
+ return unless values.size != query.count('?')
46
+
47
+ raise WeaviateRecord::Errors::InvalidWhereQueryError, 'invalid number of arguments'
48
+ end
49
+
50
+ def process_keyword_conditions(hash)
51
+ return nil if hash.empty?
52
+
53
+ conditions = hash.each_pair.map do |key, value|
54
+ create_query_condition([key.to_s, value.is_a?(Array) ? 'CONTAINS_ANY' : '=', value])
55
+ end
56
+ conditions.inject { |acc, condition| create_logical_condition(acc, 'AND', condition) }
57
+ end
58
+
59
+ def process_string_conditions(query, *values)
60
+ return nil unless query.present? && values.present?
61
+
62
+ logical_operator_match = /\s+(AND|OR)\s+/i.match(query)
63
+ return create_query_condition_from_string(query, values) unless logical_operator_match
64
+
65
+ pre_condition = create_query_condition_from_string(logical_operator_match.pre_match, values)
66
+ post_condition = process_string_conditions(logical_operator_match.post_match, *values)
67
+
68
+ create_logical_condition(pre_condition, logical_operator_match[1], post_condition)
69
+ end
70
+
71
+ def create_query_condition_from_string(condition, values)
72
+ equation = condition.split
73
+ raise WeaviateRecord::Errors::InvalidWhereQueryError, 'unable to process the query' unless equation.size == 3
74
+ raise WeaviateRecord::Errors::InvalidWhereQueryError, 'insufficient values for formatting' if values.empty?
75
+
76
+ equation[-1] = values.shift
77
+ create_query_condition(equation)
78
+ end
79
+
80
+ def combine_queries(first_query, second_query)
81
+ if first_query.present? && second_query.present?
82
+ create_logical_condition(first_query, 'And', second_query)
83
+ else
84
+ first_query.presence || second_query
85
+ end
86
+ end
87
+
88
+ def create_query_condition(equation)
89
+ return null_condition(equation[0]) if equation[2].nil?
90
+
91
+ handle_timestamps_condition(equation)
92
+ "{ path: [\"#{equation[0]}\"], " \
93
+ "operator: #{map_operator(equation[1])}, " \
94
+ "#{map_value_type(equation[2])}: #{equation[2].inspect} }"
95
+ end
96
+
97
+ def handle_timestamps_condition(equation_array)
98
+ return nil unless equation_array[0] == 'created_at' || equation_array[0] == 'updated_at'
99
+
100
+ equation_array[0] = "_#{WeaviateRecord::Constants::SPECIAL_ATTRIBUTE_MAPPINGS[equation_array[0]]}"
101
+ equation_array[2] = equation_array[2].to_datetime.strftime('%Q')
102
+ end
103
+
104
+ def null_condition(attribute)
105
+ "{ path: [\"#{attribute}\"], operator: IsNull, valueBoolean: true }"
106
+ end
107
+
108
+ def create_logical_condition(pre_condition, operator, post_condition)
109
+ "{ operator: #{operator.capitalize}, " \
110
+ "operands: [#{pre_condition}, #{post_condition}] }"
111
+ end
112
+
113
+ def map_operator(operator)
114
+ WeaviateRecord::Constants::OPERATOR_MAPPING_HASH.fetch(operator) do
115
+ raise WeaviateRecord::Errors::InvalidOperatorError, "Invalid conditional operator #{operator}"
116
+ end
117
+ end
118
+
119
+ def map_value_type(value)
120
+ WeaviateRecord::Constants::TYPE_MAPPING_HASH.fetch(value.class) do |klass|
121
+ raise WeaviateRecord::Errors::InvalidValueTypeError, "Invalid value type #{klass} for comparison"
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeaviateRecord
4
+ class Relation
5
+ # This module contains methods which helps to build query for Weaviate
6
+ module QueryBuilder
7
+ # This will return the query that will be sent to Weaviate. More like +to_sql+ in ActiveRecord.
8
+ #
9
+ # ==== Example:
10
+ # Article.select(:title, :content).near_text('friendship movie').limit(5).offset(2).to_query
11
+ # Returns:
12
+ # {:class_name=>"Article",
13
+ # :limit=>"5",
14
+ # :offset=>"2",
15
+ # :fields=>"title content",
16
+ # :near_text=>"{ concepts: [\"friendship movie\"], distance: 0.55 }"}
17
+ def to_query
18
+ query_params = basic_params
19
+ fill_up_keyword_search_param(query_params)
20
+ fill_up_similarity_search_param(query_params)
21
+ fill_up_conditions_param(query_params)
22
+ fill_up_sort_param(query_params)
23
+ fill_up_question_param(query_params)
24
+
25
+ query_params
26
+ end
27
+
28
+ private
29
+
30
+ def basic_params
31
+ { class_name: @klass.to_s, limit: @limit.to_s, offset: @offset.to_s,
32
+ fields: combined_select_attributes }
33
+ end
34
+
35
+ def fill_up_keyword_search_param(query_params)
36
+ query_params[:bm25] = @keyword_search if @keyword_search.present?
37
+ end
38
+
39
+ def fill_up_similarity_search_param(query_params)
40
+ query_params[:near_text] = formatted_near_text_value unless @near_text_options[:concepts].empty?
41
+ query_params[:near_vector] = @near_vector if @near_vector
42
+ query_params[:near_object] = @near_object if @near_object
43
+ end
44
+
45
+ def fill_up_conditions_param(query_params)
46
+ query_params[:where] = @where_query if @where_query
47
+ end
48
+
49
+ def fill_up_sort_param(query_params)
50
+ # Weaviate doesn't support sorting with bm25 search at the time of writing this code.
51
+ query_params[:sort] = @sort_options if @keyword_search.blank? && @sort_options
52
+ end
53
+
54
+ def fill_up_question_param(query_params)
55
+ query_params[:ask] = @ask if @ask
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module WeaviateRecord
6
+ # This class is used to build weaviate queries
7
+ class Relation
8
+ extend Forwardable
9
+ include Enumerable
10
+ include Queries::Bm25
11
+ include Queries::Count
12
+ include Queries::Limit
13
+ include Queries::NearText
14
+ include Queries::NearVector
15
+ include Queries::NearObject
16
+ include Queries::Offset
17
+ include Queries::Order
18
+ include Queries::Select
19
+ include Queries::Where
20
+ include Queries::Ask
21
+ include QueryBuilder
22
+
23
+ def_delegators(:records, :empty?, :present?, :[], :first, :last)
24
+
25
+ # :stopdoc:
26
+ def initialize(klass)
27
+ @select_options = { attributes: [], nested_attributes: {} }
28
+ @near_text_options = { concepts: [], distance: WeaviateRecord.config.similarity_search_threshold }
29
+ @limit = ENV['QUERY_DEFAULTS_LIMIT'] || 25
30
+ @offset = 0
31
+ @klass = klass
32
+ @records = []
33
+ @loaded = false
34
+ @connection = WeaviateRecord::Connection.new(@klass)
35
+ end
36
+ # :startdoc:
37
+
38
+ # To enumerate over each record in the Weaviate relation
39
+ def each(&block)
40
+ records.each(&block)
41
+ end
42
+
43
+ # Gets all the records from Weaviate matching the given conditions or search filters given in the query.
44
+ # This will return an array of WeaviateRecord objects.
45
+ def all
46
+ records
47
+ rescue StandardError => e
48
+ e
49
+ end
50
+
51
+ # Deletes all the records from Weaviate matching the given conditions or search filters given in the query.
52
+ # This will return the result of batch delete operation given by Weaviate.
53
+ #
54
+ # ==== Example:
55
+ # Article.where(title: nil).destroy_all
56
+ # # => {"failed"=>0, "limit"=>10000, "matches"=>3, "objects"=>nil, "successful"=>3}
57
+ def destroy_all
58
+ unless @where_query
59
+ raise WeaviateRecord::Errors::MissingWhereCondition, 'must specifiy atleast one where condition'
60
+ end
61
+
62
+ response = @connection.delete_where(Queries::Where.to_ruby_hash(@where_query))
63
+ return response['results'] if response.is_a?(Hash) && response.key?('results')
64
+
65
+ raise WeaviateRecord::Errors::ServerError,
66
+ response == '' ? 'Unauthorized' : response.dig('error', 'message').presence
67
+ end
68
+
69
+ alias inspect all
70
+ alias to_a all
71
+
72
+ private
73
+
74
+ def records
75
+ return @records if @loaded
76
+
77
+ query = to_query
78
+ custom_selected = query[:fields].present?
79
+ query[:fields] = create_or_process_select_attributes(custom_selected, query[:fields])
80
+ result = @connection.client.query.get(**query)
81
+ @loaded = true
82
+ @records = result.map { |record| @klass.new(custom_selected: custom_selected, **record) }
83
+ @records
84
+ end
85
+ end
86
+ end