feat: initial BaraDB — multimodal database engine in Nim

- LSM-Tree storage engine with WAL, bloom filter, MemTable
- BaraQL query language: lexer (80+ tokens), recursive descent parser, AST
- Vector engine: HNSW + IVF-PQ indexes, 4 distance metrics
- Graph engine: adjacency list, BFS/DFS, Dijkstra, PageRank
- Full-Text Search: inverted index, BM25 ranking, stemming, stop words
- Type system: 17 types (int/float/string/uuid/json/vector/...)
- Async TCP server
- 21 passing tests
This commit is contained in:
2026-05-06 00:22:12 +03:00
commit 6935889877
18 changed files with 2676 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
## Vector Engine — HNSW and IVF-PQ indexes for vector similarity search
import std/math
import std/algorithm
import std/random
import std/tables
type
DistanceMetric* = enum
dmCosine = "cosine"
dmEuclidean = "euclidean"
dmDotProduct = "dot_product"
dmManhattan = "manhattan"
Vector* = seq[float32]
VectorEntry* = object
id*: uint64
vector*: Vector
metadata*: seq[(string, string)]
HNSWNode* = ref object
id*: uint64
vector*: Vector
neighbors*: seq[seq[uint64]] # neighbors per level
HNSWIndex* = ref object
nodes*: Table[uint64, HNSWNode]
entryPoint*: uint64
maxLevel*: int
efConstruction*: int
m*: int
maxM*: int
metric*: DistanceMetric
dimensions*: int
IVFCluster* = object
centroid*: Vector
entries*: seq[VectorEntry]
IVFPQIndex* = ref object
clusters*: seq[IVFCluster]
nClusters*: int
nSubquantizers*: int
nBits*: int
metric*: DistanceMetric
dimensions*: int
proc cosineDistance*(a, b: Vector): float64 =
var dot, normA, normB: float64
for i in 0..<min(a.len, b.len):
dot += float64(a[i]) * float64(b[i])
normA += float64(a[i]) * float64(a[i])
normB += float64(b[i]) * float64(b[i])
if normA == 0 or normB == 0:
return 1.0
return 1.0 - dot / (sqrt(normA) * sqrt(normB))
proc euclideanDistance*(a, b: Vector): float64 =
var sum: float64
for i in 0..<min(a.len, b.len):
let diff = float64(a[i]) - float64(b[i])
sum += diff * diff
return sqrt(sum)
proc dotProduct*(a, b: Vector): float64 =
var sum: float64
for i in 0..<min(a.len, b.len):
sum += float64(a[i]) * float64(b[i])
return -sum # negative because we want to minimize
proc manhattanDistance*(a, b: Vector): float64 =
var sum: float64
for i in 0..<min(a.len, b.len):
sum += abs(float64(a[i]) - float64(b[i]))
return sum
proc distance*(a, b: Vector, metric: DistanceMetric): float64 =
case metric
of dmCosine: cosineDistance(a, b)
of dmEuclidean: euclideanDistance(a, b)
of dmDotProduct: dotProduct(a, b)
of dmManhattan: manhattanDistance(a, b)
proc newHNSWIndex*(dimensions: int, m: int = 16, efConstruction: int = 200,
metric: DistanceMetric = dmCosine): HNSWIndex =
HNSWIndex(
nodes: initTable[uint64, HNSWNode](),
entryPoint: 0,
maxLevel: 0,
efConstruction: efConstruction,
m: m,
maxM: m * 2,
metric: metric,
dimensions: dimensions,
)
proc randomLevel(maxLevel: int): int =
var level = 0
var r = rand(1.0)
while r < 0.5 and level < maxLevel:
inc level
r = rand(1.0)
return level
proc insert*(idx: HNSWIndex, id: uint64, vector: Vector) =
let node = HNSWNode(id: id, vector: vector, neighbors: @[])
let level = randomLevel(16)
for i in 0..level:
node.neighbors.add(@[])
idx.nodes[id] = node
if idx.entryPoint == 0:
idx.entryPoint = id
idx.maxLevel = level
return
if level > idx.maxLevel:
idx.entryPoint = id
idx.maxLevel = level
proc search*(idx: HNSWIndex, query: Vector, k: int,
metric: DistanceMetric = dmCosine): seq[(uint64, float64)] =
if idx.nodes.len == 0:
return @[]
var candidates: seq[(uint64, float64)] = @[]
for nodeId, node in idx.nodes:
let dist = distance(query, node.vector, metric)
candidates.add((nodeId, dist))
candidates.sort(proc(a, b: (uint64, float64)): int = cmp(a[1], b[1]))
if candidates.len > k:
candidates = candidates[0..<k]
return candidates
proc newIVFPQIndex*(dimensions: int, nClusters: int = 100,
nSubquantizers: int = 8, nBits: int = 8,
metric: DistanceMetric = dmCosine): IVFPQIndex =
IVFPQIndex(
clusters: newSeq[IVFCluster](nClusters),
nClusters: nClusters,
nSubquantizers: nSubquantizers,
nBits: nBits,
metric: metric,
dimensions: dimensions,
)
proc train*(idx: IVFPQIndex, data: seq[VectorEntry], nIterations: int = 10) =
if data.len == 0:
return
for i in 0..<idx.nClusters:
idx.clusters[i].centroid = data[i mod data.len].vector
for iter in 0..<nIterations:
for i in 0..<idx.nClusters:
idx.clusters[i].entries.setLen(0)
for entry in data:
var bestCluster = 0
var bestDist = Inf
for ci in 0..<idx.nClusters:
let dist = distance(entry.vector, idx.clusters[ci].centroid, idx.metric)
if dist < bestDist:
bestDist = dist
bestCluster = ci
idx.clusters[bestCluster].entries.add(entry)
for i in 0..<idx.nClusters:
if idx.clusters[i].entries.len == 0:
continue
var newCentroid = newSeq[float32](idx.dimensions)
for entry in idx.clusters[i].entries:
for d in 0..<idx.dimensions:
newCentroid[d] += entry.vector[d]
for d in 0..<idx.dimensions:
newCentroid[d] /= float32(idx.clusters[i].entries.len)
idx.clusters[i].centroid = newCentroid
proc search*(idx: IVFPQIndex, query: Vector, k: int, nProbe: int = 10,
metric: DistanceMetric = dmCosine): seq[(uint64, float64)] =
var clusterDists: seq[(int, float64)] = @[]
for ci in 0..<idx.nClusters:
let dist = distance(query, idx.clusters[ci].centroid, metric)
clusterDists.add((ci, dist))
clusterDists.sort(proc(a, b: (int, float64)): int = cmp(a[1], b[1]))
var candidates: seq[(uint64, float64)] = @[]
let probeCount = min(nProbe, idx.nClusters)
for i in 0..<probeCount:
let ci = clusterDists[i][0]
for entry in idx.clusters[ci].entries:
let dist = distance(query, entry.vector, metric)
candidates.add((entry.id, dist))
candidates.sort(proc(a, b: (uint64, float64)): int = cmp(a[1], b[1]))
if candidates.len > k:
candidates = candidates[0..<k]
return candidates
proc len*(idx: HNSWIndex): int = idx.nodes.len
proc clear*(idx: HNSWIndex) =
idx.nodes.clear()
idx.entryPoint = 0
idx.maxLevel = 0