Open-source project
ankane/neighbor avatar
ankane/neighbor

Neighbor: nearest neighbour search inside ActiveRecord

Nearest neighbor search for Rails

850 stars20 forksRubyMIT

At a glance

What is it?
Neighbor is a Ruby gem that adds vector search to Rails models, so you can ask for the nearest rows to an embedding with a method call instead of raw SQL. It works with Postgres through pgvector or the cube extension, with MariaDB, with MySQL where HeatWave is present, and with SQLite, and it supports exact and approximate indexes.
Who is it for?
Neighbor is a good fit for a Rails application that already stores embeddings and needs similarity search without a separate vector service, because the query stays in ActiveRecord and the same call works across Postgres, MariaDB, MySQL and SQLite, which is what makes it easy to develop locally and deploy on your existing database.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 47 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Vector search without leaving ActiveRecord

Neighbor describes itself in five words: nearest neighbor search for Rails. It is a Ruby gem, MIT licensed, that adds vector columns and a nearest neighbour query to ActiveRecord models.

The design decision is that the search happens in the database you already run. You declare a vector column in a migration, tell your model that the column holds neighbours, and then call nearest_neighbors on a record or on the class. No separate vector database, no client library, no second connection pool.

The README lists what is supported: Postgres with either the pgvector extension or the cube extension, MariaDB 11.8 and later, MySQL 9.7 and later where searching requires HeatWave, and SQLite. Two sibling projects cover the hosted options: neighbor-redis for Redis and neighbor-s3 for S3 Vectors, both by the same author.

The MySQL line is the one to read twice. It says searching requires HeatWave, and later it explains why, that the DISTANCE function is only available on HeatWave. That is not a limitation of this gem but of the database, and it means a MySQL user without HeatWave gets column storage without search.

The audience is a Rails developer who has embeddings in a table and wants a similarity query, and who would rather not maintain a second data store for it.

Installing the gem and the Postgres extensions

Installation is a Gemfile line.

ruby
gem "neighbor"

For Postgres there are two routes, and the README explains the difference in a sentence. The cube extension ships with Postgres itself, while pgvector supports more dimensions and approximate nearest neighbour search. That trade, exact search over a small dimensional space versus approximate search over a large one, decides which one you use.

The cube route is a generator and a migration:

sh
rails generate neighbor:cube
rails db:migrate

The pgvector route uses a different generator, and it assumes you have installed the extension first.

sh
rails generate neighbor:vector
rails db:migrate

The separate generators are what make the gem portable: the migration that gets written differs per database, so a project can switch backends by switching generators.

The rest of the README is organised the same way. There is a getting started section with the common API, then a section per backend with its own distances, dimension limits, index options and column types. That structure is the fastest way to answer the question that actually matters for an application, which is what this database can do rather than what the gem does.

The model API in four calls

The core API is small enough to hold in your head.

A migration adds the column. The type depends on the backend: vector for pgvector, MariaDB and MySQL, cube for the Postgres cube extension, and binary for SQLite.

ruby
class AddEmbeddingToItems < ActiveRecord::Migration[8.1]
  def change
    # pgvector, MariaDB, and MySQL
    add_column :items, :embedding, :vector, limit: 3 # dimensions

    # cube
    add_column :items, :embedding, :cube

    # SQLite
    add_column :items, :embedding, :binary
  end
end

The model then declares the column.

ruby
class Item < ApplicationRecord
  has_neighbors :embedding
end

Updating a vector is an ordinary attribute assignment.

ruby
item.update(embedding: [1.0, 1.2, 0.5])

And the search is a scope-like call that returns records, either from an existing record or from a raw vector.

ruby
item.nearest_neighbors(:embedding, distance: "euclidean").first(5)
ruby
Item.nearest_neighbors(:embedding, [0.9, 1.3, 1.1], distance: "euclidean").first(5)

Returned records carry a neighbor_distance attribute, which is the piece most hand rolled implementations forget. And there is a threshold option for the case where you want everything inside a radius rather than the k nearest.

ruby
item.nearest_neighbors(:embedding, distance: "euclidean", threshold: 0.5).first(5)

pgvector: distances, dimension limits and two index types

The pgvector section is the longest in the README, and it is the part you will spend the most time with.

Six distance functions are supported: euclidean, inner_product, cosine, taxicab, hamming and jaccard. The distance is chosen by string, so the same call site works across backends as long as the backend supports that function.

The dimension limits are the numbers to know before you design the schema, because they decide whether an index is even possible. The vector type can hold up to 16,000 dimensions, and vectors of up to 2,000 dimensions can be indexed. The halfvec type also allows up to 16,000 dimensions, with half precision vectors indexable up to 4,000 dimensions. The bit type reaches 83 million dimensions, with 64,000 indexable. The sparsevec type allows up to 16,000 non-zero elements, of which 1,000 can be indexed.

So the practical rule is that a full precision float vector is indexable to 2,000 dimensions, and half precision buys you 4,000. That is the ceiling for an approximate index, not for storage.

Two approximate index types are available, HNSW and IVFFlat, and the migration writes them as an index with an operator class.

ruby
class AddIndexToItemsEmbedding < ActiveRecord::Migration[8.1]
  def change
    add_index :items, :embedding, using: :hnsw, opclass: :vector_l2_ops
    # or
    add_index :items, :embedding, using: :ivfflat, opclass: :vector_l2_ops
  end
end

The operator class follows the distance: vector_cosine_ops for cosine distance and vector_ip_ops for inner product. The vector_l2_ops class in the example is the Euclidean one.

Both index types have a runtime knob, and the README shows both as raw session settings.

ruby
Item.connection.execute("SET hnsw.ef_search = 100")
ruby
Item.connection.execute("SET ivfflat.probes = 3")

The HNSW setting sizes the dynamic candidate list, and the IVFFlat setting sets the number of probes. Both are the standard accuracy against latency dials, and both are per session, so they belong in a database configuration rather than in application code.

Half precision, binary and sparse vectors

Three storage variants sit next to the plain vector column, and each trades accuracy for size or for indexability.

Half precision uses the halfvec type, which stores the same column with half the bytes, and it can be indexed at higher dimensions than the full precision type.

ruby
class AddEmbeddingToItems < ActiveRecord::Migration[8.1]
  def change
    add_column :items, :embedding, :halfvec, limit: 3 # dimensions
  end
end

The index is then built on an expression that casts to half precision, so the stored column stays full precision if you want that.

ruby
class AddIndexToItemsEmbedding < ActiveRecord::Migration[8.1]
  def change
    add_index :items, "(embedding::halfvec(3)) halfvec_l2_ops", using: :hnsw
  end
end

Searching at half precision is a query option rather than a separate method.

ruby
Item.nearest_neighbors(:embedding, [0.9, 1.3, 1.1], distance: "euclidean", precision: "half").first(5)

Binary vectors use the bit type, are searched by Hamming distance, and can be indexed through a binary quantization expression.

ruby
class AddIndexToItemsEmbedding < ActiveRecord::Migration[8.1]
  def change
    add_index :items, "(binary_quantize(embedding)::bit(3)) bit_hamming_ops", using: :hnsw
  end
end

Sparse vectors use the sparsevec type and are constructed with a hash of index to value pairs plus the total dimension count.

ruby
embedding = Neighbor::SparseVector.new({0 => 0.9, 1 => 1.3, 2 => 1.1}, 3)
Item.nearest_neighbors(:embedding, embedding, distance: "euclidean").first(5)

That last one is the pattern to copy for anything that is mostly zeros, which is the usual shape of a bag of words embedding.

cube, MariaDB and MySQL have their own rules

The other backends are narrower, and the differences show up immediately in the distance list.

With cube the supported distances are euclidean, cosine, taxicab and chebyshev. There is no inner product distance, and the README points to an example file for the inner product case instead. Cosine distance with cube needs the vectors normalised before they are stored, which is a model option.

