Skip to content

Commit d4c3b84

Browse files
committed
Add Filters.not() and .containsNone() capabilities
1 parent 36d6e63 commit d4c3b84

File tree

8 files changed

+1503
-36
lines changed

8 files changed

+1503
-36
lines changed

.github/workflows/main.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ env:
1212
WEAVIATE_129: 1.29.9
1313
WEAVIATE_130: 1.30.12
1414
WEAVIATE_131: 1.31.5
15-
WEAVIATE_132: 1.32.4-cdf9a3b
15+
WEAVIATE_132: 1.32.5
16+
WEAVIATE_133: 1.33.0-rc.1
1617

1718
concurrency:
1819
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -45,9 +46,10 @@ jobs:
4546
{ node: "22.x", weaviate: $WEAVIATE_129},
4647
{ node: "22.x", weaviate: $WEAVIATE_130},
4748
{ node: "22.x", weaviate: $WEAVIATE_131},
48-
{ node: "18.x", weaviate: $WEAVIATE_132},
49-
{ node: "20.x", weaviate: $WEAVIATE_132},
50-
{ node: "22.x", weaviate: $WEAVIATE_132}
49+
{ node: "22.x", weaviate: $WEAVIATE_132},
50+
{ node: "18.x", weaviate: $WEAVIATE_133},
51+
{ node: "20.x", weaviate: $WEAVIATE_133},
52+
{ node: "22.x", weaviate: $WEAVIATE_133},
5153
]
5254
steps:
5355
- uses: actions/checkout@v3

src/collections/filters/classes.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class Filters {
4343
static and(...filters: FilterValue[]): FilterValue<null> {
4444
return {
4545
operator: 'And',
46-
filters: filters,
46+
filters,
4747
value: null,
4848
};
4949
}
@@ -55,7 +55,19 @@ export class Filters {
5555
static or(...filters: FilterValue[]): FilterValue<null> {
5656
return {
5757
operator: 'Or',
58-
filters: filters,
58+
filters,
59+
value: null,
60+
};
61+
}
62+
/**
63+
* Negate a filter using the logical NOT operator.
64+
*
65+
* @param {FilterValue} filter The filter to negate.
66+
*/
67+
static not(filter: FilterValue): FilterValue<null> {
68+
return {
69+
operator: 'Not',
70+
filters: [filter],
5971
value: null,
6072
};
6173
}
@@ -140,6 +152,14 @@ export class FilterProperty<V> extends FilterBase implements FilterByProperty<V>
140152
};
141153
}
142154

