mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-13 07:12:27 +00:00
Move the public website pages into `docs/web` beside the SDK, with
shared navigation and executable example sources in `docs/web-tests`.
The combined website is assembled by
[lancedb/docs#355](https://github.com/lancedb/docs/pull/355), with six
Enterprise replacements from
[lancedb/sophon#7681](https://github.com/lancedb/sophon/pull/7681).
This includes Function guidance, runnable Quickstarts, regenerated
snippets, production contact/demo fixes, the legacy SQL route, and the
LeRobot redirect. LeRobot remains attributed to Geneva; all 34 Geneva
pages remain in the docs repository until Function officially launches.
Materialized-view guidance follows the full rebuild refresh introduced
by #4373: local refresh writes the complete result, has no `full`
argument, supports `source_version`, and no longer requires stable row
IDs. Deployment SQL refresh options and stable-ID setup are qualified by
version/configuration. Function input guidance distinguishes ordinary
stored columns from computed inputs supported by newer deployments.
**Validation:** independently accepted local checks include 95 Python
tests (real Enterprise excluded), eight TypeScript tests, Rust
Quickstart, snippet parity, typos, deterministic assembly, Mint
validation and links/anchors/redirects. The combined baseline retains
all 181 production routes and 17 legacy redirects. Examples ran locally
against source-built Python SDK 0.40.0b13; existing GitHub workflows do
not run `docs/web-tests`. New main at `069b2f7d` is 0.41.0b0 and adds
computed Function inputs; that SDK was not exercised by the accepted
tests. Latest prose corrections receive focused site validation.
**Merge/rollout:** merge this and #7681 before #355's combined CI, which
reads producer `main`. This is migration infrastructure: section
overlays, source-owned skills and automatic versioned paired releases
remain follow-ups. The current manual publisher is transitional. Hosted
validation, a supported SDK/Enterprise release pair and a real
Enterprise walkthrough remain pending.
Production Mintlify watches `lancedb/docs:deploy-freeze`, directory
`docs`; preserve that source through review. The separate SDK GitHub
Pages reference workflow rebuilds on main merges.
**Updated-head CI (9a50f8db):**
[Typos](https://github.com/lancedb/lancedb/actions/runs/37654185443/job/112904990558),
[PR
title](https://github.com/lancedb/lancedb/actions/runs/37654179507/job/112904974595)
and
[Aikido](https://app.aikido.dev/featurebranch/scan/215386145?groupId=74229)
pass. Critical findings in the example lockfiles were patched: all Sharp
copies use 0.35.5, protobufjs 7.6.6, tar 7.5.22, quinn-proto 0.11.19,
zerovec 0.11.8 and zerovec-derive 0.11.6. The SDK's product dependencies
are unchanged. The revised lockfiles pass eight TypeScript examples,
HuggingFace decode/resize/crop/rescale/normalize plus AVIF roundtrip,
and Rust Quickstart. Aikido passing does not mean every advisory is
cleared; npm still reports 29 high and five moderate advisories in the
example dependency graph. No scanner bypass or suppression was added.
The Function prose correction passes combined/standalone Mint,
deterministic assembly, routes and assets. The revised 325-file site
checksum is
`175bec06864abb400dd292e667ba93448c0479b8359fd7bf6146afabf9040647`. This
PR remains open for independent review; further merges are on hold.
---------
Co-authored-by: Brendan Clement <brendan.clement@lancedb.com>
356 lines
11 KiB
TypeScript
356 lines
11 KiB
TypeScript
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
import { expect, test } from "@jest/globals";
|
|
// --8<-- [start:import]
|
|
import * as lancedb from "@lancedb/lancedb";
|
|
// --8<-- [end:import]
|
|
// --8<-- [start:import_bin_util]
|
|
import { Field, FixedSizeList, Int32, Schema, Uint8 } from "apache-arrow";
|
|
// --8<-- [end:import_bin_util]
|
|
import { Float32, Struct } from "apache-arrow";
|
|
import { withTempDirectory } from "./util.ts";
|
|
|
|
async function buildIndexedTable(db: lancedb.Connection, name: string) {
|
|
const data = Array.from({ length: 512 }, (_, i) => ({
|
|
vector: Array.from({ length: 128 }, () => Math.random()),
|
|
id: i,
|
|
}));
|
|
const tbl = await db.createTable(name, data, { mode: "overwrite" });
|
|
await tbl.createIndex("vector", {
|
|
config: lancedb.Index.ivfPq({ numPartitions: 4, numSubVectors: 8 }),
|
|
});
|
|
return tbl;
|
|
}
|
|
|
|
test("vector search", async () => {
|
|
await withTempDirectory(async (databaseDir) => {
|
|
{
|
|
const db = await lancedb.connect(databaseDir);
|
|
|
|
const data = Array.from({ length: 10_000 }, (_, i) => ({
|
|
vector: Array(128).fill(i),
|
|
id: `${i}`,
|
|
}));
|
|
|
|
await db.createTable("my_vectors", data);
|
|
}
|
|
|
|
// --8<-- [start:search1]
|
|
const db = await lancedb.connect(databaseDir);
|
|
const tbl = await db.openTable("my_vectors");
|
|
|
|
const results1 = await tbl.search(Array(128).fill(1.2)).limit(10).toArray();
|
|
// --8<-- [end:search1]
|
|
expect(results1.length).toBe(10);
|
|
|
|
// --8<-- [start:search2]
|
|
const results2 = await (
|
|
tbl.search(Array(128).fill(1.2)) as lancedb.VectorQuery
|
|
)
|
|
.distanceType("cosine")
|
|
.limit(10)
|
|
.toArray();
|
|
// --8<-- [end:search2]
|
|
expect(results2.length).toBe(10);
|
|
|
|
// --8<-- [start:distance_range]
|
|
const results3 = await (
|
|
tbl.search(Array(128).fill(1.2)) as lancedb.VectorQuery
|
|
)
|
|
.distanceType("cosine")
|
|
.distanceRange(0.1, 0.2)
|
|
.limit(10)
|
|
.toArray();
|
|
// --8<-- [end:distance_range]
|
|
for (const r of results3) {
|
|
expect(r.distance).toBeGreaterThanOrEqual(0.1);
|
|
expect(r.distance).toBeLessThan(0.2);
|
|
}
|
|
|
|
{
|
|
// --8<-- [start:ingest_binary_data]
|
|
const schema = new Schema([
|
|
new Field("id", new Int32(), true),
|
|
new Field("vec", new FixedSizeList(32, new Field("item", new Uint8()))),
|
|
]);
|
|
const data = lancedb.makeArrowTable(
|
|
Array(1_000)
|
|
.fill(0)
|
|
.map((_, i) => ({
|
|
// the 256 bits would be store in 32 bytes,
|
|
// if your data is already in this format, you can skip the packBits step
|
|
id: i,
|
|
vec: lancedb.packBits(Array(256).fill(i % 2)),
|
|
})),
|
|
{ schema: schema },
|
|
);
|
|
|
|
const tbl = await db.createTable("binary_table", data);
|
|
await tbl.createIndex("vec", {
|
|
config: lancedb.Index.ivfFlat({
|
|
numPartitions: 10,
|
|
distanceType: "hamming",
|
|
}),
|
|
});
|
|
// --8<-- [end:ingest_binary_data]
|
|
|
|
// --8<-- [start:search_binary_data]
|
|
const query = Array(32)
|
|
.fill(1)
|
|
.map(() => Math.floor(Math.random() * 255));
|
|
const results = await tbl.query().nearestTo(query).limit(10).toArrow();
|
|
// --8<-- [end:search_binary_data]
|
|
expect(results.numRows).toBe(10);
|
|
}
|
|
});
|
|
});
|
|
|
|
test("vector search docs snippets", async () => {
|
|
await withTempDirectory(async (databaseDir) => {
|
|
const db = await lancedb.connect(databaseDir);
|
|
|
|
// ---- Nested vector column: inference and explicit selection ----
|
|
{
|
|
const nestedSchema = new Schema([
|
|
new Field("id", new Int32()),
|
|
new Field(
|
|
"image",
|
|
new Struct([
|
|
new Field(
|
|
"embedding",
|
|
new FixedSizeList(2, new Field("item", new Float32(), true)),
|
|
),
|
|
]),
|
|
),
|
|
]);
|
|
const arrowTbl = lancedb.makeArrowTable(
|
|
[{ id: 0, image: { embedding: [0.0, 1.0] } }],
|
|
{ schema: nestedSchema },
|
|
);
|
|
await db.createTable("nested", arrowTbl, { mode: "overwrite" });
|
|
|
|
// --8<-- [start:select_vector_column]
|
|
const table = await db.openTable("nested");
|
|
|
|
// Inferred: LanceDB finds the single nested vector leaf automatically.
|
|
await table.query().nearestTo([0.0, 1.0]).limit(1).toArray();
|
|
|
|
// Explicit: required when more than one vector column matches.
|
|
await table
|
|
.query()
|
|
.nearestTo([0.0, 1.0])
|
|
.column("image.embedding")
|
|
.limit(1)
|
|
.toArray();
|
|
// --8<-- [end:select_vector_column]
|
|
}
|
|
|
|
// ---- Index a nested vector column ----
|
|
{
|
|
const dim = 16;
|
|
const nestedSchema = new Schema([
|
|
new Field("id", new Int32()),
|
|
new Field(
|
|
"image",
|
|
new Struct([
|
|
new Field(
|
|
"embedding",
|
|
new FixedSizeList(dim, new Field("item", new Float32(), true)),
|
|
),
|
|
]),
|
|
),
|
|
]);
|
|
const rows = Array.from({ length: 512 }, (_, i) => ({
|
|
id: i,
|
|
image: { embedding: Array.from({ length: dim }, () => Math.random()) },
|
|
}));
|
|
const arrowTbl = lancedb.makeArrowTable(rows, { schema: nestedSchema });
|
|
const table = await db.createTable("nested_index", arrowTbl, {
|
|
mode: "overwrite",
|
|
});
|
|
|
|
// --8<-- [start:index_nested_column]
|
|
await table.createIndex("image.embedding");
|
|
// --8<-- [end:index_nested_column]
|
|
}
|
|
|
|
// ---- Indexed queries: refine, fast search, bypass index ----
|
|
{
|
|
const table = await buildIndexedTable(db, "ann_table");
|
|
const embedding = Array.from({ length: 128 }, () => Math.random());
|
|
|
|
// --8<-- [start:exact_vs_approximate]
|
|
// Indexed ANN search without refinement (fast, approximate `_distance`)
|
|
const fastResults = await (table.search(embedding) as lancedb.VectorQuery)
|
|
.limit(10)
|
|
.toArray();
|
|
|
|
// Recompute distances on full vectors for reranked candidates
|
|
const exactDistanceResults = await (
|
|
table.search(embedding) as lancedb.VectorQuery
|
|
)
|
|
.limit(10)
|
|
.refineFactor(1)
|
|
.toArray();
|
|
|
|
// Rerank a larger candidate set for better recall (higher latency)
|
|
const higherRecallResults = await (
|
|
table.search(embedding) as lancedb.VectorQuery
|
|
)
|
|
.limit(10)
|
|
.refineFactor(20)
|
|
.toArray();
|
|
// --8<-- [end:exact_vs_approximate]
|
|
expect(fastResults.length).toBe(10);
|
|
expect(exactDistanceResults.length).toBe(10);
|
|
expect(higherRecallResults.length).toBe(10);
|
|
|
|
// --8<-- [start:fast_search]
|
|
await table
|
|
.query()
|
|
.nearestTo(embedding)
|
|
.fastSearch()
|
|
.limit(5)
|
|
.toArray();
|
|
// --8<-- [end:fast_search]
|
|
|
|
// --8<-- [start:bypass_vector_index]
|
|
await table
|
|
.query()
|
|
.nearestTo(embedding)
|
|
.bypassVectorIndex()
|
|
.limit(5)
|
|
.toArray();
|
|
// --8<-- [end:bypass_vector_index]
|
|
}
|
|
|
|
// ---- Brute force search (no index) ----
|
|
{
|
|
const data = Array.from({ length: 64 }, (_, i) => ({
|
|
vector: Array(128).fill(i),
|
|
id: `${i}`,
|
|
}));
|
|
await db.createTable("my_vectors", data, { mode: "overwrite" });
|
|
|
|
// --8<-- [start:brute_force_search]
|
|
const tbl = await db.openTable("my_vectors");
|
|
|
|
const results1 = await tbl.search(Array(128).fill(1.2)).limit(3).toArray();
|
|
// --8<-- [end:brute_force_search]
|
|
expect(results1.length).toBe(3);
|
|
}
|
|
|
|
// ---- Binary (hamming) vector search ----
|
|
{
|
|
const schema = new Schema([
|
|
new Field("id", new Int32(), true),
|
|
new Field("vector", new FixedSizeList(32, new Field("item", new Uint8()))),
|
|
]);
|
|
const data = lancedb.makeArrowTable(
|
|
Array(1000)
|
|
.fill(0)
|
|
.map((_, i) => ({
|
|
// the 256 bits are stored in 32 bytes; if your data is already in
|
|
// this format, you can skip the packBits step
|
|
id: i,
|
|
vector: lancedb.packBits(Array(256).fill(i % 2)),
|
|
})),
|
|
{ schema },
|
|
);
|
|
|
|
// --8<-- [start:binary_search]
|
|
const tbl = await db.createTable("binary_vectors", data, {
|
|
mode: "overwrite",
|
|
});
|
|
await tbl.createIndex("vector", {
|
|
config: lancedb.Index.ivfFlat({
|
|
numPartitions: 10,
|
|
distanceType: "hamming",
|
|
}),
|
|
});
|
|
|
|
const query = Array(32)
|
|
.fill(1)
|
|
.map(() => Math.floor(Math.random() * 255));
|
|
const results = await tbl.query().nearestTo(query).limit(10).toArray();
|
|
// --8<-- [end:binary_search]
|
|
expect(results.length).toBeLessThanOrEqual(10);
|
|
}
|
|
|
|
// ---- Enterprise-style prefilter / postfilter / batch search ----
|
|
{
|
|
const dimensions = 768;
|
|
const rows = Array.from({ length: 50 }, (_, i) => ({
|
|
vector: Array.from({ length: dimensions }, () => Math.random() * 2 - 1),
|
|
text: `story ${i}`,
|
|
keywords: `kw${i}`,
|
|
label: i % 4,
|
|
}));
|
|
await db.createTable("lancedb-enterprise-quickstart", rows, {
|
|
mode: "overwrite",
|
|
});
|
|
|
|
{
|
|
// --8<-- [start:vector_search_prefilter]
|
|
// Generate a sample 768-dimension embedding vector (typical for BERT-based models)
|
|
// In real applications, you would get this from an embedding model
|
|
const dimensions = 768;
|
|
const queryEmbed = Array.from(
|
|
{ length: dimensions },
|
|
() => Math.random() * 2 - 1,
|
|
);
|
|
|
|
// Open table and perform search
|
|
const tableName = "lancedb-enterprise-quickstart";
|
|
const table = await db.openTable(tableName);
|
|
|
|
// Vector search with filters (pre-filtering is the default)
|
|
const vectorResults = await table
|
|
.search(queryEmbed)
|
|
.where("label > 2")
|
|
.select(["text", "keywords", "label"])
|
|
.limit(5)
|
|
.toArray();
|
|
|
|
console.log("Search results (with pre-filtering):");
|
|
console.log(vectorResults);
|
|
// --8<-- [end:vector_search_prefilter]
|
|
|
|
// --8<-- [start:vector_search_postfilter]
|
|
const vectorResultsWithPostFilter = await (
|
|
table.search(queryEmbed) as lancedb.VectorQuery
|
|
)
|
|
.where("label > 2")
|
|
.postfilter()
|
|
.select(["text", "keywords", "label"])
|
|
.limit(5)
|
|
.toArray();
|
|
|
|
console.log("Vector search results with post-filter:");
|
|
console.log(vectorResultsWithPostFilter);
|
|
// --8<-- [end:vector_search_postfilter]
|
|
|
|
// --8<-- [start:batch_search]
|
|
// Batch query
|
|
console.log("Performing batch vector search...");
|
|
const batchSize = 5;
|
|
const queryVectors = Array.from({ length: batchSize }, () =>
|
|
Array.from({ length: dimensions }, () => Math.random() * 2 - 1),
|
|
);
|
|
let batchQuery = table.search(queryVectors[0]) as lancedb.VectorQuery;
|
|
for (let i = 1; i < batchSize; i++) {
|
|
batchQuery = batchQuery.addQueryVector(queryVectors[i]);
|
|
}
|
|
const batchResults = await batchQuery
|
|
.select(["text", "keywords", "label"])
|
|
.limit(5)
|
|
.toArray();
|
|
console.log("Batch vector search results:");
|
|
console.log(batchResults);
|
|
// --8<-- [end:batch_search]
|
|
expect(batchResults.length).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
});
|
|
});
|