Add mxbai dense + bm25 hybrid search to the backend - #65
Conversation
kanungle
left a comment
There was a problem hiding this comment.
these results don't seem that good. Ranking feels weak. Can we also show more than 10 results?
| self.highlight_field = TEXT_FIELD_NAME | ||
| self.collection_name = collection_name | ||
| self.qdrant_client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY, prefer_grpc=True) | ||
| self.qdrant_client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY) |
|
Hey Neil, good catch. Ran a bunch of tests to pin down where the ranking was falling apart. It wasn't the index, that's returning the right matches. It was the embedding model (all-MiniLM), too lightweight to order 3M results well. So I kept it but added a re-ranker on top (ms-marco-MiniLM-L-6-v2) that re-scores the top matches. Threw in a small stats line too so you can see the models and latency on each search, and bumped results from 10 to 20. |
kanungle
left a comment
There was a problem hiding this comment.
Please uplift this demo to include:
- a higher dimension embedding model
- no reranker
- hybrid search option (maybe with scoreboosting)
Add context in the UI that this is a 'simple' comparison of different vector search methods
|
The model is now mxbai at 1024D, the reranker is gone, and hybrid is in as a toggle. Re-embedded all 3M, and in a before/after the hybrid wins 15 of 18 queries and runs faster (~210ms). |
kanungle
left a comment
There was a problem hiding this comment.
demo looks good but there are merge conflicts
Semantic and keyword now run on the 1024d mxbai model via Cloud inference, and a hybrid mode fuses dense + keyword with RRF. Keeps the existing neural= query param working (neural=false -> keyword, neural=true -> hybrid) so the current frontend needs no change; explicit mode= is also supported. Bumps qdrant-client for Cloud inference. Frontend untouched.
3826b6c to
4b081fe
Compare
|
Sorry about the conflicts. Fixed now, it's backend-only so it drops cleanly onto the current frontend. mxbai + hybrid, no reranker, and the existing neural= param still works, so nothing on the frontend needs to change. |
|
Heads up, this needs another pass before it lands. I was clicking around the live preview and noticed hybrid search latency was high for a dataset this size. That sent me into the code to find out why. Full write-up below, with additional findings from claude code as well: On the latency, since that is what started this. Why it matters: this is the startup search demo. Slow first impressions are the one thing it cannot afford. Suggested fix: default The structural problem underneath all of this. My understanding is that you generated the embeddings locally and uploaded them to Qdrant Cloud, and the app now queries that collection. Mixing the two is fine in itself, both sides run the same mxbai weights with the same pooling and normalization, so the vectors line up. The problem is that the script that built Must fix (blocks shipping)
Should fix
Short version: the read path was rewritten and the write path stayed on your laptop. Get the real ingest script into |
init_collection_startups now builds the collection the search path expects: a named dense (mxbai) vector, a sparse keyword vector with IDF, and the text index, all from the demo data, so the collection is reproducible instead of assumed. neural_searcher prepends the mxbai query prompt and falls back to dense-only when a query has no keyword tokens, and it reports the mode that actually ran. service maps the legacy neural flag back to semantic, presents the payload under the keys the frontend reads, and returns a generic error to the client while logging the detail on the server. config defaults cloud inference on and parses the flag even when the host keeps the surrounding quotes. sparse keeps single-character and symbol tokens like c and c++ and uses the full 32-bit index range. Dockerfile drops the unneeded client bump.
|
Fixed the issues from your review, main one being the reproducible ingest script. |
Both the dense (mxbai) and sparse (bm25) vectors are now embedded server-side via Cloud inference, so the ingest and query sides use the identical models by construction. This removes the hand-rolled sparse encoder and its consistency caveats. init renames the payload once to the schema the frontend reads (document, logo_url, homepage_url), and the search path returns it directly with no per-request mapping. bm25 also handles degenerate queries (stopword-only, punctuation) without the previous empty-vector crash.
1.14.2 cannot talk to the current Qdrant Cloud server (1.19) for inference: it fails parsing the inference response, so bm25/mxbai Document queries error. Pin the client to 1.19.0 (matches the server) and regenerate poetry.lock so the container is reproducible without the pip bump. Drop the fastembed extra: the app embeds via Cloud inference and no longer imports fastembed. Widen the Python constraint so the lock resolves on current interpreters; the image still runs 3.11.
|
Oh okay, I just finished everything. Sorry it took me a bit, I got held up on two things. The mxbai prefix turned out to be a no-op on cloud, since Qdrant already applies the query prompt server-side (measured it, recall came back identical). And the locked client 1.14.2 can't actually talk to the current server for inference, it fails parsing the response, so that one needed a real bump. Here's what I did:
Score also carries a type now, cosine vs rrf, so the UI knows the scale. |
|
@jkupchanko where's the link to the demo? I can't see it in the PR anymore |
kanungle
left a comment
There was a problem hiding this comment.
This looks good. Can we remove the latency indicator completely since this isn't a performance demo and may be critiqued?
|
Ah yeah, here it is: https://qdrant-startup-search-production.up.railway.app That's the live one, running the 3M hybrid setup from this PR. Toggle between semantic, keyword, and hybrid up top. |
|
And here's the main public one if that's easier: https://qdrant-demo-qvpw.vercel.app/ Same backend, just the hosted frontend. |
Reworks the search backend to dense + hybrid retrieval and makes the collection reproducible from the repo.
What changed
Qdrant/bm25sparse vector; hybrid fuses dense + sparse with RRF. No reranker.init_collection_startups.pybuilds the collection the search path expects: nameddenseandsparsevectors,modifier=IDFon the sparse index, and the text index ondocument. Renames the payload once at ingest todocument/logo_url/homepage_url.API
GET /api/search?q=&mode=semantic|keyword|hybrid, hybrid by default.neuralflag still works.neural=trueis semantic,neural=falseis keyword.Notes
qdrant-clientbumped to 1.19.0 with a regenerated lock. 1.14.2 can't talk to the current server for inference. Dropped thefastembedextra since the app embeds via Cloud inference.Reproduce