ruby
class Item < ApplicationRecord
  has_neighbors :embedding, normalize: true
end

Cube also caps out at 100 dimensions by default, with the README pointing to the Postgres documentation for how to raise that. It advises specifying the dimension count so that all records hold the same number, which is the sort of detail that prevents a class of runtime errors.

ruby
class Item < ApplicationRecord
  has_neighbors :embedding, dimensions: 3
end

MariaDB supports euclidean, cosine and hamming, and it has one requirement that is easy to miss: vector columns must be declared with null: false to add a vector index.

ruby
class CreateItems < ActiveRecord::Migration[8.1]
  def change
    create_table :items do |t|
      t.vector :embedding, limit: 3, null: false
      t.index :embedding, type: :vector
    end
  end
end

Its binary vectors use the bigint type and are limited to 64 dimensions. MySQL supports the same three distances, and its binary vectors use the bit type, but searching still depends on HeatWave being present, because that is where the DISTANCE function lives.

What the repository holds and where the limits are

The repository is a normal Ruby gem layout: a gemspec, a Rakefile, a lib directory, a test directory, a CHANGELOG, a LICENSE file and a gemfiles directory that usually holds a matrix of dependency sets for continuous integration. The examples directory is the interesting part, because the folders name the use cases the author had in mind: cohere, disco, hybrid, informers, openai and sparse. Those names point at embedding providers, at a recommendation example, at hybrid search combining vectors with ordinary filters, and at the sparse vector path.

That is also where the honest comparison sits. A pure Ruby nearest neighbour search, or a dedicated vector service such as the Redis or S3 Vectors options the author maintains separately, will scale further than a query inside your primary database. What Neighbor buys is that the vector lives next to the row it describes, so a similarity query joins naturally with the rest of your ActiveRecord scope and your application keeps one database.

Two facts to keep in mind before you plan around it. The repository has no GitHub releases, so there is no tag to pin, and the last push was on 2026-09-06. The gem is still published and the changelog is maintained in the repository, but the release discipline is informal, which is worth weighing if you are putting it in a production dependency set.

The licence is MIT, so there is no copyleft obligation to reason about beyond attribution.

Editorial conclusion

Neighbor is a good fit for a Rails application that already stores embeddings and needs similarity search without a separate vector service, because the query stays in ActiveRecord and the same call works across Postgres, MariaDB, MySQL and SQLite, which is what makes it easy to develop locally and deploy on your existing database. It is the wrong choice when you need billions of vectors, when approximate index tuning is part of the job, or when the database is not one of the four it supports. Verify first which distance function your data actually needs, since the supported set differs per backend and the cube extension requires normalised vectors for cosine distance, and create the index in a migration before you load data, since building an HNSW index afterwards is slower than building it alongside the rows.

Frequently asked questions

Which databases does Neighbor support?

Postgres through the pgvector and cube extensions, MariaDB 11.8 and later, MySQL 9.7 and later where searching requires HeatWave, and SQLite. Separate projects by the same author cover Redis and S3 Vectors.

How do I add vector search to a Rails model?

Add the gem to your Gemfile, run the generator for your database such as rails generate neighbor:vector or rails generate neighbor:cube, add the column in a migration, and declare has_neighbors :embedding in the model.

What is the maximum vector size Neighbor can index?

With pgvector, full precision vector columns can be indexed up to 2,000 dimensions, half precision up to 4,000 dimensions, bit vectors up to 64,000 dimensions, and sparse vectors up to 1,000 non-zero elements.

How do I tune the approximate index?

For HNSW you set the size of the dynamic candidate list with SET hnsw.ef_search, and for IVFFlat you set the number of probes with SET ivfflat.probes, both as session settings on the connection.

Official sources

  1. ankane/neighbor on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ankane-neighbor.svg)](https://hysenlabs.com/projects/ankane-neighbor)

Was this useful?