perf: optimize FTS and HNSW engines + real PostgreSQL benchmarks
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled

FTS Engine (src/barabadb/fts/engine.nim):
- Fix bm25Score doing O(n) linear scan per document
- Cache IDF per token instead of recomputing for each doc
- Use entry.termFreq directly instead of searching postings again
- Result: FTS search +438% (249 -> 1360 queries/s)

HNSW Vector Engine (src/barabadb/vector/engine.nim):
- Optimize distance functions with float32 + 4x loop unrolling
- Rewrite searchLayer: swap+pop instead of O(n) del, track worst-nearest
  instead of sorting nearest on every iteration
- Result: HNSW insert +117% (245 -> 543 ops/s), search 2.2x faster

Benchmarks:
- Add real PostgreSQL comparison script (benchmarks/pg_bench.py)
- Add report generator (benchmarks/generate_report.py)
- Fix compare.nim cpuTime() bug (was dividing by 1M incorrectly)
- Add nimble tasks: bench_pg, bench_report

Docs:
- Update README.md and docs/en/performance.md with real measured numbers
- Add benchmarks/REAL_COMPARISON.md

Version bump: 1.1.7 -> 1.1.8
This commit is contained in:
2026-05-29 17:11:22 +03:00
parent 42043f3946
commit 965ed2f675
10 changed files with 620 additions and 79 deletions
+84 -26
View File
@@ -53,32 +53,62 @@ type
NodeDist = tuple[dist: float64, id: uint64]
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))
var dot, normA, normB: float32
let len = min(a.len, b.len)
var i = 0
while i + 3 < len:
dot += a[i]*b[i] + a[i+1]*b[i+1] + a[i+2]*b[i+2] + a[i+3]*b[i+3]
normA += a[i]*a[i] + a[i+1]*a[i+1] + a[i+2]*a[i+2] + a[i+3]*a[i+3]
normB += b[i]*b[i] + b[i+1]*b[i+1] + b[i+2]*b[i+2] + b[i+3]*b[i+3]
i += 4
while i < len:
dot += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
inc i
let denom = sqrt(normA) * sqrt(normB)
if denom == 0: return 1.0
return 1.0 - float64(dot) / float64(denom)
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
var sum: float32
let len = min(a.len, b.len)
var i = 0
while i + 3 < len:
let d0 = a[i] - b[i]
let d1 = a[i+1] - b[i+1]
let d2 = a[i+2] - b[i+2]
let d3 = a[i+3] - b[i+3]
sum += d0*d0 + d1*d1 + d2*d2 + d3*d3
i += 4
while i < len:
let d = a[i] - b[i]
sum += d * d
inc i
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
var sum: float32
let len = min(a.len, b.len)
var i = 0
while i + 3 < len:
sum += a[i]*b[i] + a[i+1]*b[i+1] + a[i+2]*b[i+2] + a[i+3]*b[i+3]
i += 4
while i < len:
sum += a[i] * b[i]
inc i
return -float64(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]))
var sum: float32
let len = min(a.len, b.len)
var i = 0
while i + 3 < len:
sum += abs(a[i]-b[i]) + abs(a[i+1]-b[i+1]) + abs(a[i+2]-b[i+2]) + abs(a[i+3]-b[i+3])
i += 4
while i < len:
sum += abs(a[i] - b[i])
inc i
return sum
proc distance*(a, b: Vector, metric: DistanceMetric): float64 =
@@ -126,6 +156,7 @@ proc searchLayer(idx: HNSWIndex, entryId: uint64, query: Vector, ef: int,
var visited = initHashSet[uint64]()
var candidates: seq[NodeDist] = @[]
var nearest: seq[NodeDist] = @[]
var worstNearestDist: float64 = Inf
let entryDist = distance(query, idx.nodes[entryId].vector, metric)
candidates.add((entryDist, entryId))
@@ -133,16 +164,19 @@ proc searchLayer(idx: HNSWIndex, entryId: uint64, query: Vector, ef: int,
visited.incl(entryId)
while candidates.len > 0:
# Pop closest candidate
# Pop closest candidate (linear scan — kept simple; could be heap)
var bestIdx = 0
var bestDist = candidates[0].dist
for i in 1..<candidates.len:
if candidates[i].dist < candidates[bestIdx].dist:
if candidates[i].dist < bestDist:
bestDist = candidates[i].dist
bestIdx = i
let current = candidates[bestIdx]
candidates.del(bestIdx)
candidates[bestIdx] = candidates[^1]
candidates.setLen(candidates.len - 1)
# Stop if current is worse than the ef-th nearest
if nearest.len >= ef and current.dist > nearest[^1].dist:
if nearest.len >= ef and current.dist > worstNearestDist:
break
# Explore neighbors at this level
@@ -152,12 +186,36 @@ proc searchLayer(idx: HNSWIndex, entryId: uint64, query: Vector, ef: int,
if neighborId notin visited:
visited.incl(neighborId)
let dist = distance(query, idx.nodes[neighborId].vector, metric)
candidates.add((dist, neighborId))
# Fast path: only add to candidates if it could improve nearest
if nearest.len < ef or dist < worstNearestDist:
candidates.add((dist, neighborId))
nearest.add((dist, neighborId))
nearest.sort(nodeDistCmp)
# Track worst nearest instead of sorting every time
if nearest.len > ef:
nearest.setLen(ef)
# Find and remove the worst element in nearest
var worstIdx = 0
var worstDist = nearest[0].dist
for i in 1..<nearest.len:
if nearest[i].dist > worstDist:
worstDist = nearest[i].dist
worstIdx = i
nearest[worstIdx] = nearest[^1]
nearest.setLen(nearest.len - 1)
worstNearestDist = worstDist
# Update worstNearestDist after removal
worstNearestDist = nearest[0].dist
for i in 1..<nearest.len:
if nearest[i].dist > worstNearestDist:
worstNearestDist = nearest[i].dist
else:
if nearest.len == ef:
worstNearestDist = nearest[0].dist
for i in 1..<nearest.len:
if nearest[i].dist > worstNearestDist:
worstNearestDist = nearest[i].dist
# Final sort for return
nearest.sort(nodeDistCmp)
return nearest
proc selectNeighbors(idx: HNSWIndex, baseVector: Vector, candidates: seq[NodeDist],