Как создать эластичную векторную базу данных с согласованным хешированием, шардингом и визуализацией в реальном времени для систем RAG

В этом руководстве мы создадим симулятор эластичной векторной базы данных, который имитирует распределение вложений в современных системах RAG по узлам распределённого хранения. Мы реализуем согласованное хеширование с виртуальными узлами, чтобы обеспечить сбалансированное размещение и минимальное перетасовка данных при масштабировании системы. Мы визуализируем кольцо хеширования в режиме реального времени и интерактивно добавляем или удаляем узлы, чтобы наблюдать, как перемещается лишь малая часть вложений.

Установка среды выполнения и необходимых библиотек

Мы настроим среду выполнения и установим необходимые библиотеки, необходимые для визуализации и интерактивности. Мы импортируем все основные зависимости Python, численные и графические в одном месте, чтобы сохранить записную книжку автономной. Мы обеспечим бесперебойную работу руководства в Google Colab без внешней настройки.

«`python
import hashlib
import bisect
import random
from dataclasses import dataclass
from typing import Dict, List, Optional

import numpy as np
import networkx as nx
import matplotlib.pyplot as plt

from IPython.display import display, clear_output
import ipywidgets as widgets
«`

Реализация согласованного хеширования

Мы реализуем согласованное хеширование и определим, как узлы хранения представлены в системе. Мы вводим виртуальные узлы для улучшения балансировки нагрузки и более равномерного распределения вложений. Мы сопоставляем ключи с узлами, используя детерминированную хеш-функцию, которая сохраняет стабильность при масштабировании.

«`python
def u64hash(s: str) -> int:
h = hashlib.sha256(s.encode(«utf-8»)).digest()[:8]
return int.from_bytes(h, byteorder=»big», signed=False)

@dataclass(frozen=True)
class StorageNode:
node_id: str

class ConsistentHashRing:
def init(self, vnodespernode: int = 80):
self.vnodespernode = int(vnodespernode)
self.ring_keys: List[int] = []
self.ring_map: Dict[int, str] = {}
self.nodes: Dict[str, StorageNode] = {}

def vnodekey(self, node_id: str, v: int) -> int:
return u64hash(f»node:{node_id}#vnode:{v}»)

def add_node(self, node: StorageNode) -> None:
if node.node_id in self.nodes:
return
self.nodes[node.node_id] = node
for v in range(self.vnodespernode):
k = self.vnodekey(node.node_id, v)
if k in self.ring_map:
k = u64hash(f»node:{node.node_id}#vnode:{v}#salt:{random.random()}»)
bisect.insort(self.ring_keys, k)
self.ringmap[k] = node.nodeid

def removenode(self, nodeid: str) -> None:
if node_id not in self.nodes:
return
del self.nodes[node_id]
toremove = [k for k, nid in self.ringmap.items() if nid == node_id]
for k in to_remove:
del self.ring_map[k]
self.ringkeys = sorted(self.ringmap.keys())

def get_node(self, key: str) -> Optional[str]:
if not self.ring_keys:
return None
hk = u64hash(f»key:{key}»)
idx = bisect.bisectleft(self.ringkeys, hk)
if idx == len(self.ring_keys):
idx = 0
return self.ringmap[self.ringkeys[idx]]

def snapshot(self) -> Dict[str, int]:
counts = {nid: 0 for nid in self.nodes}
for , nid in self.ringmap.items():
counts[nid] = counts.get(nid, 0) + 1
return dict(sorted(counts.items()))
«`

Визуализация согласованного хеширования

Мы визуализируем согласованное хеширование в виде кругового графа. Мы отображаем статистику шардов и виртуальных узлов непосредственно на каждом узле. Мы динамически обновляем визуализацию, чтобы отразить изменения в состоянии системы.

«`python
def draw_ring(ring: ConsistentHashRing, dist: Dict[str, int], title: str):
node_ids = sorted(ring.nodes.keys())
plt.figure(figsize=(8, 8))
ax = plt.gca()
ax.set_title(title)

if not node_ids:
plt.text(0.5, 0.5, «Ring is empty», ha=»center», va=»center»)
plt.axis(«off»)
plt.show()
return

G = nx.Graph()
for nid in node_ids:
G.add_node(nid)
for i in range(len(node_ids)):
G.addedge(nodeids[i], nodeids[(i + 1) % len(nodeids)])

pos = nx.circular_layout(G)
vnode_counts = ring.snapshot()
labels = {
nid: f»{nid}\nkeys={dist.get(nid,0)}\nvnodes={vnode_counts.get(nid,0)}»
for nid in node_ids
}

nx.drawnetworkxedges(G, pos, alpha=0.4, width=2)
nx.drawnetworkxnodes(G, pos, node_size=2200)
nx.drawnetworkxlabels(G, pos, labels=labels, font_size=9)
plt.axis(«off»)
plt.show()
«`

