fix(node): pin automatic search to table revision

This commit is contained in:
Gatefixer
2026-08-08 13:39:02 +00:00
parent db3b448917
commit ebfacfa3fc
10 changed files with 672 additions and 23 deletions
+62 -1
View File
@@ -11,10 +11,13 @@ import * as arrow17 from "apache-arrow-17";
import * as arrow18 from "apache-arrow-18";
import {
AutoQuery,
Connection,
MatchQuery,
PhraseQuery,
Query,
Table,
VectorQuery,
connect,
tokenize,
} from "../lancedb";
@@ -1786,7 +1789,13 @@ describe("automatic search schema consistency", () => {
);
await replacement.createIndex("text", { config: Index.fts() });
const rows = await stale.search("hello").toArray();
const search = stale.search("hello");
expect(search).toBeInstanceOf(AutoQuery);
expect(search).not.toBeInstanceOf(Query);
expect(search).not.toBeInstanceOf(VectorQuery);
expect("nprobes" in search).toBe(false);
const rows = await search.toArray();
expect(rows[0].text).toBe("after hello");
expect((await stale.schema()).metadata.has("embedding_functions")).toBe(
false,
@@ -1828,6 +1837,58 @@ describe("automatic search schema consistency", () => {
second.close();
}
});
it("pins automatic search while computing an embedding", async () => {
let markStarted!: () => void;
let releaseEmbedding!: () => void;
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const released = new Promise<void>((resolve) => {
releaseEmbedding = resolve;
});
class BlockingEmbedding extends SchemaRefreshEmbedding {
async computeQueryEmbeddings(value: string) {
markStarted();
await released;
return [value.length, 1];
}
}
register("schema-refresh-blocking")(BlockingEmbedding);
const func = new BlockingEmbedding();
const schema = LanceSchema({
text: func.sourceField(new Utf8()),
vector: func.vectorField(),
});
const first = await connect(tmpDir.name, { readConsistencyInterval: 0 });
const second = await connect(tmpDir.name, { readConsistencyInterval: 0 });
try {
const table = await first.createTable(
"docs",
[{ text: "hello before" }],
{ schema },
);
const pending = table.search("hello").toArray();
await started;
const replacement = await second.createTable(
"docs",
[{ text: "hello after" }],
{ mode: "overwrite" },
);
await replacement.createIndex("text", { config: Index.fts() });
releaseEmbedding();
expect((await pending)[0].text).toBe("hello before");
} finally {
releaseEmbedding();
first.close();
second.close();
}
});
});
describe("schema evolution", function () {
+1
View File
@@ -94,6 +94,7 @@ export {
} from "./native.js";
export {
AutoQuery,
ExecutableQuery,
Query,
QueryBase,
+25 -11
View File
@@ -788,6 +788,24 @@ export class TakeQuery extends QueryBase<NativeTakeQuery> {
}
}
/**
* A builder for automatic string searches.
*
* Automatic search determines whether to use full-text or vector search from
* the table revision selected for execution. This builder exposes the common
* operations supported by both query families.
*
* @hideconstructor
*/
export class AutoQuery extends StandardQueryBase<
NativeQuery | NativeVectorQuery
> {
/** @hidden */
constructor(inner: Promise<NativeQuery | NativeVectorQuery>) {
super(inner);
}
}
/** A builder for LanceDB queries.
*
* @see {@link Table#query}, {@link Table#search}
@@ -804,24 +822,20 @@ export class Query extends StandardQueryBase<NativeQuery> {
/** @hidden */
static autoSearch(
tbl: NativeTable,
tbl: Promise<NativeTable>,
query: string,
vector: Promise<Awaited<IntoVector> | undefined>,
vector: (tbl: NativeTable) => Promise<Awaited<IntoVector> | undefined>,
columns?: string[],
): VectorQuery {
const nativeQuery = vector.then((resolved) => {
): AutoQuery {
const nativeQuery = Promise.resolve(tbl).then(async (tbl) => {
const resolved = await vector(tbl);
const inner = tbl.query();
if (resolved === undefined) {
inner.fullTextSearch({
query,
columns: columns ?? null,
});
// Native Query and NativeVectorQuery share all operations exposed by
// Table.search's union return type. Keeping the wrapper as VectorQuery
// also preserves the existing runtime type when auto search selects a
// vector query after the asynchronous schema check.
return inner as unknown as NativeVectorQuery;
return inner;
}
const raw = Array.isArray(resolved)
@@ -833,7 +847,7 @@ export class Query extends StandardQueryBase<NativeQuery> {
return inner.nearestTo(Float32Array.from(resolved as number[]));
});
return new VectorQuery(nativeQuery);
return new AutoQuery(nativeQuery);
}
/**
+17 -9
View File
@@ -40,6 +40,7 @@ import {
Table as _NativeTable,
} from "./native";
import {
AutoQuery,
FullTextQuery,
Query,
TakeQuery,
@@ -510,7 +511,7 @@ export abstract class Table {
query: string | IntoVector | MultiVector | FullTextQuery,
queryType?: string,
ftsColumns?: string | string[],
): VectorQuery | Query;
): VectorQuery | Query | AutoQuery;
/**
* Search the table with a given query vector.
*
@@ -834,10 +835,11 @@ export class LocalTable extends Table {
return this.inner.display();
}
private async getEmbeddingFunctions(): Promise<
Map<string, EmbeddingFunctionConfig>
> {
const schema = await this.schema();
private async getEmbeddingFunctions(
inner: _NativeTable = this.inner,
): Promise<Map<string, EmbeddingFunctionConfig>> {
const schemaBuf = await inner.schema();
const schema = tableFromIPC(schemaBuf).schema;
const registry = getRegistry();
return registry.parseFunctions(schema.metadata);
}
@@ -1019,7 +1021,7 @@ export class LocalTable extends Table {
query: string | IntoVector | MultiVector | FullTextQuery,
queryType: string = "auto",
ftsColumns?: string | string[],
): VectorQuery | Query {
): VectorQuery | Query | AutoQuery {
if (typeof query !== "string" && !instanceOfFullTextQuery(query)) {
if (queryType === "fts") {
throw new Error("Cannot perform full text search on a vector query");
@@ -1041,7 +1043,8 @@ export class LocalTable extends Table {
}
if (queryType === "auto" && typeof query === "string") {
const vector = this.getEmbeddingFunctions().then(async (functions) => {
const vector = async (snapshot: _NativeTable) => {
const functions = await this.getEmbeddingFunctions(snapshot);
// TODO: Support multiple embedding functions
const embeddingFunc: EmbeddingFunctionConfig | undefined = functions
.values()
@@ -1050,11 +1053,16 @@ export class LocalTable extends Table {
return undefined;
}
return await embeddingFunc.function.computeQueryEmbeddings(query);
});
};
const columns =
typeof ftsColumns === "string" ? [ftsColumns] : ftsColumns;
return Query.autoSearch(this.inner, query, vector, columns);
return Query.autoSearch(
this.inner.checkoutCurrent(),
query,
vector,
columns,
);
}
const queryPromise = this.getEmbeddingFunctions().then(
+6
View File
@@ -472,6 +472,12 @@ impl Table {
.default_error()
}
#[napi(catch_unwind)]
pub async fn checkout_current(&self) -> napi::Result<Self> {
let table = self.inner_ref()?.checkout_current().await.default_error()?;
Ok(Self::new(table))
}
#[napi(catch_unwind)]
pub async fn checkout(&self, version: i64) -> napi::Result<()> {
self.inner_ref()?