weaviate_record 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/weaviate_record/base.rb +241 -0
- data/lib/weaviate_record/concerns/attribute_concern.rb +76 -0
- data/lib/weaviate_record/concerns/record_concern.rb +80 -0
- data/lib/weaviate_record/connection.rb +53 -0
- data/lib/weaviate_record/constants.rb +25 -0
- data/lib/weaviate_record/errors.rb +69 -0
- data/lib/weaviate_record/inspect.rb +36 -0
- data/lib/weaviate_record/method_missing.rb +24 -0
- data/lib/weaviate_record/queries/ask.rb +30 -0
- data/lib/weaviate_record/queries/bm25.rb +29 -0
- data/lib/weaviate_record/queries/count.rb +21 -0
- data/lib/weaviate_record/queries/limit.rb +21 -0
- data/lib/weaviate_record/queries/near_object.rb +35 -0
- data/lib/weaviate_record/queries/near_text.rb +36 -0
- data/lib/weaviate_record/queries/near_vector.rb +32 -0
- data/lib/weaviate_record/queries/offset.rb +22 -0
- data/lib/weaviate_record/queries/order.rb +74 -0
- data/lib/weaviate_record/queries/select.rb +85 -0
- data/lib/weaviate_record/queries/where.rb +126 -0
- data/lib/weaviate_record/relation/query_builder.rb +59 -0
- data/lib/weaviate_record/relation.rb +86 -0
- data/lib/weaviate_record/schema.rb +101 -0
- data/lib/weaviate_record.rb +38 -0
- metadata +111 -0
@@ -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
|