Skip to content
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

Open
wants to merge 51 commits into
base: master
Choose a base branch
from

Conversation

reubenvarghese1
Copy link
Collaborator

@reubenvarghese1 reubenvarghese1 commented Jun 14, 2019

Issue: AO 22

This work was undertaken as part of a GSoC 2019 project. More details of the same may be found here

@reubenvarghese1
Copy link
Collaborator Author

@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).

@djazayeri
Copy link
Member

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.

@djazayeri
Copy link
Member

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).

Copy link
Member

@djazayeri djazayeri left a 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.*;
Copy link
Member

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.)

Copy link
Collaborator Author

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.*;
Copy link
Member

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){
Copy link
Member

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
Copy link
Member

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.*;
Copy link
Member

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();
Copy link
Member

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?

Copy link
Collaborator Author

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};
Copy link
Member

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("-")){
Copy link
Member

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.)

Copy link
Collaborator Author

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 :)

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;
Copy link
Member

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?

Copy link
Collaborator Author

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"),":");
Copy link
Member

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()?

Copy link
Member

@djazayeri djazayeri left a 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("-")) {
Copy link
Member

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 ?)

Copy link
Collaborator Author

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);
Copy link
Member

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,
Copy link
Member

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.

Copy link
Collaborator Author

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

Copy link
Member

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 @@
{
Copy link
Member

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)

Copy link
Member

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};
Copy link
Member

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"){
Copy link
Member

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?


Object.keys(query).forEach(function (key) {
if (query[key]){
url += ("&" + key + "=" + query[key]);
Copy link
Member

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?

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 : ""}`}/>
Copy link
Member

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));
Copy link
Member

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?

Copy link
Collaborator Author

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)

Copy link
Member

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?

Copy link
Member

@djazayeri djazayeri left a 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,
Copy link
Member

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));
Copy link
Member

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;
Copy link
Member

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 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants