Skip to content

FAISS ANN search backend for proteogram similarity - #10

Open
swapnilashtekar wants to merge 4 commits into
mainfrom
pr1-faiss-search
Open

swapnilashtekar wants to merge 4 commits into
mainfrom
pr1-faiss-search

Conversation

@swapnilashtekar

Copy link
Copy Markdown
Collaborator

Adds an optional approximate-nearest-neighbour search path as an alternative to brute-force cosine similarity, for corpora where the O(N^2) scan dominates runtime.

  • proteogram/v2/faiss_search.py: FaissIndex wrapper (IVFFlat, and IVF-PQ for very large corpora). Embeddings are L2-normalised before indexing so inner-product search is true cosine similarity.
  • Img2Vec gains four thin delegators (build/save/load/similarities_faiss) so scripts keep a single entry point.
  • measure_similarity_v2.py: --faiss, --faiss_pq, --faiss_index_file. Brute-force remains the default; the index is cached to disk and reused unless --overwrite is passed.
  • Adds faiss-cpu to dependencies and relocks.

Both search paths share the same preprocessing function so ANN results are directly comparable to the brute-force baseline.

Adds an optional approximate-nearest-neighbour search path as an
alternative to brute-force cosine similarity, for corpora where the
O(N^2) scan dominates runtime.

- proteogram/v2/faiss_search.py: FaissIndex wrapper (IVFFlat, and
  IVF-PQ for very large corpora). Embeddings are L2-normalised before
  indexing so inner-product search is true cosine similarity.
- Img2Vec gains four thin delegators (build/save/load/similarities_faiss)
  so scripts keep a single entry point.
- measure_similarity_v2.py: --faiss, --faiss_pq, --faiss_index_file.
  Brute-force remains the default; the index is cached to disk and
  reused unless --overwrite is passed.
- Adds faiss-cpu to dependencies and relocks.

Both search paths share the same preprocessing function so ANN results
are directly comparable to the brute-force baseline.
@swapnilashtekar
swapnilashtekar marked this pull request as draft September 12, 2026 05:58
Review fixes on top of the initial FAISS backend:

nlist defaulted to N instead of sqrt(N). The expression
`N ** _NLIST_SQRT_FACTOR ** 0.5` parses as `N ** (1.0 ** 0.5)` because ** is
right-associative, so every vector got its own Voronoi cell. FAISS warned on
every build and training cost scaled quadratically (3.7s vs 0.5s at 20k x 512).

search_all() silently truncated deep rankings. measure_similarity_v2.py asks
for the full corpus ordering so Recall@K works at any K, but an IVF search
only returns vectors in the cells it probes: 200 of 2008 requested results
came back per query. The blank trailing CSV cells read back as NaN and
crashed evaluate_methods_v2.py in target.split(','). search_all() now widens
nprobe to reach the requested depth, restoring it afterwards so one deep
search does not leave the index scanning exhaustively, and the CSV is sized
to the shortest ranking actually produced.

Added --faiss_top_k, because a full-corpus ranking forces an exhaustive scan
and is 15x slower than brute force at the current corpus size (1.67s vs
0.11s at N=2008). Capping the depth is what makes the ANN path worth using.

faiss-cpu moves to an optional "search" extra, matching the lazy import and
install hint already in faiss_search.py. Adds tests/test_faiss_search.py
covering the defaults, truncation, nprobe state, persistence and PQ
fallbacks.

Drops docs/improvements_v2.md: it plans three improvements, only one of
which is implemented here, and its snippets no longer match the shipped API.
@swapnilashtekar
swapnilashtekar marked this pull request as ready for review September 12, 2026 06:16
Adds docs/faiss_search.md and wires it into the README's Step 4 and the
scripts reference table.

The thing the doc leads with is that --faiss on its own is slower than the
brute-force path it replaces. measure_similarity_v2.py ranks the whole corpus
per query so Recall@K works at any K, and an exhaustive ranking gives an IVF
index nothing to skip, so it does brute-force work plus indexing overhead.
Measured on random 512-d vectors: 1.46s vs 0.11s at N=2008, and 77.37s vs
4.28s at N=13503, the released demo corpus size. Capping the depth with
--faiss_top_k 100 turns that into 1.60s vs 4.28s.

Also records what has not been established: recall on real proteogram
embeddings. On random vectors, which have no cluster structure for the coarse
quantiser to exploit, Recall@20 is 0.26 at the default nprobe and 0.71 at
nlist//2. Real embeddings are clustered by fold and superfamily so this
probably understates it, but it should be measured against the brute-force
path before any --faiss numbers are used in a GTalign/USalign/Foldseek
comparison.
@swapnilashtekar
swapnilashtekar marked this pull request as draft September 12, 2026 06:36
The performance numbers in the previous commit were measured on random
gaussian vectors and were wrong in both directions. Replaced with a sweep over
the released 13,503-proteogram corpus embeddings.

Two errors. The claimed 2.7x speedup at top-100 compared FAISS against brute
force doing a full argsort of the entire NxN matrix, when only the top K was
needed; against a chunked argpartition top-K the same comparison is 1.1x. And
the recall figures came from random vectors, which understate real embeddings
by about 3x at the same scanned fraction (Recall@10 of 0.31 vs 0.87 at 8.7% of
the corpus) because the model clusters structures by fold and superfamily,
giving the coarse quantiser real structure to exploit.

On the real corpus the feature looks considerably better than the random-vector
numbers suggested. Brute-force top-10 is 1.82s; the index reaches 4.8x at
Recall@10 0.962 (nprobe=4, 3.4% of the corpus) and 12.6x at 0.841 (nprobe=1).
The shipped default nprobe = nlist//10 sits at the conservative end, 1.9x at
0.991.

What has not changed is that a full-corpus ranking is the wrong way to use an
ANN index: at nprobe = nlist it scans everything and is 5x slower than brute
force, so --faiss_top_k is required rather than optional.

Documents the random-vs-real comparison so the mistake is not repeated, and
notes that the sweep should be re-run on substantially larger corpora since
nlist = sqrt(N) shifts the useful nprobe range.
@swapnilashtekar
swapnilashtekar marked this pull request as ready for review September 12, 2026 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant