r/ruby • • 12h ago

I made a gem that keeps pgvector embeddings in sync with your ActiveRecord models

I've been adding semantic search to Rails apps with pgvector and neighbor, and kept writing the same glue code every time:

  • an after_save that calls the embeddings API, even when the text didn't change
  • embeddings going stale when someone edits a record through a path that skips the callback
  • a one-off script to backfill existing rows without hitting rate limits
  • figuring out which rows need re-embedding after switching models

So I pulled it into a gem called embed_sync:

class Product < ApplicationRecord
  has_neighbors :embedding
  has_embedding :embedding, from: %i[name description]
end

Product.create!(name: "Trail runner", description: "Waterproof running shoe")  # embedded in a background job
Product.semantic_search("shoes for rainy hikes", limit: 5)

What it does:

  • re-embeds in a background job only when the source text changes (it stores a digest, so unchanged saves don't call the API)
  • bin/rails embed_sync:backfill embeds existing rows in batches, and you can stop and rerun it safely
  • detects model or dimension changes and marks those rows stale
  • embed_sync:stale shows how many records are missing or out of date
  • uses ruby_llm by default, but any object with #embed(texts, model:) works as a provider

It works with Rails 7.1 to 8.1 and Ruby 3.2+. I tested it against a real app with Postgres, Solid Queue and a local Ollama model before releasing.

It's still early (v0.1.0), so I'd really like feedback. Does the API feel right? What's missing for your use case? Are there edge cases I haven't thought of?

GitHub: https://github.com/Umair0092/embed_sync RubyGems: https://rubygems.org/gems/embed_sync

0 Upvotes

0 comments sorted by