Skip to content

Commit 96fb7f9

Browse files
committed
Add lucene query builder utils
1 parent deddaba commit 96fb7f9

File tree

3 files changed

+246
-205
lines changed

3 files changed

+246
-205
lines changed

src/QueryBuilder/lucene.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useState, useCallback } from 'react';
2+
import * as lucene from "@/utils/lucene";
3+
import { LuceneQuery as QueryBuilder } from '@/utils/lucene';
4+
5+
export type LuceneQueryBuilder = {
6+
query: string,
7+
parsedQuery: lucene.LuceneQuery,
8+
setQuery: (query: string) => void
9+
setParsedQuery: (query: lucene.LuceneQuery) => void
10+
}
11+
12+
export function useQueryBuilder() {
13+
const [parsedQuery, _setParsedQuery] = useState<lucene.LuceneQuery>(QueryBuilder.parse(""));
14+
const [query, _setQuery] = useState<string>("");
15+
16+
const setQuery = useCallback((query: string) => {
17+
_setQuery(query);
18+
_setParsedQuery(QueryBuilder.parse(query));
19+
}, [_setQuery, _setParsedQuery]);
20+
21+
const setParsedQuery = useCallback((query: QueryBuilder) => {
22+
_setParsedQuery(query);
23+
_setQuery(query.toString());
24+
}, [_setQuery, _setParsedQuery]);
25+
26+
return {
27+
query,
28+
parsedQuery,
29+
setQuery,
30+
setParsedQuery,
31+
}
32+
}

src/modifyQuery.ts

+2-205
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,6 @@
1-
import { isEqual } from 'lodash';
2-
import lucene, { AST, BinaryAST, LeftOnlyAST, NodeTerm } from 'lucene';
3-
1+
import { escapeFilter, escapeFilterValue, concatenate, LuceneQuery } from 'utils/lucene';
42
import { AdHocVariableFilter } from '@grafana/data';
53

6-
type ModifierType = '' | '-';
7-
8-
/**
9-
* Checks for the presence of a given label:"value" filter in the query.
10-
*/
11-
export function queryHasFilter(query: string, key: string, value: string, modifier: ModifierType = ''): boolean {
12-
return findFilterNode(query, key, value, modifier) !== null;
13-
}
14-
15-
/**
16-
* Given a query, find the NodeTerm that matches the given field and value.
17-
*/
18-
export function findFilterNode(
19-
query: string,
20-
key: string,
21-
value: string,
22-
modifier: ModifierType = ''
23-
): NodeTerm | null {
24-
const field = `${modifier}${lucene.term.escape(key)}`;
25-
value = lucene.phrase.escape(value);
26-
let ast: AST | null = parseQuery(query);
27-
if (!ast) {
28-
return null;
29-
}
30-
31-
return findNodeInTree(ast, field, value);
32-
}
33-
34-
function findNodeInTree(ast: AST, field: string, value: string): NodeTerm | null {
35-
// {}
36-
if (Object.keys(ast).length === 0) {
37-
return null;
38-
}
39-
// { left: {}, right: {} } or { left: {} }
40-
if (isAST(ast.left)) {
41-
return findNodeInTree(ast.left, field, value);
42-
}
43-
if (isNodeTerm(ast.left) && ast.left.field === field && ast.left.term === value) {
44-
return ast.left;
45-
}
46-
if (isLeftOnlyAST(ast)) {
47-
return null;
48-
}
49-
if (isNodeTerm(ast.right) && ast.right.field === field && ast.right.term === value) {
50-
return ast.right;
51-
}
52-
if (isBinaryAST(ast.right)) {
53-
return findNodeInTree(ast.right, field, value);
54-
}
55-
return null;
56-
}
57-
58-
/**
59-
* Adds a label:"value" expression to the query.
60-
*/
61-
export function addFilterToQuery(query: string, key: string, value: string, modifier: ModifierType = ''): string {
62-
if (queryHasFilter(query, key, value, modifier)) {
63-
return query;
64-
}
65-
66-
key = escapeFilter(key);
67-
value = escapeFilterValue(value);
68-
const filter = `${modifier}${key}:"${value}"`;
69-
70-
return concatenate(query, filter);
71-
}
72-
73-
/**
74-
* Merge a query with a filter.
75-
*/
76-
function concatenate(query: string, filter: string, condition = 'AND'): string {
77-
if (!filter) {
78-
return query;
79-
}
80-
return query.trim() === '' ? filter : `${query} ${condition} ${filter}`;
81-
}
82-
834
/**
845
* Adds a label:"value" expression to the query.
856
*/
@@ -96,7 +17,7 @@ export function addAddHocFilter(query: string, filter: AdHocVariableFilter): str
9617

