-
Notifications
You must be signed in to change notification settings - Fork 259
[search] support uppercase OR/AND #500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,58 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { describe, it, expect } from 'vitest'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Simple function to test the query transformation logic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // We'll extract just the normalization part to test it separately | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalizeQueryOperators = (query: string): string => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return query | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Replace standalone uppercase OR with lowercase or | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/\bOR\b/g, 'or') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Replace standalone uppercase AND with lowercase and (though AND is implicit in Zoekt) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/\bAND\b/g, 'and'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('Query transformation', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('normalizeQueryOperators', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should convert uppercase OR to lowercase or', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(normalizeQueryOperators('file:yarn.lock OR file:package.json')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toBe('file:yarn.lock or file:package.json'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should convert uppercase AND to lowercase and', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(normalizeQueryOperators('foo AND bar')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toBe('foo and bar'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should handle parenthesized expressions', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(normalizeQueryOperators('(file:yarn.lock OR file:package.json)')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toBe('(file:yarn.lock or file:package.json)'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should handle complex queries with multiple operators', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(normalizeQueryOperators('(file:*.json OR file:*.lock) AND content:react')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toBe('(file:*.json or file:*.lock) and content:react'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should not affect lowercase operators', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(normalizeQueryOperators('file:yarn.lock or file:package.json')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toBe('file:yarn.lock or file:package.json'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should not affect OR/AND when part of other words', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(normalizeQueryOperators('ORDER BY something')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toBe('ORDER BY something'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(normalizeQueryOperators('ANDROID app')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toBe('ANDROID app'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+46
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add tests to ensure quoted literals are not altered. Covers the critical case the regex would break. it('should not affect OR/AND when part of other words', () => {
expect(normalizeQueryOperators('ORDER BY something'))
.toBe('ORDER BY something');
expect(normalizeQueryOperators('ANDROID app'))
.toBe('ANDROID app');
});
+ it('should not alter quoted phrases containing OR/AND', () => {
+ expect(normalizeQueryOperators('"A OR B"')).toBe('"A OR B"');
+ expect(normalizeQueryOperators('content:"\\bOR\\b"')).toBe('content:"\\bOR\\b"');
+ });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should handle mixed case queries', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(normalizeQueryOperators('file:src OR file:test and lang:typescript')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toBe('file:src or file:test and lang:typescript'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should handle multiple ORs and ANDs', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(normalizeQueryOperators('A OR B OR C AND D AND E')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toBe('A or B or C and D and E'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,7 +36,19 @@ enum zoektPrefixes { | |
| } | ||
|
|
||
| const transformZoektQuery = async (query: string, orgId: number): Promise<string | ServiceError> => { | ||
| const prevQueryParts = query.split(" "); | ||
| // First, normalize boolean operators to lowercase (Zoekt requirement) | ||
| // Zoekt only recognizes lowercase 'or' and 'and' operators, but users often expect | ||
| // uppercase OR/AND to work. This transformation allows both cases to work. | ||
| // Examples: | ||
| // - "(file:yarn.lock OR file:package.json)" → "(file:yarn.lock or file:package.json)" | ||
| // - "foo AND bar" → "foo and bar" (though AND is implicit in Zoekt) | ||
| let normalizedQuery = query | ||
| // Replace standalone uppercase OR with lowercase or | ||
| .replace(/\bOR\b/g, 'or') | ||
| // Replace standalone uppercase AND with lowercase and (though AND is implicit in Zoekt) | ||
| .replace(/\bAND\b/g, 'and'); | ||
|
|
||
|
Comment on lines
+39
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid rewriting OR/AND inside quoted phrases; implement token-aware normalization. The regex replaces OR/AND anywhere, including within quoted literals (e.g., "A OR B" or content:"\bOR\b"), which can change query semantics, especially under case-sensitive searches. Limit normalization to tokens outside quotes and only when surrounded by whitespace/parentheses. Apply this diff to delegate to a safer helper: - let normalizedQuery = query
- // Replace standalone uppercase OR with lowercase or
- .replace(/\bOR\b/g, 'or')
- // Replace standalone uppercase AND with lowercase and (though AND is implicit in Zoekt)
- .replace(/\bAND\b/g, 'and');
+ const normalizedQuery = normalizeBoolOperators(query);Add this helper (outside the shown range, e.g., above transformZoektQuery): export const normalizeBoolOperators = (input: string): string => {
let out = "";
let i = 0;
let inQuotes = false;
const isBoundary = (ch: string | undefined) => !ch || /\s|\(|\)/.test(ch);
while (i < input.length) {
const ch = input[i];
if (ch === '"') {
inQuotes = !inQuotes;
out += ch;
i++;
continue;
}
if (!inQuotes) {
const two = input.slice(i, i + 2);
const three = input.slice(i, i + 3);
const prev = i > 0 ? input[i - 1] : undefined;
const next2 = input[i + 2];
const next3 = input[i + 3];
if (two === "OR" && isBoundary(prev) && isBoundary(next2)) {
out += "or";
i += 2;
continue;
}
if (three === "AND" && isBoundary(prev) && isBoundary(next3)) {
out += "and";
i += 3;
continue;
}
}
out += ch;
i++;
}
return out;
};
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good catch, good bot. @drew-u410 we will need to handle this case - a solution like the bot suggested looks good (as long as we pair it with some good UTs, thx for starting those 🤠). Longer term, I'd like to move to using a parser generator like Lezer where we can formally define the grammar of Sourcebot's search query language, have it generate a syntax tree for us, and then translate that syntax tree into a zoekt compatible query. Lezer has the added benefit that it's what Codemirror uses, so we could get additional benefits like error squiggles & better syntax highlighting support for our search box.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const prevQueryParts = normalizedQuery.split(" "); | ||
| const newQueryParts = []; | ||
|
|
||
| for (const part of prevQueryParts) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
andisn't actually a boolean operator that is natively supported in zoekt and by extension Sourcebot (I see that Sourcegraph supports it).I do see the value in having a explicit
andboolean operator though, but maybe something we want to roll into a separate change.