Skip to content

Commit 4b9edba

Browse files
MONGOID-5918 Support BSON::Vector field type (#6162)
Declaring a field with type: BSON::Vector now stores the value as a BSON binary of the vector subtype and reads it back as a BSON::Vector, preserving dtype and padding. The extension is guarded so Mongoid still loads on bson-ruby < 5.1, which predates BSON::Vector. Querying by an exact vector value is not supported because BSON::Vector subclasses Array; use $vectorSearch for similarity queries.
1 parent 90380d3 commit 4b9edba

3 files changed

Lines changed: 240 additions & 0 deletions

File tree

lib/mongoid/extensions.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@
2626
require 'mongoid/extensions/time'
2727
require 'mongoid/extensions/time_with_zone'
2828
require 'mongoid/extensions/true_class'
29+
require 'mongoid/extensions/vector'

lib/mongoid/extensions/vector.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Extensions
5+
# Adds type-casting behavior to BSON::Vector class so that a field
6+
# declared with +type: BSON::Vector+ is stored as a BSON binary of the
7+
# vector subtype and read back as a BSON::Vector.
8+
#
9+
# Querying by an exact vector value (e.g. +where(embedding: vector)+) is
10+
# not supported: BSON::Vector subclasses Array, so the criteria selector
11+
# treats it as a list of elements rather than a scalar. Use Atlas Vector
12+
# Search ($vectorSearch) for similarity queries on vector fields.
13+
module Vector
14+
# Turn the object from the ruby type we deal with to a Mongo friendly
15+
# type.
16+
#
17+
# @example Mongoize the object.
18+
# object.mongoize
19+
#
20+
# @return [ BSON::Binary | nil ] The object as a vector binary.
21+
def mongoize
22+
BSON::Vector.mongoize(self)
23+
end
24+
25+
module ClassMethods
26+
# Mongoize an object of any type to how it's stored in the db.
27+
#
28+
# @example Mongoize the object.
29+
# BSON::Vector.mongoize(vector)
30+
#
31+
# @param [ Object ] object The object to Mongoize.
32+
#
33+
# @return [ BSON::Binary | nil ] A vector binary or nil.
34+
def mongoize(object)
35+
case object
36+
when BSON::Vector then BSON::Binary.from_vector(object)
37+
when BSON::Binary then object
38+
end
39+
end
40+
41+
# Convert the object from its mongo friendly ruby type back to a
42+
# BSON::Vector.
43+
#
44+
# @example Demongoize the object.
45+
# BSON::Vector.demongoize(binary)
46+
#
47+
# @param [ Object ] object The object to demongoize.
48+
#
49+
# @return [ BSON::Vector | nil ] The vector or nil.
50+
def demongoize(object)
51+
case object
52+
when BSON::Binary then (object.type == :vector) ? object.as_vector : nil
53+
when BSON::Vector then object
54+
end
55+
end
56+
end
57+
end
58+
end
59+
end
60+
61+
if defined?(BSON::Vector)
62+
BSON::Vector.include Mongoid::Extensions::Vector
63+
BSON::Vector.extend(Mongoid::Extensions::Vector::ClassMethods)
64+
end
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
# BSON::Vector only exists in bson-ruby >= 5.1, so the model and all examples
6+
# are guarded by min_bson_version. The describe argument is a string (not the
7+
# constant) so this file loads cleanly on older bson versions.
8+
if defined?(BSON::Vector)
9+
class VectorEmbedding
10+
include Mongoid::Document
11+
12+
field :embedding, type: BSON::Vector
13+
end
14+
end
15+
16+
describe 'BSON::Vector field type' do
17+
min_bson_version '5.1'
18+
19+
shared_examples 'a vector round-trip' do
20+
let(:vector) do
21+
BSON::Vector.new(values, dtype, padding)
22+
end
23+
24+
let(:binary) do
25+
BSON::Binary.from_vector(vector)
26+
end
27+
28+
describe '.mongoize' do
29+
it 'returns the matching vector binary' do
30+
expect(BSON::Vector.mongoize(vector)).to eq(binary)
31+
expect(BSON::Vector.mongoize(vector).type).to eq(:vector)
32+
end
33+
end
34+
35+
describe '.demongoize' do
36+
let(:demongoized) do
37+
BSON::Vector.demongoize(binary)
38+
end
39+
40+
it 'preserves the values, dtype and padding' do
41+
expect(demongoized).to be_a(BSON::Vector)
42+
expect(demongoized.to_a).to eq(values)
43+
expect(demongoized.dtype).to eq(dtype)
44+
expect(demongoized.padding).to eq(padding)
45+
end
46+
end
47+
48+
context 'when used as a field type' do
49+
let!(:model) do
50+
VectorEmbedding.create!(embedding: vector)
51+
end
52+
53+
let(:reloaded) do
54+
VectorEmbedding.find(model._id)
55+
end
56+
57+
it 'stores the value as a vector binary' do
58+
stored = VectorEmbedding.collection.find(_id: model._id).first['embedding']
59+
expect(stored).to be_a(BSON::Binary)
60+
expect(stored.type).to eq(:vector)
61+
end
62+
63+
it 'reads the value back as an equivalent BSON::Vector' do
64+
expect(reloaded.embedding).to be_a(BSON::Vector)
65+
expect(reloaded.embedding.to_a).to eq(values)
66+
expect(reloaded.embedding.dtype).to eq(dtype)
67+
expect(reloaded.embedding.padding).to eq(padding)
68+
end
69+
end
70+
end
71+
72+
context 'with an int8 vector' do
73+
let(:values) { [ 1, 2, 3 ] }
74+
let(:dtype) { :int8 }
75+
let(:padding) { 0 }
76+
77+
it_behaves_like 'a vector round-trip'
78+
end
79+
80+
context 'with a float32 vector' do
81+
let(:values) { [ 1.5, -2.0, 0.25 ] }
82+
let(:dtype) { :float32 }
83+
let(:padding) { 0 }
84+
85+
it_behaves_like 'a vector round-trip'
86+
end
87+
88+
context 'with a packed_bit vector' do
89+
let(:values) { [ 255, 0, 128 ] }
90+
let(:dtype) { :packed_bit }
91+
let(:padding) { 3 }
92+
93+
it_behaves_like 'a vector round-trip'
94+
end
95+
96+
describe '#mongoize' do
97+
let(:vector) do
98+
BSON::Vector.new([ 1, 2, 3 ], :int8)
99+
end
100+
101+
it 'delegates to the class method and returns a vector binary' do
102+
expect(vector.mongoize).to eq(BSON::Binary.from_vector(vector))
103+
expect(vector.mongoize.type).to eq(:vector)
104+
end
105+
106+
# Regression guard: BSON::Vector < ::Array, and Array has its own Mongoid
107+
# extension. Without our override the value would be mongoized as a plain
108+
# array, losing the vector subtype.
109+
it 'does not mongoize via the Array extension' do
110+
expect(vector.mongoize).to be_a(BSON::Binary)
111+
expect(vector.mongoize).not_to be_a(Array)
112+
end
113+
end
114+
115+
describe '.mongoize' do
116+
let(:vector) do
117+
BSON::Vector.new([ 1, 2, 3 ], :int8)
118+
end
119+
120+
context 'when given a vector BSON::Binary' do
121+
it 'returns it unchanged' do
122+
binary = BSON::Binary.from_vector(vector)
123+
expect(BSON::Vector.mongoize(binary)).to eq(binary)
124+
end
125+
end
126+
127+
context 'when given nil' do
128+
it 'returns nil' do
129+
expect(BSON::Vector.mongoize(nil)).to be_nil
130+
end
131+
end
132+
133+
context 'when given a plain array' do
134+
it 'returns nil' do
135+
expect(BSON::Vector.mongoize([ 1, 2, 3 ])).to be_nil
136+
end
137+
end
138+
139+
context 'when given an uncastable type' do
140+
it 'returns nil' do
141+
expect(BSON::Vector.mongoize(true)).to be_nil
142+
end
143+
end
144+
end
145+
146+
describe '.demongoize' do
147+
let(:vector) do
148+
BSON::Vector.new([ 1, 2, 3 ], :int8)
149+
end
150+
151+
context 'when given a BSON::Vector' do
152+
it 'returns it unchanged' do
153+
expect(BSON::Vector.demongoize(vector)).to eq(vector)
154+
end
155+
end
156+
157+
context 'when given a non-vector BSON::Binary' do
158+
it 'returns nil' do
159+
expect(BSON::Vector.demongoize(BSON::Binary.new('x', :generic))).to be_nil
160+
end
161+
end
162+
163+
context 'when given nil' do
164+
it 'returns nil' do
165+
expect(BSON::Vector.demongoize(nil)).to be_nil
166+
end
167+
end
168+
169+
context 'when given an uncastable type' do
170+
it 'returns nil' do
171+
expect(BSON::Vector.demongoize(true)).to be_nil
172+
end
173+
end
174+
end
175+
end

0 commit comments

Comments
 (0)