Взаимодействие с системой

Мы объединяем интерактивность, которая позволяет нам добавлять, удалять и перебалансировать узлы в режиме реального времени. Мы наблюдаем, как распределение шардов адаптируется сразу после каждого действия. Мы используем эти взаимодействия для эмпирической проверки минимального перемещения данных в эластичных системах.

«`python
ring = ConsistentHashRing(vnodespernode=80)
sim = VectorDBSimulator(ring)
sim.add_vectors(6000)

node_name = widgets.Text(value=»nodeA», description=»Node ID:»)
addbtn = widgets.Button(description=»Add node», buttonstyle=»success»)
rmbtn = widgets.Button(description=»Remove node», buttonstyle=»danger»)
vnodes_slider = widgets.IntSlider(value=80, min=20, max=300, step=20, description=»VNodes/node»)
regenbtn = widgets.Button(description=»Rebuild ring», buttonstyle=»warning»)
status = widgets.Output()

def render(msg: str = «»):
clear_output(wait=True)
display(widgets.HBox([nodename, addbtn, rm_btn]))
display(widgets.HBox([vnodesslider, regenbtn]))
dist = sim.distribution()
title = f»Consistent Hash Ring | nodes={len(ring.nodes)} | vectors={len(sim.vectors)}»
if msg:
title += f»\n{msg}»
draw_ring(ring, dist, title)
with status:
status.clear_output()
print(«Shard distribution:», dist)
display(status)

def onadd():
before = sim.shard_map()
ring.addnode(StorageNode(nodename.value.strip() or f»node{len(ring.nodes)+1}»))
after = sim.shard_map()
moved = sim.movement_fraction(before, after)
render(f»Added node | moved={moved:.3f} (~{moved*100:.1f}%)»)

def onremove():
before = sim.shard_map()
ring.removenode(nodename.value.strip())
after = sim.shard_map()
moved = sim.movement_fraction(before, after)
render(f»Removed node | moved={moved:.3f} (~{moved*100:.1f}%)»)

def onregen():
ids = list(ring.nodes.keys())
newring = ConsistentHashRing(vnodespernode=int(vnodesslider.value))
for nid in ids:
newring.addnode(StorageNode(nid))
sim.ring = new_ring
globals()[«ring»] = new_ring
render(f»Rebuilt ring with vnodespernode={vnodes_slider.value}»)

addbtn.onclick(on_add)
rmbtn.onclick(on_remove)
regenbtn.onclick(on_regen)

render(«Add or remove nodes to observe data movement»)
«`

В заключение мы продемонстрировали, как согласованное хеширование обеспечивает масштабируемое хранение векторов, минимизируя перемещение данных во время изменений топологии. Мы наблюдали, что добавление или удаление узлов влияет только на ограниченное подмножество вложений, подтверждая основной принцип проектирования эластичных распределённых баз данных. Мы использовали визуализацию и взаимодействие, чтобы сделать поведение шардинга осязаемым, а не абстрактным. Мы закончили с чёткой ментальной моделью того, как производственные системы RAG поддерживают стабильность при динамическом масштабировании.

1. Какие основные компоненты включает в себя реализация согласованного хеширования в статье?

В статье реализация согласованного хеширования включает в себя создание класса `ConsistentHashRing`, который инициализируется с определённым количеством виртуальных узлов на узел (`vnodespernode`). В классе определены методы для добавления и удаления узлов, а также для получения узла, ответственного за конкретный ключ.

2. Какие библиотеки Python используются для визуализации согласованного хеширования?

Для визуализации согласованного хеширования используются следующие библиотеки Python: `numpy` для работы с массивами, `networkx` для создания и визуализации графов, `matplotlib.pyplot` для построения графиков и `ipywidgets` для интерактивности.

3. Как в статье обеспечивается балансировка нагрузки при распределении вложений по узлам?

В статье балансировка нагрузки обеспечивается за счёт использования виртуальных узлов. Каждый физический узел представлен несколькими виртуальными узлами, что позволяет более равномерно распределить вложения по всем узлам системы. Это минимизирует перетасовку данных при масштабировании системы.

4. Какие методы используются для добавления и удаления узлов в системе?

Для добавления узла в систему используется метод `addnode` класса `ConsistentHashRing`. Этот метод добавляет новый узел в систему и перераспределяет вложения так, чтобы минимизировать перемещение данных. Для удаления узла используется метод `removenode`, который удаляет указанный узел из системы и обновляет распределение вложений.

5. Как в статье визуализируется распределение шардов по узлам?

В статье распределение шардов по узлам визуализируется с помощью кругового графа. Каждый узел системы представлен в виде точки на круге, а связи между узлами показывают, как шарды распределены между ними. Это позволяет наглядно увидеть, как добавление или удаление узла влияет на распределение шардов.

Источник