-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
functional.ts
234 lines (218 loc) Β· 7.38 KB
/
functional.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
import {
Comparator,
Comparators,
Comparison,
Operation,
Operator,
Operators,
StructuredQuery,
} from "../../chains/query_constructor/ir.js";
import { Document } from "../../document.js";
import { BaseTranslator } from "./base.js";
import { castValue, isFilterEmpty } from "./utils.js";
/**
* A type alias for an object that maps comparison operators to string or
* number values. This is used in the comparison functions to determine
* the result of a comparison operation.
*/
type ValueType = {
eq: string | number;
ne: string | number;
lt: string | number;
lte: string | number;
gt: string | number;
gte: string | number;
};
/**
* A type alias for a function that takes a `Document` as an argument and
* returns a boolean. This function is used as a filter for documents.
*/
export type FunctionFilter = (document: Document) => boolean;
/**
* A class that extends `BaseTranslator` to translate structured queries
* into functional filters.
* @example
* ```typescript
* const functionalTranslator = new FunctionalTranslator();
* const relevantDocuments = await functionalTranslator.getRelevantDocuments(
* "Which movies are rated higher than 8.5?",
* );
* ```
*/
export class FunctionalTranslator extends BaseTranslator {
declare VisitOperationOutput: FunctionFilter;
declare VisitComparisonOutput: FunctionFilter;
declare VisitStructuredQueryOutput:
| { filter: FunctionFilter }
| { [k: string]: never };
allowedOperators: Operator[] = [Operators.and, Operators.or];
allowedComparators: Comparator[] = [
Comparators.eq,
Comparators.ne,
Comparators.gt,
Comparators.gte,
Comparators.lt,
Comparators.lte,
];
formatFunction(): string {
throw new Error("Not implemented");
}
/**
* Returns a function that performs a comparison based on the provided
* comparator.
* @param comparator The comparator to base the comparison function on.
* @returns A function that takes two arguments and returns a boolean based on the comparison.
*/
getComparatorFunction<C extends Comparator>(
comparator: Comparator
): (a: string | number, b: ValueType[C]) => boolean {
switch (comparator) {
case Comparators.eq: {
return (a: string | number, b: ValueType[C]) => a === b;
}
case Comparators.ne: {
return (a: string | number, b: ValueType[C]) => a !== b;
}
case Comparators.gt: {
return (a: string | number, b: ValueType[C]) => a > b;
}
case Comparators.gte: {
return (a: string | number, b: ValueType[C]) => a >= b;
}
case Comparators.lt: {
return (a: string | number, b: ValueType[C]) => a < b;
}
case Comparators.lte: {
return (a: string | number, b: ValueType[C]) => a <= b;
}
default: {
throw new Error("Unknown comparator");
}
}
}
/**
* Returns a function that performs an operation based on the provided
* operator.
* @param operator The operator to base the operation function on.
* @returns A function that takes two boolean arguments and returns a boolean based on the operation.
*/
getOperatorFunction(operator: Operator): (a: boolean, b: boolean) => boolean {
switch (operator) {
case Operators.and: {
return (a, b) => a && b;
}
case Operators.or: {
return (a, b) => a || b;
}
default: {
throw new Error("Unknown operator");
}
}
}
/**
* Visits the operation part of a structured query and translates it into
* a functional filter.
* @param operation The operation part of a structured query.
* @returns A function that takes a `Document` as an argument and returns a boolean based on the operation.
*/
visitOperation(operation: Operation): this["VisitOperationOutput"] {
const { operator, args } = operation;
if (this.allowedOperators.includes(operator)) {
const operatorFunction = this.getOperatorFunction(operator);
return (document: Document) => {
if (!args) {
return true;
}
return args.reduce((acc, arg) => {
const result = arg.accept(this);
if (typeof result === "function") {
return operatorFunction(acc, result(document));
} else {
throw new Error("Filter is not a function");
}
}, true);
};
} else {
throw new Error("Operator not allowed");
}
}
/**
* Visits the comparison part of a structured query and translates it into
* a functional filter.
* @param comparison The comparison part of a structured query.
* @returns A function that takes a `Document` as an argument and returns a boolean based on the comparison.
*/
visitComparison(comparison: Comparison): this["VisitComparisonOutput"] {
const { comparator, attribute, value } = comparison;
const undefinedTrue = [Comparators.ne];
if (this.allowedComparators.includes(comparator)) {
const comparatorFunction = this.getComparatorFunction(comparator);
return (document: Document) => {
const documentValue = document.metadata[attribute];
if (documentValue === undefined) {
if (undefinedTrue.includes(comparator)) {
return true;
}
return false;
}
return comparatorFunction(documentValue, castValue(value));
};
} else {
throw new Error("Comparator not allowed");
}
}
/**
* Visits a structured query and translates it into a functional filter.
* @param query The structured query to translate.
* @returns An object containing a `filter` property, which is a function that takes a `Document` as an argument and returns a boolean based on the structured query.
*/
visitStructuredQuery(
query: StructuredQuery
): this["VisitStructuredQueryOutput"] {
if (!query.filter) {
return {};
}
const filterFunction = query.filter?.accept(this);
if (typeof filterFunction !== "function") {
throw new Error("Structured query filter is not a function");
}
return { filter: filterFunction as FunctionFilter };
}
/**
* Merges two filters into one, based on the specified merge type.
* @param defaultFilter The default filter function.
* @param generatedFilter The generated filter function.
* @param mergeType The type of merge to perform. Can be 'and', 'or', or 'replace'. Default is 'and'.
* @returns A function that takes a `Document` as an argument and returns a boolean based on the merged filters, or `undefined` if both filters are empty.
*/
mergeFilters(
defaultFilter: FunctionFilter,
generatedFilter: FunctionFilter,
mergeType = "and"
): FunctionFilter | undefined {
if (isFilterEmpty(defaultFilter) && isFilterEmpty(generatedFilter)) {
return undefined;
}
if (isFilterEmpty(defaultFilter) || mergeType === "replace") {
if (isFilterEmpty(generatedFilter)) {
return undefined;
}
return generatedFilter;
}
if (isFilterEmpty(generatedFilter)) {
if (mergeType === "and") {
return undefined;
}
return defaultFilter;
}
if (mergeType === "and") {
return (document: Document) =>
defaultFilter(document) && generatedFilter(document);
} else if (mergeType === "or") {
return (document: Document) =>
defaultFilter(document) || generatedFilter(document);
} else {
throw new Error("Unknown merge type");
}
}
}