WIP3 implemeting lancedb
This commit is contained in:
158
Cargo.lock
generated
158
Cargo.lock
generated
@@ -2358,6 +2358,15 @@ version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -2535,6 +2544,21 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -2902,6 +2926,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"redb",
|
||||
"redis",
|
||||
"reqwest",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3109,6 +3134,22 @@ dependencies = [
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper 1.7.0",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.16"
|
||||
@@ -3128,9 +3169,11 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.0",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4469,6 +4512,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -4537,6 +4586,23 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b"
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework 2.11.1",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -4787,12 +4853,50 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.3",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -5583,6 +5687,8 @@ checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.4.12",
|
||||
@@ -5591,9 +5697,12 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper 1.7.0",
|
||||
"hyper-rustls 0.27.7",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
@@ -5605,6 +5714,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
@@ -6485,6 +6595,27 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.3",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tagptr"
|
||||
version = "0.2.0"
|
||||
@@ -6936,6 +7067,16 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
@@ -7256,6 +7397,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -7577,6 +7724,17 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
|
@@ -34,6 +34,7 @@ lance-index = "0.37.0"
|
||||
arrow = "55.2.0"
|
||||
lancedb = "0.22.1"
|
||||
uuid = "1.18.1"
|
||||
reqwest = { version = "0.12", features = ["blocking", "json", "rustls-tls"] }
|
||||
|
||||
[dev-dependencies]
|
||||
redis = { version = "0.24", features = ["aio", "tokio-comp"] }
|
||||
|
444
docs/lance.md
Normal file
444
docs/lance.md
Normal file
@@ -0,0 +1,444 @@
|
||||
# Lance Vector Backend (RESP + JSON-RPC)
|
||||
|
||||
This document explains how to use HeroDB’s Lance-backed vector store. It is text-first: users provide text, and HeroDB computes embeddings server-side (no manual vectors). It includes copy-pasteable RESP (redis-cli) and JSON-RPC examples for:
|
||||
|
||||
- Creating a Lance database
|
||||
- Embedding provider configuration (OpenAI, Azure OpenAI, or deterministic test provider)
|
||||
- Dataset lifecycle: CREATE, LIST, INFO, DROP
|
||||
- Ingestion: STORE text (+ optional metadata)
|
||||
- Search: QUERY with K, optional FILTER and RETURN
|
||||
- Delete by id
|
||||
- Index creation (currently a placeholder/no-op)
|
||||
|
||||
References:
|
||||
- Implementation: [src/lance_store.rs](src/lance_store.rs), [src/cmd.rs](src/cmd.rs), [src/rpc.rs](src/rpc.rs), [src/server.rs](src/server.rs), [src/embedding.rs](src/embedding.rs)
|
||||
|
||||
Notes:
|
||||
- Admin DB 0 cannot be Lance (or Tantivy). Only databases with id >= 1 can use Lance.
|
||||
- Permissions:
|
||||
- Read operations (SEARCH, LIST, INFO) require read permission.
|
||||
- Mutating operations (CREATE, STORE, CREATEINDEX, DEL, DROP, EMBEDDING CONFIG SET) require readwrite permission.
|
||||
- Backend gating:
|
||||
- If a DB is Lance, only LANCE.* and basic control commands (PING, ECHO, SELECT, INFO, CLIENT, etc.) are permitted.
|
||||
- If a DB is not Lance, LANCE.* commands return an error.
|
||||
|
||||
Storage layout and schema:
|
||||
- Files live at: <base_dir>/lance/<db_id>/<dataset>.lance
|
||||
- Records schema:
|
||||
- id: Utf8 (non-null)
|
||||
- vector: FixedSizeList<Float32, dim> (non-null)
|
||||
- text: Utf8 (nullable)
|
||||
- meta: Utf8 JSON (nullable)
|
||||
- Search is an L2 KNN brute-force scan for now (lower score = better). Index creation is a no-op placeholder to be implemented later.
|
||||
|
||||
Prerequisites:
|
||||
- Start HeroDB with RPC enabled (for management calls):
|
||||
- See [docs/basics.md](./basics.md) for flags. Example:
|
||||
```bash
|
||||
./target/release/herodb --dir /tmp/herodb --admin-secret mysecret --port 6379 --enable-rpc
|
||||
```
|
||||
|
||||
|
||||
## 0) Create a Lance-backed database (JSON-RPC)
|
||||
|
||||
Use the management API to create a database with backend "Lance". DB 0 is reserved for admin and cannot be Lance.
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "herodb_createDatabase",
|
||||
"params": [
|
||||
"Lance",
|
||||
{ "name": "vectors-db", "storage_path": null, "max_size": null, "redis_version": null },
|
||||
null
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- Response contains the allocated db_id (>= 1). Use that id below (replace 1 with your actual id).
|
||||
|
||||
Select the database over RESP:
|
||||
```bash
|
||||
redis-cli -p 6379 SELECT 1
|
||||
# → OK
|
||||
```
|
||||
|
||||
|
||||
## 1) Configure embedding provider (server-side embeddings)
|
||||
|
||||
HeroDB embeds text internally at STORE/SEARCH time using a per-dataset EmbeddingConfig sidecar. Configure provider before creating a dataset to choose dimensions and provider.
|
||||
|
||||
Supported providers:
|
||||
- openai (standard OpenAI or Azure OpenAI)
|
||||
- testhash (deterministic, CI-friendly; no network)
|
||||
|
||||
Environment variables for OpenAI:
|
||||
- Standard OpenAI: export OPENAI_API_KEY=sk-...
|
||||
- Azure OpenAI: export AZURE_OPENAI_API_KEY=...
|
||||
|
||||
RESP examples:
|
||||
```bash
|
||||
# Standard OpenAI with default dims (model-dependent, e.g. 1536)
|
||||
redis-cli -p 6379 LANCE.EMBEDDING CONFIG SET myset PROVIDER openai MODEL text-embedding-3-small
|
||||
|
||||
# OpenAI with reduced output dimension (e.g., 512) when supported
|
||||
redis-cli -p 6379 LANCE.EMBEDDING CONFIG SET myset PROVIDER openai MODEL text-embedding-3-small PARAM dim 512
|
||||
|
||||
# Azure OpenAI (set env: AZURE_OPENAI_API_KEY)
|
||||
redis-cli -p 6379 LANCE.EMBEDDING CONFIG SET myset PROVIDER openai MODEL text-embedding-3-small \
|
||||
PARAM use_azure true \
|
||||
PARAM azure_endpoint https://myresource.openai.azure.com \
|
||||
PARAM azure_deployment my-embed-deploy \
|
||||
PARAM azure_api_version 2024-02-15 \
|
||||
PARAM dim 512
|
||||
|
||||
# Deterministic test provider (no network, stable vectors)
|
||||
redis-cli -p 6379 LANCE.EMBEDDING CONFIG SET myset PROVIDER testhash MODEL any
|
||||
```
|
||||
|
||||
Read config:
|
||||
```bash
|
||||
redis-cli -p 6379 LANCE.EMBEDDING CONFIG GET myset
|
||||
# → JSON blob describing provider/model/params
|
||||
```
|
||||
|
||||
JSON-RPC examples:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "herodb_lanceSetEmbeddingConfig",
|
||||
"params": [
|
||||
1,
|
||||
"myset",
|
||||
"openai",
|
||||
"text-embedding-3-small",
|
||||
{ "dim": "512" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"method": "herodb_lanceGetEmbeddingConfig",
|
||||
"params": [1, "myset"]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 2) Create a dataset
|
||||
|
||||
Choose a dimension that matches your embedding configuration. For OpenAI text-embedding-3-small without dimension override, typical dimension is 1536; when `dim` is set (e.g., 512), use that. The current API requires an explicit DIM.
|
||||
|
||||
RESP:
|
||||
```bash
|
||||
redis-cli -p 6379 LANCE.CREATE myset DIM 512
|
||||
# → OK
|
||||
```
|
||||
|
||||
JSON-RPC:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 4,
|
||||
"method": "herodb_lanceCreate",
|
||||
"params": [1, "myset", 512]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 3) Store text documents (server-side embedding)
|
||||
|
||||
Provide your id, the text to embed, and optional META fields. The server computes the embedding using the configured provider and stores id/vector/text/meta in the Lance dataset. Upserts by id are supported via delete-then-append semantics.
|
||||
|
||||
RESP:
|
||||
```bash
|
||||
redis-cli -p 6379 LANCE.STORE myset ID doc-1 TEXT "Hello vector world" META title "Hello" category "demo"
|
||||
# → OK
|
||||
```
|
||||
|
||||
JSON-RPC:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 5,
|
||||
"method": "herodb_lanceStoreText",
|
||||
"params": [
|
||||
1,
|
||||
"myset",
|
||||
"doc-1",
|
||||
"Hello vector world",
|
||||
{ "title": "Hello", "category": "demo" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 4) Search with a text query
|
||||
|
||||
Provide a query string; the server embeds it and performs KNN search. Optional: FILTER expression and RETURN subset of fields.
|
||||
|
||||
RESP:
|
||||
```bash
|
||||
# K nearest neighbors for the query text
|
||||
redis-cli -p 6379 LANCE.SEARCH myset K 5 QUERY "greetings to vectors"
|
||||
# → Array of hits: [id, score, [k,v, ...]] pairs, lower score = closer
|
||||
|
||||
# With a filter on meta fields and return only title
|
||||
redis-cli -p 6379 LANCE.SEARCH myset K 3 QUERY "greetings to vectors" FILTER "category = 'demo'" RETURN 1 title
|
||||
```
|
||||
|
||||
JSON-RPC:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 6,
|
||||
"method": "herodb_lanceSearchText",
|
||||
"params": [1, "myset", "greetings to vectors", 5, null, null]
|
||||
}
|
||||
```
|
||||
|
||||
With filter and selected fields:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 7,
|
||||
"method": "herodb_lanceSearchText",
|
||||
"params": [1, "myset", "greetings to vectors", 3, "category = 'demo'", ["title"]]
|
||||
}
|
||||
```
|
||||
|
||||
Response shape:
|
||||
- RESP over redis-cli: an array of hits [id, score, [k, v, ...]].
|
||||
- JSON-RPC returns an object containing the RESP-encoded wire format string or a structured result depending on implementation. See [src/rpc.rs](src/rpc.rs) for details.
|
||||
|
||||
|
||||
## 5) Create an index (placeholder)
|
||||
|
||||
Index creation currently returns OK but is a no-op. It will integrate Lance vector indices in a future update.
|
||||
|
||||
RESP:
|
||||
```bash
|
||||
redis-cli -p 6379 LANCE.CREATEINDEX myset TYPE "ivf_pq" PARAM nlist 100 PARAM pq_m 16
|
||||
# → OK (no-op for now)
|
||||
```
|
||||
|
||||
JSON-RPC:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 8,
|
||||
"method": "herodb_lanceCreateIndex",
|
||||
"params": [1, "myset", "ivf_pq", { "nlist": "100", "pq_m": "16" }]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 6) Inspect datasets
|
||||
|
||||
RESP:
|
||||
```bash
|
||||
# List datasets in current Lance DB
|
||||
redis-cli -p 6379 LANCE.LIST
|
||||
|
||||
# Get dataset info
|
||||
redis-cli -p 6379 LANCE.INFO myset
|
||||
```
|
||||
|
||||
JSON-RPC:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 9,
|
||||
"method": "herodb_lanceList",
|
||||
"params": [1]
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 10,
|
||||
"method": "herodb_lanceInfo",
|
||||
"params": [1, "myset"]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 7) Delete and drop
|
||||
|
||||
RESP:
|
||||
```bash
|
||||
# Delete by id
|
||||
redis-cli -p 6379 LANCE.DEL myset doc-1
|
||||
# → OK
|
||||
|
||||
# Drop the entire dataset
|
||||
redis-cli -p 6379 LANCE.DROP myset
|
||||
# → OK
|
||||
```
|
||||
|
||||
JSON-RPC:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 11,
|
||||
"method": "herodb_lanceDel",
|
||||
"params": [1, "myset", "doc-1"]
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 12,
|
||||
"method": "herodb_lanceDrop",
|
||||
"params": [1, "myset"]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 8) End-to-end example (RESP)
|
||||
|
||||
```bash
|
||||
# 1. Select Lance DB (assume db_id=1 created via RPC)
|
||||
redis-cli -p 6379 SELECT 1
|
||||
|
||||
# 2. Configure embedding provider (OpenAI small model at 512 dims)
|
||||
redis-cli -p 6379 LANCE.EMBEDDING CONFIG SET myset PROVIDER openai MODEL text-embedding-3-small PARAM dim 512
|
||||
|
||||
# 3. Create dataset
|
||||
redis-cli -p 6379 LANCE.CREATE myset DIM 512
|
||||
|
||||
# 4. Store documents
|
||||
redis-cli -p 6379 LANCE.STORE myset ID doc-1 TEXT "The quick brown fox jumps over the lazy dog" META title "Fox" category "animal"
|
||||
redis-cli -p 6379 LANCE.STORE myset ID doc-2 TEXT "A fast auburn fox vaulted a sleepy canine" META title "Fox paraphrase" category "animal"
|
||||
|
||||
# 5. Search
|
||||
redis-cli -p 6379 LANCE.SEARCH myset K 2 QUERY "quick brown fox" RETURN 1 title
|
||||
|
||||
# 6. Dataset info and listing
|
||||
redis-cli -p 6379 LANCE.INFO myset
|
||||
redis-cli -p 6379 LANCE.LIST
|
||||
|
||||
# 7. Delete and drop
|
||||
redis-cli -p 6379 LANCE.DEL myset doc-2
|
||||
redis-cli -p 6379 LANCE.DROP myset
|
||||
```
|
||||
|
||||
|
||||
## 9) End-to-end example (JSON-RPC)
|
||||
|
||||
Assume RPC server on port 8080. Replace ids and ports as needed.
|
||||
|
||||
1) Create Lance DB:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 100,
|
||||
"method": "herodb_createDatabase",
|
||||
"params": ["Lance", { "name": "vectors-db", "storage_path": null, "max_size": null, "redis_version": null }, null]
|
||||
}
|
||||
```
|
||||
|
||||
2) Set embedding config:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 101,
|
||||
"method": "herodb_lanceSetEmbeddingConfig",
|
||||
"params": [1, "myset", "openai", "text-embedding-3-small", { "dim": "512" }]
|
||||
}
|
||||
```
|
||||
|
||||
3) Create dataset:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 102,
|
||||
"method": "herodb_lanceCreate",
|
||||
"params": [1, "myset", 512]
|
||||
}
|
||||
```
|
||||
|
||||
4) Store text:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 103,
|
||||
"method": "herodb_lanceStoreText",
|
||||
"params": [1, "myset", "doc-1", "The quick brown fox jumps over the lazy dog", { "title": "Fox", "category": "animal" }]
|
||||
}
|
||||
```
|
||||
|
||||
5) Search text:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 104,
|
||||
"method": "herodb_lanceSearchText",
|
||||
"params": [1, "myset", "quick brown fox", 2, null, ["title"]]
|
||||
}
|
||||
```
|
||||
|
||||
6) Info/list:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 105,
|
||||
"method": "herodb_lanceInfo",
|
||||
"params": [1, "myset"]
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 106,
|
||||
"method": "herodb_lanceList",
|
||||
"params": [1]
|
||||
}
|
||||
```
|
||||
|
||||
7) Delete/drop:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 107,
|
||||
"method": "herodb_lanceDel",
|
||||
"params": [1, "myset", "doc-1"]
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 108,
|
||||
"method": "herodb_lanceDrop",
|
||||
"params": [1, "myset"]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 10) Operational notes and troubleshooting
|
||||
|
||||
- If using OpenAI and you see “missing API key env”, set:
|
||||
- Standard: `export OPENAI_API_KEY=sk-...`
|
||||
- Azure: `export AZURE_OPENAI_API_KEY=...` and pass `use_azure true`, `azure_endpoint`, `azure_deployment`, `azure_api_version`.
|
||||
- Dimensions mismatch:
|
||||
- Ensure the dataset DIM equals the provider’s embedding dim. For OpenAI text-embedding-3 models, set `PARAM dim 512` (or another supported size) and use that same DIM for `LANCE.CREATE`.
|
||||
- DB 0 restriction:
|
||||
- Lance is not allowed on DB 0. Use db_id >= 1.
|
||||
- Permissions:
|
||||
- Read operations (SEARCH, LIST, INFO) require read permission.
|
||||
- Mutations (CREATE, STORE, CREATEINDEX, DEL, DROP, EMBEDDING CONFIG SET) require readwrite permission.
|
||||
- Backend gating:
|
||||
- On Lance DBs, only LANCE.* commands are accepted (plus basic control).
|
||||
- Current index behavior:
|
||||
- `LANCE.CREATEINDEX` returns OK but is a no-op. Future versions will integrate Lance vector indices.
|
||||
- Implementation files for reference:
|
||||
- [src/lance_store.rs](src/lance_store.rs), [src/cmd.rs](src/cmd.rs), [src/rpc.rs](src/rpc.rs), [src/server.rs](src/server.rs), [src/embedding.rs](src/embedding.rs)
|
200
src/embedding.rs
200
src/embedding.rs
@@ -21,6 +21,12 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::DBError;
|
||||
|
||||
// Networking for OpenAI/Azure
|
||||
use std::time::Duration;
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE, AUTHORIZATION};
|
||||
use serde_json::json;
|
||||
|
||||
/// Provider identifiers. Extend as needed to mirror LanceDB-supported providers.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -122,17 +128,203 @@ impl Embedder for TestHashEmbedder {
|
||||
}
|
||||
}
|
||||
|
||||
//// OpenAI embedder (supports OpenAI and Azure OpenAI via REST)
|
||||
struct OpenAIEmbedder {
|
||||
model: String,
|
||||
dim: usize,
|
||||
client: Client,
|
||||
endpoint: String,
|
||||
auth_header_name: HeaderName,
|
||||
auth_header_value: HeaderValue,
|
||||
use_azure: bool,
|
||||
}
|
||||
|
||||
impl OpenAIEmbedder {
|
||||
fn new_from_config(cfg: &EmbeddingConfig) -> Result<Self, DBError> {
|
||||
// Whether to use Azure OpenAI
|
||||
let use_azure = cfg
|
||||
.get_param_string("use_azure")
|
||||
.map(|s| s.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false);
|
||||
|
||||
// Resolve API key (OPENAI_API_KEY or AZURE_OPENAI_API_KEY by default)
|
||||
let api_key_env = cfg
|
||||
.get_param_string("api_key_env")
|
||||
.unwrap_or_else(|| {
|
||||
if use_azure {
|
||||
"AZURE_OPENAI_API_KEY".to_string()
|
||||
} else {
|
||||
"OPENAI_API_KEY".to_string()
|
||||
}
|
||||
});
|
||||
let api_key = std::env::var(&api_key_env)
|
||||
.map_err(|_| DBError(format!("Missing API key in env '{}'", api_key_env)))?;
|
||||
|
||||
// Resolve endpoint
|
||||
// - Standard OpenAI: https://api.openai.com/v1/embeddings (default) or params["base_url"]
|
||||
// - Azure OpenAI: {azure_endpoint}/openai/deployments/{deployment}/embeddings?api-version=...
|
||||
let endpoint = if use_azure {
|
||||
let base = cfg
|
||||
.get_param_string("azure_endpoint")
|
||||
.ok_or_else(|| DBError("Missing 'azure_endpoint' for Azure OpenAI".into()))?;
|
||||
let deployment = cfg
|
||||
.get_param_string("azure_deployment")
|
||||
.unwrap_or_else(|| cfg.model.clone());
|
||||
let api_version = cfg
|
||||
.get_param_string("azure_api_version")
|
||||
.unwrap_or_else(|| "2023-05-15".to_string());
|
||||
format!(
|
||||
"{}/openai/deployments/{}/embeddings?api-version={}",
|
||||
base.trim_end_matches('/'),
|
||||
deployment,
|
||||
api_version
|
||||
)
|
||||
} else {
|
||||
cfg.get_param_string("base_url")
|
||||
.unwrap_or_else(|| "https://api.openai.com/v1/embeddings".to_string())
|
||||
};
|
||||
|
||||
// Determine expected dimension:
|
||||
// - Prefer params["dim"] or params["dimensions"]
|
||||
// - Else default to 1536 (common for text-embedding-3-small; callers should override if needed)
|
||||
let dim = cfg
|
||||
.get_param_usize("dim")
|
||||
.or_else(|| cfg.get_param_usize("dimensions"))
|
||||
.unwrap_or(1536);
|
||||
|
||||
// Build default headers
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
||||
let (auth_name, auth_val) = if use_azure {
|
||||
let name = HeaderName::from_static("api-key");
|
||||
let val = HeaderValue::from_str(&api_key)
|
||||
.map_err(|_| DBError("Invalid API key header value".into()))?;
|
||||
(name, val)
|
||||
} else {
|
||||
let bearer = format!("Bearer {}", api_key);
|
||||
(AUTHORIZATION, HeaderValue::from_str(&bearer).map_err(|_| DBError("Invalid Authorization header".into()))?)
|
||||
};
|
||||
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.map_err(|e| DBError(format!("Failed to build HTTP client: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
model: cfg.model.clone(),
|
||||
dim,
|
||||
client,
|
||||
endpoint,
|
||||
auth_header_name: auth_name,
|
||||
auth_header_value: auth_val,
|
||||
use_azure,
|
||||
})
|
||||
}
|
||||
|
||||
fn request_many(&self, inputs: &[String]) -> Result<Vec<Vec<f32>>, DBError> {
|
||||
// Compose request body:
|
||||
// - Standard OpenAI: { "model": ..., "input": [...], "dimensions": dim? }
|
||||
// - Azure: { "input": [...], "dimensions": dim? } (model from deployment)
|
||||
let mut body = if self.use_azure {
|
||||
json!({ "input": inputs })
|
||||
} else {
|
||||
json!({ "model": self.model, "input": inputs })
|
||||
};
|
||||
if self.dim > 0 {
|
||||
body.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("dimensions".to_string(), json!(self.dim));
|
||||
}
|
||||
|
||||
let mut req = self.client.post(&self.endpoint);
|
||||
// Add auth header dynamically
|
||||
req = req.header(self.auth_header_name.clone(), self.auth_header_value.clone());
|
||||
|
||||
let resp = req
|
||||
.json(&body)
|
||||
.send()
|
||||
.map_err(|e| DBError(format!("HTTP request failed: {}", e)))?;
|
||||
if !resp.status().is_success() {
|
||||
let code = resp.status();
|
||||
let text = resp.text().unwrap_or_default();
|
||||
return Err(DBError(format!("Embeddings API error {}: {}", code, text)));
|
||||
}
|
||||
let val: serde_json::Value = resp
|
||||
.json()
|
||||
.map_err(|e| DBError(format!("Invalid JSON from embeddings API: {}", e)))?;
|
||||
|
||||
let data = val
|
||||
.get("data")
|
||||
.and_then(|d| d.as_array())
|
||||
.ok_or_else(|| DBError("Embeddings API response missing 'data' array".into()))?;
|
||||
|
||||
let mut out: Vec<Vec<f32>> = Vec::with_capacity(data.len());
|
||||
for item in data {
|
||||
let emb = item
|
||||
.get("embedding")
|
||||
.and_then(|e| e.as_array())
|
||||
.ok_or_else(|| DBError("Embeddings API item missing 'embedding'".into()))?;
|
||||
let mut v: Vec<f32> = Vec::with_capacity(emb.len());
|
||||
for n in emb {
|
||||
let f = n
|
||||
.as_f64()
|
||||
.ok_or_else(|| DBError("Embedding element is not a number".into()))?;
|
||||
v.push(f as f32);
|
||||
}
|
||||
if self.dim > 0 && v.len() != self.dim {
|
||||
return Err(DBError(format!(
|
||||
"Embedding dimension mismatch: expected {}, got {}. Configure 'dim' or 'dimensions' to match output.",
|
||||
self.dim, v.len()
|
||||
)));
|
||||
}
|
||||
out.push(v);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl Embedder for OpenAIEmbedder {
|
||||
fn name(&self) -> String {
|
||||
if self.use_azure {
|
||||
format!("azure-openai:{}", self.model)
|
||||
} else {
|
||||
format!("openai:{}", self.model)
|
||||
}
|
||||
}
|
||||
|
||||
fn dim(&self) -> usize {
|
||||
self.dim
|
||||
}
|
||||
|
||||
fn embed(&self, text: &str) -> Result<Vec<f32>, DBError> {
|
||||
let v = self.request_many(&[text.to_string()])?;
|
||||
Ok(v.into_iter().next().unwrap_or_else(|| vec![0.0; self.dim]))
|
||||
}
|
||||
|
||||
fn embed_many(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, DBError> {
|
||||
if texts.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
self.request_many(texts)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an embedder instance from a config.
|
||||
/// - TestHash: uses params["dim"] or defaults to 64
|
||||
/// - Lance* providers: return an explicit error for now; implementers can wire these up
|
||||
/// - LanceOpenAI: uses OpenAI (or Azure OpenAI) embeddings REST API
|
||||
/// - Other Lance providers can be added similarly
|
||||
pub fn create_embedder(config: &EmbeddingConfig) -> Result<Arc<dyn Embedder>, DBError> {
|
||||
match &config.provider {
|
||||
EmbeddingProvider::TestHash => {
|
||||
let dim = config.get_param_usize("dim").unwrap_or(64);
|
||||
Ok(Arc::new(TestHashEmbedder::new(dim, config.model.clone())))
|
||||
}
|
||||
EmbeddingProvider::LanceFastEmbed => Err(DBError("LanceFastEmbed provider not yet implemented in Rust embedding layer; configure 'test-hash' or implement a Lance-backed provider".into())),
|
||||
EmbeddingProvider::LanceOpenAI => Err(DBError("LanceOpenAI provider not yet implemented in Rust embedding layer; configure 'test-hash' or implement a Lance-backed provider".into())),
|
||||
EmbeddingProvider::LanceOther(p) => Err(DBError(format!("Lance provider '{}' not implemented; configure 'test-hash' or implement a Lance-backed provider", p))),
|
||||
EmbeddingProvider::LanceOpenAI => {
|
||||
let inner = OpenAIEmbedder::new_from_config(config)?;
|
||||
Ok(Arc::new(inner))
|
||||
}
|
||||
EmbeddingProvider::LanceFastEmbed => Err(DBError("LanceFastEmbed provider not yet implemented in Rust embedding layer; configure 'test-hash' or use 'openai'".into())),
|
||||
EmbeddingProvider::LanceOther(p) => Err(DBError(format!("Lance provider '{}' not implemented; configure 'openai' or 'test-hash'", p))),
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user