lancelot 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/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +18 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +152 -0
- data/Rakefile +20 -0
- data/examples/basic_usage.rb +52 -0
- data/examples/full_text_search.rb +146 -0
- data/examples/red_candle_integration.rb +87 -0
- data/examples/vector_search.rb +102 -0
- data/ext/lancelot/.gitignore +10 -0
- data/ext/lancelot/Cargo.toml +28 -0
- data/ext/lancelot/extconf.rb +4 -0
- data/ext/lancelot/src/conversion.rs +243 -0
- data/ext/lancelot/src/dataset.rs +454 -0
- data/ext/lancelot/src/lib.rs +17 -0
- data/ext/lancelot/src/schema.rs +50 -0
- data/lib/lancelot/dataset.rb +119 -0
- data/lib/lancelot/version.rb +5 -0
- data/lib/lancelot.rb +9 -0
- data/sig/lancelot.rbs +4 -0
- metadata +140 -0
@@ -0,0 +1,454 @@
|
|
1
|
+
use magnus::{Error, Ruby, RHash, RArray, Symbol, TryConvert, function, method, RClass, Module, Object};
|
2
|
+
use std::cell::RefCell;
|
3
|
+
use std::sync::Arc;
|
4
|
+
use tokio::runtime::Runtime;
|
5
|
+
use lance::Dataset;
|
6
|
+
use lance::index::vector::VectorIndexParams;
|
7
|
+
use lance_index::{IndexType, DatasetIndexExt};
|
8
|
+
use lance_index::scalar::{InvertedIndexParams, FullTextSearchQuery};
|
9
|
+
use arrow_array::{RecordBatch, RecordBatchIterator, Float32Array};
|
10
|
+
use futures::stream::TryStreamExt;
|
11
|
+
|
12
|
+
use crate::schema::build_arrow_schema;
|
13
|
+
use crate::conversion::{build_record_batch, convert_batch_to_ruby};
|
14
|
+
|
15
|
+
#[magnus::wrap(class = "Lancelot::Dataset", free_immediately, size)]
|
16
|
+
pub struct LancelotDataset {
|
17
|
+
dataset: RefCell<Option<Dataset>>,
|
18
|
+
runtime: RefCell<Runtime>,
|
19
|
+
path: String,
|
20
|
+
}
|
21
|
+
|
22
|
+
impl LancelotDataset {
|
23
|
+
pub fn new(path: String) -> Result<Self, Error> {
|
24
|
+
let runtime = Runtime::new()
|
25
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
26
|
+
|
27
|
+
Ok(Self {
|
28
|
+
dataset: RefCell::new(None),
|
29
|
+
runtime: RefCell::new(runtime),
|
30
|
+
path,
|
31
|
+
})
|
32
|
+
}
|
33
|
+
|
34
|
+
pub fn path(&self) -> String {
|
35
|
+
self.path.clone()
|
36
|
+
}
|
37
|
+
|
38
|
+
pub fn create(&self, schema_hash: RHash) -> Result<(), Error> {
|
39
|
+
let schema = build_arrow_schema(schema_hash)?;
|
40
|
+
|
41
|
+
let empty_batch = RecordBatch::new_empty(Arc::new(schema.clone()));
|
42
|
+
let batches = vec![empty_batch];
|
43
|
+
let reader = RecordBatchIterator::new(
|
44
|
+
batches.into_iter().map(Ok),
|
45
|
+
Arc::new(schema)
|
46
|
+
);
|
47
|
+
|
48
|
+
let dataset = self.runtime.borrow_mut().block_on(async {
|
49
|
+
Dataset::write(
|
50
|
+
reader,
|
51
|
+
&self.path,
|
52
|
+
None,
|
53
|
+
)
|
54
|
+
.await
|
55
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
56
|
+
})?;
|
57
|
+
|
58
|
+
self.dataset.replace(Some(dataset));
|
59
|
+
Ok(())
|
60
|
+
}
|
61
|
+
|
62
|
+
pub fn open(&self) -> Result<(), Error> {
|
63
|
+
let dataset = self.runtime.borrow_mut().block_on(async {
|
64
|
+
Dataset::open(&self.path)
|
65
|
+
.await
|
66
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
67
|
+
})?;
|
68
|
+
|
69
|
+
self.dataset.replace(Some(dataset));
|
70
|
+
Ok(())
|
71
|
+
}
|
72
|
+
|
73
|
+
pub fn add_data(&self, data: RArray) -> Result<(), Error> {
|
74
|
+
let mut dataset = self.dataset.borrow_mut();
|
75
|
+
let dataset = dataset.as_mut()
|
76
|
+
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "Dataset not opened"))?;
|
77
|
+
|
78
|
+
// Check if data is empty
|
79
|
+
if data.len() == 0 {
|
80
|
+
return Ok(()); // Nothing to add
|
81
|
+
}
|
82
|
+
|
83
|
+
// Get the dataset's schema
|
84
|
+
let schema = self.runtime.borrow_mut().block_on(async {
|
85
|
+
dataset.schema()
|
86
|
+
});
|
87
|
+
|
88
|
+
// Convert Lance schema to Arrow schema
|
89
|
+
let arrow_schema = schema.into();
|
90
|
+
|
91
|
+
let batch = build_record_batch(data, &arrow_schema)?;
|
92
|
+
|
93
|
+
let batches = vec![batch];
|
94
|
+
let reader = RecordBatchIterator::new(
|
95
|
+
batches.into_iter().map(Ok),
|
96
|
+
Arc::new(arrow_schema)
|
97
|
+
);
|
98
|
+
|
99
|
+
self.runtime.borrow_mut().block_on(async move {
|
100
|
+
dataset.append(reader, None)
|
101
|
+
.await
|
102
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
103
|
+
})?;
|
104
|
+
|
105
|
+
Ok(())
|
106
|
+
}
|
107
|
+
|
108
|
+
pub fn count_rows(&self) -> Result<i64, Error> {
|
109
|
+
let dataset = self.dataset.borrow();
|
110
|
+
let dataset = dataset.as_ref()
|
111
|
+
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "Dataset not opened"))?;
|
112
|
+
|
113
|
+
let count = self.runtime.borrow_mut().block_on(async {
|
114
|
+
dataset.count_rows(None)
|
115
|
+
.await
|
116
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
117
|
+
})?;
|
118
|
+
|
119
|
+
Ok(count as i64)
|
120
|
+
}
|
121
|
+
|
122
|
+
pub fn schema(&self) -> Result<RHash, Error> {
|
123
|
+
let dataset = self.dataset.borrow();
|
124
|
+
let _dataset = dataset.as_ref()
|
125
|
+
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "Dataset not opened"))?;
|
126
|
+
|
127
|
+
let ruby = Ruby::get().unwrap();
|
128
|
+
let hash = ruby.hash_new();
|
129
|
+
|
130
|
+
// TODO: Read actual schema from Lance dataset once we figure out the 0.31 API
|
131
|
+
// For now, return a hardcoded schema that matches what we support
|
132
|
+
hash.aset(Symbol::new("text"), "string")?;
|
133
|
+
hash.aset(Symbol::new("score"), "float32")?;
|
134
|
+
|
135
|
+
Ok(hash)
|
136
|
+
}
|
137
|
+
|
138
|
+
pub fn scan_all(&self) -> Result<RArray, Error> {
|
139
|
+
let dataset = self.dataset.borrow();
|
140
|
+
let dataset = dataset.as_ref()
|
141
|
+
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "Dataset not opened"))?;
|
142
|
+
|
143
|
+
let batches: Vec<RecordBatch> = self.runtime.borrow_mut().block_on(async {
|
144
|
+
let scanner = dataset.scan();
|
145
|
+
let stream = scanner
|
146
|
+
.try_into_stream()
|
147
|
+
.await
|
148
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
149
|
+
|
150
|
+
stream
|
151
|
+
.try_collect::<Vec<_>>()
|
152
|
+
.await
|
153
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
154
|
+
})?;
|
155
|
+
|
156
|
+
let ruby = Ruby::get().unwrap();
|
157
|
+
let result_array = ruby.ary_new();
|
158
|
+
|
159
|
+
for batch in batches {
|
160
|
+
let documents = convert_batch_to_ruby(&batch)?;
|
161
|
+
for doc in documents {
|
162
|
+
result_array.push(doc)?;
|
163
|
+
}
|
164
|
+
}
|
165
|
+
|
166
|
+
Ok(result_array)
|
167
|
+
}
|
168
|
+
|
169
|
+
pub fn scan_limit(&self, limit: i64) -> Result<RArray, Error> {
|
170
|
+
let dataset = self.dataset.borrow();
|
171
|
+
let dataset = dataset.as_ref()
|
172
|
+
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "Dataset not opened"))?;
|
173
|
+
|
174
|
+
let batches: Vec<RecordBatch> = self.runtime.borrow_mut().block_on(async {
|
175
|
+
let mut scanner = dataset.scan();
|
176
|
+
scanner.limit(Some(limit), None)
|
177
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
178
|
+
|
179
|
+
let stream = scanner
|
180
|
+
.try_into_stream()
|
181
|
+
.await
|
182
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
183
|
+
|
184
|
+
stream
|
185
|
+
.try_collect::<Vec<_>>()
|
186
|
+
.await
|
187
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
188
|
+
})?;
|
189
|
+
|
190
|
+
let ruby = Ruby::get().unwrap();
|
191
|
+
let result_array = ruby.ary_new();
|
192
|
+
|
193
|
+
for batch in batches {
|
194
|
+
let documents = convert_batch_to_ruby(&batch)?;
|
195
|
+
for doc in documents {
|
196
|
+
result_array.push(doc)?;
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
Ok(result_array)
|
201
|
+
}
|
202
|
+
|
203
|
+
pub fn create_vector_index(&self, column: String) -> Result<(), Error> {
|
204
|
+
let mut dataset = self.dataset.borrow_mut();
|
205
|
+
let dataset = dataset.as_mut()
|
206
|
+
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "Dataset not opened"))?;
|
207
|
+
|
208
|
+
self.runtime.borrow_mut().block_on(async move {
|
209
|
+
// Get row count to determine optimal number of partitions
|
210
|
+
let num_rows = dataset.count_rows(None).await
|
211
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
212
|
+
|
213
|
+
// Use fewer partitions for small datasets
|
214
|
+
let num_partitions = if num_rows < 256 {
|
215
|
+
std::cmp::max(1, (num_rows / 4) as usize)
|
216
|
+
} else {
|
217
|
+
256
|
218
|
+
};
|
219
|
+
|
220
|
+
// Create IVF_FLAT vector index parameters
|
221
|
+
let params = VectorIndexParams::ivf_flat(num_partitions, lance_linalg::distance::MetricType::L2);
|
222
|
+
|
223
|
+
dataset.create_index(
|
224
|
+
&[&column],
|
225
|
+
IndexType::Vector,
|
226
|
+
None,
|
227
|
+
¶ms,
|
228
|
+
true
|
229
|
+
)
|
230
|
+
.await
|
231
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
232
|
+
})
|
233
|
+
}
|
234
|
+
|
235
|
+
pub fn vector_search(&self, column: String, query_vector: RArray, limit: i64) -> Result<RArray, Error> {
|
236
|
+
let dataset = self.dataset.borrow();
|
237
|
+
let dataset = dataset.as_ref()
|
238
|
+
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "Dataset not opened"))?;
|
239
|
+
|
240
|
+
// Convert Ruby array to Vec<f32>
|
241
|
+
let vector: Vec<f32> = query_vector
|
242
|
+
.into_iter()
|
243
|
+
.map(|v| f64::try_convert(v).map(|f| f as f32))
|
244
|
+
.collect::<Result<Vec<_>, _>>()?;
|
245
|
+
|
246
|
+
let batches: Vec<RecordBatch> = self.runtime.borrow_mut().block_on(async {
|
247
|
+
let mut scanner = dataset.scan();
|
248
|
+
|
249
|
+
// Use nearest for vector search
|
250
|
+
scanner.nearest(&column, &Float32Array::from(vector), limit as usize)
|
251
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
252
|
+
|
253
|
+
let stream = scanner
|
254
|
+
.try_into_stream()
|
255
|
+
.await
|
256
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
257
|
+
|
258
|
+
stream
|
259
|
+
.try_collect::<Vec<_>>()
|
260
|
+
.await
|
261
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
262
|
+
})?;
|
263
|
+
|
264
|
+
let ruby = Ruby::get().unwrap();
|
265
|
+
let result_array = ruby.ary_new();
|
266
|
+
|
267
|
+
for batch in batches {
|
268
|
+
let documents = convert_batch_to_ruby(&batch)?;
|
269
|
+
for doc in documents {
|
270
|
+
result_array.push(doc)?;
|
271
|
+
}
|
272
|
+
}
|
273
|
+
|
274
|
+
Ok(result_array)
|
275
|
+
}
|
276
|
+
|
277
|
+
pub fn create_text_index(&self, column: String) -> Result<(), Error> {
|
278
|
+
let mut dataset = self.dataset.borrow_mut();
|
279
|
+
let dataset = dataset.as_mut()
|
280
|
+
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "Dataset not opened"))?;
|
281
|
+
|
282
|
+
self.runtime.borrow_mut().block_on(async move {
|
283
|
+
// Create inverted index for full-text search
|
284
|
+
let params = InvertedIndexParams::default();
|
285
|
+
|
286
|
+
dataset.create_index(
|
287
|
+
&[&column],
|
288
|
+
IndexType::Inverted,
|
289
|
+
None,
|
290
|
+
¶ms,
|
291
|
+
true
|
292
|
+
)
|
293
|
+
.await
|
294
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
295
|
+
})
|
296
|
+
}
|
297
|
+
|
298
|
+
pub fn text_search(&self, column: String, query: String, limit: i64) -> Result<RArray, Error> {
|
299
|
+
let dataset = self.dataset.borrow();
|
300
|
+
let dataset = dataset.as_ref()
|
301
|
+
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "Dataset not opened"))?;
|
302
|
+
|
303
|
+
let batches: Vec<RecordBatch> = self.runtime.borrow_mut().block_on(async {
|
304
|
+
let mut scanner = dataset.scan();
|
305
|
+
|
306
|
+
// Use full-text search with inverted index
|
307
|
+
let fts_query = FullTextSearchQuery::new(query)
|
308
|
+
.with_column(column)
|
309
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
310
|
+
|
311
|
+
scanner.full_text_search(fts_query)
|
312
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
313
|
+
|
314
|
+
// Apply limit
|
315
|
+
scanner.limit(Some(limit), None)
|
316
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
317
|
+
|
318
|
+
let stream = scanner
|
319
|
+
.try_into_stream()
|
320
|
+
.await
|
321
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
322
|
+
|
323
|
+
stream
|
324
|
+
.try_collect::<Vec<_>>()
|
325
|
+
.await
|
326
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
327
|
+
})?;
|
328
|
+
|
329
|
+
let ruby = Ruby::get().unwrap();
|
330
|
+
let result_array = ruby.ary_new();
|
331
|
+
|
332
|
+
for batch in batches {
|
333
|
+
let documents = convert_batch_to_ruby(&batch)?;
|
334
|
+
for doc in documents {
|
335
|
+
result_array.push(doc)?;
|
336
|
+
}
|
337
|
+
}
|
338
|
+
|
339
|
+
Ok(result_array)
|
340
|
+
}
|
341
|
+
|
342
|
+
pub fn multi_column_text_search(&self, columns: RArray, query: String, limit: i64) -> Result<RArray, Error> {
|
343
|
+
let dataset = self.dataset.borrow();
|
344
|
+
let dataset = dataset.as_ref()
|
345
|
+
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "Dataset not opened"))?;
|
346
|
+
|
347
|
+
// Convert Ruby array of columns to Vec<String>
|
348
|
+
let columns: Vec<String> = columns
|
349
|
+
.into_iter()
|
350
|
+
.map(|v| String::try_convert(v))
|
351
|
+
.collect::<Result<Vec<_>, _>>()?;
|
352
|
+
|
353
|
+
let batches: Vec<RecordBatch> = self.runtime.borrow_mut().block_on(async {
|
354
|
+
let mut scanner = dataset.scan();
|
355
|
+
|
356
|
+
// Create a full-text search query for multiple columns
|
357
|
+
let fts_query = FullTextSearchQuery::new(query)
|
358
|
+
.with_columns(&columns)
|
359
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
360
|
+
|
361
|
+
scanner.full_text_search(fts_query)
|
362
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
363
|
+
|
364
|
+
// Apply limit
|
365
|
+
scanner.limit(Some(limit), None)
|
366
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
367
|
+
|
368
|
+
let stream = scanner
|
369
|
+
.try_into_stream()
|
370
|
+
.await
|
371
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
372
|
+
|
373
|
+
stream
|
374
|
+
.try_collect::<Vec<_>>()
|
375
|
+
.await
|
376
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
377
|
+
})?;
|
378
|
+
|
379
|
+
let ruby = Ruby::get().unwrap();
|
380
|
+
let result_array = ruby.ary_new();
|
381
|
+
|
382
|
+
for batch in batches {
|
383
|
+
let documents = convert_batch_to_ruby(&batch)?;
|
384
|
+
for doc in documents {
|
385
|
+
result_array.push(doc)?;
|
386
|
+
}
|
387
|
+
}
|
388
|
+
|
389
|
+
Ok(result_array)
|
390
|
+
}
|
391
|
+
|
392
|
+
pub fn filter_scan(&self, filter_expr: String, limit: Option<i64>) -> Result<RArray, Error> {
|
393
|
+
let dataset = self.dataset.borrow();
|
394
|
+
let dataset = dataset.as_ref()
|
395
|
+
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "Dataset not opened"))?;
|
396
|
+
|
397
|
+
let batches: Vec<RecordBatch> = self.runtime.borrow_mut().block_on(async {
|
398
|
+
let mut scanner = dataset.scan();
|
399
|
+
|
400
|
+
// Apply SQL-like filter
|
401
|
+
scanner.filter(&filter_expr)
|
402
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
403
|
+
|
404
|
+
// Apply limit if provided
|
405
|
+
if let Some(lim) = limit {
|
406
|
+
scanner.limit(Some(lim), None)
|
407
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
408
|
+
}
|
409
|
+
|
410
|
+
let stream = scanner
|
411
|
+
.try_into_stream()
|
412
|
+
.await
|
413
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
|
414
|
+
|
415
|
+
stream
|
416
|
+
.try_collect::<Vec<_>>()
|
417
|
+
.await
|
418
|
+
.map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))
|
419
|
+
})?;
|
420
|
+
|
421
|
+
let ruby = Ruby::get().unwrap();
|
422
|
+
let result_array = ruby.ary_new();
|
423
|
+
|
424
|
+
for batch in batches {
|
425
|
+
let documents = convert_batch_to_ruby(&batch)?;
|
426
|
+
for doc in documents {
|
427
|
+
result_array.push(doc)?;
|
428
|
+
}
|
429
|
+
}
|
430
|
+
|
431
|
+
Ok(result_array)
|
432
|
+
}
|
433
|
+
}
|
434
|
+
|
435
|
+
impl LancelotDataset {
|
436
|
+
pub fn bind(class: &RClass) -> Result<(), Error> {
|
437
|
+
class.define_singleton_method("new", function!(LancelotDataset::new, 1))?;
|
438
|
+
class.define_method("path", method!(LancelotDataset::path, 0))?;
|
439
|
+
class.define_method("create", method!(LancelotDataset::create, 1))?;
|
440
|
+
class.define_method("open", method!(LancelotDataset::open, 0))?;
|
441
|
+
class.define_method("add_data", method!(LancelotDataset::add_data, 1))?;
|
442
|
+
class.define_method("count_rows", method!(LancelotDataset::count_rows, 0))?;
|
443
|
+
class.define_method("schema", method!(LancelotDataset::schema, 0))?;
|
444
|
+
class.define_method("scan_all", method!(LancelotDataset::scan_all, 0))?;
|
445
|
+
class.define_method("scan_limit", method!(LancelotDataset::scan_limit, 1))?;
|
446
|
+
class.define_method("create_vector_index", method!(LancelotDataset::create_vector_index, 1))?;
|
447
|
+
class.define_method("create_text_index", method!(LancelotDataset::create_text_index, 1))?;
|
448
|
+
class.define_method("_rust_vector_search", method!(LancelotDataset::vector_search, 3))?;
|
449
|
+
class.define_method("_rust_text_search", method!(LancelotDataset::text_search, 3))?;
|
450
|
+
class.define_method("_rust_multi_column_text_search", method!(LancelotDataset::multi_column_text_search, 3))?;
|
451
|
+
class.define_method("filter_scan", method!(LancelotDataset::filter_scan, 2))?;
|
452
|
+
Ok(())
|
453
|
+
}
|
454
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
use magnus::{define_module, Error, Ruby, Module};
|
2
|
+
|
3
|
+
mod dataset;
|
4
|
+
mod schema;
|
5
|
+
mod conversion;
|
6
|
+
|
7
|
+
use dataset::LancelotDataset;
|
8
|
+
|
9
|
+
#[magnus::init]
|
10
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
11
|
+
let module = define_module("Lancelot")?;
|
12
|
+
|
13
|
+
let dataset_class = module.define_class("Dataset", ruby.class_object())?;
|
14
|
+
LancelotDataset::bind(&dataset_class)?;
|
15
|
+
|
16
|
+
Ok(())
|
17
|
+
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
use magnus::{Error, RHash, Symbol, Value, TryConvert, r_hash::ForEach, value::ReprValue};
|
2
|
+
use arrow_schema::{DataType, Field, Schema as ArrowSchema};
|
3
|
+
use std::sync::Arc;
|
4
|
+
|
5
|
+
pub fn build_arrow_schema(schema_hash: RHash) -> Result<ArrowSchema, Error> {
|
6
|
+
let mut fields = Vec::new();
|
7
|
+
|
8
|
+
schema_hash.foreach(|key: Symbol, value: Value| {
|
9
|
+
let field_name = key.name()?.to_string();
|
10
|
+
|
11
|
+
let data_type = if value.is_kind_of(magnus::class::hash()) {
|
12
|
+
let hash = RHash::from_value(value)
|
13
|
+
.ok_or_else(|| Error::new(magnus::exception::arg_error(), "Invalid hash value"))?;
|
14
|
+
let type_str: String = hash.fetch(Symbol::new("type"))?;
|
15
|
+
|
16
|
+
match type_str.as_str() {
|
17
|
+
"vector" => {
|
18
|
+
let dimension: i32 = hash.fetch(Symbol::new("dimension"))?;
|
19
|
+
DataType::FixedSizeList(
|
20
|
+
Arc::new(Field::new("item", DataType::Float32, true)),
|
21
|
+
dimension,
|
22
|
+
)
|
23
|
+
}
|
24
|
+
_ => return Err(Error::new(
|
25
|
+
magnus::exception::arg_error(),
|
26
|
+
format!("Unknown field type: {}", type_str)
|
27
|
+
))
|
28
|
+
}
|
29
|
+
} else {
|
30
|
+
let type_str = String::try_convert(value)?;
|
31
|
+
match type_str.as_str() {
|
32
|
+
"string" => DataType::Utf8,
|
33
|
+
"float32" => DataType::Float32,
|
34
|
+
"float64" => DataType::Float64,
|
35
|
+
"int32" => DataType::Int32,
|
36
|
+
"int64" => DataType::Int64,
|
37
|
+
"boolean" => DataType::Boolean,
|
38
|
+
_ => return Err(Error::new(
|
39
|
+
magnus::exception::arg_error(),
|
40
|
+
format!("Unknown field type: {}", type_str)
|
41
|
+
))
|
42
|
+
}
|
43
|
+
};
|
44
|
+
|
45
|
+
fields.push(Field::new(field_name, data_type, true));
|
46
|
+
Ok(ForEach::Continue)
|
47
|
+
})?;
|
48
|
+
|
49
|
+
Ok(ArrowSchema::new(fields))
|
50
|
+
}
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lancelot
|
4
|
+
class Dataset
|
5
|
+
class << self
|
6
|
+
def create(path, schema:)
|
7
|
+
dataset = new(path)
|
8
|
+
dataset.create(normalize_schema(schema))
|
9
|
+
dataset
|
10
|
+
end
|
11
|
+
|
12
|
+
def open(path)
|
13
|
+
dataset = new(path)
|
14
|
+
dataset.open
|
15
|
+
dataset
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def normalize_schema(schema)
|
21
|
+
schema.transform_values do |type|
|
22
|
+
case type
|
23
|
+
when Hash
|
24
|
+
type
|
25
|
+
when :string, "string"
|
26
|
+
"string"
|
27
|
+
when :float, :float32, "float", "float32"
|
28
|
+
"float32"
|
29
|
+
when :float64, "float64"
|
30
|
+
"float64"
|
31
|
+
when :int, :int32, "int", "int32"
|
32
|
+
"int32"
|
33
|
+
when :int64, "int64"
|
34
|
+
"int64"
|
35
|
+
when :bool, :boolean, "bool", "boolean"
|
36
|
+
"boolean"
|
37
|
+
else
|
38
|
+
raise ArgumentError, "Unknown type: #{type}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_documents(documents)
|
45
|
+
add_data(documents.map { |doc| normalize_document(doc) })
|
46
|
+
end
|
47
|
+
|
48
|
+
def <<(document)
|
49
|
+
add_documents([document])
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def size
|
54
|
+
count_rows
|
55
|
+
end
|
56
|
+
|
57
|
+
alias_method :count, :size
|
58
|
+
alias_method :length, :size
|
59
|
+
|
60
|
+
def all
|
61
|
+
scan_all
|
62
|
+
end
|
63
|
+
|
64
|
+
def first(n = nil)
|
65
|
+
if n.nil?
|
66
|
+
scan_limit(1).first
|
67
|
+
else
|
68
|
+
scan_limit(n)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def each(&block)
|
73
|
+
return enum_for(:each) unless block_given?
|
74
|
+
scan_all.each(&block)
|
75
|
+
end
|
76
|
+
|
77
|
+
include Enumerable
|
78
|
+
|
79
|
+
def vector_search(query_vector, column: "vector", limit: 10)
|
80
|
+
unless query_vector.is_a?(Array)
|
81
|
+
raise ArgumentError, "Query vector must be an array of numbers"
|
82
|
+
end
|
83
|
+
|
84
|
+
_rust_vector_search(column.to_s, query_vector, limit)
|
85
|
+
end
|
86
|
+
|
87
|
+
def nearest_neighbors(vector, k: 10, column: "vector")
|
88
|
+
vector_search(vector, column: column, limit: k)
|
89
|
+
end
|
90
|
+
|
91
|
+
def text_search(query, column: nil, columns: nil, limit: 10)
|
92
|
+
unless query.is_a?(String)
|
93
|
+
raise ArgumentError, "Query must be a string"
|
94
|
+
end
|
95
|
+
|
96
|
+
if column && columns
|
97
|
+
raise ArgumentError, "Cannot specify both column and columns"
|
98
|
+
elsif columns
|
99
|
+
# Multi-column search
|
100
|
+
columns = Array(columns).map(&:to_s)
|
101
|
+
_rust_multi_column_text_search(columns, query, limit)
|
102
|
+
else
|
103
|
+
# Single column search (default to "text" if not specified)
|
104
|
+
column ||= "text"
|
105
|
+
_rust_text_search(column.to_s, query, limit)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def where(filter_expression, limit: nil)
|
110
|
+
filter_scan(filter_expression.to_s, limit)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def normalize_document(doc)
|
116
|
+
doc.transform_keys(&:to_sym)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/lancelot.rb
ADDED
data/sig/lancelot.rbs
ADDED