-
Notifications
You must be signed in to change notification settings - Fork 37
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
AO-22 Add support for Advanced Query Syntax #70
base: master
Are you sure you want to change the base?
Conversation
@djazayeri @Ruhanga please find an initial commit towards implementing AO 22. Do let me know of any changes that you find may be necessary(including the approach itself). |
High-level comment: avoid hand-coding each individual advanced parameter. I would expect that most of the supported things map directly to one field in the elasticsearch index, so I would define a js object giving these mappings, and have some code that finds things before the : and checks if they're keys on that object. |
Also, I hope we can support some common things like "exact phrase" or -exclude. Can you find an existing advanced query parsing library we can use? A quick google search came up with this: https://github.com/nepsilon/search-query-parser (though there may be better ones). |
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.
Where are your tests? :-)
import org.openmrs.addonindex.domain.AddOnInfoSummary; | ||
import org.openmrs.addonindex.domain.AddOnType; | ||
import org.openmrs.addonindex.domain.AddOnVersion; | ||
import org.openmrs.addonindex.domain.*; |
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.
Avoid wildcard imports. (Can do this in your IDE settings.)
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.
Sorry about that, I've now disabled it in my IDE settings
import org.openmrs.addonindex.domain.AddOnInfoSummary; | ||
import org.openmrs.addonindex.domain.AddOnInfoSummaryAndStats; | ||
import org.openmrs.addonindex.domain.AddOnType; | ||
import org.openmrs.addonindex.domain.*; |
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.
same
BoolQueryBuilder boolQB = QueryBuilders.boolQuery(); | ||
if (name != null){ |
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.
formatting :-) should be
if (clause) {
BoolQueryBuilder boolQB = QueryBuilders.boolQuery(); | ||
if (name != null){ | ||
//Exact match on moduleID |
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.
copy-paste error. this is name, not module id
import org.openmrs.addonindex.domain.AddOnInfoSummary; | ||
import org.openmrs.addonindex.domain.AddOnInfoSummaryAndStats; | ||
import org.openmrs.addonindex.domain.AddOnType; | ||
import org.openmrs.addonindex.domain.*; |
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.
same
let advancedQuery = this.state.query; | ||
doSearch() { | ||
if (this.state.query) { | ||
let advancedQuery = this.state.query.toLowerCase(); |
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.
bad style to call this variable advancedQuery if you don't know if it's advanced or not.
Does lowercasing it possibly mess up exact matching?
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.
Typically, lowercasing would mess it up as the keyword field is case sensitive. However, fortunately, only our name field is having a mix of case. The rest of the fields are all lowercased. Even while performing exact matching on the name field, I reckon that both "Observations Administration" and "observations administration" should produce the same result. I have ensured this by adding a normalizer to the name field. This normalizer basically helps make the keyword field case-insensitive
let url = "/search?"; | ||
// Check if query is an advanced query. We want to use the Parser only if the query is advanced | ||
if (advancedQuery.includes(":") || advancedQuery.includes("-")){ | ||
let options = {keywords: ['type', 'tag', 'query', 'moduleid', 'status', 'name'], offsets: false}; |
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.
At a minimum, pull this out to wider scope as a const. Even better would be to introduce a class that encapsulates all of the parsing and construction of the URL.
queryComponents["type"] = value.toUpperCase(); | ||
let url = "/search?"; | ||
// Check if query is an advanced query. We want to use the Parser only if the query is advanced | ||
if (advancedQuery.includes(":") || advancedQuery.includes("-")){ |
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.
I think we want a way for the user to include a : or a - in the query. Can we document what it is in the help text? (Maybe not even so important to support this.)
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.
So do you mean to say that every query should have an advanced query like structure? IMO, certain users just prefer searching for text instead of having to worry about the structure of the advanced query and I believe we should continue to support that. Those who wish to go advanced will also have the option to do so. However, should you feel that we should do away with a simple query, do let me know and I will implement it :)
src/main/ui/app/route/SearchPage.jsx
Outdated
const exclude = this.props.location.query.exclude; | ||
const moduleid = this.props.location.query.moduleid; | ||
const name = this.props.location.query.name; | ||
const status = this.props.location.query.status; |
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.
Why do we have to enumerate all of these here? Can't they be encapsulated within SearchBox? E.g. this class only needs to care about a single search string ("query"), right?
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.
Sorry you had to see that. It was very foolish of me to do so :)
//Removing all spaces to the left of search keys | ||
query = query.replace(new RegExp("\\s+:","g"),":"); | ||
//Removing all spaces to the right of search keys | ||
query = query.replace(new RegExp(":\\s+","g"),":"); |
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.
Can you pull all this into a helper function like normalizeQuery()?
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.
Made some comments. Also, this needs tests.
if (this.state.type) { | ||
url += "type=" + this.state.type; | ||
// Check if query is an advanced query. We want to use the Parser only if the query is advanced | ||
if (query.includes(":") || query.includes("-")) { |
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.
Peeking at https://www.npmjs.com/package/search-query-parser it says
If no keywords or ranges are specified, or if none are present in the given search query, then searchQuery.parse will return a string if tokenize is false
so you should rely on this, and avoid doing this test yourself. (E.g. what if I query for: notAValidKey:foo ?)
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.
@djazayeri , although from what's mentioned in the docs, it seems that the parser just returns a string, it actually returns something like query: "user query" . This makes it impossible to differentiate a normal query from an advanced one. That is why I had implemented this test to check the pre-parsed query
if (query.includes(":") || query.includes("-")) { | ||
let searchQueryObj = searchQuery.parse(query, options); | ||
Object.keys(searchQueryObj).forEach(function (key) { | ||
console.log(key); |
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.
need to remove this before merging the PR
@RequestParam(value = "q", required = false) String query, | ||
@RequestParam(value = "tag", required = false) String tag) throws Exception { | ||
return index.search(type, query, tag); | ||
@RequestParam(value = "q", required = false) String query, |
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.
In the big picture it would be nice to avoid changing method signature and adding these parameters all over the place, by just doing the query parsing entirely within the ElasticSearchIndex class.
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.
Yes, that's true, I have currently asked around on forums. Let's hope a java based parser that's similar to the one we're currently using shows up
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.
Conclusion: we didn't find a Java library for this, so we'll proceed doing it in the front end in JS.
@@ -0,0 +1,11 @@ | |||
{ |
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.
Please give some more justification of why you're doing this (ideally in a code comment somewhere)
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.
Resolved
if (this.state.type || this.state.query) { | ||
if (this.state.query) { | ||
let query = this.state.query.toLowerCase(); | ||
const options = {keywords: ['uid', 'type', 'tag', 'query', 'moduleid', 'status', 'name'], offsets: false}; |
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.
pull this const outside of the function
url += "&exclude=" + searchQueryObj[key].text; | ||
} | ||
} | ||
else if (key === "status"){ |
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.
you're handling "type" and "status" in the same way, can you combine these branches?
src/main/ui/app/route/SearchPage.jsx
Outdated
|
||
Object.keys(query).forEach(function (key) { | ||
if (query[key]){ | ||
url += ("&" + key + "=" + query[key]); |
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.
why are you constructing an url both in SearchBox and here?
src/main/ui/app/route/SearchPage.jsx
Outdated
if (this.state && this.state.error) { | ||
return <div>{this.state.error}</div> | ||
} | ||
else { | ||
return ( | ||
<div> | ||
<SearchBox initialQuery={this.props.location.query.q}/> | ||
<SearchBox initialQuery={`${query.type ? "type:" + query.type : ""} ${query.q ? "query:" + query.q : ""} ${query.tag ? "tag:" + query.tag : ""} ${query.exclude ? "-" + query.exclude : ""} ${query.name ? "name:" + query.name : ""} ${query.status ? "status:" + query.status : ""} ${query.moduleid ? "moduleId:" + query.moduleid : ""} ${query.uid ? "uid:" + query.uid : ""}`}/> |
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.
The fact that you're doing this feels like a code smell.
Maybe this would work better if SearchBox continues to just be mainly a UI element, and you move the parsing into this SearchPage class?
BoolQueryBuilder boolQB = QueryBuilders.boolQuery(); | ||
if (name != null) { | ||
//Exact match on name | ||
boolQB.filter(QueryBuilders.matchQuery("name.raw", name)); |
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.
this should be case-insensitive.
probably all of them should be. Are they?
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.
Yes, all are case-insensitive. Keywords are by default case-sensitive. To make this field case-insensitive is why I added a normalizer to it(this feature is only available starting v5.2 of elasticsearch. So, I guess we should update the ReadME to include this)
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.
Please update the README. Is that about also sourcing the -setting.json file, or do we also need to update the deployment scripts to use a later version of elasticsearch?
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.
Requesting change to README, and I think also a change to links to list add-ons by tag.
@RequestParam(value = "q", required = false) String query, | ||
@RequestParam(value = "tag", required = false) String tag) throws Exception { | ||
return index.search(type, query, tag); | ||
@RequestParam(value = "q", required = false) String query, |
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.
Conclusion: we didn't find a Java library for this, so we'll proceed doing it in the front end in JS.
BoolQueryBuilder boolQB = QueryBuilders.boolQuery(); | ||
if (name != null) { | ||
//Exact match on name | ||
boolQB.filter(QueryBuilders.matchQuery("name.raw", name)); |
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.
Please update the README. Is that about also sourcing the -setting.json file, or do we also need to update the deployment scripts to use a later version of elasticsearch?
url += "&q=" + query; | ||
} | ||
if (tag) { | ||
url += "&tag=" + tag; |
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.
This change implies there should be a change to the URL of the link for searching by tag, right? I don't see it changed in this PR.
@@ -0,0 +1,11 @@ | |||
{ |
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.
Resolved
Issue: AO 22
This work was undertaken as part of a GSoC 2019 project. More details of the same may be found here