Clone
15
S3 Vector Bucket
Chris Lu edited this page 2026-11-05 17:43:17 -08:00

S3 Vector Bucket

SeaweedFS serves the Amazon S3 Vectors API: vector buckets, vector indexes, and PutVectors / QueryVectors similarity search. Point the AWS CLI or an AWS SDK's s3vectors client at the SeaweedFS S3 endpoint and make the same calls; S3 Vectors API lists what differs from AWS.

How it works

---
config:
  flowchart:
    wrappingWidth: 420
    nodeSpacing: 50
    rankSpacing: 50
---
flowchart TB
    client["<b>Clients</b><br/>AWS CLI, boto3, any AWS SDK"]
    gateway["<b>S3 gateway</b><br/>S3, S3 Tables and S3 Vectors APIs on the S3 port"]
    subgraph buckets [" "]
        direction LR
        table["<b>table bucket</b><br/>Iceberg or Lance tables"]
        vector["<b>vector bucket</b><br/>a table bucket with format LANCE"]
        normal["<b>normal bucket</b><br/>objects"]
    end
    engine["<b>weed-vector</b><br/>similarity search over the index"]
    filers[("<b>filers</b><br/>file metadata")]
    volumes[("<b>volume servers</b><br/>file chunks")]
    client -->|"S3, S3 Tables and S3 Vectors requests"| gateway
    gateway --> table
    gateway --> vector
    gateway --> normal
    vector -->|"index and vector operations"| engine
    engine -->|"table and file metadata"| filers
    engine -->|"file chunks"| volumes

    classDef s3server fill:#dbeafe,stroke:#2563eb,color:#0f172a
    classDef other fill:#f1f5f9,stroke:#64748b,color:#0f172a
    class gateway s3server
    class client,normal,table,vector,engine,filers,volumes other
    style buckets fill:none,stroke:#94a3b8

The gateway serves all three bucket kinds on the S3 port. A vector bucket is a table bucket with format LANCE; each index is one Lance dataset under s3://<bucket>/vector/<index>/. weed-vector reads and writes it through the gateway's S3 API, and the gateway, as for any S3 request, keeps the file metadata on the filers and the chunks on the volume servers.

  • The S3 gateway (weed s3, or the S3 server inside weed server -s3 and weed mini) answers S3 Vectors requests on the normal S3 port. It tells them apart by the service name in the SigV4 credential scope: a request signed for s3vectors goes to the vectors API, everything else to the S3 API.
  • The vector engine is a separate process, weed-vector. It registers with the master, and the gateway finds it through the master on the first request that needs it. Bucket and policy operations work without it; index and vector operations need it. Several can register: the open-source gateway sends its requests to one at a time, and SeaweedFS Enterprise spreads each index and its queries over them (see S3 Vector Bucket Performance#beyond-one-engine).
  • The data is stored as Lance datasets. A vector bucket is a table bucket with format LANCE, and each index is a Lance table in the bucket's vector namespace, stored under s3://<bucket>/vector/<index>/. The engine reaches it through the SeaweedFS Lance Catalog (-s3.port.lance, default 9101, on by default) and the S3 port.

Queries are exact until a vector index is built. Without one, every query compares against all vectors of the index, and recall@10 was 1.000 in every such run of the perf suite. The Lance Maintenance Worker's lance_vector_index job builds an approximate (ANN) index, IVF, once an index holds at least min_rows vectors (50,000 by default). From then on a query searches a tenth of the index's partitions, at least 8, and up to four times that many when that leaves it short of topK results. It returns only what it finds in those partitions, so a filter that few vectors match can return fewer results than exist (S3 Vectors API has numbers). Vectors written after the build are still returned, and the worker's lance_optimize_indices job brings them into the index. Like every worker job, both start disabled. Building a vector index has the steps, and S3 Vector Bucket Performance#through-a-vector-index the measurements.

Getting weed-vector

Docker

docker run chrislusf/seaweedfs vector --masters master:9333 \
  --storage-option aws_access_key_id=admin \
  --storage-option aws_secret_access_key=admin-secret

Since release 4.52 the standard image carries the engine at /usr/bin/weed-vector on amd64 and arm64, beside the Rust volume server and the Lance maintenance worker. The vector verb listens on 0.0.0.0:8090 unless --listen or WEED_VECTOR_LISTEN says otherwise, because the gateway dials it from another container. On arm and 386 the verb says the engine is not available for the platform.

From a release

curl -fsSL https://raw.githubusercontent.com/seaweedfs/seaweedfs/master/install.sh \
  | bash -s -- --component vector

The assets are weed-vector_linux_amd64.tar.gz and weed-vector_linux_arm64.tar.gz, with md5s, on each GitHub release from 4.52 on. --component all installs it along with weed, the Rust volume server and the worker, and skips it on a platform or release that has no build. Linux only, like the worker.

From source

Build it with Rust 1.94.1 or newer, running cargo inside seaweed-vector/. The workspace points Lance's build scripts at a bundled protoc, so no system protoc is needed:

git clone https://github.com/seaweedfs/seaweedfs.git
cd seaweedfs/seaweed-vector
cargo build --release --locked -p weed-vector-server
# the binary is target/release/weed-vector

Building from another directory with --manifest-path skips that setting. The build then needs protoc on PATH (or PROTOC set), or it stops in lance-encoding with Could not find protoc.

Quick start

1. Start SeaweedFS with an S3 identity. weed mini turns the two variables into an identity with admin rights:

export AWS_ACCESS_KEY_ID=admin
export AWS_SECRET_ACCESS_KEY=admin-secret
weed mini -dir=/data/seaweedfs

2. Start the engine in another terminal, pointing it at the master and giving it the same identity:

weed-vector --masters localhost:9333 \
  --storage-option aws_access_key_id=admin \
  --storage-option aws_secret_access_key=admin-secret

The engine reads and writes the Lance files over S3 with this identity, and authenticates with it to the Lance catalog, which it finds at http://localhost:9101 by default. It listens on 127.0.0.1:8090.

Trying it without identities: start weed mini without the two variables, and the engine without --storage-option (weed-vector --masters localhost:9333). The S3 port then accepts any access key, so step 3 also works with AWS_ACCESS_KEY_ID=any and AWS_SECRET_ACCESS_KEY=any.

3. Create a bucket and an index, write vectors, query:

export AWS_ACCESS_KEY_ID=admin
export AWS_SECRET_ACCESS_KEY=admin-secret
export AWS_DEFAULT_REGION=us-east-1

aws s3vectors create-vector-bucket --endpoint-url http://localhost:8333 \
  --vector-bucket-name media

aws s3vectors create-index --endpoint-url http://localhost:8333 \
  --vector-bucket-name media --index-name movies \
  --data-type float32 --dimension 3 --distance-metric cosine

aws s3vectors put-vectors --endpoint-url http://localhost:8333 \
  --vector-bucket-name media --index-name movies \
  --vectors '[
    {"key": "star-wars", "data": {"float32": [0.1, 0.9, 0.2]}, "metadata": {"genre": "scifi", "year": 1977}},
    {"key": "alien",     "data": {"float32": [0.2, 0.8, 0.1]}, "metadata": {"genre": "scifi", "year": 1979}},
    {"key": "amelie",    "data": {"float32": [0.9, 0.1, 0.3]}, "metadata": {"genre": "romance", "year": 2001}}
  ]'

aws s3vectors query-vectors --endpoint-url http://localhost:8333 \
  --vector-bucket-name media --index-name movies \
  --query-vector '{"float32": [0.15, 0.85, 0.15]}' --top-k 2 \
  --return-distance --return-metadata
{
    "vectors": [
        {
            "distance": 0.0029967427,
            "key": "star-wars",
            "metadata": {
                "genre": "scifi",
                "year": 1977
            }
        },
        {
            "distance": 0.0037363768,
            "key": "alien",
            "metadata": {
                "genre": "scifi",
                "year": 1979
            }
        }
    ],
    "distanceMetric": "cosine"
}

With a metadata filter, only vectors from 1978 on are candidates:

aws s3vectors query-vectors --endpoint-url http://localhost:8333 \
  --vector-bucket-name media --index-name movies \
  --query-vector '{"float32": [0.15, 0.85, 0.15]}' --top-k 2 \
  --filter '{"year": {"$gte": 1978}}' --return-metadata
{
    "vectors": [
        {
            "key": "alien",
            "metadata": {
                "genre": "scifi",
                "year": 1979
            }
        },
        {
            "key": "amelie",
            "metadata": {
                "genre": "romance",
                "year": 2001
            }
        }
    ],
    "distanceMetric": "cosine"
}

