Adding HannoyTransformer#141
Conversation
for more information, see https://pre-commit.ci
Do you mean a hannoy issue (if so, link plz) or in your code?
All of them of course! Your code should not know which ones exist (except for the default): instead of accepting a string, just use the |
| # distance correction | ||
| if self.metric == "euclidean": | ||
| np.sqrt(distances, out=distances) |
There was a problem hiding this comment.
When I checked's hannoy's euclidean distance function, it seems to returns the squared distance it computes but never takes the square root. So it returns a squared Euclidean distance, not the actual Euclidean distance. You can see it in the scalar fallback at src/spaces/simple.rs:49-51:
u.iter().zip(v.iter()).map(|(u, v)| (u - v) * (u - v)).sum()
I believe that this is intentional on hannoy's side. It seems more of a free performance win internally; however, for us, it means hannoy returns a squared Euclidean instead the actual Euclidean distance. The np.sqrt here finishes the computation so the output matches what scipy and KNeighborsTransformer expect.
|
https://github.com/nnethercott/hannoy/commits/main/ Both Phil and my PRs have been merged - this could be |
|
yeah, if it all works as expected, we can ask the maintainer to cut a release in nnethercott/hannoy#127 |
…0/sklearn-ann into hannoy-implementation
for more information, see https://pre-commit.ci
flying-sheep
left a comment
There was a problem hiding this comment.
Please add this transformer to test_kneighbors/test_common.py
| estimator_type="transformer", | ||
| target_tags=TargetTags(required=False), | ||
| transformer_tags=TransformerTags(), | ||
| transformer_tags=TransformerTags(preserves_dtype=[np.float32]), |
There was a problem hiding this comment.
Will hannoy only ever output float32?
There was a problem hiding this comment.
Yes, it seems to be the case, when I was looking through the code. When looking at the hannoy's distance the function returns as -> f32 (src/distance/mod.rs:41), so every metric returns f32 distances. The Python bindings also do the same thing: by_vec → Vec<(ItemId, f32)> and by_item → Option<Vec<(ItemId, f32)>> (src/python.rs:402 and L415-L420).
| if X is None: | ||
| # by_item path | ||
| for i in range(n_samples_transform): | ||
| neighbours = self.hannoy_reader_.by_item( | ||
| i, n=self.n_neighbors, ef_search=self.ef_search | ||
| ) | ||
| indices[i, 0] = i | ||
| distances[i, 0] = 0.0 | ||
| indices[i, 1:] = [j for j, _ in neighbours] | ||
| distances[i, 1:] = [d for _, d in neighbours] | ||
| else: | ||
| # by_vec path | ||
| for i, x in enumerate(X): | ||
| neighbours = self.hannoy_reader_.by_vec( | ||
| x.tolist(), n=n_neighbors, ef_search=self.ef_search | ||
| ) | ||
| indices[i] = [j for j, _ in neighbours] | ||
| distances[i] = [d for _, d in neighbours] |
There was a problem hiding this comment.
Oof that looks slow. Are these APIs enough to avoid for loops? nnethercott/hannoy#127
If not, we should file another issue.
There was a problem hiding this comment.
From what I understand, it is not yet the case. I am having a bit of a hard time examining Rust code but, it seems like the batch query is still missing, so the loops here are needed for now.
In the issue #127 that I opened up, I mentioned three things: by_item, batch insert, and batch query. The first two were resolved and merged from what I am understanding; however, the read side has no batch method. So _transform still makes one call into Rust per row, which is the slow part you flagged. I will add another comment on this issue #127 to bring up to the attention.
This is on hannoy's side, not ours. hannoy stores its LMDB environment in a |
for more information, see https://pre-commit.ci
…0/sklearn-ann into hannoy-implementation
for more information, see https://pre-commit.ci
flying-sheep
left a comment
There was a problem hiding this comment.
looks good, I’ll do a batch query PR for hannoy, and if the once author does a release, we can finish this up
|
|
||
| Notes | ||
| ----- | ||
| Known issue where multiple Database instances silently share the first one's LMDB env. |
There was a problem hiding this comment.
Can we make this more helpful? What can a user do to avoid this issue?
This PR adds
HannoyTransformeras proposed in #123, wrapping hannoy (LMDB-backed storage) into the sklearn-ann transformer interface. The approach follows the same pattern asAnnoyTransformerand others.A few things to note:
hannoy's Python bindings don't expose
by_itemyet (it exists in the Rust code inreader.rsbutPyReaderonly hasby_vec). A PR upstream was created to add it. Until that is approved,fit_transformstores the training data and re-queries it usingby_vecinstead of the fasterby_itempath. I plan to switch toby_itemonce it's available.There's a known issue where multiple
Databaseinstances in the same process silently share the first LMDB environment.I also opened an issue on hannoy requesting batch insert/query APIs to avoid the per-vector Python to Rust loop overhead.
Two questions: which metrics do we want to support? Right now it's only
euclidean, but hannoy also hashamming,sqeuclidean,cosine, andmanhattan. Also hannoy offers binary quantized variants of consine, euclidean, and manhattan. Would we want to also use those?