A comprehensive, modular ArangoDB client library for OpenResty/Lua with full HTTP API support.
- Full ArangoDB HTTP API coverage
- Modular architecture with separate modules for each API area
- Connection pooling and keepalive support
- Both Basic and JWT authentication
- AQL query execution with cursor pagination
- Stream transactions support
- Graph operations (Gharial API)
- ArangoSearch views and analyzers
- Vector indexes for semantic similarity search (v3.12.4+)
- OpenAI-compatible embeddings generation (OpenAI, Ollama, vLLM, etc.)
- Foxx microservices management
- User and permission management
- Cluster operations support
luarocks install lua-cjson
luarocks install lbase64
luarocks install lua-resty-httpCopy the src/arangodb folder to your Lua package path.
local arangodb = require("arangodb")
-- Create client
local client = arangodb.new({
endpoint = "http://127.0.0.1:8529",
username = "root",
password = "password",
database = "_system"
})
-- Get server version
local version = client:version()
print("ArangoDB " .. version.version)
-- Execute AQL query
local results = client.query:execute(
"FOR doc IN users FILTER doc.age >= @minAge RETURN doc",
{ minAge = 21 }
)
-- Create a document
local doc = client.document:create("users", {
name = "Alice",
age = 30
})local client = arangodb.new({
-- Required
endpoint = "http://127.0.0.1:8529", -- ArangoDB server URL
-- Authentication (one of these is required)
username = "root", -- Basic auth username
password = "password", -- Basic auth password
-- OR
token = "jwt-token", -- JWT bearer token
-- Optional
database = "_system", -- Default database (default: "_system")
timeout = 30000, -- Request timeout in ms (default: 30000)
keepalive = 60000, -- Keepalive timeout in ms (default: 60000)
pool_size = 100, -- Connection pool size (default: 100)
ssl_verify = false -- Verify SSL certificates (default: false)
})-- Get server version
client:version(details) -- details: include detailed info
-- Get storage engine info
client:engine()
-- Check server availability
client:isAvailable()
-- Switch database
client:useDatabase(name)
client:getDatabase()
-- Raw HTTP methods
client:get(path, options)
client:post(path, body, options)
client:put(path, body, options)
client:patch(path, body, options)
client:delete(path, options)-- List databases
client.db:list() -- All databases
client.db:listUser() -- User-accessible databases
client.db:current() -- Current database info
client.db:exists(name) -- Check if database exists
-- Create database
client.db:create(name, options, users)
-- Drop database
client.db:drop(name)
-- Shorthand query (uses client.query:execute)
client.db:query(aql, bindVars, options)-- List collections
client.collection:list(excludeSystem)
-- Get collection info
client.collection:get(name)
client.collection:properties(name)
client.collection:exists(name)
client.collection:count(name)
client.collection:figures(name)
client.collection:revision(name)
-- Create collections
client.collection:create(name, options)
client.collection:createDocument(name, options) -- type = 2
client.collection:createEdge(name, options) -- type = 3
-- Modify collections
client.collection:rename(name, newName)
client.collection:setProperties(name, properties)
client.collection:truncate(name)
client.collection:drop(name, isSystem)
-- Memory management
client.collection:load(name)
client.collection:unload(name)
client.collection:loadIndexes(name)
client.collection:compact(name)-- Read documents
client.document:get(collection, key, options)
client.document:exists(collection, key)
client.document:head(collection, key)
client.document:getMany(collection, keys, options)
-- Create documents
client.document:create(collection, document, options)
client.document:createMany(collection, documents, options)
-- Update documents
client.document:update(collection, key, document, options)
client.document:updateMany(collection, documents, options)
-- Replace documents
client.document:replace(collection, key, document, options)
client.document:replaceMany(collection, documents, options)
-- Delete documents
client.document:delete(collection, key, options)
client.document:deleteMany(collection, keys, options)
-- Import/Export
client.document:import(collection, documents, options)
client.document:export(collection, options){
waitForSync = true, -- Wait for sync to disk
returnNew = true, -- Return new document
returnOld = true, -- Return old document
silent = false, -- Don't return metadata
overwrite = false, -- Overwrite existing
overwriteMode = "update", -- "ignore", "update", "replace", "conflict"
keepNull = true, -- Keep null values in updates
mergeObjects = true, -- Merge nested objects
ifMatch = "rev", -- Conditional by revision
}-- Execute queries
local results, cursor = client.query:execute(aql, bindVars, options)
local all_results = client.query:all(aql, bindVars, options) -- Auto-pagination
-- Cursor operations
client.query:next(cursorId)
client.query:deleteCursor(cursorId)
-- Iterator for large result sets
for doc in client.query:iterate(aql, bindVars, options) do
print(doc.name)
end
-- Query analysis
client.query:parse(aql, options)
client.query:explain(aql, bindVars, options)
-- Running queries
client.query:listRunning(all)
client.query:kill(queryId)
-- Slow query log
client.query:listSlow(all)
client.query:clearSlow(all)
-- Query tracking
client.query:getTracking()
client.query:setTracking(properties)
-- Query cache
client.query:getCacheProperties()
client.query:setCacheProperties(properties)
client.query:getCacheEntries()
client.query:clearCache()
-- AQL functions
client.query:functions(namespace)
client.query:createFunction(name, code, isDeterministic)
client.query:deleteFunction(name, group)
-- Optimizer rules
client.query:rules(){
count = true, -- Return total count
batchSize = 1000, -- Batch size for cursor
ttl = 30, -- Cursor TTL in seconds
cache = true, -- Use query cache
memoryLimit = 0, -- Memory limit in bytes
fullCount = true, -- Return full count (with LIMIT)
stream = true, -- Stream results
profile = 2, -- Profile level (0, 1, 2)
maxRuntime = 60, -- Max runtime in seconds
}-- List indexes
client.index:list(collection, withStats, withHidden)
client.index:get(indexId)
client.index:getByName(collection, indexName)
client.index:exists(indexId)
-- Create indexes
client.index:create(collection, definition)
client.index:createPersistent(collection, fields, options)
client.index:createGeo(collection, fields, options)
client.index:createFulltext(collection, fields, options) -- Deprecated
client.index:createTTL(collection, fields, expireAfter, options)
client.index:createZKD(collection, fields, options)
client.index:createMDI(collection, fields, options)
client.index:createInverted(collection, fields, options)
-- Vector index for semantic similarity search (v3.12.4+)
-- NOTE: Requires --vector-index startup option and documents must exist first
client.index:createVector(collection, field, params, options)
-- Drop index
client.index:drop(indexId)
-- Ensure index exists
local idx, created = client.index:ensure(collection, definition)ArangoDB supports vector similarity search for semantic/AI applications. Vector indexes use the Faiss library for approximate nearest neighbor (ANN) search.
Requirements:
- Server must be started with
--experimental-vector-index=true - Documents with vector embeddings must exist before creating the index
- Once enabled, vector-index cannot be disabled (permanent RocksDB change)
Creating a Vector Index:
-- Documents must have embeddings first
for i = 1, 1000 do
client.document:create("documents", {
text = "Document " .. i,
embedding = generate_embedding(...) -- 384-dim vector
})
end
-- Then create the vector index
local idx = client.index:createVector("documents", "embedding", {
metric = "cosine", -- "cosine", "l2", or "innerProduct"
dimension = 384, -- vector array length
nLists = 66, -- ~N/15 where N is document count
defaultNProbe = 10, -- neighboring centroids to search (higher = slower but better)
trainingIterations = 25 -- optional, default: 25
})Vector Search Functions:
| Function | Metric | Sort Order | Value Range | Version |
|---|---|---|---|---|
APPROX_NEAR_COSINE() |
cosine | DESC | [-1, 1] | v3.12.4+ |
APPROX_NEAR_L2() |
l2 | ASC | [0, ∞) | v3.12.4+ |
APPROX_NEAR_INNER_PRODUCT() |
innerProduct | DESC | (-∞, ∞) | v3.12.6+ |
COSINE_SIMILARITY() |
- | DESC | [-1, 1] | v3.9.0+ (exact, no index) |
Query Examples:
-- Approximate search using vector index (fast, for large datasets)
local results = client.query:execute([[
FOR doc IN documents
SORT APPROX_NEAR_COSINE(doc.embedding, @query) DESC
LIMIT 10
RETURN doc
]], { query = query_vector })
-- With similarity score and custom nProbe
local results = client.query:execute([[
FOR doc IN documents
LET similarity = APPROX_NEAR_COSINE(doc.embedding, @query, { nProbe: 20 })
SORT similarity DESC
LIMIT 10
RETURN MERGE({ similarity }, doc)
]], { query = query_vector })
-- Pre-filtering (v3.12.6+)
local results = client.query:execute([[
FOR doc IN documents
FILTER doc.category == @category
SORT APPROX_NEAR_COSINE(doc.embedding, @query) DESC
LIMIT 10
RETURN doc
]], { query = query_vector, category = "tech" })
-- Exact cosine similarity (no index needed, for small datasets)
local results = client.query:execute([[
FOR doc IN documents
LET sim = COSINE_SIMILARITY(doc.embedding, @query)
FILTER sim > 0.8
SORT sim DESC
LIMIT 10
RETURN { doc, similarity: sim }
]], { query = query_vector })
-- Batch similarity with 2D array
local results = client.query:execute([[
RETURN COSINE_SIMILARITY(@vectors, @query)
]], {
vectors = {{0,1,0,1}, {1,0,0,1}, {1,1,1,0}},
query = {1,1,1,1}
})
-- Returns: [0.707, 0.707, 0.866]Metric Selection:
| Metric | Use Case | Notes |
|---|---|---|
cosine |
Text embeddings, normalized vectors | Auto-normalizes vectors |
l2 |
Image features, spatial data | Euclidean distance |
innerProduct |
When magnitude matters | Faster than cosine (no normalization) |
-- Graph management
client.graph:list()
client.graph:get(name)
client.graph:exists(name)
client.graph:create(name, edgeDefinitions, options)
client.graph:drop(name, dropCollections)
-- Vertex collections
client.graph:listVertexCollections(graphName)
client.graph:addVertexCollection(graphName, collection, options)
client.graph:removeVertexCollection(graphName, collection, dropCollection)
-- Edge definitions
client.graph:listEdgeDefinitions(graphName)
client.graph:addEdgeDefinition(graphName, definition, options)
client.graph:replaceEdgeDefinition(graphName, edgeCollection, definition, options)
client.graph:removeEdgeDefinition(graphName, edgeCollection, options)
-- Vertex CRUD
client.graph:getVertex(graphName, collection, key, options)
client.graph:createVertex(graphName, collection, vertex, options)
client.graph:updateVertex(graphName, collection, key, vertex, options)
client.graph:replaceVertex(graphName, collection, key, vertex, options)
client.graph:deleteVertex(graphName, collection, key, options)
-- Edge CRUD
client.graph:getEdge(graphName, collection, key, options)
client.graph:createEdge(graphName, collection, edge, options)
client.graph:updateEdge(graphName, collection, key, edge, options)
client.graph:replaceEdge(graphName, collection, key, edge, options)
client.graph:deleteEdge(graphName, collection, key, options)
-- Traversal
client.graph:traverse(startVertex, options)-- JavaScript transaction (single request)
local result = client.transaction:execute({
collections = { read = {"col1"}, write = {"col2"} },
params = { userId = "123", amount = 50 },
action = [[
function(params) {
var db = require('@arangodb').db;
var user = db.col1.document(params.userId);
db.col2.insert({ debit: params.amount, user: user._key });
return "success";
}
]]
})
-- Stream transactions (multi-request)
local tx = client.transaction:begin(collections, options)
local status = client.transaction:status(tx.id)
client.transaction:commit(tx.id)
client.transaction:abort(tx.id)
client.transaction:list()
-- Transaction helper (auto commit/abort)
client.transaction:run(collections, function(txId)
-- Operations here
return result
end, options)
-- Transaction-aware operations
client.transaction:query(txId, aql, bindVars, options)
client.transaction:createDocument(txId, collection, document, options)
client.transaction:updateDocument(txId, collection, key, document, options)
client.transaction:deleteDocument(txId, collection, key, options)-- User management
client.user:list()
client.user:get(username)
client.user:exists(username)
client.user:create(username, password, options)
client.user:update(username, options)
client.user:replace(username, password, options)
client.user:delete(username)
-- Database permissions
client.user:getDatabasePermission(username, database)
client.user:setDatabasePermission(username, database, permission)
client.user:clearDatabasePermission(username, database)
client.user:listDatabasePermissions(username, full)
-- Collection permissions
client.user:getCollectionPermission(username, database, collection)
client.user:setCollectionPermission(username, database, collection, permission)
client.user:clearCollectionPermission(username, database, collection)
-- Convenience methods
client.user:grantDatabase(username, database) -- rw
client.user:grantDatabaseReadOnly(username, database) -- ro
client.user:revokeDatabase(username, database) -- none
client.user:grantCollection(username, database, collection)
client.user:grantCollectionReadOnly(username, database, collection)
client.user:revokeCollection(username, database, collection)-- Server info
client.admin:version(details)
client.admin:engine()
client.admin:serverId()
client.admin:serverRole()
client.admin:serverAvailability()
client.admin:serverMode()
client.admin:setServerMode(mode)
-- Statistics
client.admin:statistics()
client.admin:statisticsDescription()
client.admin:metrics(serverId)
-- Logs
client.admin:logs(options)
client.admin:logLevel(serverId)
client.admin:setLogLevel(levels, serverId)
-- Cluster operations
client.admin:clusterHealth()
client.admin:clusterEndpoints()
client.admin:clusterStatistics(dbserver)
client.admin:maintenance(serverId, mode, options)
client.admin:cleanOutServer(serverId)
client.admin:moveShard(options)
client.admin:rebalanceShards(options)
-- Async jobs
client.admin:jobs(status, count)
client.admin:jobResult(jobId)
client.admin:cancelJob(jobId)
client.admin:deleteJobs(jobType, stamp)
-- Tasks
client.admin:tasks()
client.admin:task(taskId)
client.admin:createTask(options)
client.admin:deleteTask(taskId)
-- Misc
client.admin:time()
client.admin:shutdown(soft)
client.admin:compact(options)-- Analyzer management
client.analyzer:list()
client.analyzer:get(name)
client.analyzer:exists(name)
client.analyzer:create(name, type, properties, features)
client.analyzer:delete(name, force)
-- Type-specific creators
client.analyzer:createIdentity(name, features)
client.analyzer:createDelimiter(name, delimiter, features)
client.analyzer:createStem(name, locale, features)
client.analyzer:createNorm(name, locale, options, features)
client.analyzer:createNgram(name, options, features)
client.analyzer:createText(name, locale, options, features)
client.analyzer:createAQL(name, queryString, options, features)
client.analyzer:createPipeline(name, pipeline, features)
client.analyzer:createStopwords(name, stopwords, options, features)
client.analyzer:createCollation(name, locale, features)
client.analyzer:createGeoJSON(name, options, features)
client.analyzer:createGeoPoint(name, options, features)-- View management
client.view:list()
client.view:get(name)
client.view:properties(name)
client.view:exists(name)
client.view:drop(name)
client.view:rename(name, newName)
-- Create views
client.view:create(name, type, properties)
client.view:createArangoSearch(name, options)
client.view:createSearchAlias(name, indexes)
client.view:createSimple(name, collection, options)
-- Modify views
client.view:updateProperties(name, properties)
client.view:replaceProperties(name, properties)
-- Link management (ArangoSearch)
client.view:addLink(viewName, collection, linkOptions)
client.view:removeLink(viewName, collection)
client.view:updateLink(viewName, collection, linkOptions)
-- Index management (search-alias)
client.view:addIndex(viewName, collection, indexName)
client.view:removeIndex(viewName, collection, indexName)-- Service management
client.foxx:list(excludeSystem)
client.foxx:get(mount)
client.foxx:exists(mount)
client.foxx:installFromUrl(mount, source, options)
client.foxx:installFromPath(mount, path, options)
client.foxx:installFromZip(mount, zipData, options)
client.foxx:replace(mount, source, options)
client.foxx:upgrade(mount, source, options)
client.foxx:uninstall(mount, options)
-- Configuration
client.foxx:getConfiguration(mount)
client.foxx:updateConfiguration(mount, configuration)
client.foxx:replaceConfiguration(mount, configuration)
-- Dependencies
client.foxx:getDependencies(mount)
client.foxx:updateDependencies(mount, dependencies)
client.foxx:replaceDependencies(mount, dependencies)
-- Development mode
client.foxx:enableDevelopment(mount)
client.foxx:disableDevelopment(mount)
-- Scripts
client.foxx:listScripts(mount)
client.foxx:runScript(mount, scriptName, args)
-- Other
client.foxx:readme(mount)
client.foxx:swagger(mount)
client.foxx:download(mount)
client.foxx:runTests(mount, options)The Embeddings module provides vector embedding generation via OpenAI-compatible APIs (OpenAI, Ollama, vLLM, LocalAI, etc.). This is a standalone client that can be used independently of the ArangoDB connection.
local arangodb = require("arangodb")
-- Create embeddings client (uses OPENAI_API_KEY env var by default)
local embeddings = arangodb.Embeddings.new({
api_key = "sk-...", -- Optional if OPENAI_API_KEY env var is set
base_url = "https://api.openai.com/v1", -- Default
model = "text-embedding-3-small", -- Default
dimensions = 256, -- Optional: reduce dimensions (supported models only)
timeout = 30000, -- Request timeout in ms
ssl_verify = false -- SSL verification (default: false)
})
-- Generate single embedding
local vector = embeddings:create("Hello world")
-- Returns: {0.023, -0.041, 0.012, ...} (1536 dimensions for text-embedding-3-small)
-- Generate batch embeddings (more efficient for multiple texts)
local vectors = embeddings:createBatch({"Hello", "World", "Test"})
-- Returns: {{...}, {...}, {...}}
-- Get embedding with metadata (usage stats)
local result = embeddings:createWithMetadata("Hello world")
-- Returns: {
-- embeddings = {{index=0, embedding={...}}},
-- model = "text-embedding-3-small",
-- usage = {prompt_tokens=2, total_tokens=2}
-- }
-- Get embedding dimension for current model
local dim = embeddings:getDimension()
-- Returns: 1536 (for text-embedding-3-small)
-- List available embedding models (OpenAI only)
local models = embeddings:listModels()Using with Ollama or other local providers:
local embeddings = arangodb.Embeddings.new({
base_url = "http://localhost:11434/v1",
model = "nomic-embed-text",
api_key = "ollama" -- Ollama doesn't require a real API key
})
local vector = embeddings:create("Search query")Complete Vector Search Workflow:
local arangodb = require("arangodb")
-- 1. Create ArangoDB client
local client = arangodb.new({
endpoint = "http://localhost:8529",
username = "root",
password = "password"
})
-- 2. Create embeddings client
local embeddings = arangodb.Embeddings.new()
-- 3. Store documents with embeddings
local texts = {"Document about cats", "Document about dogs", "Document about birds"}
for i, text in ipairs(texts) do
local vector = embeddings:create(text)
client.document:create("documents", {
text = text,
embedding = vector
})
end
-- 4. Create vector index (after documents exist)
client.index:createVector("documents", "embedding", {
metric = "cosine",
dimension = 1536,
nLists = 1 -- Small value for few documents
})
-- 5. Semantic search
local query_vector = embeddings:create("Tell me about felines")
local results = client.query:execute([[
FOR doc IN documents
SORT APPROX_NEAR_COSINE(doc.embedding, @query) DESC
LIMIT 5
RETURN doc.text
]], { query = query_vector })The project includes a docker-compose setup for testing. ArangoDB is configured with --experimental-vector-index=true to enable vector search features.
# Start ArangoDB and OpenResty
podman-compose up -d
# or
docker-compose up -d
# Run tests via HTTP
curl http://localhost:8080/test
# Test specific module
curl http://localhost:8080/test/database
curl http://localhost:8080/test/collection
curl http://localhost:8080/test/document
curl http://localhost:8080/test/query
curl http://localhost:8080/test/graph
# Interactive AQL query
curl -X POST http://localhost:8080/query \
-H "Content-Type: application/json" \
-d '{"query": "FOR i IN 1..10 RETURN i"}'The project includes comprehensive Hurl tests in tests/hurl/:
# Run all Hurl tests
hurl --test tests/hurl/*.hurl
# Run specific test
hurl --test tests/hurl/01-database.hurlThe nginx configuration uses a DNS resolver for container networking. If switching between Docker and Podman, you may need to update nginx/conf/nginx.conf:
- Docker:
resolver 127.0.0.11 ipv6=off; - Podman:
resolver 10.89.0.1 ipv6=off;(check withpodman exec <container> cat /etc/resolv.conf)
# Run the test script
resty -I src src/test.luaMIT License