* remover open source title, added docker icon on hero banner * buttons, fixes for mobile view * code widget fix * removed copying of text * changed size and positioning of the docker icon, added a link to dockerhub, some styles for mobiles * hid an installation section and an imapge on hero-banner for small mobile devices * exchanged images * fixed svg images * tutorial button => demos button, svg optimisation * removed background * new colors, separated file for the header * cleaned some redunant styles for the header * more changes according to the new design * fixed buttons animation, fixed the header on all pages for desktop * mobile view for the header and the top banner * changed the features section according to new design * moved styles for the features section to the separated file, some fixes for mobile view * reorganised lines in features-section.scss * changes slides on the main page - desctop view * added a file * autoscroll, some fixes * added an autoplay and a pause on mouseenter to the carousel on the home page * more styles * styles for articles section and subscribtion section * styles for footer * styles * footer background * fixes * more fixes * fixes for mobile devices - small sizes * fixes for mobile devices - medium sizes * update images * some fixes for mobiles, changed stack section * changed the second header (aka section title) according to new design * WIP: added some webpack configuration for updating a way of vendors scripts usage, added new carousel * replaced old slider to splidejs * fix for the slider * articles list page * article - single page * refactored main.scss, removed unused code from it and moved sections styles to separated files * renamed and added some files, added fixes to breadcrumbs * small changes in styles and layouts * vertical sizes * changes of styles and structure in articles and blog related files * added mobile styles for articles and related pages, added sass function for converting px to rem * temp picture * changes in page title * changed colors on old pages to the new design schema * fixes * deleted a file qdrant.css, moved styles to main.scss * installed qdrant-page-search, for now without an actual api url * added an actual api url * WIP replacing owl carousel with splide on the surveys page * updated page search, changed a way it's used * fixes for footer * changed common auto-container width, article font-size, made some fixes * docs auto-sync * narrow articles * re-imported blog styles, because they are used in other places * updated page-search * optimized css loading * replaced some images with webp format * undo main img, changes in image usage * fixes * fixed slider cursor, logos in the stack section, cards on solutions page, prices page * changed font-size of a form title on the subscription page * optimization * wip: added bash script for article preview images processing * bash script for article preview images processing * updated images for articles * two article card in the row, without buttons (#85) * fixes for old pages * fixes for benchmarks pages * added some info to the readme * upd case studies * changed form placeholder color and margin between buttons in the header * short solution texts + link to benchmarks on main * pricing page fixes * short text in slider * article cards borders, slider height, contact us * blockquote * move external articles to the landing * more narrow buttons in the header, no buttons on the solutions page * demo page fixes, removed target blank from the benchmarks link on the main page * slower speed of the carousel on the main page * content changes + seach upd * changed title, readme * added translate3d(0, 0, 0) to buttons for safari animations * fixing buttons in safari (maybe) * fixing buttons in safari (maybe) * fixing buttons in safari * fixes for mobiles * fixes * docs auto-sync * neural -> vector Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com> Co-authored-by: qdrant <qdrant@users.noreply.github.com>
7.7 KiB
title, short_description, description, social_preview_image, preview_dir, small_preview_image, weight, date, author, author_link
| title | short_description | description | social_preview_image | preview_dir | small_preview_image | weight | date | author | author_link |
|---|---|---|---|---|---|---|---|---|---|
| Filtrable HNSW | How to make ANN search with custom filtering? | How to make ANN search with custom filtering? Search in selected subsets without loosing the results. | /articles_data/filtrable-hnsw/social_preview.jpg | /articles_data/filtrable-hnsw/preview | /articles_data/filtrable-hnsw/global-network.svg | 60 | 2019-11-24T22:44:08+03:00 | Andrei Vasnetsov | https://blog.vasnetsov.com/ |
If you need to find some similar objects in vector space, provided e.g. by embeddings or matching NN, you can choose among a variety of libraries: Annoy, FAISS or NMSLib. All of them will give you a fast approximate neighbors search within almost any space.
But what if you need to introduce some constraints in your search? For example, you want search only for products in some category or select the most similar customer of a particular brand. I did not find any simple solutions for this. There are several discussions like this, but they only suggest to iterate over top search results and apply conditions consequently after the search.
Let's see if we could somehow modify any of ANN algorithms to be able to apply constrains during the search itself.
Annoy builds tree index over random projections. Tree index implies that we will meet same problem that appears in relational databases: if field indexes were built independently, then it is possible to use only one of them at a time. Since nobody solved this problem before, it seems that there is no easy approach.
There is another algorithm which shows top results on the benchmark. It is called HNSW which stands for Hierarchical Navigable Small World.
The original paper is well written and very easy to read, so I will only give the main idea here.
We need to build a navigation graph among all indexed points so that the greedy search on this graph will lead us to the nearest point.
This graph is constructed by sequentially adding points that are connected by a fixed number of edges to previously added points.
In the resulting graph, the number of edges at each point does not exceed a given threshold m and always contains the nearest considered points.
How can we modify it?
What if we simply apply the filter criteria to the nodes of this graph and use in the greedy search only those that meet these criteria? It turns out that even with this naive modification algorithm can cover some use cases.
One such case is if your criteria do not correlate with vector semantics. For example, you use a vector search for clothing names and want to filter out some sizes. In this case, the nodes will be uniformly filtered out from the entire cluster structure. Therefore, the theoretical conclusions obtained in the Percolation theory become applicable:
Percolation is related to the robustness of the graph (called also network). Given a random graph of
nnodes and an average degree\langle k\rangle. Next we remove randomly a fraction1-pof nodes and leave only a fractionp. There exists a critical percolation thresholdpc = \frac{1}{\langle k\rangle}below which the network becomes fragmented while abovepca giant connected component exists.
This statement also confirmed by experiments:
{{< figure src=/articles_data/filtrable-hnsw/exp_connectivity_glove_m0.png caption="Dependency of connectivity to the number of edges" >}}
{{< figure src=/articles_data/filtrable-hnsw/exp_connectivity_glove_num_elements.png caption="Dependency of connectivity to the number of point (no dependency)." >}}
There is a clear threshold when the search begins to fail.
This threshold is due to the decomposition of the graph into small connected components.
The graphs also show that this threshold can be shifted by increasing the m parameter of the algorithm, which is responsible for the degree of nodes.
Let's consider some other filtering conditions we might want to apply in the search:
- Categorical filtering
- Select only points in a specific category
- Select points which belong to a specific subset of categories
- Select points with a specific set of labels
- Numerical range
- Selection within some geographical region
In the first case, we can guarantee that the HNSW graph will be connected simply by creating additional edges inside each category separately, using the same graph construction algorithm, and then combining them into the original graph. In this case, the total number of edges will increase by no more than 2 times, regardless of the number of categories.
Second case is a little harder. A connection may be lost between two categories if they lie in different clusters.
The idea here is to build same navigation graph but not between nodes, but between categories. Distance between two categories might be defined as distance between category entry points (or, for precision, as the average distance between a random sample). Now we can estimate expected graph connectivity by number of excluded categories, not nodes. It still does not guarantee that two random categories will be connected, but allows us to switch to multiple searches in each category if connectivity threshold passed. In some cases, multiple searches can be even faster if you take advantage of parallel processing.
{{< figure src=/articles_data/filtrable-hnsw/exp_random_groups.png caption="Dependency of connectivity to the random categories included in search" >}}
Third case might be resolved in a same way it is resolved in classical databases. Depending on labeled subsets size ration we can go for one of the following scenarios:
- if at least one subset is small: perform search over the label containing smallest subset and then filter points consequently.
- if large subsets give large intersection: perform regular search with constraints expecting that intersection size fits connectivity threshold.
- if large subsets give small intersection: perform linear search over intersection expecting that it is small enough to fit a time frame.
Numerical range case can be reduces to the previous one if we split numerical range into a buckets containing equal amount of points. Next we also connect neighboring buckets to achieve graph connectivity. We still need to filter some results which presence in border buckets but do not fulfill actual constraints, but their amount might be regulated by the size of buckets.
Geographical case is a lot like a numerical one. Usual geographical search involves geohash, which matches any geo-point to a fixes length identifier.
We can use this identifiers as categories and additionally make connections between neighboring geohashes. It will ensure that any selected geographical region will also contain connected HNSW graph.
Conclusion
It is possible to enchant HNSW algorithm so that it will support filtering points in a first search phase. Filtering can be carried out on the basis of belonging to categories, which in turn is generalized to such popular cases as numerical ranges and geo.
Experiments were carried by modification python implementation of the algorithm, but real production systems require much faster version, like NMSLib.


