Module API and index lifecycle
Source:vignettes/articles/module-api-index-lifecycle.Rmd
module-api-index-lifecycle.RmdIf 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.
Search
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.