9718
const equalityFilters = ['=', '!='];
9819
if (equalityFilters.includes(filter.operator)) {
99-
return addFilterToQuery(query, filter.key, filter.value, filter.operator === '=' ? '' : '-');
20+
return LuceneQuery.parse(query).addFilter(filter.key, filter.value, filter.operator === '=' ? '' : '-').toString();
10021
}
10122
/**
10223
* Keys and values in ad hoc filters may contain characters such as
@@ -121,127 +42,3 @@ export function addAddHocFilter(query: string, filter: AdHocVariableFilter): str
12142
}
12243
return concatenate(query, addHocFilter);
12344
}
124-
125-
/**
126-
* Removes a label:"value" expression from the query.
127-
*/
128-
export function removeFilterFromQuery(query: string, key: string, value: string, modifier: ModifierType = ''): string {
129-
const node = findFilterNode(query, key, value, modifier);
130-
const ast = parseQuery(query);
131-
if (!node || !ast) {
132-
return query;
133-
}
134-
135-
return lucene.toString(removeNodeFromTree(ast, node));
136-
}
137-
138-
function removeNodeFromTree(ast: AST, node: NodeTerm): AST {
139-
// {}
140-
if (Object.keys(ast).length === 0) {
141-
return ast;
142-
}
143-
// { left: {}, right: {} } or { left: {} }
144-
if (isAST(ast.left)) {
145-
ast.left = removeNodeFromTree(ast.left, node);
146-
return ast;
147-
}
148-
if (isNodeTerm(ast.left) && isEqual(ast.left, node)) {
149-
Object.assign(
150-
ast,
151-
{
152-
left: undefined,
153-
operator: undefined,
154-
right: undefined,
155-
},
156-
'right' in ast ? ast.right : {}
157-
);
158-
return ast;
159-
}
160-
if (isLeftOnlyAST(ast)) {
161-
return ast;
162-
}
163-
if (isNodeTerm(ast.right) && isEqual(ast.right, node)) {
164-
Object.assign(ast, {
165-
right: undefined,
166-
operator: undefined,
167-
});
168-
return ast;
169-
}
170-
if (isBinaryAST(ast.right)) {
171-
ast.right = removeNodeFromTree(ast.right, node);
172-
return ast;
173-
}
174-
return ast;
175-
}
176-
177-
/**
178-
* Filters can possibly reserved characters such as colons which are part of the Lucene syntax.
179-
* Use this function to escape filter keys.
180-
*/
181-
export function escapeFilter(value: string) {
182-
return lucene.term.escape(value);
183-
}
184-
185-
/**
186-
* Values can possibly reserved special characters such as quotes.
187-
* Use this function to escape filter values.
188-
*/
189-
export function escapeFilterValue(value: string) {
190-
value = value.replace(/\\/g, '\\\\');
191-
return lucene.phrase.escape(value);
192-
}
193-
194-
/**
195-
* Normalizes the query by removing whitespace around colons, which breaks parsing.
196-
*/
197-
function normalizeQuery(query: string) {
198-
return query.replace(/(\w+)\s(:)/gi, '$1$2');
199-
}
200-
201-
function isLeftOnlyAST(ast: unknown): ast is LeftOnlyAST {
202-
if (!ast || typeof ast !== 'object') {
203-
return false;
204-
}
205-
206-
if ('left' in ast && !('right' in ast)) {
207-
return true;
208-
}
209-
210-
return false;
211-
}
212-
213-
function isBinaryAST(ast: unknown): ast is BinaryAST {
214-
if (!ast || typeof ast !== 'object') {
215-
return false;
216-
}
217-
218-
if ('left' in ast && 'right' in ast) {
219-
return true;
220-
}
221-
return false;
222-
}
223-
224-
function isAST(ast: unknown): ast is AST {
225-
return isLeftOnlyAST(ast) || isBinaryAST(ast);
226-
}
227-
228-
function isNodeTerm(ast: unknown): ast is NodeTerm {
229-
if (ast && typeof ast === 'object' && 'term' in ast) {
230-
return true;
231-
}
232-
233-
return false;
234-
}
235-
236-
function parseQuery(query: string) {
237-
try {
238-
return lucene.parse(normalizeQuery(query));
239-
} catch (e) {
240-
return null;
241-
}
242-
}
243-
244-
export function addStringFilterToQuery(query: string, filter: string, contains = true) {
245-
const expression = `"${escapeFilterValue(filter)}"`;
246-
return query.trim() ? `${query} ${contains ? 'AND' : 'NOT'} ${expression}` : `${contains ? '' : 'NOT '}${expression}`;
247-
}

0 commit comments

Comments
 (0)