get-vectors, list-vectors and delete-vectors work the same way.

The same with boto3:

import boto3

s3v = boto3.client(
    "s3vectors",
    endpoint_url="http://localhost:8333",
    aws_access_key_id="admin",
    aws_secret_access_key="admin-secret",
    region_name="us-east-1",
)

s3v.create_vector_bucket(vectorBucketName="books")
s3v.create_index(
    vectorBucketName="books", indexName="titles",
    dataType="float32", dimension=4, distanceMetric="euclidean",
    metadataConfiguration={"nonFilterableMetadataKeys": ["summary"]},
)
s3v.put_vectors(
    vectorBucketName="books", indexName="titles",
    vectors=[
        {"key": "dune", "data": {"float32": [0.9, 0.1, 0.0, 0.2]},
         "metadata": {"genre": "scifi", "pages": 412, "summary": "Desert planet politics."}},
        {"key": "emma", "data": {"float32": [0.1, 0.9, 0.3, 0.0]},
         "metadata": {"genre": "classic", "pages": 474, "summary": "Matchmaking in Highbury."}},
        {"key": "neuromancer", "data": {"float32": [0.8, 0.2, 0.1, 0.3]},
         "metadata": {"genre": "scifi", "pages": 271, "summary": "Cyberspace heist."}},
    ],
)

result = s3v.query_vectors(
    vectorBucketName="books", indexName="titles",
    queryVector={"float32": [0.85, 0.15, 0.05, 0.25]}, topK=3,
    filter={"$and": [{"genre": {"$eq": "scifi"}}, {"pages": {"$lt": 400}}]},
    returnDistance=True, returnMetadata=True,
)
for hit in result["vectors"]:
    print(hit["key"], round(hit["distance"], 4), hit["metadata"])
neuromancer 0.1 {'genre': 'scifi', 'pages': 271, 'summary': 'Cyberspace heist.'}

summary is non-filterable: it is stored and returned, but a filter on it is rejected.

Running the engine

Flag Environment variable Default Meaning
--masters WEED_VECTOR_MASTERS none Comma-separated master addresses to register with. Without it the gateway never finds the engine.
--namespace WEED_VECTOR_NAMESPACE http://localhost:9101 The Lance catalog URL: the S3 server's Lance port (-s3.port.lance).
--listen WEED_VECTOR_LISTEN 127.0.0.1:8090 gRPC address. This is the address registered on the master, so the gateway must be able to reach it.
--storage-option key=value none Repeatable. aws_access_key_id and aws_secret_access_key are used for S3 data access and for the Lance catalog. Needed when the S3 server has identities.
--read-cache-mb WEED_VECTOR_READ_CACHE_MB 512 Memory for caching reads of the index's (write-once) data files, in MiB. 0 disables it.
--nprobes WEED_VECTOR_NPROBES a tenth of the partitions, at least 8 Partitions every query searches in a vector index, in place of the default. For measurements; 0 is rejected.
--filer-signing-key WEED_FILER_SIGNING_KEY none The cluster's jwt.filer_signing.key. The Lance catalog vends credentials that can write index data only to a caller holding it.
--security-toml WEED_VECTOR_SECURITY_TOML found like weed does The cluster's security.toml, for gRPC TLS. Without it the engine looks in --config-dir, ., ~/.seaweedfs, /usr/local/etc/seaweedfs and /etc/seaweedfs.
--config-dir WEED_VECTOR_CONFIG_DIR none The directory weed -config_dir names, searched first for security.toml. ~ is expanded.
  • Ports. The default 8090 is free next to weed mini and weed server, whose volume server uses 8080. If the port is taken, weed-vector exits with Error: listen on 127.0.0.1:8090 and Address already in use (os error 98); pass another --listen.

  • Engine on another host. With a wildcard address such as --listen 0.0.0.0:8090, the engine registers the IP it uses to reach the first master.

  • gRPC TLS. With TLS configured in security.toml, the engine serves with [grpc.vector] and dials masters with [grpc.client]. The master's certificate must chain to grpc.ca, and its common name or a DNS SAN must pass grpc.client.master_allowed_server_commonNames (else grpc.client.allowed_server_commonNames, else grpc.allowed_server_commonNames) when one is set, as for weed clients. WEED_GRPC_* environment variables override the file.

  • A standby engine. The gateway sends every request to one registered engine at a time. Start a second engine (with its own --listen) and it takes over when the first stops: in our test, queries kept succeeding right after the engine in use was stopped.

