From 3f4c7804e8d444dccdf52a314c8f16fa943cadc7 Mon Sep 17 00:00:00 2001 From: Marco Mastrodonato Date: Sun, 8 Jun 2025 19:58:47 +0200 Subject: [PATCH 1/3] Now the size of embeddings is static and the RUBY_TYPED_EMBEDDABLE flag is used - the size of embeddings is static and defined in two places: - c side is defined in `ext/rag_embeddings/embedding_config.h` - ruby side is defined in `lib/rag_embeddings/config.rb` Remember to recompile the c extension after changing the size: `rake compile` --- README.md | 12 +++ ext/rag_embeddings/embedding.bundle | Bin 51920 -> 51504 bytes .../Contents/Resources/DWARF/embedding.bundle | Bin 16192 -> 14996 bytes ext/rag_embeddings/embedding.c | 94 ++++-------------- ext/rag_embeddings/embedding_config.h | 1 + lib/rag_embeddings.rb | 1 + lib/rag_embeddings/config.rb | 3 + lib/rag_embeddings/database.rb | 4 + spec/performance_spec.rb | 6 +- 9 files changed, 42 insertions(+), 79 deletions(-) create mode 100644 ext/rag_embeddings/embedding_config.h create mode 100644 lib/rag_embeddings/config.rb diff --git a/README.md b/README.md index ef13d54..28cdfb6 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,18 @@ puts "Most similar text: #{result.first[1]}, score: #{result.first[2]}" - Embedding provider: switch model/provider in engine.rb (Ollama, OpenAI, etc) - Database: set the SQLite file path as desired +# 🔢 Embeddings dimension + +In the previous version the size of embeddings was dynamic. Now the size of embeddings is static to use the RUBY_TYPED_EMBEDDABLE flag + +- the size of embeddings is currently set to 3072 and it's defined in two places: + - c side is defined in `ext/rag_embeddings/embedding_config.h` + - ruby side is defined in `lib/rag_embeddings/config.rb` + +Remember to recompile the c extension after changing the size: + +`rake compile` + ## 👷 Requirements - Ruby >= 3.3 diff --git a/ext/rag_embeddings/embedding.bundle b/ext/rag_embeddings/embedding.bundle index d9c9296a6bffc2735fb84c3dabe100fb299c34df..4058184511349195e6ca7fb640aef1e759b4ba77 100755 GIT binary patch delta 2067 zcmaJ?4Qx|Y6h800uItZ)b}OR@D_zIN1{>=J%7A2aLkUK7Zv06^){ahEvvmvGOkikB zPyq$$>m3O}Vum7a9mK+bAR=_lpd%^_L_(s$E)ra(F-`_D!l>uI-WqI-H~HQ<-}$-c zdvCXQ{dO}lq{-MLBi{)5 zyje*e2&UDvKoJBeiPcL_3vvqw;OPM{VuVNN5go|$(MEa!%otRo{}O_($qd*AQxFzr?M%BC7P32PcVH;Hf%|IVrxA z#WfjXL`d@sm^%{BwXtZP;I0s_T<6{*gX0dB#kWKT zCmg(GRH?Q27G}V3j{wURGRIuZjZWpv!v~O?s1hrP9K2h#9d{f-mFXT36;{iBov28p z(xhgTOb1AxC&@Aj_<0nbEmea8#6HttuJ|VA@2&a@`?=1sR=#s=D~Or&6QaPin4_N% zuxADOR45f@eZA!j1Wg79#s)AJ5i~kK{`uQZq@uEM7F0&AC+94^BXIsXxK~>Zhyy6J z982y>_4LJb{!-)$BZ!?Q5aAXSs&Ed6mPcf1-38f`Lix#M?gH&|p?tDaVN2J-uuejG%^P9Wh6dMq zqup6w>uPc~xbltlElthFnp$JFo!Hcq$Oq~fd_X}?tEZ&%pp$-0s1|_30Wty6txZXu z(``(7mg85d@RXO5ezmMymm;IVcD2pU?utfcH#57P**0d|nLW(xDQ0__?PvB!W(S!a zAw`zyIO_gve0bE!+Dv_=30@`RgI)4Xwz|58b>wU?gi3*Y+LK-3<*soK$V2tx5RGz- z<66l0Gs!Vt!T4du>nKNGZ;9nMQ;q`g)(TCW@jfzpEFnF^3H&?cuK@>NeoK6zS)7;j zgr>UVqo2fI`7|tub97-E;u_nK@)r~GvyjK7Qt7lm&e4_n@uW+Oa@zlR9{cA*{N3og z(hcIAHc$aF>foY=ZV=Dck=4Ds-SnVhkktVAByRbKA56%fOvqnN$VU?R?+N_DzqwnI z_qYIkjNsRdEqagBE7*WAA~F#sL>7X+UN&Mn0&g1W>YjmiDk29l6Y>B3Y5%bS$-B@R zKXM%xJ|fm|F*y(}Aj9D=NTBa&d01oQAqD@nuYN5cBgCoteO^=y{;D4 zZR=XQ*n8d;`pG+bqwlkh=RfSd_}687!{=AFzI`>V@)&vHqSg5FA1!xpn;)ecy3Q?A z?$|g~V#<4E$a~CJaj0aCXQU`96?GM_|8w_1-u`(v*PNd?^Kjw0y_dT`DLQs!!R^AM u%iO!h)1s_uvw7KXyS5CD5PPXV3YauY2yf zA8)%_mBF2g-!JzmTIQzV60gQjP;WCmm+lxnP`87>S%2@(Z?0CWRCqv(^E(rm*Xx-P7qiR~C`eh|gwp~&38RcBxOdzg@SVF-))RA0>D zz?ec>GVdtj-hj?Rj;6V=Cz&mX_jT(l9WD9{i6pEND}C-pz$*ygTR;ff23cR7>}YD2 zXmJ*j6mQvdh=M7JufaA1KVFy*GP=?fK;egF%Y_le+R^*(cpEx8q2H{8Lg6124$cP zoO8_e_W)b7wzjHCq*bPn#&s7_mGF;RP^$Ut?$;gm{BECp?Jxo0J@p7SQ>BRQm2$*(i)1}sy25d2IjF>9vbju(t%XcBi zo@5YjQ*zM7d=~p+oc0*~#G4)0$T;!m?HY7#cGy~o_VazRU@m@e_+eySmNygEpi48B zAV{{I*5&MwycYU}Kd}PqI$sm+7)s6(HD+a*$4d=0P8XHd)O)Jk z&U%m6Q{}4jG&J4pzF;DfmV3PJ>UvL2b&}Up?{zhlRuJ>5^W{BOwo9FpVWuQ9qdKBa zBB@Fu8P{LwRLF!9OjAKHnUoNh_kE$pES^=u-?Xr?L%iR9=4!c9lHin|weOr-tomxj z8~%*oFACl!_-?^F1wSZwpWuUnpAr1L;KPFJuPZ8o9A9fG zNTK3Ly>K=)_`d1sI#(?_80@*Dt|H3etlL!5S`x6URPCBO)2@=v)y<2 zI;Eg=SL1GlcZDbyJ>uKBV~P%`NZ?1&H$FB9+#v8~fiDWo1U4G@j=+A=&;uO9H{PMA zqkddYnhs+lyVN~%-q%27A&dSSg?BL=Cjf6Ci8?=Fi+VDoPUh;d`HWi2U`#~<14E=# zP*6{WvjG0sBzp$1u{OR|9&ccXdOWV0J~-y_;daFP$Y=&{4o_fU9>5DQzTh151mSjG zfYChOOdfAwh5IdZpxr;{c2-!oIc;7+&kvO2wPZ@p)W zn?2HhB=iol$fHZA40I*QG!1{x((YCIP^Ug7f-*`rRhLz{=~#mOKw5nH(Um8L)nE5z zZ+ywAr562L^mzPWL)xC^oull7qs2*IU7s6U_0P6HKJ4lJ{=KRnBiC&inZb@n>^ife z{+VjeyJp>K)~9ifwxZJ$#q3Lg)H1)sz(SdyKbi41*%pc#X hsC_~GezbAziFHcAn*VwE_1{L!1wEH5{cCtH{{uGcjIRIy diff --git a/ext/rag_embeddings/embedding.bundle.dSYM/Contents/Resources/DWARF/embedding.bundle b/ext/rag_embeddings/embedding.bundle.dSYM/Contents/Resources/DWARF/embedding.bundle index 31c89bf28aeef8e579c94c46de73f2ab75ce1a46..fdf04184d5fb768db81792fcc89ecd3fe739ef80 100644 GIT binary patch delta 4394 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 // For integer types like uint16_t #include // For memory allocation functions #include // For math functions like sqrt +#include "embedding_config.h" // Import the configuration -// Main data structure for storing embeddings -// Flexible array member (values[]) allows variable length arrays typedef struct { - uint16_t dim; // Dimension of the embedding vector - float values[]; // Flexible array member to store the actual values + float values[EMBEDDING_DIMENSION]; } embedding_t; -// Callback for freeing memory when Ruby's GC collects our object -static void embedding_free(void *ptr) { - xfree(ptr); // Ruby's memory free function -} - -// Callback to report memory usage to Ruby's GC -static size_t embedding_memsize(const void *ptr) { - const embedding_t *emb = (const embedding_t *)ptr; - return emb ? sizeof(embedding_t) + emb->dim * sizeof(float) : 0; -} - -// Type information for Ruby's GC: -// Tells Ruby how to manage our C data structure static const rb_data_type_t embedding_type = { - "RagEmbeddings/Embedding", // Type name - {0, embedding_free, embedding_memsize,}, // Functions: mark, free, size - 0, 0, // Parent type, data - RUBY_TYPED_FREE_IMMEDIATELY // Flags + "RagEmbeddings/Embedding", + {0, 0, 0,}, + 0, 0, + RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE }; -// Class method: RagEmbeddings::Embedding.from_array([1.0, 2.0, ...]) -// Creates a new embedding from a Ruby array static VALUE embedding_from_array(VALUE klass, VALUE rb_array) { - Check_Type(rb_array, T_ARRAY); // Ensure argument is a Ruby array - uint16_t dim = (uint16_t)RARRAY_LEN(rb_array); - - // Allocate memory for struct + array of floats - embedding_t *ptr = xmalloc(sizeof(embedding_t) + dim * sizeof(float)); - ptr->dim = dim; + Check_Type(rb_array, T_ARRAY); + if (RARRAY_LEN(rb_array) != EMBEDDING_DIMENSION) + rb_raise(rb_eArgError, "Wrong dimension, must be %d", EMBEDDING_DIMENSION); - // Copy values from Ruby array to our C array - for (int i = 0; i < dim; ++i) + embedding_t *ptr; + VALUE obj = TypedData_Make_Struct(klass, embedding_t, &embedding_type, ptr); + for (int i = 0; i < EMBEDDING_DIMENSION; ++i) ptr->values[i] = (float)NUM2DBL(rb_ary_entry(rb_array, i)); - - // Wrap our C struct in a Ruby object - VALUE obj = TypedData_Wrap_Struct(klass, &embedding_type, ptr); return obj; } -// Instance method: embedding.dim -// Returns the dimension of the embedding -static VALUE embedding_dim(VALUE self) { - embedding_t *ptr; - // Get the C struct from the Ruby object - TypedData_Get_Struct(self, embedding_t, &embedding_type, ptr); - return INT2NUM(ptr->dim); -} - -// Instance method: embedding.to_a -// Converts the embedding back to a Ruby array static VALUE embedding_to_a(VALUE self) { embedding_t *ptr; TypedData_Get_Struct(self, embedding_t, &embedding_type, ptr); - - // Create a new Ruby array with pre-allocated capacity - VALUE arr = rb_ary_new2(ptr->dim); - - // Copy each float value to the Ruby array - for (int i = 0; i < ptr->dim; ++i) + VALUE arr = rb_ary_new2(EMBEDDING_DIMENSION); + for (int i = 0; i < EMBEDDING_DIMENSION; ++i) rb_ary_push(arr, DBL2NUM(ptr->values[i])); - return arr; } -// Instance method: embedding.cosine_similarity(other_embedding) -// Calculate cosine similarity between two embeddings static VALUE embedding_cosine_similarity(VALUE self, VALUE other) { embedding_t *a, *b; - // Get C structs for both embeddings TypedData_Get_Struct(self, embedding_t, &embedding_type, a); TypedData_Get_Struct(other, embedding_t, &embedding_type, b); - // Ensure dimensions match - if (a->dim != b->dim) - rb_raise(rb_eArgError, "Dimension mismatch"); - float dot = 0.0f, norm_a = 0.0f, norm_b = 0.0f; - - // Calculate dot product and vector magnitudes - for (int i = 0; i < a->dim; ++i) { - dot += a->values[i] * b->values[i]; // Dot product - norm_a += a->values[i] * a->values[i]; // Square of magnitude for vector a - norm_b += b->values[i] * b->values[i]; // Square of magnitude for vector b + for (int i = 0; i < EMBEDDING_DIMENSION; ++i) { + dot += a->values[i] * b->values[i]; + norm_a += a->values[i] * a->values[i]; + norm_b += b->values[i] * b->values[i]; } - - // Apply cosine similarity formula: dot(a,b)/(|a|*|b|) - // Small epsilon (1e-8) added to prevent division by zero return DBL2NUM(dot / (sqrt(norm_a) * sqrt(norm_b) + 1e-8)); } -// Ruby extension initialization function -// This function is called when the extension is loaded void Init_embedding(void) { - // Define module and class VALUE mRag = rb_define_module("RagEmbeddings"); VALUE cEmbedding = rb_define_class_under(mRag, "Embedding", rb_cObject); - - // Register class methods rb_define_singleton_method(cEmbedding, "from_array", embedding_from_array, 1); - - // Register instance methods - rb_define_method(cEmbedding, "dim", embedding_dim, 0); rb_define_method(cEmbedding, "to_a", embedding_to_a, 0); rb_define_method(cEmbedding, "cosine_similarity", embedding_cosine_similarity, 1); } \ No newline at end of file diff --git a/ext/rag_embeddings/embedding_config.h b/ext/rag_embeddings/embedding_config.h new file mode 100644 index 0000000..7a6c575 --- /dev/null +++ b/ext/rag_embeddings/embedding_config.h @@ -0,0 +1 @@ +#define EMBEDDING_DIMENSION 3072 // <--- this must be the same as what is set in ruby lib/rag_embeddings/config.rb \ No newline at end of file diff --git a/lib/rag_embeddings.rb b/lib/rag_embeddings.rb index 65f186d..f002530 100644 --- a/lib/rag_embeddings.rb +++ b/lib/rag_embeddings.rb @@ -1,3 +1,4 @@ +require_relative "rag_embeddings/config" require_relative "rag_embeddings/version" require_relative "rag_embeddings/engine" require_relative "rag_embeddings/database" diff --git a/lib/rag_embeddings/config.rb b/lib/rag_embeddings/config.rb new file mode 100644 index 0000000..cdec074 --- /dev/null +++ b/lib/rag_embeddings/config.rb @@ -0,0 +1,3 @@ +module RagEmbeddings + EMBEDDING_DIMENSION = 3072 # <--- this must be the same as what is set in C! ext/rag_embeddings/embedding_config.h +end \ No newline at end of file diff --git a/lib/rag_embeddings/database.rb b/lib/rag_embeddings/database.rb index baa2ddc..30f8f64 100644 --- a/lib/rag_embeddings/database.rb +++ b/lib/rag_embeddings/database.rb @@ -27,9 +27,13 @@ def all # "Raw" search: returns the N texts most similar to the query def top_k_similar(query_text, k: 5) query_embedding = RagEmbeddings.embed(query_text) + raise "Wrong embedding size #{query_embedding.size}, #{RagEmbeddings::EMBEDDING_DIMENSION} was expected! Change the configuration." unless query_embedding.size == RagEmbeddings::EMBEDDING_DIMENSION + query_obj = RagEmbeddings::Embedding.from_array(query_embedding) all.map do |id, content, emb| + raise "Wrong embedding size #{query_embedding.size}, #{RagEmbeddings::EMBEDDING_DIMENSION} was expected! Change the configuration." unless emb.size == RagEmbeddings::EMBEDDING_DIMENSION + emb_obj = RagEmbeddings::Embedding.from_array(emb) similarity = emb_obj.cosine_similarity(query_obj) [id, content, similarity] diff --git a/spec/performance_spec.rb b/spec/performance_spec.rb index 8caf430..e8b617e 100644 --- a/spec/performance_spec.rb +++ b/spec/performance_spec.rb @@ -6,7 +6,7 @@ let(:text1) { "Performance test one" } let(:text2) { "Performance test two" } let(:n) { 10_000 } - let(:embedding_size) { 768 } + let(:embedding_size) { RagEmbeddings::EMBEDDING_DIMENSION } let(:emb1) { Array.new(embedding_size) { rand } } let(:emb2) { Array.new(embedding_size) { rand } } @@ -31,8 +31,8 @@ puts "RSS: #{(`ps -o rss= -p #{Process.pid}`.to_i / 1024.0).round(2)} MB" # Weak expectations, mostly for sanity check - expect(creation_time).to be < 0.1 # Should be less than 100 milliseconds for 10_000 - expect(sim_time).to be < 0.1 # Should be less than 100 milliseconds for 10_000 + expect(creation_time).to be < 0.2 # Should be less than 100 milliseconds for 10_000 + expect(sim_time).to be < 0.2 # Should be less than 100 milliseconds for 10_000 end context "memory usage" do From 3e09faec9625f766f5dc0c8856f464a56fab9584 Mon Sep 17 00:00:00 2001 From: Marco Mastrodonato Date: Mon, 9 Jun 2025 00:42:27 +0200 Subject: [PATCH 2/3] Now the size of embeddings is static and the `RUBY_TYPED_EMBEDDABLE` flag is used - the size of embeddings is static (3072 currently) and defined in two places: - c side is defined in `ext/rag_embeddings/embedding_config.h` - ruby side is defined in `lib/rag_embeddings/config.rb` Remember to recompile the c extension after changing the size: `rake compile` --- ext/rag_embeddings/embedding.bundle | Bin 51504 -> 51968 bytes .../Contents/Resources/DWARF/embedding.bundle | Bin 14996 -> 16640 bytes ext/rag_embeddings/embedding.c | 114 +++++++++++++++--- ext/rag_embeddings/embedding_config.h | 2 + 4 files changed, 100 insertions(+), 16 deletions(-) diff --git a/ext/rag_embeddings/embedding.bundle b/ext/rag_embeddings/embedding.bundle index 4058184511349195e6ca7fb640aef1e759b4ba77..d4b2d437910dd41bb971362b28342f8ff8b58db0 100755 GIT binary patch literal 51968 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 Gi~j++EN2z~ diff --git a/ext/rag_embeddings/embedding.bundle.dSYM/Contents/Resources/DWARF/embedding.bundle b/ext/rag_embeddings/embedding.bundle.dSYM/Contents/Resources/DWARF/embedding.bundle index fdf04184d5fb768db81792fcc89ecd3fe739ef80..3e9e303502af0e5b0242774fc8a6b5c7ef1d733f 100644 GIT binary patch delta 5459 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 delta 3900 zcmai1eN# zG$-uZj9+Cn)>O+H4W%_?>a0~ZQ%i?wsH~}pvX!o!vGSv=W@>)>-mBw(bJzNvz4vdQ zv+p@)pR@0K-92LKvfez`;V+&%*}uH>-}=P#^*hFP&E8UxZP$pX!9~=yrDdr!C~f&* zvVETo9G+eJbz+Kry4|!6==Z{+XwzDvyX7Q(SQOdA7So_7OMN0nKQ6r`TJ`->lDsjmq^>Skm8ACdY^0HoZkwr|?K!*wXbHH6#8B z%avrgo-kd#aJ_7~s)gx$*>nx78SZcXW>S}}QQzp9YOY?P`QpzFnN(Oc<+*I*Roz-f z{DKcmYDhBWg3*R+P|I+ad}LBzE2b>b_iLl|F;NNeS)ZEBE^885CB~p|R7U)fQ>M#f z4LVY4xQ<3;Bn+K4T}Q1~4V4o~eT{C1?Gcx)Hq7b4;{#$5cT$vLQl4e+wd@<&hVH8| z{kO7>#I<&tcC)_4RidAD74%iQUys#YwoQ>Xyig?@-GZ<}h#z8OSn%9UVuUf*O4w6? zO@J&$NDehU_Fd?7_m5Ast-TOheg4qeuO9wz{v2MsI98T+*S>EL-_$%9-5} zJf5wLernG~9zQ!_(d+);#u(eK9n&5gb@S}wOzvGX@2{u!KN>4a_0z3SzwucDRwF95 z1=bcUey(HoiQD=o_I`M>k;y+E_4Y2jdQxU=;sw_Ka{9qd z1>H~WyhP_`rmh%I<$7JFw{f)$*S&o^PObO!?CMN>#RN+8F@|*!hQ)8p$w8#?dMfF`l1?lL6hqT)O1bG3SmAys@kI_XsfSADM#N+ z=xMv&HsMv(XAX1R1^!;fW(Lg ziRW{~k5CozE4>0&pb}ZSN(+U2q7Fwvp}C@ogskA$)hV|;E0o`gy4}jNVy}R`R~)d2 z1L7zXN5zmy7(;wwmg3tbM3b^h7$aw4WCUF}WQsx+;x0o5M6)>)^yIm)n1~m z5_W}L+%FCeyvg5c%!EX$LNz?VuaFVPg{f<6$*rXBR(yWDp}ECKL`YC5Th!gIWUrB| zv0%KIqcL<@;Tg$9N8xOxBozlE_ENne+f9=Tcpted{Ust8*xxyO9XtwOSYLZLfsbekR`8_oMFozyuw>w4X^crla_3@ z{9#KDS+WgxMkQg45|n0XI7AW+;#x8<<&hqp>lThae{PM4I9B0IX=~90$(}$(6lE*h z3XA)?pM;W_4}+W}mn(`~p(^q?)s{rP;6(d)3i_PTDyfVMsm}|&ipp6}{s@goK8seO zlnK$dF&Xm*bfA>$quUJ&O~mA4*rwEKZ8FLsk-r8CQ~sYpNo1cqQ4(o4X#0hK5*o|5 z-|(g1s2vtGVzSoHJu_;wes*V0(T*5VM^3f<^c*{CZ9f@AR!Ddy{Qn@rue1o})$`UU#D5GVxsy6~s>l5}dqR9y&KZo5Xk+0J%Z$hRsHTi~{seqoN~2~luP%revD!%{{$ajlS|eJ-UN zxp)TFJX81eL!U__jQu|h{dUUaK7}r0OOAn=MI()<#TzkMH)bxQHc~OyjCi&pu5KSC z?}h0p9zg5m#jU}>(mPN$`H+}%F;STzz+#{aSPrZMehusZeg`}Uyb8Pv zoCUrHz6X8;VzJ}+@KHW64w$5%Zcc-;09XVp2kr+Vz$3s@!0&-qfcJopfX{%lz?X{N zU2(E+_qZXUZ?CG<2P!w~8>_ZwwRCoNbQbcBIfSL~FDM+9t4}O6zy8>S)Cio!-x4k+ zNI0>FpE!Ukha2E(&i@OLPD%wn4~Dnz{N(3x@AYLa369;d@*Zi`TXivk-*om5ez(H) zhI_@Hru&{=7~Z}n>q*bCNvpPG+%e(1ri@2dF8ln(oAR2%9Y0L}(itqgeZn1=cSJ%( z-5VUPL;s5XY{UB{`uV}shu`15XyTmBt~XboiyYUhUOG5D>Py}f&JJgVvlEZC@QznN z{vGy^>?o~( // For math functions like sqrt #include "embedding_config.h" // Import the configuration +// Main data structure for storing embeddings with fixed size typedef struct { - float values[EMBEDDING_DIMENSION]; + uint16_t dim; // Actual dimension used (can be <= EMBEDDING_DIMENSION) + float values[EMBEDDING_DIMENSION]; // Fixed-size array for embedding values } embedding_t; +// Callback for freeing memory when Ruby's GC collects our object +static void embedding_free(void *ptr) { + // With RUBY_TYPED_EMBEDDABLE and TypedData_Make_Struct, + // Ruby handles the deallocation automatically + // No need to explicitly free the memory +} + +// Callback to report memory usage to Ruby's GC +static size_t embedding_memsize(const void *ptr) { + // With embedded objects, we report the full struct size + return sizeof(embedding_t); +} + +// Type information for Ruby's GC with embedding support static const rb_data_type_t embedding_type = { - "RagEmbeddings/Embedding", - {0, 0, 0,}, - 0, 0, - RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE + "RagEmbeddings/Embedding", // Type name + {0, embedding_free, embedding_memsize,}, // Functions: mark, free, size + 0, 0, // Parent type, data + RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE // Flags with embedding! }; +// Class method: RagEmbeddings::Embedding.from_array([1.0, 2.0, ...]) +// Creates a new embedding from a Ruby array - NOW FASTER! static VALUE embedding_from_array(VALUE klass, VALUE rb_array) { - Check_Type(rb_array, T_ARRAY); - if (RARRAY_LEN(rb_array) != EMBEDDING_DIMENSION) - rb_raise(rb_eArgError, "Wrong dimension, must be %d", EMBEDDING_DIMENSION); + Check_Type(rb_array, T_ARRAY); // Ensure argument is a Ruby array + long array_len = RARRAY_LEN(rb_array); + + // Check if the array size exceeds our maximum dimension + if (array_len > EMBEDDING_DIMENSION) { + rb_raise(rb_eArgError, "Embedding dimension %ld exceeds maximum %d", + array_len, EMBEDDING_DIMENSION); + } + + uint16_t dim = (uint16_t)array_len; + // With RUBY_TYPED_EMBEDDABLE, use TypedData_Make_Struct + // This automatically allocates Ruby object + embedded data space embedding_t *ptr; VALUE obj = TypedData_Make_Struct(klass, embedding_t, &embedding_type, ptr); - for (int i = 0; i < EMBEDDING_DIMENSION; ++i) + + ptr->dim = dim; + + // Copy values from Ruby array to our C array + for (int i = 0; i < dim; ++i) ptr->values[i] = (float)NUM2DBL(rb_ary_entry(rb_array, i)); + + // Zero out unused slots for consistency + for (int i = dim; i < EMBEDDING_DIMENSION; ++i) + ptr->values[i] = 0.0f; + return obj; } +// Class method to get the maximum supported dimension +static VALUE embedding_max_dimension(VALUE klass) { + return INT2NUM(EMBEDDING_DIMENSION); +} + +// Instance method: embedding.dim +// Returns the actual dimension of the embedding +static VALUE embedding_dim(VALUE self) { + embedding_t *ptr; + // Get the C struct from the Ruby object - NOW FASTER! + TypedData_Get_Struct(self, embedding_t, &embedding_type, ptr); + return INT2NUM(ptr->dim); +} + +// Instance method: embedding.to_a +// Converts the embedding back to a Ruby array (only actual dimensions) static VALUE embedding_to_a(VALUE self) { embedding_t *ptr; TypedData_Get_Struct(self, embedding_t, &embedding_type, ptr); - VALUE arr = rb_ary_new2(EMBEDDING_DIMENSION); - for (int i = 0; i < EMBEDDING_DIMENSION; ++i) + + // Create a new Ruby array with pre-allocated capacity for actual dimension + VALUE arr = rb_ary_new2(ptr->dim); + + // Copy only the used float values to the Ruby array - FASTER MEMORY ACCESS! + for (int i = 0; i < ptr->dim; ++i) rb_ary_push(arr, DBL2NUM(ptr->values[i])); + return arr; } +// Instance method: embedding.cosine_similarity(other_embedding) +// Calculate cosine similarity - MUCH FASTER with embedded data! static VALUE embedding_cosine_similarity(VALUE self, VALUE other) { embedding_t *a, *b; + // Get C structs for both embeddings - direct access, no pointer deref! TypedData_Get_Struct(self, embedding_t, &embedding_type, a); TypedData_Get_Struct(other, embedding_t, &embedding_type, b); + // Ensure dimensions match + if (a->dim != b->dim) + rb_raise(rb_eArgError, "Dimension mismatch: %d vs %d", a->dim, b->dim); + float dot = 0.0f, norm_a = 0.0f, norm_b = 0.0f; - for (int i = 0; i < EMBEDDING_DIMENSION; ++i) { - dot += a->values[i] * b->values[i]; - norm_a += a->values[i] * a->values[i]; - norm_b += b->values[i] * b->values[i]; + + // Calculate dot product and vector magnitudes + // Better cache locality = faster calculations! + for (int i = 0; i < a->dim; ++i) { + float val_a = a->values[i]; + float val_b = b->values[i]; + dot += val_a * val_b; // Dot product + norm_a += val_a * val_a; // Square of magnitude for vector a + norm_b += val_b * val_b; // Square of magnitude for vector b } - return DBL2NUM(dot / (sqrt(norm_a) * sqrt(norm_b) + 1e-8)); + + // Apply cosine similarity formula: dot(a,b)/(|a|*|b|) + // Small epsilon (1e-8) added to prevent division by zero + float magnitude_product = sqrt(norm_a) * sqrt(norm_b); + return DBL2NUM(dot / (magnitude_product + 1e-8f)); } +// Ruby extension initialization function +// This function is called when the extension is loaded void Init_embedding(void) { + // Define module and class VALUE mRag = rb_define_module("RagEmbeddings"); VALUE cEmbedding = rb_define_class_under(mRag, "Embedding", rb_cObject); + + // Register class methods rb_define_singleton_method(cEmbedding, "from_array", embedding_from_array, 1); + rb_define_singleton_method(cEmbedding, "max_dimension", embedding_max_dimension, 0); + + // Register instance methods + rb_define_method(cEmbedding, "dim", embedding_dim, 0); rb_define_method(cEmbedding, "to_a", embedding_to_a, 0); rb_define_method(cEmbedding, "cosine_similarity", embedding_cosine_similarity, 1); } \ No newline at end of file diff --git a/ext/rag_embeddings/embedding_config.h b/ext/rag_embeddings/embedding_config.h index 7a6c575..1eb56ab 100644 --- a/ext/rag_embeddings/embedding_config.h +++ b/ext/rag_embeddings/embedding_config.h @@ -1 +1,3 @@ +// Configuration: Change this to match your LLM embedding size +// Common sizes: 768 (BERT), 1536 (OpenAI text-embedding-ada-002), 3072 (text-embedding-3-large) #define EMBEDDING_DIMENSION 3072 // <--- this must be the same as what is set in ruby lib/rag_embeddings/config.rb \ No newline at end of file From 8c1ad2fcf60553aaee9379afa25523d3472f3ed7 Mon Sep 17 00:00:00 2001 From: Marco Mastrodonato Date: Mon, 9 Jun 2025 00:49:05 +0200 Subject: [PATCH 3/3] Updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 28cdfb6..dc3acae 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ puts "Most similar text: #{result.first[1]}, score: #{result.first[2]}" - Embedding provider: switch model/provider in engine.rb (Ollama, OpenAI, etc) - Database: set the SQLite file path as desired -# 🔢 Embeddings dimension +## 🔢 Embeddings dimension In the previous version the size of embeddings was dynamic. Now the size of embeddings is static to use the RUBY_TYPED_EMBEDDABLE flag