-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(lxl-web, supersearch): Narrow search query when editing parts (L…
…WS-273) (#1190) * Get editedRanges by parsing the syntax tree server-side * Add tests * Only do search request if trimmed value isn't empty * Capitalize BooleanOperator rule in grammar (needed to be able to identify the operators) * Narrow down search query when editing qualifier parts * Keep _q as is for now (but add _qualifier param)
- Loading branch information
1 parent
af47567
commit 3aef8ac
Showing
11 changed files
with
338 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
lxl-web/src/routes/api/[[lang=lang]]/supersearch/getEditedPartEntries.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { describe, it, expect } from 'vitest'; | ||
import getEditedPartEntries from './getEditedPartEntries'; | ||
|
||
describe('getEditedPartEntries', () => { | ||
it('narrows down search query when editing qualifier parts', () => { | ||
expect(getEditedPartEntries('hello title:"hej"', 16)).toEqual([['_qualifier', 'title:"hej"']]); | ||
}); | ||
it('keeps query as is when editing year qualifiers', () => { | ||
expect(getEditedPartEntries('hello ÅR:2024', 13)).toEqual([]); | ||
}); | ||
it('narrows down search query by base class for query codes', () => { | ||
expect(getEditedPartEntries('astrid lindgren subject:"winter"', 27)).toEqual([ | ||
['_qualifier', `"rdf:type":Topic "winter"`], | ||
['min-reverseLinks.totalItems', '1'] | ||
]); | ||
}); | ||
it('otherwise keeps query as is', () => { | ||
expect(getEditedPartEntries('hello', 5)).toEqual([]); | ||
}); | ||
}); |
65 changes: 65 additions & 0 deletions
65
lxl-web/src/routes/api/[[lang=lang]]/supersearch/getEditedPartEntries.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import getEditedRanges from './getEditedRanges.js'; | ||
|
||
/** | ||
* TODO: How should we handle translated query codes and qualifier keys? | ||
*/ | ||
|
||
const QUALIFIER_KEY_BY_BASE_CLASS = { | ||
Library: 'itemHeldBy', | ||
Agent: 'contributor', | ||
Topic: 'subject', | ||
Subject: 'subject', | ||
Language: 'SPRÅK', | ||
GenreForm: 'genreForm', | ||
Person: 'person', | ||
Work: 'titel' | ||
}; | ||
|
||
const SKIP_QUALIFIERS = ['år']; | ||
|
||
/** | ||
* Gets the URLSearchParams entries which should be appended/replaced with new values when editing a part of a query. | ||
*/ | ||
|
||
function getEditedPartEntries(query: string, cursor: number): [string, string][] { | ||
const editedRanges = getEditedRanges(query, cursor); | ||
|
||
/** | ||
* Narrow down search query when editing qualifier parts | ||
*/ | ||
if (editedRanges.qualifierKey && editedRanges.qualifierOperator && editedRanges.qualifierValue) { | ||
const qualifierKey = query.slice(editedRanges.qualifierKey.from, editedRanges.qualifierKey.to); | ||
const qualifierOperator = query.slice( | ||
editedRanges.qualifierOperator.from, | ||
editedRanges.qualifierOperator.to | ||
); | ||
const qualifierValue = query.slice( | ||
editedRanges.qualifierValue.from, | ||
editedRanges.qualifierValue.to | ||
); | ||
|
||
if (SKIP_QUALIFIERS.includes(qualifierKey.toLowerCase())) { | ||
return []; // Keep query as is when editing year qualifiers | ||
} | ||
|
||
const baseClass = Object.entries(QUALIFIER_KEY_BY_BASE_CLASS).find( | ||
([, key]) => key === qualifierKey | ||
)?.[0]; | ||
|
||
if (baseClass) { | ||
return [ | ||
['_qualifier', `"rdf:type":${baseClass} ${qualifierValue}`], | ||
['min-reverseLinks.totalItems', '1'] // ensure results are linked/used atleast once | ||
]; | ||
} | ||
|
||
return [['_qualifier', qualifierKey + qualifierOperator + qualifierValue]]; | ||
} | ||
|
||
/** | ||
* Otherwise keep query entries as is | ||
*/ | ||
return []; | ||
} | ||
|
||
export default getEditedPartEntries; |
119 changes: 119 additions & 0 deletions
119
lxl-web/src/routes/api/[[lang=lang]]/supersearch/getEditedRanges.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { describe, it, expect } from 'vitest'; | ||
import getEditedRanges from './getEditedRanges'; | ||
|
||
describe('getEditedRanges', () => { | ||
it('calculates the edited range for a simple free-text query', () => { | ||
const query = 'hello'; | ||
const editedRanges = getEditedRanges(query, 5); | ||
expect(editedRanges).toEqual({ from: 0, to: 5 }); | ||
expect(query.slice(editedRanges.from, editedRanges.to)).toBe('hello'); | ||
}); | ||
|
||
it('calculates the edited range (with included ranges of qualifier parts) when editing a qualifier', () => { | ||
const query = 'hasTitle:"a"'; | ||
const editedRanges = getEditedRanges(query, 11); | ||
expect(editedRanges).toEqual({ | ||
from: 0, | ||
to: 12, | ||
qualifierKey: { | ||
from: 0, | ||
to: 8 | ||
}, | ||
qualifierOperator: { | ||
from: 8, | ||
to: 9 | ||
}, | ||
qualifierValue: { | ||
from: 9, | ||
to: 12 | ||
} | ||
}); | ||
expect(query.slice(editedRanges.from, editedRanges.to)).toBe('hasTitle:"a"'); | ||
expect(query.slice(editedRanges.qualifierKey?.from, editedRanges.qualifierKey?.to)).toBe( | ||
'hasTitle' | ||
); | ||
expect( | ||
query.slice(editedRanges.qualifierOperator?.from, editedRanges.qualifierOperator?.to) | ||
).toBe(':'); | ||
expect(query.slice(editedRanges.qualifierValue?.from, editedRanges.qualifierValue?.to)).toBe( | ||
'"a"' | ||
); | ||
}); | ||
|
||
it('calculates the edited range when editing a string', () => { | ||
expect(getEditedRanges('"hello"', 6)).toEqual({ | ||
from: 0, | ||
to: 7 | ||
}); | ||
}); | ||
|
||
it('calculates the edited range when editing a group', () => { | ||
expect(getEditedRanges('(hello world)', 9)).toEqual({ | ||
from: 0, | ||
to: 13 | ||
}); | ||
}); | ||
|
||
it('calculates the edited range when editing a part after a qualifier', () => { | ||
expect(getEditedRanges('hasTitle:"a" hello', 18)).toEqual({ | ||
from: 12, | ||
to: 18 | ||
}); | ||
}); | ||
|
||
it('calculates the edited range when editing a part after a qualifier', () => { | ||
expect(getEditedRanges('hasTitle:"a" hello', 18)).toEqual({ | ||
from: 12, | ||
to: 18 | ||
}); | ||
}); | ||
|
||
it('calculates the edited range when editing a part before a qualifier', () => { | ||
expect(getEditedRanges('hello hasTitle:"a"', 5)).toEqual({ | ||
from: 0, | ||
to: 6 | ||
}); | ||
}); | ||
|
||
it('calculates the edited range when editing a part surrounded by qualifiers', () => { | ||
expect(getEditedRanges('hasTitle:"a" hello hasTitle:"b', 18)).toEqual({ | ||
from: 12, | ||
to: 19 | ||
}); | ||
}); | ||
|
||
it('calculates the edited range when editing a part after a group', () => { | ||
expect(getEditedRanges('(hi) hello', 7)).toEqual({ | ||
from: 4, | ||
to: 10 | ||
}); | ||
}); | ||
|
||
it('calculates the edited range when editing a part before a group', () => { | ||
expect(getEditedRanges('hello (hi)', 3)).toEqual({ | ||
from: 0, | ||
to: 6 | ||
}); | ||
}); | ||
|
||
it('calculates the edited range when editing a part surrounded by groups', () => { | ||
expect(getEditedRanges('(hi) hello (hej)', 7)).toEqual({ | ||
from: 4, | ||
to: 11 | ||
}); | ||
}); | ||
|
||
it('calculates the edited range when editing a part before a boolean operator', () => { | ||
expect(getEditedRanges('hello AND world', 5)).toEqual({ | ||
from: 0, | ||
to: 6 | ||
}); | ||
}); | ||
|
||
it('calculates the edited range when editing a part after a boolean operator', () => { | ||
expect(getEditedRanges('hello AND world', 12)).toEqual({ | ||
from: 9, | ||
to: 15 | ||
}); | ||
}); | ||
}); |
84 changes: 84 additions & 0 deletions
84
lxl-web/src/routes/api/[[lang=lang]]/supersearch/getEditedRanges.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { lxlQuery } from 'codemirror-lang-lxlquery'; | ||
|
||
type Range = { | ||
from: number; | ||
to: number; | ||
}; | ||
|
||
export type EditedRanges = Range & { | ||
qualifierKey?: Range; | ||
qualifierOperator?: Range; | ||
qualifierValue?: Range; | ||
}; | ||
|
||
function getEditedRanges(query: string, cursor: number): EditedRanges { | ||
const tree = lxlQuery.language.parser.parse(query); | ||
const innerNode = tree.resolveInner(cursor); | ||
|
||
/** | ||
* Return `from` and `to` from qualifier parts if editing qualifier value | ||
*/ | ||
if (innerNode.parent?.type.is('QualifierValue')) { | ||
const qualifierNode = innerNode.parent.parent; | ||
const qualifierKeyNode = qualifierNode?.getChild('QualifierKey'); | ||
const qualifierOperatorNode = qualifierNode?.getChild('QualifierOperator'); | ||
const qualiferValueNode = qualifierNode?.getChild('QualifierValue'); | ||
if (qualifierNode) { | ||
return { | ||
from: qualifierNode.from, | ||
to: qualifierNode.to, | ||
...(qualifierKeyNode && { | ||
qualifierKey: { from: qualifierKeyNode.from, to: qualifierKeyNode.to } | ||
}), | ||
...(qualifierOperatorNode && { | ||
qualifierOperator: { from: qualifierOperatorNode.from, to: qualifierOperatorNode.to } | ||
}), | ||
...(qualiferValueNode && { | ||
qualifierValue: { from: qualiferValueNode.from, to: qualiferValueNode.to } | ||
}) | ||
}; | ||
} | ||
} | ||
|
||
let from = 0; | ||
let to = query.length; | ||
|
||
/** | ||
* Adjust `from` and `to` if enclosed qualifiers or groups are found BEFORE the edited part | ||
* */ | ||
tree.iterate({ | ||
from: 0, | ||
to: cursor, | ||
enter(node) { | ||
if (node.type.is('Qualifier') || node.type.is('Group') || node.type.is('BooleanOperator')) { | ||
if (node.to > cursor) { | ||
from = node.from; | ||
to = node.to; | ||
} else { | ||
from = node.to; | ||
} | ||
} | ||
} | ||
}); | ||
|
||
/** | ||
* Adjust `from` and `to` if enclosed qualifiers or groups are found AFTER the edited part | ||
* */ | ||
tree.iterate({ | ||
from, | ||
to, | ||
enter(node) { | ||
if ( | ||
(node.type.is('Qualifier') || node.type.is('Group') || node.type.is('BooleanOperator')) && | ||
node.from > cursor && | ||
node.from < to | ||
) { | ||
to = node.from; | ||
} | ||
} | ||
}); | ||
|
||
return { from, to }; | ||
} | ||
|
||
export default getEditedRanges; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.