diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eebaf86d..8ccd6559e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ This driver uses semantic versioning: - A change in the major version (e.g. 1.Y.Z -> 2.0.0) indicates _breaking_ changes that require changes in your code to upgrade. +## [10.1.2] - 2025-06-30 + +### Added + +- Added vector index support + ## [10.1.1] - 2025-01-13 ### Changed @@ -2468,6 +2474,7 @@ For a detailed list of changes between pre-release versions of v7 see the Graph methods now only return the relevant part of the response body. +[10.1.2]: https://github.com/arangodb/arangojs/compare/v10.1.1...v10.1.2 [10.1.1]: https://github.com/arangodb/arangojs/compare/v10.1.0...v10.1.1 [10.1.0]: https://github.com/arangodb/arangojs/compare/v10.0.0...v10.1.0 [10.0.0]: https://github.com/arangodb/arangojs/compare/v9.3.0...v10.0.0 diff --git a/MIGRATING.md b/MIGRATING.md index f9df2657b..3637d97d5 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -618,3 +618,356 @@ using the `db.collections` and `collection.truncate` methods: + db.collections().map((collection) => collection.truncate()) +); ``` + + +## v6 to v7 + +### Configuration changes + +The `db.useDatabase` method has been deprecated in v7. + +Previously the primary use of this method was to set the database name of the +arangojs instance. The database name can now be specified using the +`databaseName` option in the arangojs configuration: + +```diff + const db = new Database({ + url: "http://localhost:8529", ++ databaseName: "my_database", + }); +-db.useDatabase("my_database"); +``` + +### Shared connection pool + +It is now possible to have multiple `Database` objects using the same +underlying connection pool: + +```diff +-const db1 = new Database(); +-db1.useDatabase("database1"); +-const db2 = new Database(); +-db2.useDatabase("database2"); ++const db1 = new Database({ databaseName: "database1" }); ++const db2 = db1.database("database2"); +``` + +### Indexes + +The helper methods for creating specific index types, e.g. `createHashIndex`, +have been removed and replaced with the generic `ensureIndex` method (which +was previously called `createIndex`): + +```diff +-await collection.createGeoIndex(["lat", "lng"]); ++await collection.createIndex({ type: "geo", fields: ["lat", "lng"] }); +``` + +### Document and edge collections + +Version 7 no longer provides different methods for accessing document and edge +collections as both types are now implemented using the same underlying class: + +```diff + const myDocumentCollection = db.collection("documents"); +-const myEdgeCollection = db.edgeCollection("edges"); ++const myEdgeCollection = db.collection("edges"); +``` + +When using TypeScript the collection instances can still be cast to the more +specific `DocumentCollection` and `EdgeCollection` interfaces: + +```ts +interface EdgeType { + color: string; +} +const myEdgeCollection = db.collection("edges") as EdgeCollection; +``` + +### Saving edge documents + +The `save` method no longer supports positional arguments for `_from` and `_to` +values. These now need to be supplied as part of the document data: + +```diff + await edges.save( +- "vertices/start", +- "vertices/end", +- { color: "red" } ++ { _from: "vertices/start", _to: "vertices/end", color: "red" } + ); +``` + +### Accessing documents + +The `edge` method has been removed from the low-level collection API as it was +an alias for the `document` method, which still exists: + +```diff +-const edges = db.edgeCollection("edges"); ++const edges = db.collection("edges"); + +-const edge = await edges.edge("my-edge"); ++const edge = await edges.document("my-edge"); +``` + +Graph vertex and edge collections instead only retain their specific `vertex` +and `edge` methods which access the collection using the high-level graph API: + +```diff + const vertices = graph.vertexCollection("vertices"); +-const vertex = await vertices.document("my-vertex"); ++const vertex = await vertices.vertex("my-vertex"); + + const edges = graph.edgeCollection("edges"); +-const edge = await edges.document("my-edge"); ++const edge = await edges.edge("my-edge"); +``` + +### Graph collections + +Graph vertex and edge collections no longer implement the generic collection +API methods to avoid confusion between operations that are aware of the graph +definition (and can trigger graph-related side-effects) and those that directly +access low-level operations. + +As a convenience both graph collection types still provide access to the +low-level collection interface via the `collection` property: + +```diff + const graphEdges = graph.edgeCollection("edges"); +-const outEdges = graphEdges.outEdges("vertices/start"); ++const outEdges = graphEdges.collection.outEdges("vertices/start"); +``` + +### Cursor methods + +The method `each` is now called `forEach`. The method `hasNext` has been +replaced with a getter. + +The methods `some` and `every` have been removed. These methods previously +allowed iterating over cursor results in order to derive a boolean value by +applying a callback function to each value in the result. + +In most cases these methods can be avoided by writing a more efficient AQL +query: + +```diff +-const cursor = await db.query(aql` +- FOR bowl IN porridges +- RETURN bowl +-`); +-const someJustRight = await cursor.some( +- (bowl) => bowl.temperature < TOO_HOT && bowl.temperature > TOO_COLD +-); ++const cursor = await db.query(aql` ++ FOR bowl IN porridges ++ FILTER bowl.temperature < ${TOO_HOT} ++ FILTER bowl.temperature > ${TOO_COLD} ++ LIMIT 1 ++ RETURN 1 ++`); ++const someJustRight = Boolean(await cursor.next()); +``` + +If this is not an option, the old behavior can be emulated using the `forEach` +method (previously called `each`) instead: + +```diff +-const someJustRight = await cursor.some( +- (bowl) => bowl.temperature < TOO_HOT && bowl.temperature > TOO_COLD +-); ++const someJustRight = !(await cursor.forEach( ++ (bowl) => bowl.temperature === TOO_HOT || bowl.temperature === TOO_COLD ++)); +``` + +### Batch cursor API + +Cursors now provide a low-level API for iterating over the result batches +instead of individual items, which is exposed via the `batches` property. + +The methods `hasMore` and `nextBatch` have been replaced with the getter +`batches.hasMore` and the method `batches.next`: + +```diff +-if (cursor.hasMore()) { +- return await cursor.nextBatch(); ++if (cursor.batches.hasMore) { ++ return await cursor.batches.next(); + } +``` + +### Simple queries + +Collection methods for using simple queries (e.g. `all`, `any` and `list`) +have been deprecated in ArangoDB 3.0 and are now also deprecated in arangojs. + +See the documentation of each method for an example for how to perform the same +query using an AQL query instead. + +Additionally the `list` method now returns a cursor instead of an array. + +### ArangoSearch Views + +The database methods `arangoSearchView` and `createArangoSearchView` have been +renamed to `view` and `createView` respectively as there currently is no other +view type available in ArangoDB: + +```diff +-await db.createArangoSearchView("my-view"); +-const view = db.arangoSearchView("my-view"); ++await db.createView("my-view"); ++const view = db.view("my-view"); +``` + +### Query options + +The `options` argument of `db.query` has been flattened. Options that were +previously nested in an `options` property of that argument are now specified +directly on the argument itself: + +```diff + const cursor = await db.query( + aql` + FOR doc IN ${collection} + RETURN doc + `, + { + cache: false, +- options: { fullCount: true }, ++ fullCount: true, + } + ); +``` + +### Bulk imports + +The default value of the `type` option now depends on the input type instead +of always defaulting to `"auto"`. If you previously relied on the default +value being set to `"auto"`, you may now need to explicitly set this option: + +```diff +-await collection.import(data); ++await collection.import(data, { type: "auto" }); +``` + +### Bulk operations + +The collection method `bulkUpdate` has been removed and the methods +`save`, `update`, `replace` and `remove` no longer accept arrays as input. + +Bulk operations can now be performed using the dedicated methods +`saveAll`, `updateAll`, `replaceAll` and `removeAll`: + +```diff +-await collection.save([{ _key: "a" }, { _key: "b" }]); ++await collection.saveAll([{ _key: "a" }, { _key: "b" }]); +``` + +### Cross-collection operations + +Collection methods no longer accept document IDs from other collections. +Previously passing a document ID referring to a different collection would +result in the collection performing a request to that collection instead. Now +mismatching IDs will result in an error instead: + +```js +const collection1 = db.collection("collection1"); +const doc = await collection1.document("collection2/xyz"); // ERROR +``` + +### Creating graphs + +The signatures of `db.createGraph` and `graph.create` have changed to always +take an array of edge definitions as the first argument instead of taking the +edge definitions as a property of the `properties` argument. + +Additionally the `properties` and `options` arguments have been merged: + +```diff + await graph.create( ++ [{ collection: "edges", from: ["a"], to: ["b"] }], + { +- edgeDefinitions: [{ collection: "edges", from: ["a"], to: ["b"] }], + isSmart: true, +- }, +- { + waitForSync: true, + } + ); +``` + +### Transactions + +The transaction method `run` has been renamed to `step` to make it more obvious +that it is intended to only perform a single "step" of the transaction. + +See the method's documentation for examples of how to use the method correctly. + +Additionally the method `transaction` no longer acts as an alias for +`executeTransaction`: + +```diff +-const result = await db.transaction(collections, action); ++const result = await db.executeTransaction(collections, action); +``` + +### Service development mode + +The methods `enableServiceDevelopmentMode` and `disableServiceDevelopmentMode` +have been replaced with the method `setServiceDevelopmentMode`: + +```diff +-await db.enableServiceDevelopmentMode("/my-foxx"); ++await db.setServiceDevelopmentMode("/my-foxx", true); +``` + +### System services + +The default value of the method `listServices` option `excludeSystem` has been +changed from `false` to `true`: + +```diff +-const services = await db.listServices(true); ++const services = await db.listServices(); +``` + +### Query tracking + +The method `setQueryTracking` has been merged into `queryTracking`: + +```diff +-await db.setQueryTracking({ enabled: true }); ++await db.queryTracking({ enabled: true }); +``` + +### Collection properties + +The method `setProperties` has been merged into `properties`: + +```diff +-await collection.setProperties({ waitForSync: true }); ++await collection.properties({ waitForSync: true }); +``` + +### View properties + +The View method `setProperties` has been renamed to `updateProperties`: + +```diff +-await view.setProperties({ consolidationIntervalMsec: 234 }); ++await view.updateProperties({ consolidationIntervalMsec: 234 }); +``` + +### Truncating collections + +The `db.truncate` method has been removed. The behavior can still be mimicked +using the `db.collections` and `collection.truncate` methods: + +```diff +-await db.truncate(); ++await Promise.all( ++ db.collections().map((collection) => collection.truncate()) ++); +``` \ No newline at end of file diff --git a/package.json b/package.json index 2b5579500..d58b90fa3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "type": "module", "name": "arangojs", - "version": "10.1.1", + "version": "10.1.2", "engines": { "node": ">=20" }, diff --git a/src/collections.ts b/src/collections.ts index 9c2c60be8..0d7a5a846 100644 --- a/src/collections.ts +++ b/src/collections.ts @@ -688,7 +688,7 @@ export interface DocumentCollection< * ```js * const db = new Database(); * const collection = db.collection("some-collection"); - * const result = await collection.setProperties({ waitForSync: true }); + * const result = await collection.properties({ waitForSync: true }); * // the collection will now wait for data being written to disk * // whenever a document is changed * ``` @@ -1466,7 +1466,7 @@ export interface DocumentCollection< import( data: Buffer | Blob | string, options?: documents.ImportDocumentsOptions & { - type?: "documents" | "list" | "auto"; + type?: "" | "documents" | "list" | "auto"; } ): Promise; //#endregion @@ -1612,33 +1612,6 @@ export interface DocumentCollection< indexes.MdiIndexDescription & { isNewlyCreated: boolean } > >; - /** - * Creates a prefixed multi-dimensional index on the collection if it does - * not already exist. - * - * @param options - Options for creating the prefixed multi-dimensional index. - * - * @example - * ```js - * const db = new Database(); - * const collection = db.collection("some-points"); - * // Create a multi-dimensional index for the attributes x, y and z - * await collection.ensureIndex({ - * type: "mdi-prefixed", - * fields: ["x", "y", "z"], - * prefixFields: ["x"], - * fieldValueTypes: "double" - * }); - * ``` - * ``` - */ - ensureIndex( - options: indexes.EnsureMdiPrefixedIndexOptions - ): Promise< - connection.ArangoApiResponse< - indexes.MdiPrefixedIndexDescription & { isNewlyCreated: boolean } - > - >; /** * Creates a prefixed multi-dimensional index on the collection if it does not already exist. * @@ -1712,6 +1685,33 @@ export interface DocumentCollection< indexes.InvertedIndexDescription & { isNewlyCreated: boolean } > >; + /** + * Creates a vector index on the collection if it does not already exist. + * + * @param options - Options for creating the vector index. + * + * @example + * ```js + * const db = new Database(); + * const collection = db.collection("some-collection"); + * await collection.ensureIndex({ + * type: "vector", + * fields: ["embedding"], + * params: { + * metric: "cosine", + * dimension: 128, + * nLists: 100 + * } + * }); + * ``` + */ + ensureIndex( + options: indexes.EnsureVectorIndexOptions + ): Promise< + connection.ArangoApiResponse< + indexes.VectorIndexDescription & { isNewlyCreated: boolean } + > + >; /** * Creates an index on the collection if it does not already exist. * @@ -2296,7 +2296,7 @@ export interface EdgeCollection< import( data: Buffer | Blob | string, options?: documents.ImportDocumentsOptions & { - type?: "documents" | "list" | "auto"; + type?: "" | "documents" | "list" | "auto"; } ): Promise; //#endregion @@ -2875,12 +2875,12 @@ export class Collection< import( data: Buffer | Blob | string | any[], options: documents.ImportDocumentsOptions & { - type?: "documents" | "list" | "auto"; + type?: "" | "documents" | "list" | "auto"; } = {} ): Promise { const search = { ...options, collection: this._name }; if (Array.isArray(data)) { - search.type = Array.isArray(data[0]) ? undefined : "documents"; + search.type = Array.isArray(data[0]) ? "" : "documents"; const lines = data as any[]; data = lines.map((line) => JSON.stringify(line)).join("\r\n") + "\r\n"; } diff --git a/src/cursors.ts b/src/cursors.ts index 1e9c1adc7..4ac018dc6 100644 --- a/src/cursors.ts +++ b/src/cursors.ts @@ -342,9 +342,9 @@ export class BatchCursor { * aql`FOR x IN 1..5 RETURN x`, * { batchSize: 1 } * ); - * console.log(cursor.hasMore); // true + * console.log(cursor.batches.hasMore); // true * await cursor.batches.loadAll(); - * console.log(cursor.hasMore); // false + * console.log(cursor.batches.hasMore); // false * console.log(cursor.hasNext); // true * for await (const item of cursor) { * console.log(item); @@ -417,7 +417,7 @@ export class BatchCursor { * Advances the cursor by applying the `callback` function to each item in * the cursor's remaining result list until the cursor is depleted or * `callback` returns the exact value `false`. Returns a promise that - * evalues to `true` unless the function returned `false`. + * evaluates to `true` unless the function returned `false`. * * **Note**: If the result set spans multiple batches, any remaining batches * will only be fetched on demand. Depending on the cursor's TTL and the @@ -732,14 +732,14 @@ export class BatchCursor { * @example * ```js * const cursor1 = await db.query(aql`FOR x IN 1..5 RETURN x`); - * console.log(cursor1.hasMore); // false + * console.log(cursor1.batches.hasMore); // false * await cursor1.kill(); // no effect * * const cursor2 = await db.query( * aql`FOR x IN 1..5 RETURN x`, * { batchSize: 2 } * ); - * console.log(cursor2.hasMore); // true + * console.log(cursor2.batches.hasMore); // true * await cursor2.kill(); // cursor is depleted * ``` */ @@ -1220,14 +1220,14 @@ export class Cursor { * @example * ```js * const cursor1 = await db.query(aql`FOR x IN 1..5 RETURN x`); - * console.log(cursor1.hasMore); // false + * console.log(cursor1.batches.hasMore); // false * await cursor1.kill(); // no effect * * const cursor2 = await db.query( * aql`FOR x IN 1..5 RETURN x`, * { batchSize: 2 } * ); - * console.log(cursor2.hasMore); // true + * console.log(cursor2.batches.hasMore); // true * await cursor2.kill(); // cursor is depleted * ``` */ diff --git a/src/databases.ts b/src/databases.ts index 4ed4e2d9f..f48a3189f 100644 --- a/src/databases.ts +++ b/src/databases.ts @@ -3067,7 +3067,7 @@ export class Database { * ```js * const db = new Database(); * // track up to 5 slow queries exceeding 5 seconds execution time - * await db.setQueryTracking({ + * await db.queryTracking({ * enabled: true, * trackSlowQueries: true, * maxSlowQueries: 5, diff --git a/src/indexes.ts b/src/indexes.ts index ecd686a11..3a2b0fcad 100644 --- a/src/indexes.ts +++ b/src/indexes.ts @@ -62,7 +62,8 @@ export type EnsureIndexOptions = | EnsureTtlIndexOptions | EnsureMdiIndexOptions | EnsureMdiPrefixedIndexOptions - | EnsureInvertedIndexOptions; + | EnsureInvertedIndexOptions + | EnsureVectorIndexOptions; /** * Shared attributes of all index creation options. @@ -558,6 +559,56 @@ export type InvertedIndexStoredValueOptions = { */ cache?: boolean; }; + +/** + * Options for creating a vector index. + */ +export type EnsureVectorIndexOptions = EnsureIndexOptionsType< + "vector", + [string], + { + /** + * The number of threads to use for indexing. Default is 2. + */ + parallelism?: number; + + /** + * Vector index parameters, following Faiss configuration. + */ + params: { + /** + * Whether to use cosine or l2 (Euclidean) distance. + */ + metric: "cosine" | "l2"; + + /** + * Vector dimension. Must match the length of vectors in documents. + */ + dimension: number; + + /** + * Number of Voronoi cells (centroids) for IVF. Affects accuracy and index build time. + */ + nLists: number; + + /** + * How many neighboring centroids to probe by default. Higher = slower, better recall. + */ + defaultNProbe?: number; + + /** + * Training iterations for index build. Default is 25. + */ + trainingIterations?: number; + + /** + * Advanced Faiss index factory string. + * If not specified, defaults to IVF,Flat. + */ + factory?: string; + }; + } +>; //#endregion //#region IndexDescription @@ -572,7 +623,8 @@ export type IndexDescription = | MdiIndexDescription | MdiPrefixedIndexDescription | InvertedIndexDescription - | SystemIndexDescription; + | SystemIndexDescription + | VectorIndexDescription; /** * An object representing a system index. @@ -862,6 +914,26 @@ export type HiddenIndexDescription = ( */ progress?: number; }; + +/** + * An object representing a vector index. + */ +export type VectorIndexDescription = IndexDescriptionType< + "vector", + [string], + { + parallelism: number; + inBackground: boolean; + params: { + metric: "cosine" | "l2"; + dimension: number; + nLists: number; + defaultNProbe?: number; + trainingIterations?: number; + factory?: string; + }; + } +>; //#endregion //#region Index selectors diff --git a/src/routes.ts b/src/routes.ts index c63f62c47..d7e77fd27 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -142,7 +142,7 @@ export class Route { * ```js * const db = new Database(); * const foxx = db.route("/my-foxx-service"); - * const user = foxx.roue("/users/admin"); + * const user = foxx.route("/users/admin"); * const res = await user.delete(); * ``` */ diff --git a/src/test/06-managing-functions.ts b/src/test/06-managing-functions.ts index 4e8f30dcb..53e097122 100644 --- a/src/test/06-managing-functions.ts +++ b/src/test/06-managing-functions.ts @@ -18,7 +18,7 @@ describe("Managing functions", function () { system.close(); } }); - describe("database.listFunctions", () => { + describe("database.listUserFunctions", () => { it("should be empty per default", async () => { const result = await db.listUserFunctions(); expect(result).to.be.instanceof(Array); @@ -37,7 +37,7 @@ describe("Managing functions", function () { isDeterministic: false, }); }); - describe("database.createFunction", () => { + describe("database.createUserFunction", () => { it("should create a function", async () => { const info = await db.createUserFunction( "myfunctions::temperature::celsiustofahrenheit2", @@ -47,7 +47,7 @@ describe("Managing functions", function () { expect(info).to.have.property("error", false); }); }); - describe("database.dropFunction", () => { + describe("database.dropUserFunction", () => { it("should drop a existing function", async () => { const name = "myfunctions::temperature::celsiustofahrenheit"; await db.createUserFunction( diff --git a/src/test/10-manipulating-collections.ts b/src/test/10-manipulating-collections.ts index f53908c86..2dcdad613 100644 --- a/src/test/10-manipulating-collections.ts +++ b/src/test/10-manipulating-collections.ts @@ -65,7 +65,7 @@ describe("Manipulating collections", function () { expect(info).to.have.property("type", 3); // edge collection }); }); - describe("collection.setProperties", () => { + describe("collection.properties", () => { it("should change properties", async () => { const info = await collection.properties({ waitForSync: true }); expect(info).to.have.property("name", collection.name); diff --git a/src/test/11-managing-indexes.ts b/src/test/11-managing-indexes.ts index f3ca332d8..76a9005ba 100644 --- a/src/test/11-managing-indexes.ts +++ b/src/test/11-managing-indexes.ts @@ -29,6 +29,34 @@ describe("Managing indexes", function () { system.close(); } }); + describe("collection.ensureIndex#vector", () => { + it.skip("should create a vector index", async () => { + // Available in ArangoDB 3.12.4+. + // Only enabled with the --experimental-vector-index startup option. + const data = Array.from({ length: 128 }, (_, cnt) => ({ + _key: `vec${cnt}`, + embedding: Array(128).fill(cnt), + })); + await collection.import(data); + const info = await collection.ensureIndex({ + type: "vector", + fields: ["embedding"], + params: { + metric: "cosine", + dimension: 128, + nLists: 2, + }, + }); + expect(info).to.have.property("id"); + expect(info).to.have.property("type", "vector"); + expect(info).to.have.property("fields"); + expect(info.fields).to.eql(["embedding"]); + expect(info).to.have.property("isNewlyCreated", true); + expect(info).to.have.nested.property("params.metric", "cosine"); + expect(info).to.have.nested.property("params.dimension", 128); + expect(info).to.have.nested.property("params.nLists", 2); + }); + }); describe("collection.ensureIndex#persistent", () => { it("should create a persistent index", async () => { const info = await collection.ensureIndex({ diff --git a/src/test/22-foxx-api.ts b/src/test/22-foxx-api.ts index 51e9c0606..b8370ea33 100644 --- a/src/test/22-foxx-api.ts +++ b/src/test/22-foxx-api.ts @@ -855,7 +855,7 @@ describe("Foxx service", () => { "getServiceDependencies", (mount: string) => db.getServiceDependencies(mount), ], - ["listServiceScripts", (mount: string) => db.getServiceScripts(mount)], + ["getServiceScripts", (mount: string) => db.getServiceScripts(mount)], ["upgradeService", (mount: string) => db.upgradeService(mount, {} as any)], [ "updateServiceConfiguration", @@ -882,11 +882,11 @@ describe("Foxx service", () => { ["uninstallService", (mount: string) => db.uninstallService(mount)], ["downloadService", (mount: string) => db.downloadService(mount)], [ - "enableServiceDevelopmentMode", + "setServiceDevelopmentMode_true", (mount: string) => db.setServiceDevelopmentMode(mount, true), ], [ - "disableServiceDevelopmentMode", + "setServiceDevelopmentMode_false", (mount: string) => db.setServiceDevelopmentMode(mount, false), ], [ diff --git a/src/test/24-accessing-views.ts b/src/test/24-accessing-views.ts index 0cdc063bd..663bc6b17 100644 --- a/src/test/24-accessing-views.ts +++ b/src/test/24-accessing-views.ts @@ -21,7 +21,7 @@ describe("Accessing views", function () { system.close(); } }); - describe("database.arangoSearchView", () => { + describe("database.view", () => { it("returns a View instance for the view", () => { const name = "potato"; const view = db.view(name);