;
+ }
+
+ /**
+ * Define the metrics to be returned for a BOOL or BOOL_ARRAY property when aggregating over a collection.
+ *
+ * If none of the arguments are provided then all metrics will be returned.
+ *
+ * @param {('count' | 'percentageFalse' | 'percentageTrue' | 'totalFalse' | 'totalTrue')[]} metrics The metrics to return.
+ * @returns {MetricsBoolean} The metrics for the property.
+ */
+ public boolean(
+ metrics?: ('count' | 'percentageFalse' | 'percentageTrue' | 'totalFalse' | 'totalTrue')[]
+ ): MetricsBoolean
{
+ if (metrics === undefined || metrics.length === 0) {
+ metrics = ['count', 'percentageFalse', 'percentageTrue', 'totalFalse', 'totalTrue'];
+ }
+ return {
+ ...this.map(metrics),
+ kind: 'boolean',
+ propertyName: this.propertyName,
+ };
+ }
+
+ /**
+ * Define the metrics to be returned for a DATE or DATE_ARRAY property when aggregating over a collection.
+ *
+ * If none of the arguments are provided then all metrics will be returned.
+ *
+ * @param {('count' | 'maximum' | 'median' | 'minimum' | 'mode')[]} metrics The metrics to return.
+ * @returns {MetricsDate
} The metrics for the property.
+ */
+ public date(metrics?: ('count' | 'maximum' | 'median' | 'minimum' | 'mode')[]): MetricsDate
{
+ if (metrics === undefined || metrics.length === 0) {
+ metrics = ['count', 'maximum', 'median', 'minimum', 'mode'];
+ }
+ return {
+ ...this.map(metrics),
+ kind: 'date',
+ propertyName: this.propertyName,
+ };
+ }
+
+ /**
+ * Define the metrics to be returned for an INT or INT_ARRAY property when aggregating over a collection.
+ *
+ * If none of the arguments are provided then all metrics will be returned.
+ *
+ * @param {('count' | 'maximum' | 'mean' | 'median' | 'minimum' | 'mode' | 'sum')[]} metrics The metrics to return.
+ * @returns {MetricsInteger
} The metrics for the property.
+ */
+ public integer(
+ metrics?: ('count' | 'maximum' | 'mean' | 'median' | 'minimum' | 'mode' | 'sum')[]
+ ): MetricsInteger
{
+ if (metrics === undefined || metrics.length === 0) {
+ metrics = ['count', 'maximum', 'mean', 'median', 'minimum', 'mode', 'sum'];
+ }
+ return {
+ ...this.map(metrics),
+ kind: 'integer',
+ propertyName: this.propertyName,
+ };
+ }
+
+ /**
+ * Define the metrics to be returned for a NUMBER or NUMBER_ARRAY property when aggregating over a collection.
+ *
+ * If none of the arguments are provided then all metrics will be returned.
+ *
+ * @param {('count' | 'maximum' | 'mean' | 'median' | 'minimum' | 'mode' | 'sum')[]} metrics The metrics to return.
+ * @returns {MetricsNumber
} The metrics for the property.
+ */
+ public number(
+ metrics?: ('count' | 'maximum' | 'mean' | 'median' | 'minimum' | 'mode' | 'sum')[]
+ ): MetricsNumber
{
+ if (metrics === undefined || metrics.length === 0) {
+ metrics = ['count', 'maximum', 'mean', 'median', 'minimum', 'mode', 'sum'];
+ }
+ return {
+ ...this.map(metrics),
+ kind: 'number',
+ propertyName: this.propertyName,
+ };
+ }
+
+ // public reference(metrics: 'pointingTo'[]): MetricsReference {
+ // return {
+ // ...this.map(metrics),
+ // kind: 'reference',
+ // propertyName: this.propertyName,
+ // };
+ // }
+
+ /**
+ * Define the metrics to be returned for a TEXT or TEXT_ARRAY property when aggregating over a collection.
+ *
+ * If none of the arguments are provided then all metrics will be returned.
+ *
+ * @param {('count' | 'topOccurrencesOccurs' | 'topOccurrencesValue')[]} metrics The metrics to return.
+ * @param {number} [minOccurrences] The how many top occurrences to return.
+ * @returns {MetricsText} The metrics for the property.
+ */
+ public text(
+ metrics?: ('count' | 'topOccurrencesOccurs' | 'topOccurrencesValue')[],
+ minOccurrences?: number
+ ): MetricsText
{
+ if (metrics === undefined || metrics.length === 0) {
+ metrics = ['count', 'topOccurrencesOccurs', 'topOccurrencesValue'];
+ }
+ return {
+ count: metrics.includes('count'),
+ topOccurrences:
+ metrics.includes('topOccurrencesOccurs') || metrics.includes('topOccurrencesValue')
+ ? {
+ occurs: metrics.includes('topOccurrencesOccurs'),
+ value: metrics.includes('topOccurrencesValue'),
+ }
+ : undefined,
+ minOccurrences,
+ kind: 'text',
+ propertyName: this.propertyName,
+ };
+ }
+}
+
+type KindToAggregateType = K extends 'text'
+ ? AggregateText
+ : K extends 'date'
+ ? AggregateDate
+ : K extends 'integer'
+ ? AggregateNumber
+ : K extends 'number'
+ ? AggregateNumber
+ : K extends 'boolean'
+ ? AggregateBoolean
+ : K extends 'reference'
+ ? AggregateReference
+ : never;
+
+export type AggregateType = AggregateBoolean | AggregateDate | AggregateNumber | AggregateText;
+
+export type AggregateResult | undefined = undefined> = {
+ properties: T extends undefined
+ ? Record
+ : M extends MetricsInput[]
+ ? {
+ [K in M[number] as K['propertyName']]: KindToAggregateType;
+ }
+ : M extends MetricsInput
+ ? {
+ [K in M as K['propertyName']]: KindToAggregateType;
+ }
+ : undefined;
+ totalCount: number;
+};
+
+export type AggregateGroupByResult<
+ T,
+ M extends PropertiesMetrics | undefined = undefined
+> = AggregateResult & {
+ groupedBy: {
+ prop: string;
+ value: string;
+ };
+};
+
+class AggregateManager implements Aggregate {
+ connection: Connection;
+ groupBy: AggregateGroupBy;
+ name: string;
+ dbVersionSupport: DbVersionSupport;
+ consistencyLevel?: ConsistencyLevel;
+ tenant?: string;
+
+ private constructor(
+ connection: Connection,
+ name: string,
+ dbVersionSupport: DbVersionSupport,
+ consistencyLevel?: ConsistencyLevel,
+ tenant?: string
+ ) {
+ this.connection = connection;
+ this.name = name;
+ this.dbVersionSupport = dbVersionSupport;
+ this.consistencyLevel = consistencyLevel;
+ this.tenant = tenant;
+
+ this.groupBy = {
+ nearImage: async | undefined = undefined>(
+ image: string | Buffer,
+ opts?: AggregateGroupByNearOptions
+ ): Promise[]> => {
+ const builder = this.base(opts?.returnMetrics, opts?.filters, opts?.groupBy).withNearImage({
+ image: await toBase64FromMedia(image),
+ certainty: opts?.certainty,
+ distance: opts?.distance,
+ targetVectors: opts?.targetVector ? [opts.targetVector] : undefined,
+ });
+ if (opts?.objectLimit) {
+ builder.withObjectLimit(opts?.objectLimit);
+ }
+ return this.doGroupBy(builder);
+ },
+ nearObject: | undefined = undefined>(
+ id: string,
+ opts?: AggregateGroupByNearOptions
+ ): Promise[]> => {
+ const builder = this.base(opts?.returnMetrics, opts?.filters, opts?.groupBy).withNearObject({
+ id: id,
+ certainty: opts?.certainty,
+ distance: opts?.distance,
+ targetVectors: opts?.targetVector ? [opts.targetVector] : undefined,
+ });
+ if (opts?.objectLimit) {
+ builder.withObjectLimit(opts.objectLimit);
+ }
+ return this.doGroupBy(builder);
+ },
+ nearText: | undefined = undefined>(
+ query: string | string[],
+ opts?: AggregateGroupByNearOptions
+ ): Promise[]> => {
+ const builder = this.base(opts?.returnMetrics, opts?.filters, opts?.groupBy).withNearText({
+ concepts: Array.isArray(query) ? query : [query],
+ certainty: opts?.certainty,
+ distance: opts?.distance,
+ targetVectors: opts?.targetVector ? [opts.targetVector] : undefined,
+ });
+ if (opts?.objectLimit) {
+ builder.withObjectLimit(opts.objectLimit);
+ }
+ return this.doGroupBy(builder);
+ },
+ nearVector: | undefined = undefined>(
+ vector: number[],
+ opts?: AggregateGroupByNearOptions
+ ): Promise[]> => {
+ const builder = this.base(opts?.returnMetrics, opts?.filters, opts?.groupBy).withNearVector({
+ vector: vector,
+ certainty: opts?.certainty,
+ distance: opts?.distance,
+ targetVectors: opts?.targetVector ? [opts.targetVector] : undefined,
+ });
+ if (opts?.objectLimit) {
+ builder.withObjectLimit(opts.objectLimit);
+ }
+ return this.doGroupBy(builder);
+ },
+ overAll: | undefined = undefined>(
+ opts: AggregateGroupByOptions
+ ): Promise[]> => {
+ const builder = this.base(opts?.returnMetrics, opts?.filters, opts?.groupBy);
+ return this.doGroupBy(builder);
+ },
+ };
+ }
+
+ query() {
+ return new Aggregator(this.connection);
+ }
+
+ base(
+ metrics?: PropertiesMetrics,
+ filters?: FilterValue,
+ groupBy?: (keyof T & string) | GroupByAggregate
+ ) {
+ let fields = 'meta { count }';
+ let builder = this.query().withClassName(this.name);
+ if (metrics) {
+ if (Array.isArray(metrics)) {
+ fields += metrics.map((m) => this.metrics(m)).join(' ');
+ } else {
+ fields += this.metrics(metrics);
+ }
+ }
+ if (groupBy) {
+ builder = builder.withGroupBy(typeof groupBy === 'string' ? [groupBy] : [groupBy.property]);
+ fields += 'groupedBy { path value }';
+ if (typeof groupBy !== 'string' && groupBy?.limit) {
+ builder = builder.withLimit(groupBy.limit);
+ }
+ }
+ if (fields !== '') {
+ builder = builder.withFields(fields);
+ }
+ if (filters) {
+ builder = builder.withWhere(Serialize.filtersREST(filters));
+ }
+ if (this.tenant) {
+ builder = builder.withTenant(this.tenant);
+ }
+ return builder;
+ }
+
+ metrics(metrics: MetricsInput<(keyof T & string) | string>) {
+ let body = '';
+ const { kind, propertyName, ...rest } = metrics;
+ switch (kind) {
+ case 'text': {
+ const { minOccurrences, ...restText } = rest as MetricsText;
+ body = Object.entries(restText)
+ .map(([key, value]) => {
+ if (value) {
+ return value instanceof Object
+ ? `topOccurrences${minOccurrences ? `(limit: ${minOccurrences})` : ''} { ${
+ value.occurs ? 'occurs' : ''
+ } ${value.value ? 'value' : ''} }`
+ : key;
+ }
+ })
+ .join(' ');
+ break;
+ }
+ default:
+ body = Object.entries(rest)
+ .map(([key, value]) => (value ? key : ''))
+ .join(' ');
+ }
+ return `${propertyName} { ${body} }`;
+ }
+
+ static use(
+ connection: Connection,
+ name: string,
+ dbVersionSupport: DbVersionSupport,
+ consistencyLevel?: ConsistencyLevel,
+ tenant?: string
+ ): AggregateManager {
+ return new AggregateManager(connection, name, dbVersionSupport, consistencyLevel, tenant);
+ }
+
+ async nearImage>(
+ image: string | Buffer,
+ opts?: AggregateNearOptions
+ ): Promise> {
+ const builder = this.base(opts?.returnMetrics, opts?.filters).withNearImage({
+ image: await toBase64FromMedia(image),
+ certainty: opts?.certainty,
+ distance: opts?.distance,
+ targetVectors: opts?.targetVector ? [opts.targetVector] : undefined,
+ });
+ if (opts?.objectLimit) {
+ builder.withObjectLimit(opts?.objectLimit);
+ }
+ return this.do(builder);
+ }
+
+ nearObject>(
+ id: string,
+ opts?: AggregateNearOptions
+ ): Promise> {
+ const builder = this.base(opts?.returnMetrics, opts?.filters).withNearObject({
+ id: id,
+ certainty: opts?.certainty,
+ distance: opts?.distance,
+ targetVectors: opts?.targetVector ? [opts.targetVector] : undefined,
+ });
+ if (opts?.objectLimit) {
+ builder.withObjectLimit(opts.objectLimit);
+ }
+ return this.do(builder);
+ }
+
+ nearText>(
+ query: string | string[],
+ opts?: AggregateNearOptions
+ ): Promise> {
+ const builder = this.base(opts?.returnMetrics, opts?.filters).withNearText({
+ concepts: Array.isArray(query) ? query : [query],
+ certainty: opts?.certainty,
+ distance: opts?.distance,
+ targetVectors: opts?.targetVector ? [opts.targetVector] : undefined,
+ });
+ if (opts?.objectLimit) {
+ builder.withObjectLimit(opts.objectLimit);
+ }
+ return this.do(builder);
+ }
+
+ nearVector>(
+ vector: number[],
+ opts?: AggregateNearOptions
+ ): Promise> {
+ const builder = this.base(opts?.returnMetrics, opts?.filters).withNearVector({
+ vector: vector,
+ certainty: opts?.certainty,
+ distance: opts?.distance,
+ targetVectors: opts?.targetVector ? [opts.targetVector] : undefined,
+ });
+ if (opts?.objectLimit) {
+ builder.withObjectLimit(opts.objectLimit);
+ }
+ return this.do(builder);
+ }
+
+ overAll>(opts?: AggregateOptions): Promise> {
+ const builder = this.base(opts?.returnMetrics, opts?.filters);
+ return this.do(builder);
+ }
+
+ do = | undefined = undefined>(
+ query: Aggregator
+ ): Promise> => {
+ return query
+ .do()
+ .then(({ data }: any) => {
+ const { meta, ...rest } = data.Aggregate[this.name][0];
+ return {
+ properties: rest,
+ totalCount: meta?.count,
+ };
+ })
+ .catch((err: Error) => {
+ throw new WeaviateQueryError(err.message, 'GraphQL');
+ });
+ };
+
+ doGroupBy = | undefined = undefined>(
+ query: Aggregator
+ ): Promise[]> => {
+ return query
+ .do()
+ .then(({ data }: any) =>
+ data.Aggregate[this.name].map((item: any) => {
+ const { groupedBy, meta, ...rest } = item;
+ return {
+ groupedBy: {
+ prop: groupedBy.path[0],
+ value: groupedBy.value,
+ },
+ properties: rest.length > 0 ? rest : undefined,
+ totalCount: meta?.count,
+ };
+ })
+ )
+ .catch((err: Error) => {
+ throw new WeaviateQueryError(err.message, 'GraphQL');
+ });
+ };
+}
+
+export interface Aggregate {
+ /** This namespace contains methods perform a group by search while aggregating metrics. */
+ groupBy: AggregateGroupBy;
+ /**
+ * Aggregate metrics over the objects returned by a near image vector search on this collection.
+ *
+ * At least one of `certainty`, `distance`, or `object_limit` must be specified here for the vector search.
+ *
+ * This method requires a vectorizer capable of handling base64-encoded images, e.g. `img2vec-neural`, `multi2vec-clip`, and `multi2vec-bind`.
+ *
+ * @param {string | Buffer} image The image to search on. This can be a base64 string, a file path string, or a buffer.
+ * @param {AggregateNearOptions} [opts] The options for the request.
+ * @returns {Promise[]>} The aggregated metrics for the objects returned by the vector search.
+ */
+ nearImage>(
+ image: string | Buffer,
+ opts?: AggregateNearOptions
+ ): Promise>;
+ /**
+ * Aggregate metrics over the objects returned by a near object search on this collection.
+ *
+ * At least one of `certainty`, `distance`, or `object_limit` must be specified here for the vector search.
+ *
+ * This method requires that the objects in the collection have associated vectors.
+ *
+ * @param {string} id The ID of the object to search for.
+ * @param {AggregateNearOptions} [opts] The options for the request.
+ * @returns {Promise[]>} The aggregated metrics for the objects returned by the vector search.
+ */
+ nearObject>(
+ id: string,
+ opts?: AggregateNearOptions
+ ): Promise>;
+ /**
+ * Aggregate metrics over the objects returned by a near vector search on this collection.
+ *
+ * At least one of `certainty`, `distance`, or `object_limit` must be specified here for the vector search.
+ *
+ * This method requires that the objects in the collection have associated vectors.
+ *
+ * @param {number[]} query The text query to search for.
+ * @param {AggregateNearOptions} [opts] The options for the request.
+ * @returns {Promise[]>} The aggregated metrics for the objects returned by the vector search.
+ */
+ nearText>(
+ query: string | string[],
+ opts?: AggregateNearOptions
+ ): Promise>;
+ /**
+ * Aggregate metrics over the objects returned by a near vector search on this collection.
+ *
+ * At least one of `certainty`, `distance`, or `object_limit` must be specified here for the vector search.
+ *
+ * This method requires that the objects in the collection have associated vectors.
+ *
+ * @param {number[]} vector The vector to search for.
+ * @param {AggregateNearOptions} [opts] The options for the request.
+ * @returns {Promise[]>} The aggregated metrics for the objects returned by the vector search.
+ */
+ nearVector>(
+ vector: number[],
+ opts?: AggregateNearOptions
+ ): Promise>;
+ /**
+ * Aggregate metrics over all the objects in this collection without any vector search.
+ *
+ * @param {AggregateOptions} [opts] The options for the request.
+ * @returns {Promise[]>} The aggregated metrics for the objects in the collection.
+ */
+ overAll>(opts?: AggregateOptions): Promise>;
+}
+
+export interface AggregateGroupBy {
+ /**
+ * Aggregate metrics over the objects returned by a near image vector search on this collection.
+ *
+ * At least one of `certainty`, `distance`, or `object_limit` must be specified here for the vector search.
+ *
+ * This method requires a vectorizer capable of handling base64-encoded images, e.g. `img2vec-neural`, `multi2vec-clip`, and `multi2vec-bind`.
+ *
+ * @param {string | Buffer} image The image to search on. This can be a base64 string, a file path string, or a buffer.
+ * @param {AggregateGroupByNearOptions} [opts] The options for the request.
+ * @returns {Promise[]>} The aggregated metrics for the objects returned by the vector search.
+ */
+ nearImage>(
+ image: string | Buffer,
+ opts?: AggregateGroupByNearOptions
+ ): Promise[]>;
+ /**
+ * Aggregate metrics over the objects returned by a near object search on this collection.
+ *
+ * At least one of `certainty`, `distance`, or `object_limit` must be specified here for the vector search.
+ *
+ * This method requires that the objects in the collection have associated vectors.
+ *
+ * @param {string} id The ID of the object to search for.
+ * @param {AggregateGroupByNearOptions} [opts] The options for the request.
+ * @returns {Promise[]>} The aggregated metrics for the objects returned by the vector search.
+ */
+ nearObject>(
+ id: string,
+ opts?: AggregateGroupByNearOptions
+ ): Promise[]>;
+ /**
+ * Aggregate metrics over the objects returned by a near text vector search on this collection.
+ *
+ * At least one of `certainty`, `distance`, or `object_limit` must be specified here for the vector search.
+ *
+ * This method requires a vectorizer capable of handling text, e.g. `text2vec-contextionary`, `text2vec-openai`, etc.
+ *
+ * @param {string | string[]} query The text to search for.
+ * @param {AggregateGroupByNearOptions} [opts] The options for the request.
+ * @returns {Promise[]>} The aggregated metrics for the objects returned by the vector search.
+ */
+ nearText>(
+ query: string | string[],
+ opts: AggregateGroupByNearOptions
+ ): Promise[]>;
+ /**
+ * Aggregate metrics over the objects returned by a near vector search on this collection.
+ *
+ * At least one of `certainty`, `distance`, or `object_limit` must be specified here for the vector search.
+ *
+ * This method requires that the objects in the collection have associated vectors.
+ *
+ * @param {number[]} vector The vector to search for.
+ * @param {AggregateGroupByNearOptions} [opts] The options for the request.
+ * @returns {Promise[]>} The aggregated metrics for the objects returned by the vector search.
+ */
+ nearVector>(
+ vector: number[],
+ opts?: AggregateGroupByNearOptions
+ ): Promise[]>;
+ /**
+ * Aggregate metrics over all the objects in this collection without any vector search.
+ *
+ * @param {AggregateGroupByOptions} [opts] The options for the request.
+ * @returns {Promise[]>} The aggregated metrics for the objects in the collection.
+ */
+ overAll>(
+ opts?: AggregateGroupByOptions
+ ): Promise[]>;
+}
+
+export default AggregateManager.use;
diff --git a/src/collections/aggregate/integration.test.ts b/src/collections/aggregate/integration.test.ts
new file mode 100644
index 00000000..06f1f506
--- /dev/null
+++ b/src/collections/aggregate/integration.test.ts
@@ -0,0 +1,390 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
+import { WeaviateQueryError, WeaviateUnsupportedFeatureError } from '../../errors.js';
+import weaviate, { AggregateText, WeaviateClient } from '../../index.js';
+import { Collection } from '../collection/index.js';
+import { CrossReference } from '../references/index.js';
+import { DataObject } from '../types/index.js';
+
+describe('Testing of the collection.aggregate methods', () => {
+ type TestCollectionAggregate = {
+ text: string;
+ texts: string[];
+ int: number;
+ ints: number[];
+ number: number;
+ numbers: number[];
+ date: string;
+ dates: string[];
+ boolean: boolean;
+ booleans: boolean[];
+ ref?: CrossReference;
+ };
+
+ let client: WeaviateClient;
+ let collection: Collection;
+ const collectionName = 'TestCollectionAggregate';
+
+ const date0 = '2023-01-01T00:00:00Z';
+ const date1 = '2023-01-01T00:00:00Z';
+ const date2 = '2023-01-02T00:00:00Z';
+ const dateMid = '2023-01-01T12:00:00Z';
+
+ afterAll(async () => {
+ return (await client).collections.delete(collectionName).catch((err) => {
+ console.error(err);
+ throw err;
+ });
+ });
+
+ beforeAll(async () => {
+ client = await weaviate.connectToLocal();
+ collection = client.collections.get(collectionName);
+ return client.collections
+ .create({
+ name: collectionName,
+ properties: [
+ {
+ name: 'text',
+ dataType: 'text',
+ },
+ {
+ name: 'texts',
+ dataType: 'text[]',
+ },
+ {
+ name: 'int',
+ dataType: 'int',
+ },
+ {
+ name: 'ints',
+ dataType: 'int[]',
+ },
+ {
+ name: 'number',
+ dataType: 'number',
+ },
+ {
+ name: 'numbers',
+ dataType: 'number[]',
+ },
+ {
+ name: 'date',
+ dataType: 'date',
+ },
+ {
+ name: 'dates',
+ dataType: 'date[]',
+ },
+ {
+ name: 'boolean',
+ dataType: 'boolean',
+ },
+ {
+ name: 'booleans',
+ dataType: 'boolean[]',
+ },
+ // {
+ // name: 'ref',
+ // dataType: [collectionName],
+ // },
+ ],
+ vectorizers: weaviate.configure.vectorizer.text2VecContextionary({
+ vectorizeCollectionName: false,
+ }),
+ })
+ .then(async () => {
+ const data: DataObject[] = [];
+ for (let i = 0; i < 100; i++) {
+ data.push({
+ properties: {
+ text: 'test',
+ texts: ['tests', 'tests'],
+ int: 1,
+ ints: [1, 2],
+ number: 1.0,
+ numbers: [1.0, 2.0],
+ date: date0,
+ dates: [date1, date2],
+ boolean: true,
+ booleans: [true, false],
+ },
+ });
+ }
+ const res = (await collection).data.insertMany(data);
+ return res;
+ });
+ // .then(async (res) => {
+ // const uuid1 = res.uuids[0];
+ // await collection.data.referenceAddMany({
+ // refs: Object.values(res.uuids).map((uuid) => {
+ // return {
+ // fromProperty: 'ref',
+ // fromUuid: uuid1,
+ // reference: Reference.to({ uuids: [uuid] })
+ // }
+ // })
+ // })
+ // })
+ });
+
+ it('should aggregate data without a search and no property metrics', async () => {
+ const result = await collection.aggregate.overAll();
+ expect(result.totalCount).toEqual(100);
+ });
+
+ it('should aggregate grouped by data without a search and no property metrics', async () => {
+ const result = await collection.aggregate.groupBy.overAll({ groupBy: 'text' });
+ expect(result.length).toEqual(1);
+ expect(result[0].totalCount).toEqual(100);
+ expect(result[0].groupedBy.prop).toEqual('text');
+ expect(result[0].groupedBy.value).toEqual('test');
+ expect(result[0].properties).toBeUndefined();
+ });
+
+ it('should aggregate grouped by data with a near text search and no property metrics', async () => {
+ const result = await collection.aggregate.groupBy.nearText('test', {
+ groupBy: 'text',
+ certainty: 0.1,
+ });
+ expect(result.length).toEqual(1);
+ expect(result[0].totalCount).toEqual(100);
+ expect(result[0].groupedBy.prop).toEqual('text');
+ expect(result[0].groupedBy.value).toEqual('test');
+ expect(result[0].properties).toBeUndefined();
+ });
+
+ it('should aggregate data without a search and one generic property metric', async () => {
+ const result = await collection.aggregate.overAll({
+ returnMetrics: collection.metrics
+ .aggregate('text')
+ .text(['count', 'topOccurrencesOccurs', 'topOccurrencesValue']),
+ });
+ expect(result.totalCount).toEqual(100);
+ expect(result.properties.text.count).toEqual(100);
+ expect(result.properties.text.topOccurrences![0].occurs).toEqual(100);
+ expect(result.properties.text.topOccurrences![0].value).toEqual('test');
+ });
+
+ it('should aggregate data without a search and one non-generic property metric', async () => {
+ const result = await (await client).collections.get(collectionName).aggregate.overAll({
+ returnMetrics: collection.metrics
+ .aggregate('text')
+ .text(['count', 'topOccurrencesOccurs', 'topOccurrencesValue']),
+ });
+ expect(result.totalCount).toEqual(100);
+ expect(result.properties.text.count).toEqual(100);
+ expect((result.properties.text as AggregateText).topOccurrences![0].occurs).toEqual(100);
+ expect((result.properties.text as AggregateText).topOccurrences![0].value).toEqual('test');
+ });
+
+ it('should aggregate data without a search and all property metrics', async () => {
+ const result = await collection.aggregate.overAll({
+ returnMetrics: [
+ collection.metrics.aggregate('text').text(['count', 'topOccurrencesOccurs', 'topOccurrencesValue']),
+ collection.metrics.aggregate('texts').text(['count', 'topOccurrencesOccurs', 'topOccurrencesValue']),
+ collection.metrics
+ .aggregate('int')
+ .integer(['count', 'maximum', 'mean', 'median', 'minimum', 'mode', 'sum']),
+ collection.metrics
+ .aggregate('ints')
+ .integer(['count', 'maximum', 'mean', 'median', 'minimum', 'mode', 'sum']),
+ collection.metrics
+ .aggregate('number')
+ .number(['count', 'maximum', 'mean', 'median', 'minimum', 'mode', 'sum']),
+ collection.metrics
+ .aggregate('numbers')
+ .number(['count', 'maximum', 'mean', 'median', 'minimum', 'mode', 'sum']),
+ collection.metrics.aggregate('date').date(['count', 'maximum', 'median', 'minimum', 'mode']),
+ collection.metrics.aggregate('dates').date(['count', 'maximum', 'median', 'minimum']), // 'mode' flakes between date1 and date2
+ collection.metrics
+ .aggregate('boolean')
+ .boolean(['count', 'percentageFalse', 'percentageTrue', 'totalFalse', 'totalTrue']),
+ collection.metrics
+ .aggregate('booleans')
+ .boolean(['count', 'percentageFalse', 'percentageTrue', 'totalFalse', 'totalTrue']),
+ // Metrics.aggregate('ref').reference(['pointingTo'])
+ ],
+ });
+ expect(result).toEqual({
+ totalCount: 100,
+ properties: {
+ text: {
+ count: 100,
+ topOccurrences: [{ occurs: 100, value: 'test' }],
+ },
+ texts: {
+ count: 200,
+ topOccurrences: [{ occurs: 200, value: 'tests' }],
+ },
+ int: {
+ count: 100,
+ maximum: 1,
+ mean: 1,
+ median: 1,
+ minimum: 1,
+ mode: 1,
+ sum: 100,
+ },
+ ints: {
+ count: 200,
+ maximum: 2,
+ mean: 1.5,
+ median: 1.5,
+ minimum: 1,
+ mode: 1,
+ sum: 300,
+ },
+ number: {
+ count: 100,
+ maximum: 1,
+ mean: 1,
+ median: 1,
+ minimum: 1,
+ mode: 1,
+ sum: 100,
+ },
+ numbers: {
+ count: 200,
+ maximum: 2,
+ mean: 1.5,
+ median: 1.5,
+ minimum: 1,
+ mode: 1,
+ sum: 300,
+ },
+ date: {
+ count: 100,
+ maximum: date0,
+ median: date0,
+ minimum: date0,
+ mode: date0,
+ },
+ dates: {
+ count: 200,
+ maximum: date2,
+ median: dateMid,
+ minimum: date1,
+ // mode: date1, // randomly switches between date1 and date2
+ },
+ boolean: {
+ count: 100,
+ percentageFalse: 0,
+ percentageTrue: 1,
+ totalFalse: 0,
+ totalTrue: 100,
+ },
+ booleans: {
+ count: 200,
+ percentageFalse: 0.5,
+ percentageTrue: 0.5,
+ totalFalse: 100,
+ totalTrue: 100,
+ },
+ // ref: {
+ // pointingTo: collectionName
+ // }
+ },
+ });
+ });
+});
+
+describe('Testing of the collection.aggregate methods with named vectors', () => {
+ let client: WeaviateClient;
+ let collection: Collection;
+ const collectionName = 'TestCollectionAggregateVectors';
+ type TestCollectionAggregateVectors = {
+ text: string;
+ };
+
+ afterAll(async () => {
+ return (await client).collections.delete(collectionName).catch((err) => {
+ console.error(err);
+ throw err;
+ });
+ });
+
+ beforeAll(async () => {
+ client = await weaviate.connectToLocal();
+ collection = client.collections.get(collectionName);
+ const query = () =>
+ client.collections.create({
+ name: collectionName,
+ properties: [
+ {
+ name: 'text',
+ dataType: 'text',
+ },
+ ],
+ vectorizers: [
+ weaviate.configure.vectorizer.text2VecContextionary({
+ name: 'text',
+ sourceProperties: ['text'],
+ vectorIndexConfig: weaviate.configure.vectorIndex.hnsw(),
+ }),
+ ],
+ });
+ if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 24, 0))) {
+ await expect(query()).rejects.toThrow(WeaviateUnsupportedFeatureError);
+ return;
+ }
+ return query();
+ });
+
+ it('should aggregate data with a near text search over a named vector', async () => {
+ if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 24, 0))) {
+ return;
+ }
+ const result = await collection.aggregate.nearText('test', { certainty: 0.9, targetVector: 'text' });
+ expect(result.totalCount).toEqual(0);
+ });
+});
+
+describe('Testing of collection.aggregate.overAll with a multi-tenancy collection', () => {
+ let client: WeaviateClient;
+ let collection: Collection;
+ const collectionName = 'TestCollectionAggregate';
+
+ afterAll(async () => {
+ return (await client).collections.delete(collectionName).catch((err) => {
+ console.error(err);
+ throw err;
+ });
+ });
+
+ beforeAll(async () => {
+ client = await weaviate.connectToLocal();
+ return client.collections
+ .create({
+ name: collectionName,
+ properties: [
+ {
+ name: 'text',
+ dataType: 'text',
+ },
+ ],
+ multiTenancy: { enabled: true },
+ })
+ .then(async (created) => {
+ const tenants = await created.tenants.create({ name: 'test' });
+ collection = created.withTenant(tenants[0].name);
+ const data: Array = [];
+ for (let i = 0; i < 100; i++) {
+ data.push({
+ properties: {
+ text: 'test',
+ },
+ });
+ }
+ await collection.data.insertMany(data);
+ });
+ });
+
+ it('should aggregate data without a search and no property metrics over the tenant', () =>
+ collection.aggregate.overAll().then((result) => expect(result.totalCount).toEqual(100)));
+
+ it('should throw an error for a non-existant tenant', () =>
+ expect(collection.withTenant('non-existing-tenant').aggregate.overAll()).rejects.toThrow(
+ WeaviateQueryError
+ ));
+});
diff --git a/src/collections/backup/client.ts b/src/collections/backup/client.ts
new file mode 100644
index 00000000..83262618
--- /dev/null
+++ b/src/collections/backup/client.ts
@@ -0,0 +1,195 @@
+import {
+ Backend,
+ BackupCompressionLevel,
+ BackupCreateStatusGetter,
+ BackupCreator,
+ BackupRestoreStatusGetter,
+ BackupRestorer,
+} from '../../backup/index.js';
+import Connection from '../../connection/index.js';
+import { WeaviateBackupFailed } from '../../errors.js';
+import {
+ BackupCreateResponse,
+ BackupCreateStatusResponse,
+ BackupRestoreResponse,
+ BackupRestoreStatusResponse,
+} from '../../openapi/types.js';
+
+/** Configuration options available when creating a backup */
+export type BackupConfigCreate = {
+ /** The size of the chunks to use for the backup. */
+ chunkSize?: number;
+ /** The standard of compression to use for the backup. */
+ compressionLevel?: BackupCompressionLevel;
+ /** The percentage of CPU to use for the backup creation job. */
+ cpuPercentage?: number;
+};
+
+/** Configuration options available when restoring a backup */
+export type BackupConfigRestore = {
+ /** The percentage of CPU to use for the backuop restoration job. */
+ cpuPercentage?: number;
+};
+
+/** The arguments required to create and restore backups. */
+export type BackupArgs = {
+ /** The ID of the backup. */
+ backupId: string;
+ /** The backend to use for the backup. */
+ backend: Backend;
+ /** The collections to include in the backup. */
+ includeCollections?: string[];
+ /** The collections to exclude from the backup. */
+ excludeCollections?: string[];
+ /** Whether to wait for the backup to complete. */
+ waitForCompletion?: boolean;
+ /** The configuration options for the backup. */
+ config?: C;
+};
+
+/** The arguments required to get the status of a backup. */
+export type BackupStatusArgs = {
+ /** The ID of the backup. */
+ backupId: string;
+ /** The backend to use for the backup. */
+ backend: Backend;
+};
+
+export const backup = (connection: Connection) => {
+ const getCreateStatus = (args: BackupStatusArgs): Promise => {
+ return new BackupCreateStatusGetter(connection)
+ .withBackupId(args.backupId)
+ .withBackend(args.backend)
+ .do();
+ };
+ const getRestoreStatus = (args: BackupStatusArgs): Promise => {
+ return new BackupRestoreStatusGetter(connection)
+ .withBackupId(args.backupId)
+ .withBackend(args.backend)
+ .do();
+ };
+ return {
+ create: async (args: BackupArgs): Promise => {
+ let builder = new BackupCreator(connection, new BackupCreateStatusGetter(connection))
+ .withBackupId(args.backupId)
+ .withBackend(args.backend);
+ if (args.includeCollections) {
+ builder = builder.withIncludeClassNames(...args.includeCollections);
+ }
+ if (args.excludeCollections) {
+ builder = builder.withExcludeClassNames(...args.excludeCollections);
+ }
+ if (args.config) {
+ builder = builder.withConfig({
+ ChunkSize: args.config.chunkSize,
+ CompressionLevel: args.config.compressionLevel,
+ CPUPercentage: args.config.cpuPercentage,
+ });
+ }
+ let res: BackupCreateResponse;
+ try {
+ res = await builder.do();
+ } catch (err) {
+ throw new Error(`Backup creation failed: ${err}`);
+ }
+ if (res.status === 'FAILED') {
+ throw new Error(`Backup creation failed: ${res.error}`);
+ }
+ let status: BackupCreateStatusResponse | undefined;
+ if (args.waitForCompletion) {
+ let wait = true;
+ while (wait) {
+ const res = await getCreateStatus(args); // eslint-disable-line no-await-in-loop
+ if (res.status === 'SUCCESS') {
+ wait = false;
+ status = res;
+ }
+ if (res.status === 'FAILED') {
+ throw new WeaviateBackupFailed(res.error ? res.error : '', 'creation');
+ }
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // eslint-disable-line no-await-in-loop
+ }
+ }
+ return status ? { ...status, classes: res.classes } : res;
+ },
+ getCreateStatus: getCreateStatus,
+ getRestoreStatus: getRestoreStatus,
+ restore: async (args: BackupArgs): Promise => {
+ let builder = new BackupRestorer(connection, new BackupRestoreStatusGetter(connection))
+ .withBackupId(args.backupId)
+ .withBackend(args.backend);
+ if (args.includeCollections) {
+ builder = builder.withIncludeClassNames(...args.includeCollections);
+ }
+ if (args.excludeCollections) {
+ builder = builder.withExcludeClassNames(...args.excludeCollections);
+ }
+ if (args.config) {
+ builder = builder.withConfig({
+ CPUPercentage: args.config.cpuPercentage,
+ });
+ }
+ let res: BackupRestoreResponse;
+ try {
+ res = await builder.do();
+ } catch (err) {
+ throw new Error(`Backup restoration failed: ${err}`);
+ }
+ if (res.status === 'FAILED') {
+ throw new Error(`Backup restoration failed: ${res.error}`);
+ }
+ let status: BackupRestoreStatusResponse | undefined;
+ if (args.waitForCompletion) {
+ let wait = true;
+ while (wait) {
+ const res = await getRestoreStatus(args); // eslint-disable-line no-await-in-loop
+ if (res.status === 'SUCCESS') {
+ wait = false;
+ status = res;
+ }
+ if (res.status === 'FAILED') {
+ throw new WeaviateBackupFailed(res.error ? res.error : '', 'restoration');
+ }
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // eslint-disable-line no-await-in-loop
+ }
+ }
+ return status
+ ? {
+ ...status,
+ classes: res.classes,
+ }
+ : res;
+ },
+ };
+};
+
+export interface Backup {
+ /**
+ * Create a backup of the database.
+ *
+ * @param {BackupArgs} args The arguments for the request.
+ * @returns {Promise} The response from Weaviate.
+ */
+ create(args: BackupArgs): Promise;
+ /**
+ * Get the status of a backup creation.
+ *
+ * @param {BackupStatusArgs} args The arguments for the request.
+ * @returns {Promise} The status of the backup creation.
+ */
+ getCreateStatus(args: BackupStatusArgs): Promise;
+ /**
+ * Get the status of a backup restore.
+ *
+ * @param {BackupStatusArgs} args The arguments for the request.
+ * @returns {Promise} The status of the backup restore.
+ */
+ getRestoreStatus(args: BackupStatusArgs): Promise;
+ /**
+ * Restore a backup of the database.
+ *
+ * @param {BackupArgs} args The arguments for the request.
+ * @returns {Promise} The response from Weaviate.
+ */
+ restore(args: BackupArgs): Promise;
+}
diff --git a/src/collections/backup/collection.ts b/src/collections/backup/collection.ts
new file mode 100644
index 00000000..956fbb33
--- /dev/null
+++ b/src/collections/backup/collection.ts
@@ -0,0 +1,68 @@
+import { Backend } from '../../backup/index.js';
+import Connection from '../../connection/index.js';
+import {
+ BackupCreateResponse,
+ BackupCreateStatusResponse,
+ BackupRestoreResponse,
+ BackupRestoreStatusResponse,
+} from '../../openapi/types.js';
+import { BackupStatusArgs, backup } from './client.js';
+
+/** The arguments required to create and restore backups. */
+export type BackupCollectionArgs = {
+ /** The ID of the backup. */
+ backupId: string;
+ /** The backend to use for the backup. */
+ backend: Backend;
+ /** The collections to include in the backup. */
+ waitForCompletion?: boolean;
+};
+
+export const backupCollection = (connection: Connection, name: string) => {
+ const handler = backup(connection);
+ return {
+ create: (args: BackupCollectionArgs) =>
+ handler.create({
+ ...args,
+ includeCollections: [name],
+ }),
+ getCreateStatus: handler.getCreateStatus,
+ getRestoreStatus: handler.getRestoreStatus,
+ restore: (args: BackupCollectionArgs) =>
+ handler.restore({
+ ...args,
+ includeCollections: [name],
+ }),
+ };
+};
+
+export interface BackupCollection {
+ /**
+ * Create a backup of this collection.
+ *
+ * @param {BackupArgs} args The arguments for the request.
+ * @returns {Promise} The response from Weaviate.
+ */
+ create(args: BackupCollectionArgs): Promise;
+ /**
+ * Get the status of a backup.
+ *
+ * @param {BackupStatusArgs} args The arguments for the request.
+ * @returns {Promise} The status of the backup.
+ */
+ getCreateStatus(args: BackupStatusArgs): Promise;
+ /**
+ * Get the status of a restore.
+ *
+ * @param {BackupStatusArgs} args The arguments for the request.
+ * @returns {Promise} The status of the restore.
+ */
+ getRestoreStatus(args: BackupStatusArgs): Promise;
+ /**
+ * Restore a backup of this collection.
+ *
+ * @param {BackupArgs} args The arguments for the request.
+ * @returns {Promise} The response from Weaviate.
+ */
+ restore(args: BackupCollectionArgs): Promise;
+}
diff --git a/src/collections/backup/index.ts b/src/collections/backup/index.ts
new file mode 100644
index 00000000..5b5c3e11
--- /dev/null
+++ b/src/collections/backup/index.ts
@@ -0,0 +1,8 @@
+export type {
+ Backup,
+ BackupArgs,
+ BackupConfigCreate,
+ BackupConfigRestore,
+ BackupStatusArgs,
+} from './client.js';
+export type { BackupCollection, BackupCollectionArgs } from './collection.js';
diff --git a/src/collections/backup/integration.test.ts b/src/collections/backup/integration.test.ts
new file mode 100644
index 00000000..cf8888a6
--- /dev/null
+++ b/src/collections/backup/integration.test.ts
@@ -0,0 +1,105 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
+/* eslint-disable no-await-in-loop */
+import { Backend } from '../../backup/index.js';
+import weaviate, { Collection, WeaviateClient } from '../../index.js';
+
+// These must run sequentially because Weaviate is not capable of running multiple backups at the same time
+describe('Integration testing of backups', () => {
+ const clientPromise = weaviate.connectToLocal({
+ port: 8090,
+ grpcPort: 50061,
+ });
+
+ const getCollection = (client: WeaviateClient) => client.collections.get('TestBackupCollection');
+
+ beforeAll(() =>
+ clientPromise.then((client) =>
+ Promise.all([
+ client.collections.create({ name: 'TestBackupClient' }).then((col) => col.data.insert()),
+ client.collections.create({ name: 'TestBackupCollection' }).then((col) => col.data.insert()),
+ ])
+ )
+ );
+
+ afterAll(() => clientPromise.then((client) => client.collections.deleteAll()));
+
+ const testClientWaitForCompletion = async (client: WeaviateClient) => {
+ const res = await client.backup.create({
+ backupId: `test-backup-${randomBackupId()}`,
+ backend: 'filesystem',
+ waitForCompletion: true,
+ });
+ expect(res.status).toBe('SUCCESS');
+ return client;
+ };
+
+ const testClientNoWaitForCompletion = async (client: WeaviateClient) => {
+ const res = await client.backup.create({
+ backupId: `test-backup-${randomBackupId()}`,
+ backend: 'filesystem',
+ });
+ expect(res.status).toBe('STARTED');
+ const status = await client.backup.getCreateStatus({
+ backupId: res.id as string,
+ backend: res.backend as 'filesystem',
+ });
+ expect(status).not.toBe('SUCCESS'); // can be 'STARTED' or 'TRANSFERRING' depending on the speed of the test machine
+
+ // wait to complete so that other tests can run without colliding with Weaviate's lack of simultaneous backups
+ let wait = true;
+ while (wait) {
+ const { status, error } = await client.backup.getCreateStatus({
+ backupId: res.id as string,
+ backend: res.backend as Backend,
+ });
+ if (status === 'SUCCESS') {
+ wait = false;
+ }
+ if (status === 'FAILED') {
+ throw new Error(`Backup creation failed: ${error}`);
+ }
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
+
+ return client;
+ };
+
+ const testCollectionWaitForCompletion = async (collection: Collection) => {
+ const res = await collection.backup.create({
+ backupId: `test-backup-${randomBackupId()}`,
+ backend: 'filesystem',
+ waitForCompletion: true,
+ });
+ expect(res.status).toBe('SUCCESS');
+ expect(res.classes).toEqual(['TestBackupCollection']);
+ return collection;
+ };
+
+ const testCollectionNoWaitForCompletion = async (collection: Collection) => {
+ const res = await collection.backup.create({
+ backupId: `test-backup-${randomBackupId()}`,
+ backend: 'filesystem',
+ });
+ expect(res.status).toBe('STARTED');
+ expect(res.classes).toEqual(['TestBackupCollection']);
+ const status = await collection.backup.getCreateStatus({
+ backupId: res.id as string,
+ backend: res.backend as 'filesystem',
+ });
+ expect(status).not.toBe('SUCCESS'); // can be 'STARTED' or 'TRANSFERRING' depending on the speed of the test machine
+ return collection;
+ };
+
+ it('run', () =>
+ clientPromise
+ .then(testClientWaitForCompletion)
+ .then(testClientNoWaitForCompletion)
+ .then(getCollection)
+ .then(testCollectionWaitForCompletion)
+ .then(testCollectionNoWaitForCompletion));
+});
+
+function randomBackupId() {
+ return 'backup-id-' + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
+}
diff --git a/src/collections/cluster/index.ts b/src/collections/cluster/index.ts
new file mode 100644
index 00000000..1f16a844
--- /dev/null
+++ b/src/collections/cluster/index.ts
@@ -0,0 +1,46 @@
+import { NodesStatusGetter } from '../../cluster/index.js';
+import Connection from '../../connection/index.js';
+import { BatchStats, NodeShardStatus, NodeStats } from '../../openapi/types.js';
+
+export type Output = 'minimal' | 'verbose' | undefined;
+
+export type NodesOptions = {
+ /** The name of the collection to get the status of. */
+ collection?: string;
+ /** Set the desired output verbosity level. Can be `minimal | verbose | undefined` with `undefined` defaulting to `minimal`. */
+ output: O;
+};
+
+export type Node = {
+ name: string;
+ status: 'HEALTHY' | 'UNHEALTHY' | 'UNAVAILABLE';
+ version: string;
+ gitHash: string;
+ stats: O extends 'minimal' | undefined ? undefined : Required;
+ batchStats: Required;
+ shards: O extends 'minimal' | undefined ? null : Required[];
+};
+
+const cluster = (connection: Connection) => {
+ return {
+ nodes: (opts?: NodesOptions): Promise[]> => {
+ let builder = new NodesStatusGetter(connection).withOutput(opts?.output ? opts.output : 'minimal');
+ if (opts?.collection) {
+ builder = builder.withClassName(opts.collection);
+ }
+ return builder.do().then((res) => res.nodes) as Promise[]>;
+ },
+ };
+};
+
+export default cluster;
+
+export interface Cluster {
+ /**
+ * Get the status of all nodes in the cluster.
+ *
+ * @param {NodesOptions} [opts] The options for the request.
+ * @returns {Promise[]>} The status of all nodes in the cluster.
+ */
+ nodes: (opts?: NodesOptions) => Promise[]>;
+}
diff --git a/src/collections/cluster/integration.test.ts b/src/collections/cluster/integration.test.ts
new file mode 100644
index 00000000..1ec26095
--- /dev/null
+++ b/src/collections/cluster/integration.test.ts
@@ -0,0 +1,71 @@
+import weaviate, { WeaviateClient } from '../../index.js';
+
+describe('Testing of the client.cluster methods', () => {
+ let client: WeaviateClient;
+
+ const one = 'TestClusterCollectionOne';
+ const two = 'TestClusterCollectionTwo';
+
+ afterAll(async () => {
+ await (await client).collections.delete(one);
+ });
+
+ beforeAll(async () => {
+ client = await weaviate.connectToLocal();
+ return Promise.all([client.collections.create({ name: one }), client.collections.create({ name: two })]);
+ });
+
+ it('should return the default node statuses', async () => {
+ const nodes = await client.cluster.nodes();
+ expect(nodes).toBeDefined();
+ expect(nodes.length).toBeGreaterThan(0);
+ expect(nodes[0].gitHash).toBeDefined();
+ expect(nodes[0].version).toBeDefined();
+ expect(nodes[0].status).toEqual('HEALTHY');
+ expect(nodes[0].stats).toBeUndefined();
+ expect(nodes[0].shards).toBeNull();
+ expect(nodes[0].batchStats.queueLength).toBeGreaterThanOrEqual(0);
+ expect(nodes[0].batchStats.ratePerSecond).toBeGreaterThanOrEqual(0);
+ });
+
+ it('should return the minimal node statuses', async () => {
+ const nodes = await client.cluster.nodes({ output: 'minimal' });
+ expect(nodes).toBeDefined();
+ expect(nodes.length).toBeGreaterThan(0);
+ expect(nodes[0].gitHash).toBeDefined();
+ expect(nodes[0].version).toBeDefined();
+ expect(nodes[0].status).toEqual('HEALTHY');
+ expect(nodes[0].stats).toBeUndefined();
+ expect(nodes[0].shards).toBeNull();
+ expect(nodes[0].batchStats.queueLength).toBeGreaterThanOrEqual(0);
+ expect(nodes[0].batchStats.ratePerSecond).toBeGreaterThanOrEqual(0);
+ });
+
+ it('should return the verbose node statuses', async () => {
+ const nodes = await client.cluster.nodes({ output: 'verbose' });
+ expect(nodes).toBeDefined();
+ expect(nodes.length).toBeGreaterThan(0);
+ expect(nodes[0].gitHash).toBeDefined();
+ expect(nodes[0].version).toBeDefined();
+ expect(nodes[0].status).toEqual('HEALTHY');
+ expect(nodes[0].stats.shardCount).toBeDefined();
+ expect(nodes[0].stats.objectCount).toBeDefined();
+ expect(nodes[0].shards.length).toBeGreaterThanOrEqual(0);
+ expect(nodes[0].batchStats.queueLength).toBeGreaterThanOrEqual(0);
+ expect(nodes[0].batchStats.ratePerSecond).toBeGreaterThanOrEqual(0);
+ });
+
+ it('should return the node statuses for a specific collection', async () => {
+ const nodes = await client.cluster.nodes({ collection: one, output: 'verbose' });
+ expect(nodes).toBeDefined();
+ expect(nodes.length).toBeGreaterThan(0);
+ expect(nodes[0].gitHash).toBeDefined();
+ expect(nodes[0].version).toBeDefined();
+ expect(nodes[0].status).toEqual('HEALTHY');
+ expect(nodes[0].stats.shardCount).toBeDefined();
+ expect(nodes[0].stats.objectCount).toBeDefined();
+ expect(nodes[0].shards.length).toBeGreaterThanOrEqual(0);
+ expect(nodes[0].batchStats.queueLength).toBeGreaterThanOrEqual(0);
+ expect(nodes[0].batchStats.ratePerSecond).toBeGreaterThanOrEqual(0);
+ });
+});
diff --git a/src/collections/collection/index.ts b/src/collections/collection/index.ts
new file mode 100644
index 00000000..6433ddef
--- /dev/null
+++ b/src/collections/collection/index.ts
@@ -0,0 +1,156 @@
+import Connection from '../../connection/grpc.js';
+import { ConsistencyLevel } from '../../data/index.js';
+import { WeaviateInvalidInputError } from '../../errors.js';
+import ClassExists from '../../schema/classExists.js';
+import { DbVersionSupport } from '../../utils/dbVersion.js';
+
+import aggregate, { Aggregate, Metrics, metrics } from '../aggregate/index.js';
+import { BackupCollection, backupCollection } from '../backup/collection.js';
+import config, { Config } from '../config/index.js';
+import data, { Data } from '../data/index.js';
+import filter, { Filter } from '../filters/index.js';
+import generate, { Generate } from '../generate/index.js';
+import { Iterator } from '../iterator/index.js';
+import query, { Query } from '../query/index.js';
+import sort, { Sort } from '../sort/index.js';
+import tenants, { Tenant, Tenants } from '../tenants/index.js';
+import { QueryMetadata, QueryProperty, QueryReference } from '../types/index.js';
+
+export interface Collection {
+ /** This namespace includes all the querying methods available to you when using Weaviate's standard aggregation capabilities. */
+ aggregate: Aggregate;
+ /** This namespace includes all the backup methods available to you when backing up a collection in Weaviate. */
+ backup: BackupCollection;
+ /** This namespace includes all the CRUD methods available to you when modifying the configuration of the collection in Weaviate. */
+ config: Config;
+ /** This namespace includes all the CUD methods available to you when modifying the data of the collection in Weaviate. */
+ data: Data;
+ /** This namespace includes the methods by which you can create the `FilterValue` values for use when filtering queries over your collection. */
+ filter: Filter;
+ /** This namespace includes all the querying methods available to you when using Weaviate's generative capabilities. */
+ generate: Generate;
+ /** This namespace includes the methods by which you can create the `MetricsX` values for use when aggregating over your collection. */
+ metrics: Metrics;
+ /** The name of the collection. */
+ name: N;
+ /** This namespace includes all the querying methods available to you when using Weaviate's standard query capabilities. */
+ query: Query;
+ /** This namespaces includes the methods by which you can create the `Sorting` values for use when sorting queries over your collection. */
+ sort: Sort;
+ /** This namespace includes all the CRUD methods available to you when modifying the tenants of a multi-tenancy-enabled collection in Weaviate. */
+ tenants: Tenants;
+ /**
+ * Use this method to check if the collection exists in Weaviate.
+ *
+ * @returns {Promise} A promise that resolves to `true` if the collection exists, and `false` otherwise.
+ */
+ exists: () => Promise;
+ /**
+ * Use this method to return an iterator over the objects in the collection.
+ *
+ * This iterator keeps a record of the last object that it returned to be used in each subsequent call to Weaviate.
+ * Once the collection is exhausted, the iterator exits.
+ *
+ * @param {IteratorOptions} opts The options to use when fetching objects from Weaviate.
+ * @returns {Iterator} An iterator over the objects in the collection as an async generator.
+ *
+ * @description If `return_properties` is not provided, all the properties of each object will be
+ * requested from Weaviate except for its vector as this is an expensive operation. Specify `include_vector`
+ * to request the vector back as well. In addition, if `return_references=None` then none of the references
+ * are returned. Use `wvc.QueryReference` to specify which references to return.
+ */
+ iterator: (opts?: IteratorOptions) => Iterator;
+ /**
+ * Use this method to return a collection object specific to a single consistency level.
+ *
+ * If replication is not configured for this collection then Weaviate will throw an error.
+ *
+ * This method does not send a request to Weaviate. It only returns a new collection object that is specific to the consistency level you specify.
+ *
+ * @param {ConsistencyLevel} consistencyLevel The consistency level to use.
+ * @returns {Collection} A new collection object specific to the consistency level you specified.
+ */
+ withConsistency: (consistencyLevel: ConsistencyLevel) => Collection;
+ /**
+ * Use this method to return a collection object specific to a single tenant.
+ *
+ * If multi-tenancy is not configured for this collection then Weaviate will throw an error.
+ *
+ * This method does not send a request to Weaviate. It only returns a new collection object that is specific to the tenant you specify.
+ *
+ * @param {string | Tenant} tenant The tenant name or tenant object to use.
+ * @returns {Collection} A new collection object specific to the tenant you specified.
+ */
+ withTenant: (tenant: string | Tenant) => Collection;
+}
+
+export type IteratorOptions = {
+ includeVector?: boolean | string[];
+ returnMetadata?: QueryMetadata;
+ returnProperties?: QueryProperty[];
+ returnReferences?: QueryReference[];
+};
+
+const isString = (value: any): value is string => typeof value === 'string';
+
+const capitalizeCollectionName = (name: N): N =>
+ (name.charAt(0).toUpperCase() + name.slice(1)) as N;
+
+const collection = (
+ connection: Connection,
+ name: N,
+ dbVersionSupport: DbVersionSupport,
+ consistencyLevel?: ConsistencyLevel,
+ tenant?: Tenant
+): Collection => {
+ if (!isString(name)) {
+ throw new WeaviateInvalidInputError(`The collection name must be a string, got: ${typeof name}`);
+ }
+ const capitalizedName = capitalizeCollectionName(name);
+ const queryCollection = query(
+ connection,
+ capitalizedName,
+ dbVersionSupport,
+ consistencyLevel,
+ tenant?.name
+ );
+ return {
+ aggregate: aggregate(connection, capitalizedName, dbVersionSupport, consistencyLevel, tenant?.name),
+ backup: backupCollection(connection, capitalizedName),
+ config: config(connection, capitalizedName, dbVersionSupport, tenant?.name),
+ data: data(connection, capitalizedName, dbVersionSupport, consistencyLevel, tenant?.name),
+ filter: filter(),
+ generate: generate(connection, capitalizedName, dbVersionSupport, consistencyLevel, tenant?.name),
+ metrics: metrics(),
+ name: name,
+ query: queryCollection,
+ sort: sort(),
+ tenants: tenants(connection, capitalizedName, dbVersionSupport),
+ exists: () => new ClassExists(connection).withClassName(capitalizedName).do(),
+ iterator: (opts?: IteratorOptions) =>
+ new Iterator((limit: number, after?: string) =>
+ queryCollection
+ .fetchObjects({
+ limit,
+ after,
+ includeVector: opts?.includeVector,
+ returnMetadata: opts?.returnMetadata,
+ returnProperties: opts?.returnProperties,
+ returnReferences: opts?.returnReferences,
+ })
+ .then((res) => res.objects)
+ ),
+ withConsistency: (consistencyLevel: ConsistencyLevel) =>
+ collection(connection, capitalizedName, dbVersionSupport, consistencyLevel, tenant),
+ withTenant: (tenant: string | Tenant) =>
+ collection(
+ connection,
+ capitalizedName,
+ dbVersionSupport,
+ consistencyLevel,
+ typeof tenant === 'string' ? { name: tenant } : tenant
+ ),
+ };
+};
+
+export default collection;
diff --git a/src/collections/config/classes.ts b/src/collections/config/classes.ts
new file mode 100644
index 00000000..2269780f
--- /dev/null
+++ b/src/collections/config/classes.ts
@@ -0,0 +1,137 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import {
+ WeaviateClass,
+ WeaviateInvertedIndexConfig,
+ WeaviateReplicationConfig,
+ WeaviateVectorIndexConfig,
+ WeaviateVectorsConfig,
+} from '../../openapi/types.js';
+import { QuantizerGuards } from '../configure/parsing.js';
+import {
+ InvertedIndexConfigUpdate,
+ ReplicationConfigUpdate,
+ VectorConfigUpdate,
+ VectorIndexConfigFlatUpdate,
+ VectorIndexConfigHNSWUpdate,
+} from '../configure/types/index.js';
+import { CollectionConfigUpdate, VectorIndexType } from './types/index.js';
+
+export class MergeWithExisting {
+ static schema(
+ current: WeaviateClass,
+ supportsNamedVectors: boolean,
+ update?: CollectionConfigUpdate
+ ): WeaviateClass {
+ if (update === undefined) return current;
+ if (update.description !== undefined) current.description = update.description;
+ if (update.invertedIndex !== undefined)
+ current.invertedIndexConfig = MergeWithExisting.invertedIndex(
+ current.invertedIndexConfig,
+ update.invertedIndex
+ );
+ if (update.replication !== undefined)
+ current.replicationConfig = MergeWithExisting.replication(
+ current.replicationConfig!,
+ update.replication
+ );
+ if (update.vectorizers !== undefined) {
+ if (Array.isArray(update.vectorizers)) {
+ current.vectorConfig = MergeWithExisting.vectors(current.vectorConfig, update.vectorizers);
+ } else if (supportsNamedVectors) {
+ const updateVectorizers = {
+ ...update.vectorizers,
+ name: 'default',
+ };
+ current.vectorConfig = MergeWithExisting.vectors(current.vectorConfig, [updateVectorizers]);
+ } else {
+ current.vectorIndexConfig =
+ update.vectorizers?.vectorIndex.name === 'hnsw'
+ ? MergeWithExisting.hnsw(current.vectorIndexConfig, update.vectorizers.vectorIndex.config)
+ : MergeWithExisting.flat(current.vectorIndexConfig, update.vectorizers.vectorIndex.config);
+ }
+ }
+ return current;
+ }
+
+ static invertedIndex(
+ current: WeaviateInvertedIndexConfig,
+ update?: InvertedIndexConfigUpdate
+ ): WeaviateInvertedIndexConfig {
+ if (current === undefined) throw Error('Inverted index config is missing from the class schema.');
+ if (update === undefined) return current;
+ const { bm25, stopwords, ...rest } = update;
+ const merged = { ...current, ...rest };
+ if (bm25 !== undefined) merged.bm25 = { ...current.bm25!, ...bm25 };
+ if (stopwords !== undefined) merged.stopwords = { ...current.stopwords!, ...stopwords };
+ return merged;
+ }
+
+ static replication(
+ current: WeaviateReplicationConfig,
+ update?: ReplicationConfigUpdate
+ ): WeaviateReplicationConfig {
+ if (current === undefined) throw Error('Replication config is missing from the class schema.');
+ if (update === undefined) return current;
+ return { ...current, ...update };
+ }
+
+ static vectors(
+ current: WeaviateVectorsConfig,
+ update?: VectorConfigUpdate[]
+ ): WeaviateVectorsConfig {
+ if (current === undefined) throw Error('Vector index config is missing from the class schema.');
+ if (update === undefined) return current;
+ update.forEach((v) => {
+ const existing = current[v.name];
+ if (existing !== undefined) {
+ current[v.name].vectorIndexConfig =
+ v.vectorIndex.name === 'hnsw'
+ ? MergeWithExisting.hnsw(existing.vectorIndexConfig, v.vectorIndex.config)
+ : MergeWithExisting.flat(existing.vectorIndexConfig, v.vectorIndex.config);
+ }
+ });
+ return current;
+ }
+
+ static flat(
+ current: WeaviateVectorIndexConfig,
+ update?: VectorIndexConfigFlatUpdate
+ ): WeaviateVectorIndexConfig {
+ if (update === undefined) return current;
+ if (
+ (QuantizerGuards.isPQUpdate(update.quantizer) && (current?.bq as any).enabled) ||
+ (QuantizerGuards.isBQUpdate(update.quantizer) && (current?.pq as any).enabled)
+ )
+ throw Error(`Cannot update the quantizer type of an enabled vector index.`);
+ const { quantizer, ...rest } = update;
+ const merged: WeaviateVectorIndexConfig = { ...current, ...rest };
+ if (QuantizerGuards.isBQUpdate(quantizer)) {
+ const { type, ...quant } = quantizer;
+ merged.bq = { ...current!.bq!, ...quant, enabled: true };
+ }
+ return merged;
+ }
+
+ static hnsw(
+ current: WeaviateVectorIndexConfig,
+ update?: VectorIndexConfigHNSWUpdate
+ ): WeaviateVectorIndexConfig {
+ if (update === undefined) return current;
+ if (
+ (QuantizerGuards.isBQUpdate(update.quantizer) && ((current?.bq as any) || {}).enabled) ||
+ (QuantizerGuards.isPQUpdate(update.quantizer) && ((current?.pq as any) || {}).enabled)
+ )
+ throw Error(`Cannot update the quantizer type of an enabled vector index.`);
+ const { quantizer, ...rest } = update;
+ const merged: WeaviateVectorIndexConfig = { ...current, ...rest };
+ if (QuantizerGuards.isBQUpdate(quantizer)) {
+ const { type, ...quant } = quantizer;
+ merged.bq = { ...current!.bq!, ...quant, enabled: true };
+ }
+ if (QuantizerGuards.isPQUpdate(quantizer)) {
+ const { type, ...quant } = quantizer;
+ merged.pq = { ...current!.pq!, ...quant, enabled: true };
+ }
+ return merged;
+ }
+}
diff --git a/src/collections/config/index.ts b/src/collections/config/index.ts
new file mode 100644
index 00000000..e7065224
--- /dev/null
+++ b/src/collections/config/index.ts
@@ -0,0 +1,177 @@
+import Connection from '../../connection/index.js';
+import { WeaviateDeserializationError } from '../../errors.js';
+import { WeaviateShardStatus } from '../../openapi/types.js';
+import ClassUpdater from '../../schema/classUpdater.js';
+import { ClassGetter, PropertyCreator, ShardUpdater } from '../../schema/index.js';
+import ShardsGetter from '../../schema/shardsGetter.js';
+import { DbVersionSupport } from '../../utils/dbVersion.js';
+import {
+ PropertyConfigCreate,
+ ReferenceMultiTargetConfigCreate,
+ ReferenceSingleTargetConfigCreate,
+} from '../configure/types/index.js';
+import { MergeWithExisting } from './classes.js';
+import {
+ BQConfig,
+ CollectionConfig,
+ CollectionConfigUpdate,
+ PQConfig,
+ VectorIndexConfig,
+ VectorIndexConfigDynamic,
+ VectorIndexConfigFlat,
+ VectorIndexConfigHNSW,
+} from './types/index.js';
+import { classToCollection, resolveProperty, resolveReference } from './utils.js';
+
+const config = (
+ connection: Connection,
+ name: string,
+ dbVersionSupport: DbVersionSupport,
+ tenant?: string
+): Config => {
+ const getRaw = new ClassGetter(connection).withClassName(name).do;
+ return {
+ addProperty: (property: PropertyConfigCreate