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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +190 -0
- data/README.md +189 -0
- data/Rakefile +83 -0
- data/examples/basic.rb +63 -0
- data/examples/with_ruby_llm.rb +79 -0
- data/ext/zvec/extconf.rb +100 -0
- data/ext/zvec/zvec_ext.cpp +771 -0
- data/lib/zvec/active_record.rb +113 -0
- data/lib/zvec/collection.rb +165 -0
- data/lib/zvec/data_types.rb +63 -0
- data/lib/zvec/doc.rb +107 -0
- data/lib/zvec/query.rb +28 -0
- data/lib/zvec/ruby_llm.rb +108 -0
- data/lib/zvec/schema.rb +70 -0
- data/lib/zvec/version.rb +3 -0
- data/lib/zvec.rb +22 -0
- data/test/test_active_record.rb +55 -0
- data/test/test_collection.rb +312 -0
- data/test/test_data_types.rb +165 -0
- data/test/test_doc.rb +271 -0
- data/test/test_ext_bindings.rb +313 -0
- data/test/test_helper.rb +170 -0
- data/test/test_query.rb +64 -0
- data/test/test_ruby_llm_store.rb +166 -0
- data/test/test_schema.rb +133 -0
- data/test/test_version.rb +19 -0
- data/zvec.gemspec +43 -0
- metadata +129 -0
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
require "minitest/autorun"
|
|
2
|
+
require "minitest/pride"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
# The C++ extension requires zvec to be installed. For pure-Ruby layer tests,
|
|
7
|
+
# we stub the Ext module when the native extension is unavailable.
|
|
8
|
+
begin
|
|
9
|
+
require "zvec"
|
|
10
|
+
NATIVE_EXT_AVAILABLE = true
|
|
11
|
+
rescue LoadError
|
|
12
|
+
NATIVE_EXT_AVAILABLE = false
|
|
13
|
+
|
|
14
|
+
# Minimal stubs so pure-Ruby logic can be tested without the compiled extension.
|
|
15
|
+
module Zvec
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
module Ext
|
|
19
|
+
# Stub enums as simple modules with constants
|
|
20
|
+
module DataType
|
|
21
|
+
UNDEFINED = :undefined
|
|
22
|
+
BINARY = :binary; STRING = :string; BOOL = :bool
|
|
23
|
+
INT32 = :int32; INT64 = :int64; UINT32 = :uint32; UINT64 = :uint64
|
|
24
|
+
FLOAT = :float; DOUBLE = :double
|
|
25
|
+
VECTOR_FP32 = :vector_fp32; VECTOR_FP64 = :vector_fp64
|
|
26
|
+
VECTOR_FP16 = :vector_fp16; VECTOR_INT8 = :vector_int8
|
|
27
|
+
SPARSE_VECTOR_FP32 = :sparse_vector_fp32; SPARSE_VECTOR_FP16 = :sparse_vector_fp16
|
|
28
|
+
ARRAY_STRING = :array_string; ARRAY_INT32 = :array_int32
|
|
29
|
+
ARRAY_INT64 = :array_int64; ARRAY_FLOAT = :array_float
|
|
30
|
+
ARRAY_DOUBLE = :array_double; ARRAY_BOOL = :array_bool
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module MetricType
|
|
34
|
+
UNDEFINED = :undefined; L2 = :l2; IP = :ip; COSINE = :cosine; MIPSL2 = :mipsl2
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module IndexType
|
|
38
|
+
UNDEFINED = :undefined; HNSW = :hnsw; IVF = :ivf; FLAT = :flat; INVERT = :invert
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module QuantizeType
|
|
42
|
+
UNDEFINED = :undefined; FP16 = :fp16; INT8 = :int8; INT4 = :int4
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Doc
|
|
46
|
+
attr_accessor :pk, :score
|
|
47
|
+
def initialize; @pk = ""; @score = 0.0; @fields = {}; end
|
|
48
|
+
def field_names; @fields.keys; end
|
|
49
|
+
def has?(f); @fields.key?(f); end
|
|
50
|
+
def has_value?(f); @fields.key?(f) && !@fields[f].nil?; end
|
|
51
|
+
def is_empty; @fields.empty?; end
|
|
52
|
+
alias_method :empty?, :is_empty
|
|
53
|
+
def set_null(f); @fields[f] = nil; end
|
|
54
|
+
def set_string(f, v); @fields[f] = v; end
|
|
55
|
+
def set_bool(f, v); @fields[f] = v; end
|
|
56
|
+
def set_int32(f, v); @fields[f] = v; end
|
|
57
|
+
def set_int64(f, v); @fields[f] = v; end
|
|
58
|
+
def set_uint32(f, v); @fields[f] = v; end
|
|
59
|
+
def set_uint64(f, v); @fields[f] = v; end
|
|
60
|
+
def set_float(f, v); @fields[f] = v; end
|
|
61
|
+
def set_double(f, v); @fields[f] = v; end
|
|
62
|
+
def set_float_vector(f, v); @fields[f] = v; end
|
|
63
|
+
def set_double_vector(f, v); @fields[f] = v; end
|
|
64
|
+
def set_string_array(f, v); @fields[f] = v; end
|
|
65
|
+
def get_string(f); @fields[f].is_a?(String) ? @fields[f] : nil; end
|
|
66
|
+
def get_bool(f); [true, false].include?(@fields[f]) ? @fields[f] : nil; end
|
|
67
|
+
def get_int32(f); @fields[f].is_a?(Integer) ? @fields[f] : nil; end
|
|
68
|
+
def get_int64(f); @fields[f].is_a?(Integer) ? @fields[f] : nil; end
|
|
69
|
+
def get_float(f); @fields[f].is_a?(Float) ? @fields[f] : nil; end
|
|
70
|
+
def get_double(f); @fields[f].is_a?(Float) ? @fields[f] : nil; end
|
|
71
|
+
def get_float_vector(f); @fields[f].is_a?(Array) && @fields[f].first.is_a?(Float) ? @fields[f] : nil; end
|
|
72
|
+
def get_double_vector(f); get_float_vector(f); end
|
|
73
|
+
def get_string_array(f); @fields[f].is_a?(Array) && @fields[f].first.is_a?(String) ? @fields[f] : nil; end
|
|
74
|
+
def to_s; "[pk:#{@pk}, score:#{@score}, fields:#{@fields.size}]"; end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class FieldSchema
|
|
78
|
+
attr_reader :name, :data_type
|
|
79
|
+
attr_accessor :dimension, :nullable
|
|
80
|
+
def initialize(name, data_type); @name = name; @data_type = data_type; @dimension = 0; @nullable = false; end
|
|
81
|
+
def set_index_params(_); end
|
|
82
|
+
def vector_field?; [:vector_fp32, :vector_fp64, :vector_fp16, :vector_int8].include?(@data_type); end
|
|
83
|
+
def to_s; "FieldSchema(#{@name}, #{@data_type})"; end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class CollectionSchema
|
|
87
|
+
attr_reader :name
|
|
88
|
+
def initialize(name); @name = name; @fields = {}; end
|
|
89
|
+
def add_field(fs); @fields[fs.name] = fs; end
|
|
90
|
+
def has_field?(n); @fields.key?(n); end
|
|
91
|
+
def field_names; @fields.keys; end
|
|
92
|
+
def all_field_names; @fields.keys; end
|
|
93
|
+
alias_method :field_names, :all_field_names
|
|
94
|
+
def fields; @fields.values; end
|
|
95
|
+
def vector_fields; @fields.values.select(&:vector_field?); end
|
|
96
|
+
def forward_fields; @fields.values.reject(&:vector_field?); end
|
|
97
|
+
def to_s; "CollectionSchema(#{@name})"; end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
class HnswIndexParams
|
|
101
|
+
def initialize(metric, m: 16, ef_construction: 200, quantize_type: nil); end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class FlatIndexParams
|
|
105
|
+
def initialize(metric, quantize_type: nil); end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class IVFIndexParams
|
|
109
|
+
def initialize(metric, n_list: 1024, n_iters: 10, use_soar: false, quantize_type: nil); end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class InvertIndexParams
|
|
113
|
+
def initialize(enable_range_optimization: true, enable_extended_wildcard: false); end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class HnswQueryParams
|
|
117
|
+
attr_reader :ef
|
|
118
|
+
def initialize(ef: 200); @ef = ef; end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class CollectionOptions
|
|
122
|
+
attr_accessor :read_only, :enable_mmap, :max_buffer_size
|
|
123
|
+
def initialize; @read_only = false; @enable_mmap = true; @max_buffer_size = 64 * 1024 * 1024; end
|
|
124
|
+
alias_method :read_only?, :read_only
|
|
125
|
+
alias_method :enable_mmap?, :enable_mmap
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class VectorQuery
|
|
129
|
+
attr_accessor :topk, :field_name, :filter, :include_vector
|
|
130
|
+
def initialize; @topk = 10; @field_name = ""; @filter = ""; @include_vector = false; end
|
|
131
|
+
def set_query_vector(arr); @query_vector = arr; end
|
|
132
|
+
def set_output_fields(f); @output_fields = f; end
|
|
133
|
+
def set_query_params(p); @query_params = p; end
|
|
134
|
+
alias_method :include_vector?, :include_vector
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
class CollectionStats
|
|
138
|
+
attr_accessor :doc_count
|
|
139
|
+
def initialize; @doc_count = 0; end
|
|
140
|
+
def index_completeness; {}; end
|
|
141
|
+
def to_s; "CollectionStats(doc_count=#{@doc_count})"; end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class Status
|
|
145
|
+
def initialize(ok = true, msg = ""); @ok = ok; @msg = msg; end
|
|
146
|
+
def ok?; @ok; end
|
|
147
|
+
def message; @msg; end
|
|
148
|
+
def to_s; @ok ? "OK" : @msg; end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
require_relative "../lib/zvec/version"
|
|
153
|
+
require_relative "../lib/zvec/data_types"
|
|
154
|
+
require_relative "../lib/zvec/schema"
|
|
155
|
+
require_relative "../lib/zvec/doc"
|
|
156
|
+
require_relative "../lib/zvec/query"
|
|
157
|
+
|
|
158
|
+
include DataTypes
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Helper to create a temporary directory that is cleaned up after the test
|
|
163
|
+
module TempDirHelper
|
|
164
|
+
def with_temp_dir(prefix = "zvec_test")
|
|
165
|
+
dir = Dir.mktmpdir(prefix)
|
|
166
|
+
yield dir
|
|
167
|
+
ensure
|
|
168
|
+
FileUtils.rm_rf(dir) if dir && Dir.exist?(dir)
|
|
169
|
+
end
|
|
170
|
+
end
|
data/test/test_query.rb
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
class TestVectorQuery < Minitest::Test
|
|
4
|
+
def test_initialize_basic
|
|
5
|
+
q = Zvec::VectorQuery.new(
|
|
6
|
+
field_name: "embedding",
|
|
7
|
+
vector: [1.0, 2.0, 3.0]
|
|
8
|
+
)
|
|
9
|
+
assert_kind_of Zvec::VectorQuery, q
|
|
10
|
+
assert_kind_of Zvec::Ext::VectorQuery, q.ext_query
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_field_name
|
|
14
|
+
q = Zvec::VectorQuery.new(field_name: "vec", vector: [1.0])
|
|
15
|
+
assert_equal "vec", q.ext_query.field_name
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_topk_default
|
|
19
|
+
q = Zvec::VectorQuery.new(field_name: "vec", vector: [1.0])
|
|
20
|
+
assert_equal 10, q.ext_query.topk
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_topk_custom
|
|
24
|
+
q = Zvec::VectorQuery.new(field_name: "vec", vector: [1.0], topk: 5)
|
|
25
|
+
assert_equal 5, q.ext_query.topk
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_filter
|
|
29
|
+
q = Zvec::VectorQuery.new(
|
|
30
|
+
field_name: "vec", vector: [1.0],
|
|
31
|
+
filter: "year > 2024"
|
|
32
|
+
)
|
|
33
|
+
assert_equal "year > 2024", q.ext_query.filter
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_include_vector_default_false
|
|
37
|
+
q = Zvec::VectorQuery.new(field_name: "vec", vector: [1.0])
|
|
38
|
+
refute q.ext_query.include_vector?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_include_vector_true
|
|
42
|
+
q = Zvec::VectorQuery.new(
|
|
43
|
+
field_name: "vec", vector: [1.0],
|
|
44
|
+
include_vector: true
|
|
45
|
+
)
|
|
46
|
+
assert q.ext_query.include_vector?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_symbol_field_name
|
|
50
|
+
q = Zvec::VectorQuery.new(field_name: :embedding, vector: [1.0])
|
|
51
|
+
assert_equal "embedding", q.ext_query.field_name
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_integer_vector_converted_to_float
|
|
55
|
+
q = Zvec::VectorQuery.new(field_name: "vec", vector: [1, 2, 3])
|
|
56
|
+
# Should not raise — integers get .to_f
|
|
57
|
+
assert_kind_of Zvec::VectorQuery, q
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_ext_query_accessible
|
|
61
|
+
q = Zvec::VectorQuery.new(field_name: "vec", vector: [1.0])
|
|
62
|
+
refute_nil q.ext_query
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
# Tests for the RubyLLM::Store integration.
|
|
4
|
+
# Requires native extension.
|
|
5
|
+
class TestRubyLLMStore < Minitest::Test
|
|
6
|
+
include TempDirHelper
|
|
7
|
+
|
|
8
|
+
def setup
|
|
9
|
+
skip "Native extension not available" unless NATIVE_EXT_AVAILABLE
|
|
10
|
+
require "zvec/ruby_llm"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_initialize_creates_collection
|
|
14
|
+
with_temp_dir do |dir|
|
|
15
|
+
path = File.join(dir, "store")
|
|
16
|
+
store = Zvec::RubyLLM::Store.new(path, dimension: 4)
|
|
17
|
+
assert_kind_of Zvec::Collection, store.collection
|
|
18
|
+
assert_equal 4, store.dimension
|
|
19
|
+
store.collection.destroy
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_initialize_with_metric
|
|
24
|
+
with_temp_dir do |dir|
|
|
25
|
+
[:cosine, :l2, :ip].each_with_index do |metric, i|
|
|
26
|
+
path = File.join(dir, "store_#{i}")
|
|
27
|
+
store = Zvec::RubyLLM::Store.new(path, dimension: 4, metric: metric)
|
|
28
|
+
assert_kind_of Zvec::Collection, store.collection
|
|
29
|
+
store.collection.destroy
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_initialize_invalid_metric
|
|
35
|
+
with_temp_dir do |dir|
|
|
36
|
+
path = File.join(dir, "store")
|
|
37
|
+
assert_raises(ArgumentError) do
|
|
38
|
+
Zvec::RubyLLM::Store.new(path, dimension: 4, metric: :invalid)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_add_and_count
|
|
44
|
+
with_temp_dir do |dir|
|
|
45
|
+
path = File.join(dir, "store")
|
|
46
|
+
store = Zvec::RubyLLM::Store.new(path, dimension: 4)
|
|
47
|
+
|
|
48
|
+
store.add("doc1", embedding: [0.1, 0.2, 0.3, 0.4], content: "Hello")
|
|
49
|
+
assert_equal 1, store.count
|
|
50
|
+
|
|
51
|
+
store.add("doc2", embedding: [0.4, 0.3, 0.2, 0.1], content: "World")
|
|
52
|
+
assert_equal 2, store.count
|
|
53
|
+
|
|
54
|
+
store.collection.destroy
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_add_many
|
|
59
|
+
with_temp_dir do |dir|
|
|
60
|
+
path = File.join(dir, "store")
|
|
61
|
+
store = Zvec::RubyLLM::Store.new(path, dimension: 4)
|
|
62
|
+
|
|
63
|
+
docs = [
|
|
64
|
+
{ id: "a", embedding: [0.1, 0.2, 0.3, 0.4], content: "A" },
|
|
65
|
+
{ id: "b", embedding: [0.4, 0.3, 0.2, 0.1], content: "B" },
|
|
66
|
+
{ id: "c", embedding: [0.2, 0.4, 0.1, 0.3], content: "C" },
|
|
67
|
+
]
|
|
68
|
+
store.add_many(docs)
|
|
69
|
+
assert_equal 3, store.count
|
|
70
|
+
|
|
71
|
+
store.collection.destroy
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_search
|
|
76
|
+
with_temp_dir do |dir|
|
|
77
|
+
path = File.join(dir, "store")
|
|
78
|
+
store = Zvec::RubyLLM::Store.new(path, dimension: 4)
|
|
79
|
+
|
|
80
|
+
store.add("near", embedding: [1.0, 0.0, 0.0, 0.0], content: "Near")
|
|
81
|
+
store.add("far", embedding: [0.0, 0.0, 0.0, 1.0], content: "Far")
|
|
82
|
+
|
|
83
|
+
results = store.search([1.0, 0.0, 0.0, 0.0], top_k: 2)
|
|
84
|
+
assert_kind_of Array, results
|
|
85
|
+
assert_equal 2, results.size
|
|
86
|
+
|
|
87
|
+
# Each result is a hash
|
|
88
|
+
first = results.first
|
|
89
|
+
assert_kind_of Hash, first
|
|
90
|
+
assert first.key?(:id)
|
|
91
|
+
assert first.key?(:score)
|
|
92
|
+
assert first.key?(:content)
|
|
93
|
+
assert first.key?(:metadata)
|
|
94
|
+
|
|
95
|
+
# Nearest should be "near"
|
|
96
|
+
assert_equal "near", first[:id]
|
|
97
|
+
|
|
98
|
+
store.collection.destroy
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_search_returns_content
|
|
103
|
+
with_temp_dir do |dir|
|
|
104
|
+
path = File.join(dir, "store")
|
|
105
|
+
store = Zvec::RubyLLM::Store.new(path, dimension: 4)
|
|
106
|
+
|
|
107
|
+
store.add("d1", embedding: [1.0, 0.0, 0.0, 0.0], content: "Ruby rocks")
|
|
108
|
+
results = store.search([1.0, 0.0, 0.0, 0.0], top_k: 1)
|
|
109
|
+
assert_equal "Ruby rocks", results.first[:content]
|
|
110
|
+
|
|
111
|
+
store.collection.destroy
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_delete
|
|
116
|
+
with_temp_dir do |dir|
|
|
117
|
+
path = File.join(dir, "store")
|
|
118
|
+
store = Zvec::RubyLLM::Store.new(path, dimension: 4)
|
|
119
|
+
|
|
120
|
+
store.add("del1", embedding: [0.1, 0.2, 0.3, 0.4])
|
|
121
|
+
store.add("del2", embedding: [0.4, 0.3, 0.2, 0.1])
|
|
122
|
+
assert_equal 2, store.count
|
|
123
|
+
|
|
124
|
+
store.delete("del1")
|
|
125
|
+
assert_equal 1, store.count
|
|
126
|
+
|
|
127
|
+
store.collection.destroy
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_flush
|
|
132
|
+
with_temp_dir do |dir|
|
|
133
|
+
path = File.join(dir, "store")
|
|
134
|
+
store = Zvec::RubyLLM::Store.new(path, dimension: 4)
|
|
135
|
+
store.add("f1", embedding: [0.1, 0.2, 0.3, 0.4])
|
|
136
|
+
store.flush
|
|
137
|
+
# Should not raise
|
|
138
|
+
store.collection.destroy
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def test_custom_vector_field
|
|
143
|
+
with_temp_dir do |dir|
|
|
144
|
+
path = File.join(dir, "store")
|
|
145
|
+
store = Zvec::RubyLLM::Store.new(path, dimension: 4, vector_field: "my_vec")
|
|
146
|
+
store.add("v1", embedding: [0.1, 0.2, 0.3, 0.4])
|
|
147
|
+
assert_equal 1, store.count
|
|
148
|
+
store.collection.destroy
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_custom_content_field
|
|
153
|
+
with_temp_dir do |dir|
|
|
154
|
+
path = File.join(dir, "store")
|
|
155
|
+
store = Zvec::RubyLLM::Store.new(path, dimension: 4, content_field: "text")
|
|
156
|
+
store.add("c1", embedding: [0.1, 0.2, 0.3, 0.4], content: "Custom")
|
|
157
|
+
results = store.search([0.1, 0.2, 0.3, 0.4], top_k: 1)
|
|
158
|
+
assert_equal "Custom", results.first[:content]
|
|
159
|
+
store.collection.destroy
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def test_reopen_existing_store
|
|
164
|
+
skip "Cannot reopen without explicit close (collection locks on open)"
|
|
165
|
+
end
|
|
166
|
+
end
|
data/test/test_schema.rb
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
class TestSchema < Minitest::Test
|
|
4
|
+
def test_initialize_with_name
|
|
5
|
+
schema = Zvec::Schema.new("test_schema")
|
|
6
|
+
assert_equal "test_schema", schema.name
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def test_initialize_with_block
|
|
10
|
+
schema = Zvec::Schema.new("test") do
|
|
11
|
+
string "title"
|
|
12
|
+
int32 "count"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
assert schema.has_field?("title")
|
|
16
|
+
assert schema.has_field?("count")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_string_field
|
|
20
|
+
schema = Zvec::Schema.new("test") { string "name" }
|
|
21
|
+
assert schema.has_field?("name")
|
|
22
|
+
assert_equal Zvec::DataTypes::STRING, schema.field_type("name")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_int32_field
|
|
26
|
+
schema = Zvec::Schema.new("test") { int32 "count" }
|
|
27
|
+
assert schema.has_field?("count")
|
|
28
|
+
assert_equal Zvec::DataTypes::INT32, schema.field_type("count")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_int64_field
|
|
32
|
+
schema = Zvec::Schema.new("test") { int64 "big_count" }
|
|
33
|
+
assert schema.has_field?("big_count")
|
|
34
|
+
assert_equal Zvec::DataTypes::INT64, schema.field_type("big_count")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_float_field
|
|
38
|
+
schema = Zvec::Schema.new("test") { float "score" }
|
|
39
|
+
assert schema.has_field?("score")
|
|
40
|
+
assert_equal Zvec::DataTypes::FLOAT, schema.field_type("score")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_double_field
|
|
44
|
+
schema = Zvec::Schema.new("test") { double "precise" }
|
|
45
|
+
assert schema.has_field?("precise")
|
|
46
|
+
assert_equal Zvec::DataTypes::DOUBLE, schema.field_type("precise")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_bool_field
|
|
50
|
+
schema = Zvec::Schema.new("test") { bool "active" }
|
|
51
|
+
assert schema.has_field?("active")
|
|
52
|
+
assert_equal Zvec::DataTypes::BOOL, schema.field_type("active")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_vector_field
|
|
56
|
+
schema = Zvec::Schema.new("test") do
|
|
57
|
+
vector "embedding", dimension: 128
|
|
58
|
+
end
|
|
59
|
+
assert schema.has_field?("embedding")
|
|
60
|
+
assert_equal Zvec::DataTypes::VECTOR_FP32, schema.field_type("embedding")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_vector_field_custom_type
|
|
64
|
+
schema = Zvec::Schema.new("test") do
|
|
65
|
+
vector "embedding", dimension: 128, type: Zvec::DataTypes::VECTOR_FP64
|
|
66
|
+
end
|
|
67
|
+
assert_equal Zvec::DataTypes::VECTOR_FP64, schema.field_type("embedding")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_field_with_explicit_type
|
|
71
|
+
schema = Zvec::Schema.new("test")
|
|
72
|
+
schema.field("tags", Zvec::DataTypes::ARRAY_STRING)
|
|
73
|
+
assert schema.has_field?("tags")
|
|
74
|
+
assert_equal Zvec::DataTypes::ARRAY_STRING, schema.field_type("tags")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_field_names
|
|
78
|
+
schema = Zvec::Schema.new("test") do
|
|
79
|
+
string "a"
|
|
80
|
+
int32 "b"
|
|
81
|
+
vector "c", dimension: 4
|
|
82
|
+
end
|
|
83
|
+
names = schema.field_names
|
|
84
|
+
assert_includes names, "a"
|
|
85
|
+
assert_includes names, "b"
|
|
86
|
+
assert_includes names, "c"
|
|
87
|
+
assert_equal 3, names.size
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_has_field_returns_false_for_missing
|
|
91
|
+
schema = Zvec::Schema.new("test")
|
|
92
|
+
refute schema.has_field?("nonexistent")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_field_type_returns_nil_for_missing
|
|
96
|
+
schema = Zvec::Schema.new("test")
|
|
97
|
+
assert_nil schema.field_type("nonexistent")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_symbol_field_name
|
|
101
|
+
schema = Zvec::Schema.new("test") { string "name" }
|
|
102
|
+
# field_type converts to string internally
|
|
103
|
+
assert_equal Zvec::DataTypes::STRING, schema.field_type(:name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_field_returns_self_for_chaining
|
|
107
|
+
schema = Zvec::Schema.new("test")
|
|
108
|
+
result = schema.field("a", Zvec::DataTypes::STRING)
|
|
109
|
+
assert_same schema, result
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_to_s_returns_string
|
|
113
|
+
schema = Zvec::Schema.new("test") { string "x" }
|
|
114
|
+
assert_kind_of String, schema.to_s
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_ext_schema_accessible
|
|
118
|
+
schema = Zvec::Schema.new("test")
|
|
119
|
+
assert_kind_of Zvec::Ext::CollectionSchema, schema.ext_schema
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_multiple_fields_in_block
|
|
123
|
+
schema = Zvec::Schema.new("docs") do
|
|
124
|
+
string "title"
|
|
125
|
+
string "body", nullable: true
|
|
126
|
+
int32 "year"
|
|
127
|
+
float "rating"
|
|
128
|
+
bool "published"
|
|
129
|
+
vector "embedding", dimension: 384
|
|
130
|
+
end
|
|
131
|
+
assert_equal 6, schema.field_names.size
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
class TestVersion < Minitest::Test
|
|
4
|
+
def test_version_is_defined
|
|
5
|
+
refute_nil Zvec::VERSION
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_version_is_a_string
|
|
9
|
+
assert_kind_of String, Zvec::VERSION
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_version_format
|
|
13
|
+
assert_match(/\A\d+\.\d+\.\d+/, Zvec::VERSION)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_version_value
|
|
17
|
+
assert_equal "0.1.0", Zvec::VERSION
|
|
18
|
+
end
|
|
19
|
+
end
|
data/zvec.gemspec
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require_relative "lib/zvec/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "zvec-ruby"
|
|
5
|
+
spec.version = Zvec::VERSION
|
|
6
|
+
spec.authors = ["Johannes Dwi Cahyo"]
|
|
7
|
+
spec.summary = "Ruby bindings for zvec vector database"
|
|
8
|
+
spec.description = "Ruby gem wrapping the zvec C++ vector database (https://github.com/alibaba/zvec) " \
|
|
9
|
+
"using Rice. Provides Collection, Doc, Schema, and VectorQuery classes for " \
|
|
10
|
+
"high-performance vector similarity search, plus integrations for ruby_llm and ActiveRecord."
|
|
11
|
+
spec.homepage = "https://github.com/johannesdwicahyo/zvec-ruby"
|
|
12
|
+
spec.license = "Apache-2.0"
|
|
13
|
+
|
|
14
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
15
|
+
|
|
16
|
+
spec.files = Dir[
|
|
17
|
+
"lib/**/*.rb",
|
|
18
|
+
"ext/**/*.{rb,cpp,h,hpp}",
|
|
19
|
+
"examples/**/*.rb",
|
|
20
|
+
"test/**/*.rb",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"Rakefile",
|
|
25
|
+
"zvec.gemspec"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
spec.metadata = {
|
|
29
|
+
"homepage_uri" => spec.homepage,
|
|
30
|
+
"source_code_uri" => spec.homepage,
|
|
31
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
|
|
32
|
+
"bug_tracker_uri" => "#{spec.homepage}/issues",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
spec.extensions = ["ext/zvec/extconf.rb"]
|
|
36
|
+
spec.require_paths = ["lib"]
|
|
37
|
+
|
|
38
|
+
spec.add_dependency "rice", ">= 4.0"
|
|
39
|
+
|
|
40
|
+
spec.add_development_dependency "rake-compiler", "~> 1.2"
|
|
41
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
42
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
|
43
|
+
end
|