diff --git a/.gitignore b/.gitignore index 835e87a..10104e6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ mkmf.log *.gem *.db .env +/ext/rag_embeddings/target /ext/rag_embeddings/embedding.bundle* \ No newline at end of file diff --git a/.tool-versions b/.tool-versions index ca745c6..147432f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ ruby 3.4.4 +rust 1.87.0 diff --git a/Gemfile b/Gemfile index c8718b9..a633ae7 100644 --- a/Gemfile +++ b/Gemfile @@ -12,4 +12,6 @@ gem "rubocop" gem "faraday" gem "rspec" gem "dotenv", require: false -gem "debug" \ No newline at end of file +gem "debug" +gem "rb_sys", "~> 0.9" +gem "rake-compiler", "~> 1.2" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 96a01ea..3eebbe1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,7 @@ PATH rag_embeddings (0.2.2) faraday langchainrb + rb_sys (~> 0.9) sqlite3 GEM @@ -66,6 +67,11 @@ GEM racc (1.8.1) rainbow (3.1.1) rake (13.3.0) + rake-compiler (1.3.0) + rake + rake-compiler-dock (1.9.1) + rb_sys (0.9.116) + rake-compiler-dock (= 1.9.1) rdoc (6.14.0) erb psych (>= 4.0.0) @@ -118,6 +124,8 @@ DEPENDENCIES langchainrb rag_embeddings! rake + rake-compiler (~> 1.2) + rb_sys (~> 0.9) rspec rubocop sqlite3 diff --git a/Rakefile b/Rakefile index 7fd0e26..70c26e2 100644 --- a/Rakefile +++ b/Rakefile @@ -2,7 +2,9 @@ task :compile do Dir.chdir("ext/rag_embeddings") do # Delete embedding.so or embedding.o # Delete embedding.bundle and the folder embedding.bundle.* - FileUtils.rm_rf(Dir["embedding.so", "embedding.o", "embedding.bundle", "embedding.bundle.*"]) + puts "๐Ÿงน Cleaning artifacts..." + system('cargo clean') if File.exist?('Cargo.toml') + FileUtils.rm_rf(Dir["embedding.so", "embedding.o", "embedding.bundle", "embedding.bundle.*", "target"]) ruby "extconf.rb" system("make") end diff --git a/ext/rag_embeddings/Cargo.lock b/ext/rag_embeddings/Cargo.lock new file mode 100644 index 0000000..0857909 --- /dev/null +++ b/ext/rag_embeddings/Cargo.lock @@ -0,0 +1,347 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embedding" +version = "0.3.0" +dependencies = [ + "magnus", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.173" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "magnus" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d87ae53030f3a22e83879e666cb94e58a7bdf31706878a0ba48752994146dab" +dependencies = [ + "magnus-macros", + "rb-sys", + "rb-sys-env", + "seq-macro", +] + +[[package]] +name = "magnus-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rb-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7059846f68396df83155779c75336ca24567741cb95256e6308c9fcc370e8dad" +dependencies = [ + "rb-sys-build", +] + +[[package]] +name = "rb-sys-build" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac217510df41b9ffc041573e68d7a02aaff770c49943c7494441c4b224b0ecd0" +dependencies = [ + "bindgen", + "lazy_static", + "proc-macro2", + "quote", + "regex", + "shell-words", + "syn", +] + +[[package]] +name = "rb-sys-env" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb" + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" diff --git a/ext/rag_embeddings/Cargo.toml b/ext/rag_embeddings/Cargo.toml new file mode 100644 index 0000000..6b13424 --- /dev/null +++ b/ext/rag_embeddings/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "embedding" +version = "0.3.0" +edition = "2024" +authors = ["Marco Mastrodonato "] +description = "Fast embedding operations for Ruby using Rust" +license = "MIT" +repository = "https://github.com/marcomd/rag_embeddings" + +[lib] +crate-type = ["cdylib"] +path = "embedding.rs" + +[dependencies] +magnus = "0.7" + +# Optional: for better performance +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" \ No newline at end of file diff --git a/ext/rag_embeddings/embedding.rs b/ext/rag_embeddings/embedding.rs new file mode 100644 index 0000000..a8a081e --- /dev/null +++ b/ext/rag_embeddings/embedding.rs @@ -0,0 +1,112 @@ +use magnus::{function, method, prelude::*, Error, Ruby, DataTypeFunctions, TypedData}; +use std::cell::RefCell; + +#[derive(TypedData)] +#[magnus(class = "RagEmbeddings::Embedding", free_immediately)] +struct Embedding { + values: RefCell>, +} + +impl DataTypeFunctions for Embedding { + fn size(&self) -> usize { + std::mem::size_of::() + self.values.borrow().capacity() * std::mem::size_of::() + } +} + +impl Embedding { + fn from_array(arr: Vec) -> Result { + if arr.is_empty() { + return Err(Error::new( + magnus::exception::arg_error(), + "Cannot create embedding from empty array", + )); + } + if arr.len() > u16::MAX as usize { + return Err(Error::new( + magnus::exception::arg_error(), + format!( + "Array too large: maximum {} dimensions allowed", + u16::MAX + ), + )); + } + Ok(Self { + values: RefCell::new(arr), + }) + } + + fn dim(&self) -> usize { + self.values.borrow().len() + } + + fn to_a(&self) -> Vec { + self.values.borrow().clone() + } + + fn cosine_similarity(&self, other: &Embedding) -> Result { + let a = self.values.borrow(); + let b = other.values.borrow(); + if a.len() != b.len() { + return Err(Error::new( + magnus::exception::arg_error(), + format!("Dimension mismatch: {} vs {}", a.len(), b.len()), + )); + } + let mut dot = 0.0f64; + let mut norm_a = 0.0f64; + let mut norm_b = 0.0f64; + for (ai, bi) in a.iter().zip(b.iter()) { + dot += *ai as f64 * *bi as f64; + norm_a += (*ai as f64) * (*ai as f64); + norm_b += (*bi as f64) * (*bi as f64); + } + if norm_a == 0.0 || norm_b == 0.0 { + return Ok(0.0); + } + let sim = dot / (norm_a * norm_b).sqrt(); + Ok(sim.clamp(-1.0, 1.0)) + } + + fn magnitude(&self) -> f64 { + let a = self.values.borrow(); + let mut sum = 0.0f64; + for v in a.iter() { + sum += (*v as f64) * (*v as f64); + } + sum.sqrt() + } + + fn normalize_bang(&self) -> Result<(), Error> { + let mut values = self.values.borrow_mut(); + let mut sum = 0.0f64; + for v in values.iter() { + sum += (*v as f64) * (*v as f64); + } + let magnitude = sum.sqrt(); + if magnitude == 0.0 { + return Err(Error::new( + magnus::exception::zero_div_error(), + "Cannot normalize zero vector", + )); + } + let inv_mag = 1.0 / magnitude as f32; + for v in values.iter_mut() { + *v *= inv_mag; + } + Ok(()) + } +} + +#[magnus::init] +fn init(ruby: &Ruby) -> Result<(), Error> { + let m_rag = ruby.define_module("RagEmbeddings")?; + let class = m_rag.define_class("Embedding", ruby.class_object())?; + class.undef_default_alloc_func(); + class.define_singleton_method("from_array", function!(Embedding::from_array, 1))?; + class.define_method("dim", method!(Embedding::dim, 0))?; + class.define_method("to_a", method!(Embedding::to_a, 0))?; + class.define_method("cosine_similarity", method!(Embedding::cosine_similarity, 1))?; + class.define_method("magnitude", method!(Embedding::magnitude, 0))?; + class.define_method("normalize!", method!(Embedding::normalize_bang, 0))?; + Ok(()) +} diff --git a/ext/rag_embeddings/extconf.rb b/ext/rag_embeddings/extconf.rb index 617416e..f0994fe 100644 --- a/ext/rag_embeddings/extconf.rb +++ b/ext/rag_embeddings/extconf.rb @@ -1,2 +1,18 @@ -require "mkmf" -create_makefile("rag_embeddings/embedding") \ No newline at end of file +# frozen_string_literal: true + +require 'mkmf' +require "rb_sys/mkmf" + +# Check if Rust toolchain is available +def rust_available? + system('cargo --version > /dev/null 2>&1') +end + +# Main build logic +if rust_available? + puts "๐Ÿ”ง Rust toolchain detected, building Rust extension..." + create_rust_makefile("rag_embeddings/embedding") +else + puts "๐Ÿ“ฆ Building C extension..." + create_makefile("rag_embeddings/embedding") +end \ No newline at end of file diff --git a/lib/rag_embeddings.rb b/lib/rag_embeddings.rb index 50fecf0..a55a3bb 100644 --- a/lib/rag_embeddings.rb +++ b/lib/rag_embeddings.rb @@ -2,7 +2,7 @@ require_relative "rag_embeddings/engine" require_relative "rag_embeddings/database" -# Loads the compiled C extension +# Loads the compiled extension require "rag_embeddings/embedding" require "faraday" diff --git a/rag_embeddings.gemspec b/rag_embeddings.gemspec index 1db5139..03d38ea 100644 --- a/rag_embeddings.gemspec +++ b/rag_embeddings.gemspec @@ -7,11 +7,11 @@ Gem::Specification.new do |spec| spec.email = ["m.mastrodonato@gmail.com"] spec.summary = "Efficient RAG embedding storage and retrieval" - spec.description = "Manage AI vector embeddings in C with Ruby integration" + spec.description = "Manage AI vector embeddings in C/Rust with Ruby integration" spec.homepage = "https://rubygems.org/gems/rag_embeddings" spec.license = "MIT" - spec.files = Dir["README.md", "LICENSE", "lib/**/*.rb", "ext/**/*.{c,rb}", "Rakefile"] + spec.files = Dir["README.md", "LICENSE", "lib/**/*.rb", "ext/**/*.{c,rb,rs,toml}", "Rakefile"] spec.extensions = ["ext/rag_embeddings/extconf.rb"] spec.require_paths = ["lib", "ext"] @@ -21,7 +21,9 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency "sqlite3" spec.add_runtime_dependency "langchainrb" spec.add_runtime_dependency "faraday" + spec.add_runtime_dependency "rb_sys", "~> 0.9" + spec.add_development_dependency "rake-compiler", "~> 1.2" spec.add_development_dependency "rake" spec.add_development_dependency "rspec" spec.add_development_dependency "rubocop"