weaviate_record 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|