|
| 1 | +# the overlay graph problem |
| 2 | +In this document I've compiled all relevant information about vectorlink in order to talk about the overlay graph problem. |
| 3 | + |
| 4 | +The overlay graph problem is this: |
| 5 | +How do we create a good overlay graph to augment an NSW (navigable small world) graph with long-distance links? |
| 6 | + |
| 7 | +This document will first briefly explain NSW, what we use it for, and what operations can be applied to it. It'll then go into the limitations and explain the need for an overlay graph. |
| 8 | + |
| 9 | +## NSW and relevant operators |
| 10 | +We have the following graph data structure `G: (V, d, N)`, where |
| 11 | +- `V`: a set of nodes |
| 12 | +- `d`: a distance function, which given two nodes calculates a nonnegative real number. |
| 13 | +- `N`: a set of neighborhoods, which are `(V, [V;NSIZE])` tuples, listing `NSIZE` (approximate) closest neighbors for each node. |
| 14 | + |
| 15 | +We have a graph constructor like so: |
| 16 | +`C: V -> d -> N` |
| 17 | + |
| 18 | +In other words, given a set of nodes and a distance function over those nodes, generate a set of neighborhoods. |
| 19 | + |
| 20 | +We would like one neighborhood for each node `v` in V, which contains the `NSIZE` closest elements in `V` for that node (excluding itself), sorted by distance. Doing this perfectly however is prohibitively expensive. Instead, any practical implementation of `C` will use some sort of approximate nearest neighbor algorithm. |
| 21 | + |
| 22 | +For this problem, we do not have to worry too much about the implementation details of `C`, and we can just assume it produces a graph where it is statistically likely that close elements in `V` are connected. |
| 23 | + |
| 24 | +### Application |
| 25 | +The purpose of this graph is to support two operations, namely search and nearest-neighbor. |
| 26 | + |
| 27 | +#### Search |
| 28 | +`G->V->->[..initial search queue..]->count->[V;count]` |
| 29 | + |
| 30 | +In other words, given a graph, a query vector, an initial search queue, and a desired amount of results, produce the `count` closest matches to this query vector according to the graph's distance function. |
| 31 | + |
| 32 | +This is implemented by traversing the neighborhoods. The pseudocode is roughly as follows |
| 33 | +``` |
| 34 | +search_queue = initial_search_queue; // this is either coming in from a search in a higher layer (supernodes), or it is initialized to a single initial node |
| 35 | +while ..search queue changed since last iteration.. // keep going until we hit a fixpoint |
| 36 | + node = ..closest unvisited node in the queue.. |
| 37 | + neighbors = neighbors_for(node) |
| 38 | + merged = merge_by_distance(search_queue, neighbors, query_vector, d) |
| 39 | + search_queue = truncate(merged, MAX_QUEUE_LEN) |
| 40 | +
|
| 41 | +return search_queue |
| 42 | +``` |
| 43 | + |
| 44 | +Here, `neighbors_for` picks the appropriate neighbors list from `N`. |
| 45 | + |
| 46 | +`merge_by_distance` is a function that calculates the distance from the query vector for each element in two input lists, and then produces a merged list of results, sorted by that distance. |
| 47 | + |
| 48 | +`truncate` truncates the list to a maximum queue length. |
| 49 | + |
| 50 | +In english, this will keep around a list of match candidates, and improve on those candidates by merging in the neighborhoods of these candidates. Each iteration should either get us better matches, or do nothing, at which point we can return the candidates. |
| 51 | + |
| 52 | +#### Nearest Neighbors |
| 53 | +Given a well-constructed graph, a set of nearest neighbors for a node can easily be extracted by taking the list of neighbors, and repeatedly merging in its neighbors. |
| 54 | + |
| 55 | +### Optimization |
| 56 | +A graph can be improved for a particular vector using the following algorithm: |
| 57 | +``` |
| 58 | +ideal_neighbors = search(self) |
| 59 | +for n in ideal_neighbors: |
| 60 | + if ..self is a better candidate in n's neighborhood: |
| 61 | + ..insert self into neighborhood of n, evicting a more distant entry |
| 62 | +``` |
| 63 | + |
| 64 | +The entire graph can be improved in this way by just looping over all nodes and performing this operation. |
| 65 | + |
| 66 | +The way we actually generate graph is by creating a best-effort graph as a first pass, then iteratively optimizing that until no more significant improvements are made. |
| 67 | + |
| 68 | +### Hierarchical NSW |
| 69 | +As described above, search takes in an initial list of nodes to initiate the search with. For a small well-connected graph any random selection (or even a static selection, like the first node in the graph) will do, but beyond a certain size this is no longer possible, because the number of hops (neighborhood traversals) becomes troo large. |
| 70 | + |
| 71 | +HNSW (Hierarchical Navigable Small World) aims to solve this by introducing supernode graphs. We take a random selection of nodes from our graph that is an order of magnitude smaller, then generate a new NSW with just those nodes. This process can be repeated until we end up with a top-level graph that is easily searchable. |
| 72 | + |
| 73 | +Search is then implemented by first searching in the top layer, using the search result to initiate the search in the layer below, and so on until the bottom layer is reached. |
| 74 | + |
| 75 | +Which nodes to select as supernodes is a bit of an open question. Right now, we're just doing a random selection, but we're also experimenting with promotions and demotions based on measured connectivity. |
| 76 | + |
| 77 | +## The problem |
| 78 | +Our graphs are having recall issues. No matter how much we optimize (or maybe because of how we optimize) we end up with local minima, very tight neighborhoods of closely connected things, which then do not connect to anything a bit further out. |
| 79 | + |
| 80 | +The recall issues compound. In an HNSW with several layers, unreachability on one layer will propagate to the layers below. While a perfect recall in an approximate data structure is not necessarily achievable, we would like an approach that could at least get us closer, if we were willing to just throw more computational resources at the optimization. Ideally of course, we converge quickly to a good solution. |
| 81 | + |
| 82 | +## A potential solution: an overlay graph |
| 83 | +The way to get out of local minima is to establish additional graph connections that lead out of the local minimal group. Different approaches can be imagined. |
| 84 | + |
| 85 | +### Ideal links |
| 86 | +There probably exists an algorithm that generates an ideal overlay graph for a particular graph. Unfortunately, no such algorithm is known to us, not even an approximate version. |
| 87 | + |
| 88 | +### Random links |
| 89 | +For each node, we can generate an additional number of virtual connections by randomly selecting nodes to connect to. Given a static seed to the pseudo-random number generator (for example, a combination of layer id, node id and a salt), we could always generate the same number of random connections, avoiding the need of actually having to maintain this additional graph. |
| 90 | + |
| 91 | +### Circulant graphs |
| 92 | +Given a list of generator numbers `C`, for each `c in C`, we can imagine that each node `v` (here considered to be a nonnegative integer adressing an element in `V`) is additionally connected to `v+c` and `v-c` (mod |V|). |
| 93 | + |
| 94 | +The choice of `C` is an interesting parameter. Right now we're just using the lowest 12 primes. |
| 95 | + |
| 96 | +## Early outcomes |
| 97 | +We've tested our graphs with both additional random links, and with an overlay generated from circulant graphs, and found that both improve our recall and convergence rate, but cirulant graphs were outperforming random links. |
| 98 | + |
| 99 | +Since our nodes are uncorrelated, a circulant graph overlay should effectively also establishes random links, but distributed in a far more grid-like fashion. |
| 100 | + |
| 101 | +It is interesting that this works at all. The extra links aren't likely to be 'good', meaning, they won't be close links. They're also not followed often, as the search algorithm will first consider all 'proper', low distance neighbors, which in many cases will lead to evictions of a lot of these extra links. Nevertheless, this appears to work. |
0 commit comments