Skip to contents

If you want to add items to an index over time, delete them, or save the index for later, you can use the Rcpp Module classes directly. The usual hnsw_build() and hnsw_search() functions use these same classes, so an index you’ve already built with hnsw_build() has the methods described here too.

Create an index

Class Distance
HnswEuclidean Euclidean
HnswL2 Squared Euclidean
HnswCosine One minus cosine similarity
HnswIp One minus inner product

All four classes have the same constructors and methods. We’ll use HnswL2 here and leave room for 100 items to begin with. The constructor takes the dimension, capacity, M, construction ef, and an optional random seed (default 100), in that order. The order matters: Module arguments are matched by position, even if you give them names.

library(RcppHNSW)

items <- as.matrix(iris[, -5])
dimension <- ncol(items)
capacity <- 100
M <- 16
ef_construction <- 200
ann <- new(HnswL2, dimension, capacity, M, ef_construction, 100)
ann$addItems(items[1:100, ])

For help choosing M, construction ef, and the seed, see the parameter guide.

Add and retrieve items

Method Input or result
addItem(v) Add one vector.
addItems(m) Add each row of a matrix.
addItemsCol(m) Add each column of a matrix.
getItems(ids) Return one row per requested label, in the order supplied.

Items get labels in the order you add them, starting at one. So ann$getItems(c(1, 10)) retrieves the first and tenth items. If you’re using a cosine index, the returned vectors have been normalized to unit length.

Coordinates are stored in single precision; see numeric limits for the accepted range.

Set search ef with ann$setEf(ef). As with hnsw_search(), increasing it usually finds more of the true nearest neighbors, but takes longer. k must be positive and no greater than the number of undeleted items. If a search cannot find k neighbors, try increasing ef; a poorly connected index may need rebuilding with a larger M.

Method Query layout Result
getNNs(v, k) One vector Vector of labels
getAllNNs(m, k) One item per row n × k matrix of labels
getAllNNsCol(m, k) One item per column k × n matrix of labels

The corresponding getNNsList(), getAllNNsList(), and getAllNNsListCol() methods take a third argument, include_distances. They return labels as item and, when that argument is TRUE, distances as distance, with the same shapes as above.

For example, here’s how to get both labels and distances for the first five items:

ann$setEf(50)
result <- ann$getAllNNsList(items[1:5, ], 4, TRUE)
result$item
##      [,1] [,2] [,3] [,4]
## [1,]    1   18    5   29
## [2,]    2   13   46   35
## [3,]    3   48    4    7
## [4,]    4   48   30   31
## [5,]    5    1   38   18
result$distance
##      [,1]       [,2]       [,3]       [,4]
## [1,]    0 0.01000000 0.01999996 0.01999996
## [2,]    0 0.01999998 0.01999998 0.01999998
## [3,]    0 0.01999998 0.06000003 0.07000001
## [4,]    0 0.02000003 0.03000002 0.05000012
## [5,]    0 0.01999996 0.01999998 0.02999996

Batch insertion and search use the settings from setNumThreads(n_threads) and setGrainSize(grain_size). See threads and reproducibility.

Resize and delete

We’ve filled the 100 spaces we asked for, but there are another 50 items in iris. To add those, we first need to make room with resizeIndex(capacity). The new capacity must be at least one and at least size(), which includes deleted items.

ann$resizeIndex(nrow(items))
ann$addItems(items[101:150, ])
ann$size()
## [1] 150

markDeleted(label) hides an item from search and getItems(). The “mark” part of the name is doing some work here: the item still occupies space in the index. Deleting it doesn’t free memory or capacity, and size() still counts it:

ann$markDeleted(1)
ann$size()
## [1] 150

If an insertion fails after it starts modifying the index, or a native resize fails, discard the index and rebuild or reload it. Invalid inputs rejected before modification leave the index usable.

Save and load an index

Once you have an index you’re happy with, ann$save(filename) saves the graph, its items, capacity, and deletion state. Let’s save ours and load it into a new object:

path <- tempfile(fileext = ".hnsw")
ann$save(path)
loaded <- new(HnswL2, dimension, path)
loaded$setEf(50)
loaded$size()
## [1] 150
unlink(path)

Use the original dimension and distance class when loading. HnswL2 and HnswEuclidean can also load each other’s checkpoints. Search ef isn’t saved: loading resets it to 10, which is why we set it again above.

To change capacity while loading, use new(HnswL2, dimension, path, capacity). Omitting capacity restores the saved value. An explicit positive capacity takes effect when it is at least the stored item count; smaller values fall back to the saved capacity.

These files use hnswlib’s raw format, and compatibility depends on the hnswlib version and platform. Keep the original data too, in case you need to rebuild the index elsewhere.