zvec-ruby 0.1.0

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.
@@ -0,0 +1,55 @@
1
+ require_relative "test_helper"
2
+
3
+ # Unit tests for the ActiveRecord::Vectorize concern (pure Ruby logic only).
4
+ # These test the module structure and configuration without requiring Rails
5
+ # or a real database.
6
+ class TestActiveRecordVectorize < Minitest::Test
7
+ def setup
8
+ @ar_available = begin
9
+ require "zvec/active_record"
10
+ true
11
+ rescue LoadError
12
+ false
13
+ end
14
+ end
15
+
16
+ def test_module_defined
17
+ skip "active_record deps not available" unless @ar_available
18
+ assert defined?(Zvec::ActiveRecord::Vectorize)
19
+ end
20
+
21
+ def test_instance_methods_module_defined
22
+ skip "active_record deps not available" unless @ar_available
23
+ assert defined?(Zvec::ActiveRecord::Vectorize::InstanceMethods)
24
+ end
25
+
26
+ def test_search_methods_module_defined
27
+ skip "active_record deps not available" unless @ar_available
28
+ assert defined?(Zvec::ActiveRecord::Vectorize::SearchMethods)
29
+ end
30
+
31
+ def test_instance_methods_has_zvec_update_embedding
32
+ skip "active_record deps not available" unless @ar_available
33
+ assert Zvec::ActiveRecord::Vectorize::InstanceMethods.instance_methods.include?(:zvec_update_embedding!)
34
+ end
35
+
36
+ def test_instance_methods_has_zvec_remove_embedding
37
+ skip "active_record deps not available" unless @ar_available
38
+ assert Zvec::ActiveRecord::Vectorize::InstanceMethods.instance_methods.include?(:zvec_remove_embedding!)
39
+ end
40
+
41
+ def test_instance_methods_has_zvec_embedding
42
+ skip "active_record deps not available" unless @ar_available
43
+ assert Zvec::ActiveRecord::Vectorize::InstanceMethods.instance_methods.include?(:zvec_embedding)
44
+ end
45
+
46
+ def test_search_methods_has_zvec_store
47
+ skip "active_record deps not available" unless @ar_available
48
+ assert Zvec::ActiveRecord::Vectorize::SearchMethods.instance_methods.include?(:zvec_store)
49
+ end
50
+
51
+ def test_search_methods_has_vector_search
52
+ skip "active_record deps not available" unless @ar_available
53
+ assert Zvec::ActiveRecord::Vectorize::SearchMethods.instance_methods.include?(:vector_search)
54
+ end
55
+ end
@@ -0,0 +1,312 @@
1
+ require_relative "test_helper"
2
+
3
+ # These tests require the native extension and a working zvec installation.
4
+ # They are skipped when NATIVE_EXT_AVAILABLE is false.
5
+ class TestCollection < Minitest::Test
6
+ include TempDirHelper
7
+
8
+ def setup
9
+ skip "Native extension not available" unless NATIVE_EXT_AVAILABLE
10
+
11
+ @schema = Zvec::Schema.new("test_collection") do
12
+ string "title"
13
+ int32 "year"
14
+ float "rating"
15
+ vector "embedding", dimension: 4,
16
+ index: Zvec::Ext::HnswIndexParams.new(Zvec::COSINE)
17
+ end
18
+ end
19
+
20
+ # --- Creation ---
21
+
22
+ def test_create_and_open
23
+ with_temp_dir do |dir|
24
+ path = File.join(dir, "col")
25
+ col = Zvec::Collection.create_and_open(path, @schema)
26
+ assert_kind_of Zvec::Collection, col
27
+ col.destroy
28
+ end
29
+ end
30
+
31
+ def test_path
32
+ with_temp_dir do |dir|
33
+ path = File.join(dir, "col")
34
+ col = Zvec::Collection.create_and_open(path, @schema)
35
+ assert_equal path, col.path
36
+ col.destroy
37
+ end
38
+ end
39
+
40
+ def test_schema_preserved
41
+ with_temp_dir do |dir|
42
+ path = File.join(dir, "col")
43
+ col = Zvec::Collection.create_and_open(path, @schema)
44
+ assert_same @schema, col.schema
45
+ col.destroy
46
+ end
47
+ end
48
+
49
+ # --- Open existing ---
50
+
51
+ def test_open_existing
52
+ skip "Cannot reopen without explicit close (collection locks on open)"
53
+ end
54
+
55
+ # --- Insert ---
56
+
57
+ def test_insert_single_doc
58
+ with_temp_dir do |dir|
59
+ path = File.join(dir, "col")
60
+ col = Zvec::Collection.create_and_open(path, @schema)
61
+ doc = Zvec::Doc.new(pk: "doc1", schema: @schema)
62
+ doc["title"] = "Test"
63
+ doc["year"] = 2025
64
+ doc["rating"] = 4.5
65
+ doc["embedding"] = [0.1, 0.2, 0.3, 0.4]
66
+ result = col.insert(doc)
67
+ assert_kind_of Array, result
68
+ col.destroy
69
+ end
70
+ end
71
+
72
+ def test_insert_multiple_docs
73
+ with_temp_dir do |dir|
74
+ path = File.join(dir, "col")
75
+ col = Zvec::Collection.create_and_open(path, @schema)
76
+ docs = 3.times.map do |i|
77
+ d = Zvec::Doc.new(pk: "doc#{i}", schema: @schema)
78
+ d["title"] = "Title #{i}"
79
+ d["year"] = 2020 + i
80
+ d["rating"] = i.to_f
81
+ d["embedding"] = [0.1 * i, 0.2, 0.3, 0.4]
82
+ d
83
+ end
84
+ result = col.insert(docs)
85
+ assert_equal 3, result.size
86
+ col.destroy
87
+ end
88
+ end
89
+
90
+ # --- Add convenience ---
91
+
92
+ def test_add_hash
93
+ with_temp_dir do |dir|
94
+ path = File.join(dir, "col")
95
+ col = Zvec::Collection.create_and_open(path, @schema)
96
+ col.add(pk: "d1", title: "Hi", year: 2025, rating: 5.0,
97
+ embedding: [0.1, 0.2, 0.3, 0.4])
98
+ assert_equal 1, col.doc_count
99
+ col.destroy
100
+ end
101
+ end
102
+
103
+ # --- Doc count ---
104
+
105
+ def test_doc_count
106
+ with_temp_dir do |dir|
107
+ path = File.join(dir, "col")
108
+ col = Zvec::Collection.create_and_open(path, @schema)
109
+ assert_equal 0, col.doc_count
110
+
111
+ 3.times do |i|
112
+ col.add(pk: "d#{i}", title: "T", year: 2025, rating: 1.0,
113
+ embedding: [0.1, 0.2, 0.3, 0.4])
114
+ end
115
+ assert_equal 3, col.doc_count
116
+ col.destroy
117
+ end
118
+ end
119
+
120
+ # --- Fetch ---
121
+
122
+ def test_fetch_existing
123
+ with_temp_dir do |dir|
124
+ path = File.join(dir, "col")
125
+ col = Zvec::Collection.create_and_open(path, @schema)
126
+ col.add(pk: "f1", title: "Fetched", year: 2025, rating: 3.0,
127
+ embedding: [0.1, 0.2, 0.3, 0.4])
128
+
129
+ result = col.fetch("f1")
130
+ assert_kind_of Hash, result
131
+ assert result.key?("f1")
132
+ doc = result["f1"]
133
+ assert_kind_of Zvec::Doc, doc
134
+ assert_equal "Fetched", doc["title"]
135
+ col.destroy
136
+ end
137
+ end
138
+
139
+ def test_fetch_multiple
140
+ with_temp_dir do |dir|
141
+ path = File.join(dir, "col")
142
+ col = Zvec::Collection.create_and_open(path, @schema)
143
+ col.add(pk: "a", title: "A", year: 2025, rating: 1.0, embedding: [0.1, 0.2, 0.3, 0.4])
144
+ col.add(pk: "b", title: "B", year: 2025, rating: 2.0, embedding: [0.4, 0.3, 0.2, 0.1])
145
+
146
+ result = col.fetch("a", "b")
147
+ assert_equal 2, result.size
148
+ col.destroy
149
+ end
150
+ end
151
+
152
+ # --- Search ---
153
+
154
+ def test_search
155
+ with_temp_dir do |dir|
156
+ path = File.join(dir, "col")
157
+ col = Zvec::Collection.create_and_open(path, @schema)
158
+ col.add(pk: "s1", title: "Near", year: 2025, rating: 5.0,
159
+ embedding: [1.0, 0.0, 0.0, 0.0])
160
+ col.add(pk: "s2", title: "Far", year: 2025, rating: 1.0,
161
+ embedding: [0.0, 0.0, 0.0, 1.0])
162
+
163
+ results = col.search([1.0, 0.0, 0.0, 0.0], top_k: 2)
164
+ assert_kind_of Array, results
165
+ assert_equal 2, results.size
166
+ results.each { |r| assert_kind_of Zvec::Doc, r }
167
+ # The nearest doc should be "s1"
168
+ assert_equal "s1", results.first.pk
169
+ col.destroy
170
+ end
171
+ end
172
+
173
+ def test_search_with_field
174
+ with_temp_dir do |dir|
175
+ path = File.join(dir, "col")
176
+ col = Zvec::Collection.create_and_open(path, @schema)
177
+ col.add(pk: "x", title: "X", year: 2025, rating: 1.0,
178
+ embedding: [1.0, 0.0, 0.0, 0.0])
179
+
180
+ results = col.search([1.0, 0.0, 0.0, 0.0], field: :embedding, top_k: 1)
181
+ assert_equal 1, results.size
182
+ col.destroy
183
+ end
184
+ end
185
+
186
+ # --- Query ---
187
+
188
+ def test_query_with_filter
189
+ with_temp_dir do |dir|
190
+ path = File.join(dir, "col")
191
+ col = Zvec::Collection.create_and_open(path, @schema)
192
+ col.add(pk: "q1", title: "Old", year: 2020, rating: 1.0, embedding: [1.0, 0.0, 0.0, 0.0])
193
+ col.add(pk: "q2", title: "New", year: 2025, rating: 5.0, embedding: [0.9, 0.1, 0.0, 0.0])
194
+
195
+ results = col.query(
196
+ field_name: "embedding",
197
+ vector: [1.0, 0.0, 0.0, 0.0],
198
+ topk: 10,
199
+ filter: "year=2025"
200
+ )
201
+ assert results.all? { |r| r.is_a?(Zvec::Doc) }
202
+ col.destroy
203
+ end
204
+ end
205
+
206
+ # --- Upsert ---
207
+
208
+ def test_upsert
209
+ with_temp_dir do |dir|
210
+ path = File.join(dir, "col")
211
+ col = Zvec::Collection.create_and_open(path, @schema)
212
+ doc = Zvec::Doc.new(pk: "u1", schema: @schema)
213
+ doc["title"] = "Original"
214
+ doc["year"] = 2025
215
+ doc["rating"] = 1.0
216
+ doc["embedding"] = [0.1, 0.2, 0.3, 0.4]
217
+ col.insert(doc)
218
+
219
+ doc2 = Zvec::Doc.new(pk: "u1", schema: @schema)
220
+ doc2["title"] = "Updated"
221
+ doc2["year"] = 2026
222
+ doc2["rating"] = 5.0
223
+ doc2["embedding"] = [0.4, 0.3, 0.2, 0.1]
224
+ col.upsert(doc2)
225
+
226
+ assert_equal 1, col.doc_count
227
+ col.destroy
228
+ end
229
+ end
230
+
231
+ # --- Delete ---
232
+
233
+ def test_delete
234
+ with_temp_dir do |dir|
235
+ path = File.join(dir, "col")
236
+ col = Zvec::Collection.create_and_open(path, @schema)
237
+ col.add(pk: "del1", title: "Delete me", year: 2025, rating: 1.0,
238
+ embedding: [0.1, 0.2, 0.3, 0.4])
239
+ assert_equal 1, col.doc_count
240
+
241
+ col.delete("del1")
242
+ assert_equal 0, col.doc_count
243
+ col.destroy
244
+ end
245
+ end
246
+
247
+ def test_delete_multiple
248
+ with_temp_dir do |dir|
249
+ path = File.join(dir, "col")
250
+ col = Zvec::Collection.create_and_open(path, @schema)
251
+ 3.times { |i| col.add(pk: "d#{i}", title: "T", year: 2025, rating: 1.0, embedding: [0.1, 0.2, 0.3, 0.4]) }
252
+ assert_equal 3, col.doc_count
253
+
254
+ col.delete("d0", "d1")
255
+ assert_equal 1, col.doc_count
256
+ col.destroy
257
+ end
258
+ end
259
+
260
+ # --- Flush ---
261
+
262
+ def test_flush
263
+ with_temp_dir do |dir|
264
+ path = File.join(dir, "col")
265
+ col = Zvec::Collection.create_and_open(path, @schema)
266
+ col.add(pk: "fl1", title: "Flush", year: 2025, rating: 1.0,
267
+ embedding: [0.1, 0.2, 0.3, 0.4])
268
+ result = col.flush
269
+ assert_same col, result
270
+ col.destroy
271
+ end
272
+ end
273
+
274
+ # --- Optimize ---
275
+
276
+ def test_optimize
277
+ with_temp_dir do |dir|
278
+ path = File.join(dir, "col")
279
+ col = Zvec::Collection.create_and_open(path, @schema)
280
+ col.add(pk: "o1", title: "Opt", year: 2025, rating: 1.0,
281
+ embedding: [0.1, 0.2, 0.3, 0.4])
282
+ result = col.optimize
283
+ assert_same col, result
284
+ col.destroy
285
+ end
286
+ end
287
+
288
+ # --- Stats ---
289
+
290
+ def test_stats
291
+ with_temp_dir do |dir|
292
+ path = File.join(dir, "col")
293
+ col = Zvec::Collection.create_and_open(path, @schema)
294
+ stats = col.stats
295
+ assert_respond_to stats, :doc_count
296
+ assert_respond_to stats, :index_completeness
297
+ col.destroy
298
+ end
299
+ end
300
+
301
+ # --- Destroy ---
302
+
303
+ def test_destroy
304
+ with_temp_dir do |dir|
305
+ path = File.join(dir, "col")
306
+ col = Zvec::Collection.create_and_open(path, @schema)
307
+ col.destroy
308
+ # After destroy, the collection directory should be gone
309
+ refute Dir.exist?(path)
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,165 @@
1
+ require_relative "test_helper"
2
+
3
+ class TestDataTypes < Minitest::Test
4
+ # --- Scalar data type constants ---
5
+
6
+ def test_string_type_defined
7
+ refute_nil Zvec::DataTypes::STRING
8
+ end
9
+
10
+ def test_bool_type_defined
11
+ refute_nil Zvec::DataTypes::BOOL
12
+ end
13
+
14
+ def test_int32_type_defined
15
+ refute_nil Zvec::DataTypes::INT32
16
+ end
17
+
18
+ def test_int64_type_defined
19
+ refute_nil Zvec::DataTypes::INT64
20
+ end
21
+
22
+ def test_uint32_type_defined
23
+ refute_nil Zvec::DataTypes::UINT32
24
+ end
25
+
26
+ def test_uint64_type_defined
27
+ refute_nil Zvec::DataTypes::UINT64
28
+ end
29
+
30
+ def test_float_type_defined
31
+ refute_nil Zvec::DataTypes::FLOAT
32
+ end
33
+
34
+ def test_double_type_defined
35
+ refute_nil Zvec::DataTypes::DOUBLE
36
+ end
37
+
38
+ # --- Vector data type constants ---
39
+
40
+ def test_vector_fp32_defined
41
+ refute_nil Zvec::DataTypes::VECTOR_FP32
42
+ end
43
+
44
+ def test_vector_fp64_defined
45
+ refute_nil Zvec::DataTypes::VECTOR_FP64
46
+ end
47
+
48
+ def test_vector_fp16_defined
49
+ refute_nil Zvec::DataTypes::VECTOR_FP16
50
+ end
51
+
52
+ def test_vector_int8_defined
53
+ refute_nil Zvec::DataTypes::VECTOR_INT8
54
+ end
55
+
56
+ # --- Sparse vector data type constants ---
57
+
58
+ def test_sparse_vector_fp32_defined
59
+ refute_nil Zvec::DataTypes::SPARSE_VECTOR_FP32
60
+ end
61
+
62
+ def test_sparse_vector_fp16_defined
63
+ refute_nil Zvec::DataTypes::SPARSE_VECTOR_FP16
64
+ end
65
+
66
+ # --- Array data type constants ---
67
+
68
+ def test_array_string_defined
69
+ refute_nil Zvec::DataTypes::ARRAY_STRING
70
+ end
71
+
72
+ def test_array_int32_defined
73
+ refute_nil Zvec::DataTypes::ARRAY_INT32
74
+ end
75
+
76
+ def test_array_float_defined
77
+ refute_nil Zvec::DataTypes::ARRAY_FLOAT
78
+ end
79
+
80
+ def test_array_bool_defined
81
+ refute_nil Zvec::DataTypes::ARRAY_BOOL
82
+ end
83
+
84
+ # --- Metric type constants ---
85
+
86
+ def test_l2_metric_defined
87
+ refute_nil Zvec::DataTypes::L2
88
+ end
89
+
90
+ def test_ip_metric_defined
91
+ refute_nil Zvec::DataTypes::IP
92
+ end
93
+
94
+ def test_cosine_metric_defined
95
+ refute_nil Zvec::DataTypes::COSINE
96
+ end
97
+
98
+ # --- Top-level module includes DataTypes ---
99
+
100
+ def test_zvec_includes_cosine
101
+ refute_nil Zvec::COSINE
102
+ end
103
+
104
+ def test_zvec_includes_vector_fp32
105
+ refute_nil Zvec::VECTOR_FP32
106
+ end
107
+
108
+ def test_zvec_includes_string
109
+ refute_nil Zvec::STRING
110
+ end
111
+
112
+ # --- Dispatch tables ---
113
+
114
+ def test_setter_for_has_string
115
+ assert_equal :set_string, Zvec::DataTypes::SETTER_FOR[Zvec::Ext::DataType::STRING]
116
+ end
117
+
118
+ def test_setter_for_has_bool
119
+ assert_equal :set_bool, Zvec::DataTypes::SETTER_FOR[Zvec::Ext::DataType::BOOL]
120
+ end
121
+
122
+ def test_setter_for_has_int32
123
+ assert_equal :set_int32, Zvec::DataTypes::SETTER_FOR[Zvec::Ext::DataType::INT32]
124
+ end
125
+
126
+ def test_setter_for_has_int64
127
+ assert_equal :set_int64, Zvec::DataTypes::SETTER_FOR[Zvec::Ext::DataType::INT64]
128
+ end
129
+
130
+ def test_setter_for_has_float
131
+ assert_equal :set_float, Zvec::DataTypes::SETTER_FOR[Zvec::Ext::DataType::FLOAT]
132
+ end
133
+
134
+ def test_setter_for_has_double
135
+ assert_equal :set_double, Zvec::DataTypes::SETTER_FOR[Zvec::Ext::DataType::DOUBLE]
136
+ end
137
+
138
+ def test_setter_for_has_vector_fp32
139
+ assert_equal :set_float_vector, Zvec::DataTypes::SETTER_FOR[Zvec::Ext::DataType::VECTOR_FP32]
140
+ end
141
+
142
+ def test_setter_for_has_vector_fp64
143
+ assert_equal :set_double_vector, Zvec::DataTypes::SETTER_FOR[Zvec::Ext::DataType::VECTOR_FP64]
144
+ end
145
+
146
+ def test_setter_for_has_array_string
147
+ assert_equal :set_string_array, Zvec::DataTypes::SETTER_FOR[Zvec::Ext::DataType::ARRAY_STRING]
148
+ end
149
+
150
+ def test_setter_for_is_frozen
151
+ assert Zvec::DataTypes::SETTER_FOR.frozen?
152
+ end
153
+
154
+ def test_getter_for_has_string
155
+ assert_equal :get_string, Zvec::DataTypes::GETTER_FOR[Zvec::Ext::DataType::STRING]
156
+ end
157
+
158
+ def test_getter_for_has_float_vector
159
+ assert_equal :get_float_vector, Zvec::DataTypes::GETTER_FOR[Zvec::Ext::DataType::VECTOR_FP32]
160
+ end
161
+
162
+ def test_getter_for_is_frozen
163
+ assert Zvec::DataTypes::GETTER_FOR.frozen?
164
+ end
165
+ end