@lossless.org/client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +37 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.losslessorgclient.d.ts +32 -0
- package/dist_ts/classes.losslessorgclient.js +103 -0
- package/dist_ts/core/classes.error.d.ts +16 -0
- package/dist_ts/core/classes.error.js +29 -0
- package/dist_ts/core/classes.operation.d.ts +17 -0
- package/dist_ts/core/classes.operation.js +70 -0
- package/dist_ts/core/interfaces.d.ts +18 -0
- package/dist_ts/core/interfaces.js +2 -0
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +3 -0
- package/dist_ts/nosqldb/classes.atomicdelete.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicdelete.js +44 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +27 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +86 -0
- package/dist_ts/nosqldb/classes.atomicupdate.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicupdate.js +55 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.d.ts +14 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.js +80 -0
- package/dist_ts/nosqldb/classes.bsonassertion.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonassertion.js +29 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.js +50 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.d.ts +3 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.js +126 -0
- package/dist_ts/nosqldb/classes.cached-document.d.ts +27 -0
- package/dist_ts/nosqldb/classes.cached-document.js +112 -0
- package/dist_ts/nosqldb/classes.collection.d.ts +302 -0
- package/dist_ts/nosqldb/classes.collection.js +1568 -0
- package/dist_ts/nosqldb/classes.collectionfactory.d.ts +30 -0
- package/dist_ts/nosqldb/classes.collectionfactory.js +65 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.d.ts +4 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.js +15 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.d.ts +34 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.js +134 -0
- package/dist_ts/nosqldb/classes.collectiontopology.d.ts +47 -0
- package/dist_ts/nosqldb/classes.collectiontopology.js +388 -0
- package/dist_ts/nosqldb/classes.connection.d.ts +18 -0
- package/dist_ts/nosqldb/classes.connection.js +23 -0
- package/dist_ts/nosqldb/classes.convenience.d.ts +1 -0
- package/dist_ts/nosqldb/classes.convenience.js +5 -0
- package/dist_ts/nosqldb/classes.cursor.d.ts +15 -0
- package/dist_ts/nosqldb/classes.cursor.js +82 -0
- package/dist_ts/nosqldb/classes.cursoroperation.d.ts +7 -0
- package/dist_ts/nosqldb/classes.cursoroperation.js +36 -0
- package/dist_ts/nosqldb/classes.db.d.ts +98 -0
- package/dist_ts/nosqldb/classes.db.js +434 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.d.ts +52 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.js +314 -0
- package/dist_ts/nosqldb/classes.doc.d.ts +722 -0
- package/dist_ts/nosqldb/classes.doc.js +2976 -0
- package/dist_ts/nosqldb/classes.easystore.d.ts +42 -0
- package/dist_ts/nosqldb/classes.easystore.js +199 -0
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +141 -0
- package/dist_ts/nosqldb/classes.exactpersistence.js +1110 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.d.ts +173 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.js +623 -0
- package/dist_ts/nosqldb/classes.mongoadministration.d.ts +73 -0
- package/dist_ts/nosqldb/classes.mongoadministration.js +497 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.d.ts +42 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.js +138 -0
- package/dist_ts/nosqldb/classes.operationbudget.d.ts +22 -0
- package/dist_ts/nosqldb/classes.operationbudget.js +65 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.d.ts +18 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.js +71 -0
- package/dist_ts/nosqldb/classes.persistence.d.ts +14 -0
- package/dist_ts/nosqldb/classes.persistence.js +34 -0
- package/dist_ts/nosqldb/classes.session.d.ts +28 -0
- package/dist_ts/nosqldb/classes.session.js +246 -0
- package/dist_ts/nosqldb/classes.storedinspection.d.ts +44 -0
- package/dist_ts/nosqldb/classes.storedinspection.js +100 -0
- package/dist_ts/nosqldb/classes.testdatabase.d.ts +56 -0
- package/dist_ts/nosqldb/classes.testdatabase.js +250 -0
- package/dist_ts/nosqldb/classes.watcher.d.ts +37 -0
- package/dist_ts/nosqldb/classes.watcher.js +98 -0
- package/dist_ts/nosqldb/index.d.ts +27 -0
- package/dist_ts/nosqldb/index.js +18 -0
- package/dist_ts/nosqldb/logging.d.ts +2 -0
- package/dist_ts/nosqldb/logging.js +3 -0
- package/dist_ts/nosqldb/plugins.d.ts +17 -0
- package/dist_ts/nosqldb/plugins.js +20 -0
- package/dist_ts/nosqldb/shim.d.ts +1 -0
- package/dist_ts/nosqldb/shim.js +8 -0
- package/dist_ts/nosqldb/testsupport.d.ts +3 -0
- package/dist_ts/nosqldb/testsupport.js +3 -0
- package/dist_ts/objectstorage/classes.bucket.d.ts +227 -0
- package/dist_ts/objectstorage/classes.bucket.js +939 -0
- package/dist_ts/objectstorage/classes.connection.d.ts +21 -0
- package/dist_ts/objectstorage/classes.connection.js +27 -0
- package/dist_ts/objectstorage/classes.directory.d.ts +124 -0
- package/dist_ts/objectstorage/classes.directory.js +335 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.js +18 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.js +19 -0
- package/dist_ts/objectstorage/classes.file.d.ts +91 -0
- package/dist_ts/objectstorage/classes.file.js +255 -0
- package/dist_ts/objectstorage/classes.listcursor.d.ts +41 -0
- package/dist_ts/objectstorage/classes.listcursor.js +67 -0
- package/dist_ts/objectstorage/classes.metadata.d.ts +57 -0
- package/dist_ts/objectstorage/classes.metadata.js +113 -0
- package/dist_ts/objectstorage/classes.smartbucket.d.ts +57 -0
- package/dist_ts/objectstorage/classes.smartbucket.js +357 -0
- package/dist_ts/objectstorage/classes.trash.d.ts +11 -0
- package/dist_ts/objectstorage/classes.trash.js +22 -0
- package/dist_ts/objectstorage/classes.watcher.d.ts +79 -0
- package/dist_ts/objectstorage/classes.watcher.js +249 -0
- package/dist_ts/objectstorage/helpers.d.ts +25 -0
- package/dist_ts/objectstorage/helpers.js +215 -0
- package/dist_ts/objectstorage/index.d.ts +13 -0
- package/dist_ts/objectstorage/index.js +13 -0
- package/dist_ts/objectstorage/interfaces.d.ts +276 -0
- package/dist_ts/objectstorage/interfaces.js +2 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1069 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.d.ts +40 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.js +382 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.js +1104 -0
- package/dist_ts/objectstorage/internal.multipart.planner.d.ts +8 -0
- package/dist_ts/objectstorage/internal.multipart.planner.js +63 -0
- package/dist_ts/objectstorage/internal.multipart.stream.d.ts +17 -0
- package/dist_ts/objectstorage/internal.multipart.stream.js +440 -0
- package/dist_ts/objectstorage/internal.multipart.types.d.ts +82 -0
- package/dist_ts/objectstorage/internal.multipart.types.js +2 -0
- package/dist_ts/objectstorage/plugins.d.ts +17 -0
- package/dist_ts/objectstorage/plugins.js +23 -0
- package/dist_ts/plugins.d.ts +4 -0
- package/dist_ts/plugins.js +5 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +39 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.js +225 -0
- package/dist_ts/sqldb/classes.sqlconnection.d.ts +50 -0
- package/dist_ts/sqldb/classes.sqlconnection.js +376 -0
- package/dist_ts/sqldb/classes.sqltable.d.ts +29 -0
- package/dist_ts/sqldb/classes.sqltable.js +70 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.d.ts +27 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +89 -0
- package/dist_ts/sqldb/clickhouse/index.d.ts +9 -0
- package/dist_ts/sqldb/clickhouse/index.js +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.d.ts +88 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +304 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +28 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +46 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.d.ts +43 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +155 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.d.ts +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.js +36 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.d.ts +49 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +106 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.d.ts +51 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +96 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.d.ts +5 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.js +6 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.d.ts +53 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.js +41 -0
- package/dist_ts/sqldb/index.d.ts +6 -0
- package/dist_ts/sqldb/index.js +6 -0
- package/dist_ts/sqldb/interfaces.d.ts +53 -0
- package/dist_ts/sqldb/interfaces.js +19 -0
- package/dist_ts/sqldb/internal.sqlpool.d.ts +51 -0
- package/dist_ts/sqldb/internal.sqlpool.js +214 -0
- package/dist_ts/sqldb/plugins.clickhouse.d.ts +4 -0
- package/dist_ts/sqldb/plugins.clickhouse.js +5 -0
- package/dist_ts/sqldb/plugins.mariadb.d.ts +4 -0
- package/dist_ts/sqldb/plugins.mariadb.js +5 -0
- package/docs/source/smartbucket/license.md +19 -0
- package/docs/source/smartclickhouse/license.md +19 -0
- package/license.md +21 -0
- package/package.json +101 -0
- package/readme.md +157 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.losslessorgclient.ts +104 -0
- package/ts/core/classes.error.ts +31 -0
- package/ts/core/classes.operation.ts +60 -0
- package/ts/core/interfaces.ts +24 -0
- package/ts/index.ts +4 -0
- package/ts/nosqldb/classes.atomicdelete.ts +81 -0
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +152 -0
- package/ts/nosqldb/classes.atomicupdate.ts +99 -0
- package/ts/nosqldb/classes.bootstraprecord.ts +107 -0
- package/ts/nosqldb/classes.bsonassertion.ts +25 -0
- package/ts/nosqldb/classes.bsonobjectid.ts +45 -0
- package/ts/nosqldb/classes.bsonsnapshot.ts +105 -0
- package/ts/nosqldb/classes.cached-document.ts +67 -0
- package/ts/nosqldb/classes.collection.ts +2399 -0
- package/ts/nosqldb/classes.collectionfactory.ts +87 -0
- package/ts/nosqldb/classes.collectionlifecycle.ts +24 -0
- package/ts/nosqldb/classes.collectionpreparation.ts +164 -0
- package/ts/nosqldb/classes.collectiontopology.ts +645 -0
- package/ts/nosqldb/classes.connection.ts +31 -0
- package/ts/nosqldb/classes.convenience.ts +5 -0
- package/ts/nosqldb/classes.cursor.ts +88 -0
- package/ts/nosqldb/classes.cursoroperation.ts +38 -0
- package/ts/nosqldb/classes.db.ts +637 -0
- package/ts/nosqldb/classes.distributedcoordinator.ts +305 -0
- package/ts/nosqldb/classes.doc.ts +4928 -0
- package/ts/nosqldb/classes.easystore.ts +154 -0
- package/ts/nosqldb/classes.exactpersistence.ts +1974 -0
- package/ts/nosqldb/classes.lucene.adapter.ts +780 -0
- package/ts/nosqldb/classes.mongoadministration.ts +867 -0
- package/ts/nosqldb/classes.namespaceinspection.ts +199 -0
- package/ts/nosqldb/classes.operationbudget.ts +75 -0
- package/ts/nosqldb/classes.ordinarypersistence.ts +84 -0
- package/ts/nosqldb/classes.persistence.ts +62 -0
- package/ts/nosqldb/classes.session.ts +389 -0
- package/ts/nosqldb/classes.storedinspection.ts +151 -0
- package/ts/nosqldb/classes.testdatabase.ts +272 -0
- package/ts/nosqldb/classes.watcher.ts +96 -0
- package/ts/nosqldb/index.ts +51 -0
- package/ts/nosqldb/logging.ts +3 -0
- package/ts/nosqldb/plugins.ts +37 -0
- package/ts/nosqldb/shim.ts +6 -0
- package/ts/nosqldb/testsupport.ts +13 -0
- package/ts/objectstorage/classes.bucket.ts +1114 -0
- package/ts/objectstorage/classes.connection.ts +36 -0
- package/ts/objectstorage/classes.directory.ts +425 -0
- package/ts/objectstorage/classes.exactpathpurgeerror.ts +24 -0
- package/ts/objectstorage/classes.exactuploaderror.ts +25 -0
- package/ts/objectstorage/classes.file.ts +303 -0
- package/ts/objectstorage/classes.listcursor.ts +89 -0
- package/ts/objectstorage/classes.metadata.ts +134 -0
- package/ts/objectstorage/classes.smartbucket.ts +445 -0
- package/ts/objectstorage/classes.trash.ts +30 -0
- package/ts/objectstorage/classes.watcher.ts +289 -0
- package/ts/objectstorage/helpers.ts +254 -0
- package/ts/objectstorage/index.ts +13 -0
- package/ts/objectstorage/interfaces.ts +389 -0
- package/ts/objectstorage/internal.exactpathpurge.operations.ts +1936 -0
- package/ts/objectstorage/internal.exactupload.capability.ts +580 -0
- package/ts/objectstorage/internal.exactupload.operations.ts +1715 -0
- package/ts/objectstorage/internal.multipart.planner.ts +90 -0
- package/ts/objectstorage/internal.multipart.stream.ts +606 -0
- package/ts/objectstorage/internal.multipart.types.ts +106 -0
- package/ts/objectstorage/plugins.ts +35 -0
- package/ts/plugins.ts +4 -0
- package/ts/sqldb/classes.clickhouseconnection.ts +189 -0
- package/ts/sqldb/classes.sqlconnection.ts +350 -0
- package/ts/sqldb/classes.sqltable.ts +70 -0
- package/ts/sqldb/clickhouse/classes.metricwriter.ts +72 -0
- package/ts/sqldb/clickhouse/index.ts +16 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.ts +372 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.ts +52 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.ts +120 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.resultset.ts +44 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.ts +123 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.ts +114 -0
- package/ts/sqldb/clickhouse/smartclickhouse.plugins.ts +5 -0
- package/ts/sqldb/clickhouse/smartclickhouse.types.ts +134 -0
- package/ts/sqldb/index.ts +6 -0
- package/ts/sqldb/interfaces.ts +61 -0
- package/ts/sqldb/internal.sqlpool.ts +179 -0
- package/ts/sqldb/plugins.clickhouse.ts +4 -0
- package/ts/sqldb/plugins.mariadb.ts +4 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lucene to MongoDB query adapter for SmartData
|
|
3
|
+
*/
|
|
4
|
+
import * as plugins from './plugins.js';
|
|
5
|
+
import { logger } from './logging.js';
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
type NodeType =
|
|
9
|
+
| 'TERM'
|
|
10
|
+
| 'PHRASE'
|
|
11
|
+
| 'FIELD'
|
|
12
|
+
| 'AND'
|
|
13
|
+
| 'OR'
|
|
14
|
+
| 'NOT'
|
|
15
|
+
| 'RANGE'
|
|
16
|
+
| 'WILDCARD'
|
|
17
|
+
| 'FUZZY'
|
|
18
|
+
| 'GROUP';
|
|
19
|
+
|
|
20
|
+
interface QueryNode {
|
|
21
|
+
type: NodeType;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface TermNode extends QueryNode {
|
|
25
|
+
type: 'TERM';
|
|
26
|
+
value: string;
|
|
27
|
+
boost?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PhraseNode extends QueryNode {
|
|
31
|
+
type: 'PHRASE';
|
|
32
|
+
value: string;
|
|
33
|
+
proximity?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface FieldNode extends QueryNode {
|
|
37
|
+
type: 'FIELD';
|
|
38
|
+
field: string;
|
|
39
|
+
value: AnyQueryNode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface BooleanNode extends QueryNode {
|
|
43
|
+
type: 'AND' | 'OR' | 'NOT';
|
|
44
|
+
left: AnyQueryNode;
|
|
45
|
+
right: AnyQueryNode;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface RangeNode extends QueryNode {
|
|
49
|
+
type: 'RANGE';
|
|
50
|
+
field: string;
|
|
51
|
+
lower: string;
|
|
52
|
+
upper: string;
|
|
53
|
+
includeLower: boolean;
|
|
54
|
+
includeUpper: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface WildcardNode extends QueryNode {
|
|
58
|
+
type: 'WILDCARD';
|
|
59
|
+
value: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface FuzzyNode extends QueryNode {
|
|
63
|
+
type: 'FUZZY';
|
|
64
|
+
value: string;
|
|
65
|
+
maxEdits: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface GroupNode extends QueryNode {
|
|
69
|
+
type: 'GROUP';
|
|
70
|
+
value: AnyQueryNode;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type AnyQueryNode =
|
|
74
|
+
| TermNode
|
|
75
|
+
| PhraseNode
|
|
76
|
+
| FieldNode
|
|
77
|
+
| BooleanNode
|
|
78
|
+
| RangeNode
|
|
79
|
+
| WildcardNode
|
|
80
|
+
| FuzzyNode
|
|
81
|
+
| GroupNode;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Lucene query parser
|
|
85
|
+
*/
|
|
86
|
+
export class LuceneParser {
|
|
87
|
+
private pos: number = 0;
|
|
88
|
+
private input: string = '';
|
|
89
|
+
private tokens: string[] = [];
|
|
90
|
+
|
|
91
|
+
constructor() {}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse a Lucene query string into an AST
|
|
95
|
+
*/
|
|
96
|
+
parse(query: string): AnyQueryNode {
|
|
97
|
+
this.input = query.trim();
|
|
98
|
+
this.pos = 0;
|
|
99
|
+
this.tokens = this.tokenize(this.input);
|
|
100
|
+
|
|
101
|
+
return this.parseQuery();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Tokenize the input string into tokens
|
|
106
|
+
*/
|
|
107
|
+
private tokenize(input: string): string[] {
|
|
108
|
+
const specialChars = /[()\[\]{}"~^:]/;
|
|
109
|
+
const operators = /AND|OR|NOT|TO/;
|
|
110
|
+
|
|
111
|
+
let tokens: string[] = [];
|
|
112
|
+
let current = '';
|
|
113
|
+
let inQuote = false;
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < input.length; i++) {
|
|
116
|
+
const char = input[i];
|
|
117
|
+
|
|
118
|
+
// Handle quoted strings
|
|
119
|
+
if (char === '"') {
|
|
120
|
+
if (inQuote) {
|
|
121
|
+
tokens.push(current + char);
|
|
122
|
+
current = '';
|
|
123
|
+
inQuote = false;
|
|
124
|
+
} else {
|
|
125
|
+
if (current) tokens.push(current);
|
|
126
|
+
current = char;
|
|
127
|
+
inQuote = true;
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (inQuote) {
|
|
133
|
+
current += char;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle whitespace
|
|
138
|
+
if (char === ' ' || char === '\t' || char === '\n') {
|
|
139
|
+
if (current) {
|
|
140
|
+
tokens.push(current);
|
|
141
|
+
current = '';
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Handle special characters
|
|
147
|
+
if (specialChars.test(char)) {
|
|
148
|
+
if (current) {
|
|
149
|
+
tokens.push(current);
|
|
150
|
+
current = '';
|
|
151
|
+
}
|
|
152
|
+
tokens.push(char);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
current += char;
|
|
157
|
+
|
|
158
|
+
// Check if current is an operator
|
|
159
|
+
if (operators.test(current) && (i + 1 === input.length || /\s/.test(input[i + 1]))) {
|
|
160
|
+
tokens.push(current);
|
|
161
|
+
current = '';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (current) tokens.push(current);
|
|
166
|
+
|
|
167
|
+
return tokens;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parse the main query expression
|
|
172
|
+
*/
|
|
173
|
+
private parseQuery(): AnyQueryNode {
|
|
174
|
+
const left = this.parseBooleanOperand();
|
|
175
|
+
|
|
176
|
+
if (this.pos < this.tokens.length) {
|
|
177
|
+
const token = this.tokens[this.pos];
|
|
178
|
+
|
|
179
|
+
if (token === 'AND' || token === 'OR') {
|
|
180
|
+
this.pos++;
|
|
181
|
+
const right = this.parseQuery();
|
|
182
|
+
return {
|
|
183
|
+
type: token as 'AND' | 'OR',
|
|
184
|
+
left,
|
|
185
|
+
right,
|
|
186
|
+
};
|
|
187
|
+
} else if (token === 'NOT' || token === '-') {
|
|
188
|
+
this.pos++;
|
|
189
|
+
const right = this.parseQuery();
|
|
190
|
+
return {
|
|
191
|
+
type: 'NOT',
|
|
192
|
+
left,
|
|
193
|
+
right,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return left;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parse boolean operands (terms, phrases, fields, groups)
|
|
203
|
+
*/
|
|
204
|
+
private parseBooleanOperand(): AnyQueryNode {
|
|
205
|
+
if (this.pos >= this.tokens.length) {
|
|
206
|
+
throw new Error('Unexpected end of input');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const token = this.tokens[this.pos];
|
|
210
|
+
|
|
211
|
+
// Handle grouping with parentheses
|
|
212
|
+
if (token === '(') {
|
|
213
|
+
this.pos++;
|
|
214
|
+
const group = this.parseQuery();
|
|
215
|
+
|
|
216
|
+
if (this.pos < this.tokens.length && this.tokens[this.pos] === ')') {
|
|
217
|
+
this.pos++;
|
|
218
|
+
return { type: 'GROUP', value: group } as GroupNode;
|
|
219
|
+
} else {
|
|
220
|
+
throw new Error('Unclosed group');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Handle fields (field:value)
|
|
225
|
+
if (this.pos + 1 < this.tokens.length && this.tokens[this.pos + 1] === ':') {
|
|
226
|
+
const field = token;
|
|
227
|
+
this.pos += 2; // Skip field and colon
|
|
228
|
+
|
|
229
|
+
if (this.pos < this.tokens.length) {
|
|
230
|
+
const value = this.parseBooleanOperand();
|
|
231
|
+
return { type: 'FIELD', field, value } as FieldNode;
|
|
232
|
+
} else {
|
|
233
|
+
throw new Error('Expected value after field');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handle range queries
|
|
238
|
+
if (token === '[' || token === '{') {
|
|
239
|
+
return this.parseRange();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Handle phrases ("term term")
|
|
243
|
+
if (token.startsWith('"') && token.endsWith('"')) {
|
|
244
|
+
const phrase = token.slice(1, -1);
|
|
245
|
+
this.pos++;
|
|
246
|
+
|
|
247
|
+
// Check for proximity operator
|
|
248
|
+
let proximity: number | undefined;
|
|
249
|
+
if (this.pos < this.tokens.length && this.tokens[this.pos] === '~') {
|
|
250
|
+
this.pos++;
|
|
251
|
+
if (this.pos < this.tokens.length && /^\d+$/.test(this.tokens[this.pos])) {
|
|
252
|
+
proximity = parseInt(this.tokens[this.pos], 10);
|
|
253
|
+
this.pos++;
|
|
254
|
+
} else {
|
|
255
|
+
throw new Error('Expected number after proximity operator');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { type: 'PHRASE', value: phrase, proximity } as PhraseNode;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Handle wildcards
|
|
263
|
+
if (token.includes('*') || token.includes('?')) {
|
|
264
|
+
this.pos++;
|
|
265
|
+
return { type: 'WILDCARD', value: token } as WildcardNode;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Handle fuzzy searches
|
|
269
|
+
if (this.pos + 1 < this.tokens.length && this.tokens[this.pos + 1] === '~') {
|
|
270
|
+
const term = token;
|
|
271
|
+
this.pos += 2; // Skip term and tilde
|
|
272
|
+
|
|
273
|
+
let maxEdits = 2; // Default
|
|
274
|
+
if (this.pos < this.tokens.length && /^\d+$/.test(this.tokens[this.pos])) {
|
|
275
|
+
maxEdits = parseInt(this.tokens[this.pos], 10);
|
|
276
|
+
this.pos++;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { type: 'FUZZY', value: term, maxEdits } as FuzzyNode;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Simple term
|
|
283
|
+
this.pos++;
|
|
284
|
+
return { type: 'TERM', value: token } as TermNode;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Parse range queries
|
|
289
|
+
*/
|
|
290
|
+
private parseRange(): RangeNode {
|
|
291
|
+
const includeLower = this.tokens[this.pos] === '[';
|
|
292
|
+
const includeUpper = this.tokens[this.pos + 4] === ']';
|
|
293
|
+
|
|
294
|
+
// Ensure tokens for lower, TO, upper, and closing bracket exist
|
|
295
|
+
if (this.pos + 4 >= this.tokens.length) {
|
|
296
|
+
throw new Error('Invalid range query syntax');
|
|
297
|
+
}
|
|
298
|
+
this.pos++; // Skip open bracket
|
|
299
|
+
|
|
300
|
+
const lower = this.tokens[this.pos];
|
|
301
|
+
this.pos++;
|
|
302
|
+
|
|
303
|
+
if (this.tokens[this.pos] !== 'TO') {
|
|
304
|
+
throw new Error('Expected TO in range query');
|
|
305
|
+
}
|
|
306
|
+
this.pos++;
|
|
307
|
+
|
|
308
|
+
const upper = this.tokens[this.pos];
|
|
309
|
+
this.pos++;
|
|
310
|
+
|
|
311
|
+
if (this.tokens[this.pos] !== (includeLower ? ']' : '}')) {
|
|
312
|
+
throw new Error('Invalid range query closing bracket');
|
|
313
|
+
}
|
|
314
|
+
this.pos++;
|
|
315
|
+
|
|
316
|
+
// For simplicity, assuming the field is handled separately
|
|
317
|
+
return {
|
|
318
|
+
type: 'RANGE',
|
|
319
|
+
field: '', // This will be filled by the field node
|
|
320
|
+
lower,
|
|
321
|
+
upper,
|
|
322
|
+
includeLower,
|
|
323
|
+
includeUpper,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Transformer for Lucene AST to MongoDB query
|
|
330
|
+
* FIXED VERSION - proper MongoDB query structure
|
|
331
|
+
*/
|
|
332
|
+
export class LuceneToMongoTransformer {
|
|
333
|
+
private defaultFields: string[];
|
|
334
|
+
constructor(defaultFields: string[] = []) {
|
|
335
|
+
this.defaultFields = defaultFields;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Escape special characters for use in RegExp patterns
|
|
339
|
+
*/
|
|
340
|
+
private escapeRegex(input: string): string {
|
|
341
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Transform a Lucene AST node to a MongoDB query
|
|
346
|
+
*/
|
|
347
|
+
transform(node: AnyQueryNode, searchFields?: string[]): any {
|
|
348
|
+
switch (node.type) {
|
|
349
|
+
case 'TERM':
|
|
350
|
+
return this.transformTerm(node, searchFields);
|
|
351
|
+
case 'PHRASE':
|
|
352
|
+
return this.transformPhrase(node, searchFields);
|
|
353
|
+
case 'FIELD':
|
|
354
|
+
return this.transformField(node);
|
|
355
|
+
case 'AND':
|
|
356
|
+
return this.transformAnd(node);
|
|
357
|
+
case 'OR':
|
|
358
|
+
return this.transformOr(node);
|
|
359
|
+
case 'NOT':
|
|
360
|
+
return this.transformNot(node);
|
|
361
|
+
case 'RANGE':
|
|
362
|
+
return this.transformRange(node);
|
|
363
|
+
case 'WILDCARD':
|
|
364
|
+
return this.transformWildcard(node, searchFields);
|
|
365
|
+
case 'FUZZY':
|
|
366
|
+
return this.transformFuzzy(node, searchFields);
|
|
367
|
+
case 'GROUP':
|
|
368
|
+
return this.transform(node.value, searchFields);
|
|
369
|
+
default:
|
|
370
|
+
throw new Error(`Unsupported node type: ${(node as any).type}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Transform a term to MongoDB query
|
|
376
|
+
* FIXED: properly structured $or query for multiple fields
|
|
377
|
+
*/
|
|
378
|
+
private transformTerm(node: TermNode, searchFields?: string[]): any {
|
|
379
|
+
// Build regex pattern, support wildcard (*) and fuzzy (?) if present
|
|
380
|
+
const term = node.value;
|
|
381
|
+
// Determine regex pattern: wildcard conversion or exact escape
|
|
382
|
+
let pattern: string;
|
|
383
|
+
if (term.includes('*') || term.includes('?')) {
|
|
384
|
+
pattern = this.luceneWildcardToRegex(term);
|
|
385
|
+
} else {
|
|
386
|
+
pattern = this.escapeRegex(term);
|
|
387
|
+
}
|
|
388
|
+
// Search across provided fields or default fields
|
|
389
|
+
const fields = searchFields && searchFields.length > 0 ? searchFields : this.defaultFields;
|
|
390
|
+
const orConditions = fields.map((field) => ({
|
|
391
|
+
[field]: { $regex: pattern, $options: 'i' },
|
|
392
|
+
}));
|
|
393
|
+
return { $or: orConditions };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Transform a phrase to MongoDB query
|
|
398
|
+
* FIXED: properly structured $or query for multiple fields
|
|
399
|
+
*/
|
|
400
|
+
private transformPhrase(node: PhraseNode, searchFields?: string[]): any {
|
|
401
|
+
// Use regex across provided fields or default fields, respecting word boundaries
|
|
402
|
+
const parts = node.value.split(/\s+/).map((t) => this.escapeRegex(t));
|
|
403
|
+
const pattern = parts.join('\\s+');
|
|
404
|
+
const fields = searchFields && searchFields.length > 0 ? searchFields : this.defaultFields;
|
|
405
|
+
const orConditions = fields.map((field) => ({
|
|
406
|
+
[field]: { $regex: pattern, $options: 'i' },
|
|
407
|
+
}));
|
|
408
|
+
return { $or: orConditions };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Transform a field query to MongoDB query
|
|
413
|
+
*/
|
|
414
|
+
private transformField(node: FieldNode): any {
|
|
415
|
+
// Handle special case for range queries on fields
|
|
416
|
+
if (node.value.type === 'RANGE') {
|
|
417
|
+
const rangeNode = node.value as RangeNode;
|
|
418
|
+
rangeNode.field = node.field;
|
|
419
|
+
return this.transformRange(rangeNode);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Handle special case for wildcards on fields
|
|
423
|
+
if (node.value.type === 'WILDCARD') {
|
|
424
|
+
return {
|
|
425
|
+
[node.field]: {
|
|
426
|
+
$regex: this.luceneWildcardToRegex((node.value as WildcardNode).value),
|
|
427
|
+
$options: 'i',
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Handle special case for fuzzy searches on fields
|
|
433
|
+
if (node.value.type === 'FUZZY') {
|
|
434
|
+
return {
|
|
435
|
+
[node.field]: {
|
|
436
|
+
$regex: this.createFuzzyRegex((node.value as FuzzyNode).value),
|
|
437
|
+
$options: 'i',
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Special case for exact term matches on fields (supporting wildcard characters)
|
|
443
|
+
if (node.value.type === 'TERM') {
|
|
444
|
+
const val = (node.value as TermNode).value;
|
|
445
|
+
if (val.includes('*') || val.includes('?')) {
|
|
446
|
+
const regex = this.luceneWildcardToRegex(val);
|
|
447
|
+
return { [node.field]: { $regex: regex, $options: 'i' } };
|
|
448
|
+
}
|
|
449
|
+
return { [node.field]: { $regex: val, $options: 'i' } };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Special case for phrase matches on fields
|
|
453
|
+
if (node.value.type === 'PHRASE') {
|
|
454
|
+
return {
|
|
455
|
+
[node.field]: {
|
|
456
|
+
$regex: `${(node.value as PhraseNode).value.replace(/\s+/g, '\\s+')}`,
|
|
457
|
+
$options: 'i',
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// For other cases, we'll transform the value and apply it to the field
|
|
463
|
+
const transformedValue = this.transform(node.value);
|
|
464
|
+
|
|
465
|
+
// If the transformed value uses $text, we need to adapt it for the field
|
|
466
|
+
if (transformedValue.$text) {
|
|
467
|
+
return { [node.field]: { $regex: transformedValue.$text.$search, $options: 'i' } };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Handle $or and $and cases
|
|
471
|
+
if (transformedValue.$or || transformedValue.$and) {
|
|
472
|
+
// This is a bit complex - we need to restructure the query to apply the field
|
|
473
|
+
// For now, simplify by just using a regex on the field
|
|
474
|
+
const term = this.extractTermFromBooleanQuery(transformedValue);
|
|
475
|
+
if (term) {
|
|
476
|
+
return { [node.field]: { $regex: term, $options: 'i' } };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return { [node.field]: transformedValue };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Extract a term from a boolean query (simplification)
|
|
485
|
+
*/
|
|
486
|
+
private extractTermFromBooleanQuery(query: any): string | null {
|
|
487
|
+
if (query.$or && Array.isArray(query.$or) && query.$or.length > 0) {
|
|
488
|
+
const firstClause = query.$or[0];
|
|
489
|
+
for (const field in firstClause) {
|
|
490
|
+
if (firstClause[field].$regex) {
|
|
491
|
+
return firstClause[field].$regex;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (query.$and && Array.isArray(query.$and) && query.$and.length > 0) {
|
|
497
|
+
const firstClause = query.$and[0];
|
|
498
|
+
for (const field in firstClause) {
|
|
499
|
+
if (firstClause[field].$regex) {
|
|
500
|
+
return firstClause[field].$regex;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Transform AND operator to MongoDB query
|
|
510
|
+
* FIXED: $and must be an array
|
|
511
|
+
*/
|
|
512
|
+
private transformAnd(node: BooleanNode): any {
|
|
513
|
+
return { $and: [this.transform(node.left), this.transform(node.right)] };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Transform OR operator to MongoDB query
|
|
518
|
+
* FIXED: $or must be an array
|
|
519
|
+
*/
|
|
520
|
+
private transformOr(node: BooleanNode): any {
|
|
521
|
+
return { $or: [this.transform(node.left), this.transform(node.right)] };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Transform NOT operator to MongoDB query
|
|
526
|
+
* FIXED: $and must be an array and $not usage
|
|
527
|
+
*/
|
|
528
|
+
private transformNot(node: BooleanNode): any {
|
|
529
|
+
const leftQuery = this.transform(node.left);
|
|
530
|
+
const rightQuery = this.transform(node.right);
|
|
531
|
+
|
|
532
|
+
// Create a query that includes left but excludes right
|
|
533
|
+
if (rightQuery.$text) {
|
|
534
|
+
// For text searches, we need a different approach
|
|
535
|
+
// We'll use a negated regex instead
|
|
536
|
+
const searchTerm = rightQuery.$text.$search.replace(/"/g, '');
|
|
537
|
+
|
|
538
|
+
// Determine the fields to apply the negation to
|
|
539
|
+
const notConditions: any[] = [];
|
|
540
|
+
|
|
541
|
+
for (const field in leftQuery) {
|
|
542
|
+
if (field !== '$or' && field !== '$and') {
|
|
543
|
+
notConditions.push({
|
|
544
|
+
[field]: { $not: { $regex: searchTerm, $options: 'i' } },
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// If left query has $or or $and, we need to handle it differently
|
|
550
|
+
if (leftQuery.$or) {
|
|
551
|
+
return {
|
|
552
|
+
$and: [leftQuery, { $nor: [{ $or: notConditions }] }],
|
|
553
|
+
};
|
|
554
|
+
} else {
|
|
555
|
+
// Simple case - just add $not to each field
|
|
556
|
+
return {
|
|
557
|
+
$and: [leftQuery, { $and: notConditions }],
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
// For other queries, we can use $not directly
|
|
562
|
+
// We need to handle different structures based on the rightQuery
|
|
563
|
+
let notQuery = {};
|
|
564
|
+
|
|
565
|
+
if (rightQuery.$or) {
|
|
566
|
+
notQuery = { $nor: rightQuery.$or };
|
|
567
|
+
} else if (rightQuery.$and) {
|
|
568
|
+
// Convert $and to $nor
|
|
569
|
+
notQuery = { $nor: rightQuery.$and };
|
|
570
|
+
} else {
|
|
571
|
+
// Simple field condition
|
|
572
|
+
for (const field in rightQuery) {
|
|
573
|
+
notQuery[field] = { $not: rightQuery[field] };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return { $and: [leftQuery, notQuery] };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Transform range query to MongoDB query
|
|
583
|
+
*/
|
|
584
|
+
private transformRange(node: RangeNode): any {
|
|
585
|
+
const range: any = {};
|
|
586
|
+
|
|
587
|
+
if (node.lower !== '*') {
|
|
588
|
+
range[node.includeLower ? '$gte' : '$gt'] = this.parseValue(node.lower);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (node.upper !== '*') {
|
|
592
|
+
range[node.includeUpper ? '$lte' : '$lt'] = this.parseValue(node.upper);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return { [node.field]: range };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Transform wildcard query to MongoDB query
|
|
600
|
+
* FIXED: properly structured for multiple fields
|
|
601
|
+
*/
|
|
602
|
+
private transformWildcard(node: WildcardNode, searchFields?: string[]): any {
|
|
603
|
+
// Convert Lucene wildcards to MongoDB regex
|
|
604
|
+
const regex = this.luceneWildcardToRegex(node.value);
|
|
605
|
+
|
|
606
|
+
// If specific fields are provided, search wildcard across those fields
|
|
607
|
+
if (searchFields && searchFields.length > 0) {
|
|
608
|
+
const orConditions = searchFields.map((field) => ({
|
|
609
|
+
[field]: { $regex: regex, $options: 'i' },
|
|
610
|
+
}));
|
|
611
|
+
|
|
612
|
+
return { $or: orConditions };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// By default, apply to the default field
|
|
616
|
+
return { $regex: regex, $options: 'i' };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Transform fuzzy query to MongoDB query
|
|
621
|
+
* FIXED: properly structured for multiple fields
|
|
622
|
+
*/
|
|
623
|
+
private transformFuzzy(node: FuzzyNode, searchFields?: string[]): any {
|
|
624
|
+
// MongoDB doesn't have built-in fuzzy search
|
|
625
|
+
// This is a very basic approach using regex
|
|
626
|
+
const regex = this.createFuzzyRegex(node.value);
|
|
627
|
+
|
|
628
|
+
// If specific fields are provided, search fuzzy term across those fields
|
|
629
|
+
if (searchFields && searchFields.length > 0) {
|
|
630
|
+
const orConditions = searchFields.map((field) => ({
|
|
631
|
+
[field]: { $regex: regex, $options: 'i' },
|
|
632
|
+
}));
|
|
633
|
+
|
|
634
|
+
return { $or: orConditions };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// By default, apply to the default field
|
|
638
|
+
return { $regex: regex, $options: 'i' };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Convert Lucene wildcards to MongoDB regex patterns
|
|
643
|
+
*/
|
|
644
|
+
public luceneWildcardToRegex(wildcardPattern: string): string {
|
|
645
|
+
// Replace Lucene wildcards with regex equivalents
|
|
646
|
+
// * => .*
|
|
647
|
+
// ? => .
|
|
648
|
+
// Also escape regex special chars
|
|
649
|
+
return wildcardPattern
|
|
650
|
+
.replace(/([.+^${}()|\\])/g, '\\$1') // Escape regex special chars
|
|
651
|
+
.replace(/\*/g, '.*')
|
|
652
|
+
.replace(/\?/g, '.');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Create a simplified fuzzy search regex
|
|
657
|
+
*/
|
|
658
|
+
private createFuzzyRegex(term: string): string {
|
|
659
|
+
// For a very simple approach, we allow some characters to be optional
|
|
660
|
+
let regex = '';
|
|
661
|
+
for (let i = 0; i < term.length; i++) {
|
|
662
|
+
// Make every other character optional (simplified fuzzy)
|
|
663
|
+
if (i % 2 === 1) {
|
|
664
|
+
regex += term[i] + '?';
|
|
665
|
+
} else {
|
|
666
|
+
regex += term[i];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return regex;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Parse string values to appropriate types (numbers, dates, etc.)
|
|
674
|
+
*/
|
|
675
|
+
private parseValue(value: string): any {
|
|
676
|
+
// Try to parse as number
|
|
677
|
+
if (/^-?\d+$/.test(value)) {
|
|
678
|
+
return parseInt(value, 10);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (/^-?\d+\.\d+$/.test(value)) {
|
|
682
|
+
return parseFloat(value);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Try to parse as date (simplified)
|
|
686
|
+
const date = new Date(value);
|
|
687
|
+
if (!isNaN(date.getTime())) {
|
|
688
|
+
return date;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Default to string
|
|
692
|
+
return value;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Main adapter class
|
|
698
|
+
*/
|
|
699
|
+
export class SmartdataLuceneAdapter {
|
|
700
|
+
private parser: LuceneParser;
|
|
701
|
+
private transformer: LuceneToMongoTransformer;
|
|
702
|
+
private defaultSearchFields: string[] = [];
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* @param defaultSearchFields - Optional array of field names to search across when no field is specified
|
|
706
|
+
*/
|
|
707
|
+
constructor(defaultSearchFields?: string[]) {
|
|
708
|
+
this.parser = new LuceneParser();
|
|
709
|
+
// Pass default searchable fields into transformer
|
|
710
|
+
this.transformer = new LuceneToMongoTransformer(defaultSearchFields || []);
|
|
711
|
+
if (defaultSearchFields) {
|
|
712
|
+
this.defaultSearchFields = defaultSearchFields;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Convert a Lucene query string to a MongoDB query object
|
|
718
|
+
* @param luceneQuery - The Lucene query string to convert
|
|
719
|
+
* @param searchFields - Optional array of field names to search across (overrides defaultSearchFields)
|
|
720
|
+
*/
|
|
721
|
+
convert(luceneQuery: string, searchFields?: string[]): any {
|
|
722
|
+
try {
|
|
723
|
+
// For simple single-term queries (no field:, boolean, grouping), use simpler regex
|
|
724
|
+
if (
|
|
725
|
+
!luceneQuery.includes(':') &&
|
|
726
|
+
!luceneQuery.includes(' AND ') &&
|
|
727
|
+
!luceneQuery.includes(' OR ') &&
|
|
728
|
+
!luceneQuery.includes(' NOT ') &&
|
|
729
|
+
!luceneQuery.includes('(') &&
|
|
730
|
+
!luceneQuery.includes('[')
|
|
731
|
+
) {
|
|
732
|
+
const fieldsToSearch = searchFields || this.defaultSearchFields;
|
|
733
|
+
if (fieldsToSearch && fieldsToSearch.length > 0) {
|
|
734
|
+
// Handle wildcard characters in query
|
|
735
|
+
let pattern = luceneQuery;
|
|
736
|
+
if (luceneQuery.includes('*') || luceneQuery.includes('?')) {
|
|
737
|
+
// Use transformer to convert wildcard pattern
|
|
738
|
+
pattern = this.transformer.luceneWildcardToRegex(luceneQuery);
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
$or: fieldsToSearch.map((field) => ({
|
|
742
|
+
[field]: { $regex: pattern, $options: 'i' },
|
|
743
|
+
})),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// For more complex queries, use the full parser
|
|
749
|
+
// Parse the Lucene query into an AST
|
|
750
|
+
const ast = this.parser.parse(luceneQuery);
|
|
751
|
+
|
|
752
|
+
// Use provided searchFields, fall back to defaultSearchFields
|
|
753
|
+
const fieldsToSearch = searchFields || this.defaultSearchFields;
|
|
754
|
+
|
|
755
|
+
// Transform the AST to a MongoDB query
|
|
756
|
+
return this.transformWithFields(ast, fieldsToSearch);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
logger.log('error', `Failed to convert Lucene query "${luceneQuery}":`, error);
|
|
759
|
+
throw new Error(`Failed to convert Lucene query: ${error}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Helper method to transform the AST with field information
|
|
765
|
+
*/
|
|
766
|
+
private transformWithFields(node: AnyQueryNode, searchFields: string[]): any {
|
|
767
|
+
// Special case for term nodes without a specific field
|
|
768
|
+
if (
|
|
769
|
+
node.type === 'TERM' ||
|
|
770
|
+
node.type === 'PHRASE' ||
|
|
771
|
+
node.type === 'WILDCARD' ||
|
|
772
|
+
node.type === 'FUZZY'
|
|
773
|
+
) {
|
|
774
|
+
return this.transformer.transform(node, searchFields);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// For other node types, use the standard transformation
|
|
778
|
+
return this.transformer.transform(node);
|
|
779
|
+
}
|
|
780
|
+
}
|