155+
public containsNone<U extends ContainsValue<V>>(value: U[]): FilterValue<U[]> {
156+
return {
157+
operator: 'ContainsNone',
158+
target: this.targetPath(),
159+
value: value,
160+
};
161+
}
162+
143163
public containsAll<U extends ContainsValue<V>>(value: U[]): FilterValue<U[]> {
144164
return {
145165
operator: 'ContainsAll',

src/collections/filters/integration.test.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-non-null-assertion */
22
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
3+
import { requireAtLeast } from '../../../test/version.js';
34
import weaviate, { WeaviateClient } from '../../index.js';
45
import { Collection } from '../collection/index.js';
56
import { CrossReference, Reference } from '../references/index.js';
@@ -120,6 +121,34 @@ describe('Testing of the filter class with a simple collection', () => {
120121
expect(obj.uuid).toEqual(ids[1]);
121122
});
122123

124+
it('should filter a fetch objects query with a contains-all filter', async () => {
125+
const res = await collection.query.fetchObjects({
126+
filters: collection.filter.byProperty('text').containsAll(['two']),
127+
});
128+
expect(res.objects.length).toEqual(1);
129+
const obj = res.objects[0];
130+
expect(obj.properties.text).toEqual('two');
131+
});
132+
133+
it('should filter a fetch objects query with a contains-any filter', async () => {
134+
const res = await collection.query.fetchObjects({
135+
filters: collection.filter.byProperty('text').containsAny(['two', 'three']),
136+
});
137+
expect(res.objects.length).toEqual(2);
138+
const texts = res.objects.map((o) => o.properties.text);
139+
expect(texts).toContain('two');
140+
expect(texts).toContain('three');
141+
});
142+
143+
requireAtLeast(1, 33, 0).it('should filter a fetch objects query with a contains-none filter', async () => {
144+
const res = await collection.query.fetchObjects({
145+
filters: collection.filter.byProperty('text').containsNone(['one', 'three']),
146+
});
147+
expect(res.objects.length).toEqual(1);
148+
const obj = res.objects[0];
149+
expect(obj.properties.text).toEqual('two');
150+
});
151+
123152
it('should filter a fetch objects query with an AND filter', async () => {
124153
const res = await collection.query.fetchObjects({
125154
filters: Filters.and(
@@ -147,15 +176,16 @@ describe('Testing of the filter class with a simple collection', () => {
147176
// Return of fetch not necessarily in order due to filter
148177
expect(res.objects.map((o) => o.properties.text)).toContain('two');
149178
expect(res.objects.map((o) => o.properties.text)).toContain('three');
179+
});
150180

151-
expect(res.objects.map((o) => o.properties.int)).toContain(2);
152-
expect(res.objects.map((o) => o.properties.int)).toContain(3);
153-
154-
expect(res.objects.map((o) => o.properties.float)).toContain(2.2);
155-
expect(res.objects.map((o) => o.properties.float)).toContain(3.3);
181+
requireAtLeast(1, 33, 0).it('should filter a fetch objects query with a NOT filter', async () => {
182+
const res = await collection.query.fetchObjects({
183+
filters: Filters.not(collection.filter.byProperty('text').equal('one')),
184+
});
185+
expect(res.objects.length).toEqual(2);
156186

157-
expect(res.objects.map((o) => o.uuid)).toContain(ids[1]);
158-
expect(res.objects.map((o) => o.uuid)).toContain(ids[2]);
187+
expect(res.objects.map((o) => o.properties.text)).toContain('two');
188+
expect(res.objects.map((o) => o.properties.text)).toContain('three');
159189
});
160190

161191
it('should filter a fetch objects query with a reference filter', async () => {

src/collections/filters/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ export type Operator =
1313
| 'WithinGeoRange'
1414
| 'ContainsAny'
1515
| 'ContainsAll'
16+
| 'ContainsNone'
1617
| 'And'
17-
| 'Or';
18+
| 'Or'
19+
| 'Not';
1820

1921
export type FilterValue<V = any> = {
2022
filters?: FilterValue[];
@@ -133,6 +135,13 @@ export interface FilterByProperty<T> {
133135
* @returns {FilterValue<U[]>} The filter value.
134136
*/
135137
containsAll: <U extends ContainsValue<T>>(value: U[]) => FilterValue<U[]>;
138+
/**
139+
* Filter on whether the property contains none of the given values.
140+
*
141+
* @param {U[]} value The values to filter on.
142+
* @returns {FilterValue<U[]>} The filter value.
143+
*/
144+
containsNone: <U extends ContainsValue<T>>(value: U[]) => FilterValue<U[]>;
136145
/**
137146
* Filter on whether the property is equal to the given value.
138147
*

src/collections/filters/unit.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('Unit testing of filters', () => {
2525
});
2626
});
2727

28-
it('should create a contains all filter with a primitive type', () => {
28+
it('should create a contains all filter with an array type', () => {
2929
const f = filter.byProperty('name').containsAll(['John', 'Doe']);
3030
expect(f).toEqual<FilterValue<string[]>>({
3131
operator: 'ContainsAll',
@@ -47,7 +47,7 @@ describe('Unit testing of filters', () => {
4747
});
4848
});
4949

50-
it('should create a contains any filter with a primitive type', () => {
50+
it('should create a contains any filter with an array type', () => {
5151
const f = filter.byProperty('name').containsAny(['John', 'Doe']);
5252
expect(f).toEqual<FilterValue<string[]>>({
5353
operator: 'ContainsAny',
@@ -69,6 +69,17 @@ describe('Unit testing of filters', () => {
6969
});
7070
});
7171

72+
it('should create a contains none filter with an array type', () => {
73+
const f = filter.byProperty('friends').containsNone(['John', 'Doe']);
74+
expect(f).toEqual<FilterValue<string[]>>({
75+
operator: 'ContainsNone',
76+
target: {
77+
property: 'friends',
78+
},
79+
value: ['John', 'Doe'],
80+
});
81+
});
82+
7283
it('should create an equal filter', () => {
7384
const f = filter.byProperty('name').equal('John');
7485
expect(f).toEqual<FilterValue<string>>({
@@ -893,5 +904,20 @@ describe('Unit testing of filters', () => {
893904
],
894905
});
895906
});
907+
908+
it('should map a NOT filter', () => {
909+
const f = Filters.not(filter.byProperty('name').equal('John'));
910+
const s = Serialize.filtersREST(f);
911+
expect(s).toEqual<WhereFilter>({
912+
operator: 'Not',
913+
operands: [
914+
{
915+
operator: 'Equal',
916+
path: ['name'],
917+
valueText: 'John',
918+
},
919+
],
920+
});
921+
});
896922
});
897923
});

src/collections/serialize/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1504,6 +1504,11 @@ export class Serialize {
15041504
operator: Filters_Operator.OPERATOR_OR,
15051505
filters: resolveFilters(filters),
15061506
});
1507+
case 'Not':
1508+
return FiltersGRPC.fromPartial({
1509+
operator: Filters_Operator.OPERATOR_NOT,
1510+
filters: resolveFilters(filters),
1511+
});
15071512
default:
15081513
return FiltersGRPC.fromPartial({
15091514
operator: Serialize.operator(filters.operator),
@@ -1568,7 +1573,7 @@ export class Serialize {
15681573

15691574
public static filtersREST = (filters: FilterValue): WhereFilter => {
15701575
const { value } = filters;
1571-
if (filters.operator === 'And' || filters.operator === 'Or') {
1576+
if (filters.operator === 'And' || filters.operator === 'Or' || filters.operator === 'Not') {
15721577
return {
15731578
operator: filters.operator,
15741579
operands: filters.filters?.map(Serialize.filtersREST),
@@ -1660,6 +1665,8 @@ export class Serialize {
16601665
return Filters_Operator.OPERATOR_CONTAINS_ANY;
16611666
case 'ContainsAll':
16621667
return Filters_Operator.OPERATOR_CONTAINS_ALL;
1668+
case 'ContainsNone':
1669+
return Filters_Operator.OPERATOR_CONTAINS_NONE;
16631670
case 'GreaterThan':
16641671
return Filters_Operator.OPERATOR_GREATER_THAN;
16651672
case 'GreaterThanEqual':

0 commit comments

Comments
 (0)