Building a vector index

Without a vector index every query compares against all vectors of the index. The Lance Maintenance Worker (weed-worker) builds the vector index and keeps it covering new vectors; weed-vector uses it once it exists. The steps below continue the quick start, with weed mini's admin server on port 12535.

1. Start the worker, with the engine's identity:

weed-worker --admin localhost:12535 --namespace http://localhost:9101 \
  --access-key admin --secret-key admin-secret

It logs connected to admin, and its job types appear in curl http://localhost:12535/api/plugin/job-types, among them lance_vector_index and lance_optimize_indices.

2. Turn the two jobs on, in the admin UI at /plugin or through its API. They start disabled:

for job in lance_vector_index lance_optimize_indices; do
  curl -X PUT http://localhost:12535/api/plugin/job-types/$job/config \
    -H 'Content-Type: application/json' -d '{"admin_runtime": {"enabled": true}}'
done

From then on the admin server asks the worker for indexes that need work every 5 minutes. A second index, loaded with 50,000 vectors after the jobs were turned on, had its vector index within five minutes with no further command.

3. Or run a job now, which also works while the job is disabled:

curl -X POST http://localhost:12535/api/plugin/job-types/lance_vector_index/run
{"canceled_count":0,"detected_count":1,"error_count":0,"executed_count":1,"job_type":"lance_vector_index","ready_to_execute_count":1,"skipped_active_count":0,"success_count":1}

detected_count is the number of indexes that needed a build and success_count the number built. The call waits for the builds, up to 5 minutes; -d '{"timeout_seconds": 1800}' raises that to the maximum of 30. An index of 50,000 vectors of 16 dimensions was built in half a second. A second run reports "detected_count":0: an index that has a vector index is left alone.

Which indexes get one. Those holding at least min_rows vectors, 50,000 by default. To change it, send the setting with the same PUT:

curl -X PUT http://localhost:12535/api/plugin/job-types/lance_vector_index/config \
  -H 'Content-Type: application/json' \
  -d '{"admin_runtime": {"enabled": true}, "worker_config_values": {"min_rows": {"int64_value": "10000"}}}'

Vectors written afterwards are returned by queries right away, and lance_optimize_indices adds them to the vector index once they amount to more than a tenth of the vectors already in it. After 6,000 vectors were added to the 50,000 above, a run of that job reported "detected_count":1,"success_count":1, and none were left outside the vector index. See Lance Maintenance Worker#the-jobs for the rules and settings.

Checking the result. weed-vector-cli, built from source (cargo build --release --locked -p weed-vector-core --bin weed-vector-cli inside seaweed-vector/), prints what the vector index of movies in bucket media covers:

weed-vector-cli --access-key admin --secret-key admin-secret coverage 'media$vector$movies'
[
  {
    "index_name": "vector_idx",
    "segments": 1,
    "partitions": 224,
    "coverage_known": true,
    "indexed_fragments": [0, 1, 2, ...],
    "unindexed_fragments": [],
    "unindexed_rows": 0
  }
]

An empty list means no vector index has been built. unindexed_rows counts the vectors written since the last build or optimize run.

Limits

What Limit
Data type float32 only
Dimension 1 to 4096
Distance metric euclidean (default), cosine, dotproduct
Vectors per PutVectors 1 to 500
Keys per GetVectors 1 to 100
Keys per DeleteVectors 1 to 500
topK 1 to 10000
ListVectors page 500 by default, maxResults up to 1000
ListVectors segments up to 16
ListVectorBuckets / ListIndexes maxResults up to 500
Request body 32 MiB

Bucket names are 3 to 63 lowercase letters, digits and hyphens, starting and ending with a letter or digit. Index names use lowercase letters, digits, - and _, and start with a letter or digit. See S3 Vectors API for the details.

Reading an index with LanceDB

Each index is an ordinary Lance table in namespace [<bucket>, "vector"], so Lance tools can read it through the SeaweedFS Lance Catalog. The catalog takes the S3 credential as an x-api-key header of the form access_key:secret_key:

import lancedb

db = lancedb.connect_namespace(
    "rest",
    {"uri": "http://localhost:9101", "header.x-api-key": "admin:admin-secret"},
    storage_options={
        "aws_endpoint": "http://localhost:8333",
        "allow_http": "true",
        "aws_access_key_id": "admin",
        "aws_secret_access_key": "admin-secret",
        "aws_region": "us-east-1",
    },
)
table = db.open_table("docs", namespace_path=["interop", "vector"])
print(db.list_tables(namespace_path=["interop", "vector"]).tables)
print(table.schema)
print(table.search([1, 0, 0]).limit(1).to_list())

For an index docs (dimension 3, cosine) in bucket interop, holding two vectors:

['interop$vector$docs']
key: string not null
vector: fixed_size_list<item: float>[3] not null
  child 0, item: float
  -- field metadata --
  distance_metric: 'COSINE'
metadata: large_binary
  -- field metadata --
  ARROW:extension:name: 'lance.json'
[{'key': 'a', 'vector': [1.0, 0.0, 0.0], 'metadata': '{"lang":"en"}', '_distance': 0.0}]

The table has three columns: key, vector (with the metric in the field metadata) and metadata (JSON).

Treat it as read-only from Lance. The catalog refuses to drop, rename, register or deregister tables in the vector namespace (the vector namespace of a vector bucket is read-only; manage its indexes through S3 Vectors). Appending rows with LanceDB does reach the dataset, and the S3 Vectors API then returns them, but such writes bypass the API and its validation.

Troubleshooting

The S3 server logs engine failures at its default log level, as W (no engine) or E (engine error) lines that name the operation, the bucket and the index:

W1102 08:32:24.229055 handlers.go:220 S3 Vectors: CreateIndex media/movies: rpc error: code = Unavailable desc = no weed-vector registered on master
E1102 08:32:37.582141 handlers.go:223 S3 Vectors: CreateIndex media/movies: rpc error: code = Internal desc = HTTP status client error (403 Forbidden) for url (http://localhost:9101/v1/table/media$vector$movies/describe)

To also see the engine register and be found, put -v=1 before the command, e.g. weed -v=1 mini -dir=/data/seaweedfs. When the engine registers, the master logs:

Cluster: vector node 127.0.0.1:8090 added to group ''

and on the first request that needs the engine, the gateway logs:

S3 Vectors: discovered weed-vector at 127.0.0.1:8090 via master 172.17.0.3:9333
Symptom Cause
ServiceUnavailableException: vector service unavailable on CreateIndex, PutVectors or QueryVectors, and the gateway logs no weed-vector registered on master No engine is registered: it is not running, it has no --masters, or it exited (for example on a busy --listen port). Bucket operations keep working.
InternalFailure: internal error, see the S3 gateway log on CreateIndex, and the gateway logs (403 Forbidden) for url (http://localhost:9101/v1/table/.../describe) The S3 server has identities and the engine has no credential. Pass --storage-option aws_access_key_id=... and --storage-option aws_secret_access_key=....
The same, with 401 Unauthorized The engine's access key is unknown or its secret is wrong.
weed-vector exits with Address already in use (os error 98) The --listen port is taken; see the ports note above.
PutVectors fails with ValidationException: request body exceeds 32 MiB Send fewer vectors per call. In our tests 500 vectors of 4096 dimensions took 9.8 to 33.2 MiB as JSON, depending on the digits per value.
A plain HTTP request to /ListVectorBuckets gets 405 Only requests signed for the s3vectors service reach the vectors API. Use an AWS CLI or SDK s3vectors client.

A failed CreateIndex leaves no index behind, so it is safe to retry once the engine is fixed.

Monitoring

S3 Vectors requests are counted in the S3 server's Prometheus metrics, with the operation in the type label as s3vectors.<Operation>. Enable them with -metricsPort; the listener binds to the server's IP address, not to localhost.

SeaweedFS_s3_request_total{bucket="",code="200",type="s3vectors.QueryVectors"} 1
SeaweedFS_s3_request_seconds_bucket{bucket="",type="s3vectors.CreateIndex",le="0.0001"} 0
SeaweedFS_s3_in_flight_requests{type="s3vectors.QueryVectors"} 0