-
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?
Changes from 2 commits
33accff
a381e4a
03ead9f
6e88bf2
3102de9
e9ec7fe
0706d4c
fb9d5b6
efdcda1
613134d
c289432
d226200
3259cd0
ca0ca4d
00f7783
bdfcd55
f24fe19
0a5cca6
a6904ca
0804487
f635bc5
a71f7f7
c6cc91c
80a93b4
1a30516
d8a3c47
4f30974
b4c6a57
828ec46
51755a1
069ed9f
62725a5
bf095a1
51e8adb
d41064c
f3cc839
bff4db5
418f919
db524ac
3b2ef2a
534f175
514b05f
df696ba
88cb630
0e39fa3
7ec8a3d
f5fae33
45506f9
77e0a78
f250080
05d4ab8
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 |
---|---|---|
|
@@ -24,10 +24,7 @@ | |
import org.elasticsearch.index.query.QueryBuilders; | ||
import org.elasticsearch.search.builder.SearchSourceBuilder; | ||
import org.elasticsearch.search.sort.SortOrder; | ||
import org.openmrs.addonindex.domain.AddOnInfoAndVersions; | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. same |
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
|
@@ -98,8 +95,21 @@ public void index(AddOnInfoAndVersions infoAndVersions) throws IOException { | |
} | ||
|
||
@Override | ||
public Collection<AddOnInfoSummary> search(AddOnType type, String query, String tag) throws IOException { | ||
public Collection<AddOnInfoSummary> search(AddOnType type, String query, String tag, String uid, | ||
String name, String exclude, AddOnStatus status) throws IOException { | ||
BoolQueryBuilder boolQB = QueryBuilders.boolQuery(); | ||
if (name != null){ | ||
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. formatting :-) should be
|
||
//Exact match on moduleID | ||
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. copy-paste error. this is name, not module id |
||
boolQB.filter(QueryBuilders.matchQuery("name.raw", name)); | ||
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 should be case-insensitive. 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. 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 commentThe 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? |
||
} | ||
if (uid != null){ | ||
//Exact match on moduleID | ||
boolQB.filter(QueryBuilders.matchQuery("moduleId", uid)); | ||
} | ||
if (status != null){ | ||
//Exact match on status | ||
boolQB.filter(QueryBuilders.matchQuery("status", status)); | ||
} | ||
if (type != null) { | ||
//Exact match on type | ||
boolQB.filter(QueryBuilders.matchQuery("type", type)); | ||
|
@@ -127,6 +137,11 @@ public Collection<AddOnInfoSummary> search(AddOnType type, String query, String | |
boolQB.should(QueryBuilders.matchPhraseQuery("name", query).slop(2).fuzziness("AUTO")); | ||
boolQB.minimumNumberShouldMatch(1); | ||
} | ||
|
||
if (exclude != null){ | ||
boolQB.mustNot(QueryBuilders.multiMatchQuery(exclude , "name", "tags", "description", | ||
"moduleId", "status", "_id", "type")); | ||
} | ||
|
||
BoostingQueryBuilder boostingQB = QueryBuilders.boostingQuery(); | ||
boostingQB.positive(boolQB); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,10 +13,7 @@ | |
import java.util.Collection; | ||
import java.util.List; | ||
|
||
import org.openmrs.addonindex.domain.AddOnInfoAndVersions; | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. same |
||
|
||
/** | ||
* Interface for accessing the datastore where we have indexed information about add-ons and versions. | ||
|
@@ -25,7 +22,8 @@ public interface Index { | |
|
||
void index(AddOnInfoAndVersions infoAndVersions) throws Exception; | ||
|
||
Collection<AddOnInfoSummary> search(AddOnType type, String query, String tag) throws Exception; | ||
Collection<AddOnInfoSummary> search(AddOnType type, String query, String tag, String uid, | ||
String name, String exclude, AddOnStatus status) throws Exception; | ||
|
||
Collection<AddOnInfoAndVersions> getAllByType(AddOnType type) throws Exception; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ | |
import {Component} from "react"; | ||
import {withRouter} from "react-router"; | ||
import {Button, Col, Form, FormControl, InputGroup} from "react-bootstrap"; | ||
import searchQuery from "search-query-parser"; | ||
|
||
class SearchBox extends Component { | ||
|
||
|
@@ -20,68 +21,50 @@ class SearchBox extends Component { | |
} | ||
|
||
setQuery(query) { | ||
this.setState({ query: query }, function() { | ||
this.parseQuery(); | ||
})} | ||
|
||
setSplitQuery(splitQuery) { | ||
this.setState({ | ||
splitQuery: splitQuery | ||
query: query | ||
}); | ||
} | ||
|
||
parseQuery(){ | ||
if (this.state.query){ | ||
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 commentThe 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. 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. 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 |
||
//Basic Regex Matching to fix query inconsistencies | ||
//Removing all extra spaces i.e. two or more | ||
advancedQuery = advancedQuery.replace(/\s+/g,' ').trim(); | ||
//Removing all spaces to the left of search keys | ||
advancedQuery = advancedQuery.replace(new RegExp("\\s+:","g"),":"); | ||
//Removing all spaces to the right of search keys | ||
advancedQuery = advancedQuery.replace(new RegExp(":\\s+","g"),":"); | ||
let queryComponents = {}; | ||
//Determining query type | ||
if(advancedQuery.includes(":")){ | ||
let querySplit = advancedQuery.split(' '); | ||
let tempQuery = ""; | ||
querySplit.forEach(m => { | ||
if (m.includes(":")){ | ||
let [key, value] = m.split(':'); | ||
if (key === "type"){ | ||
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 commentThe 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 commentThe 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 :) |
||
let options = {keywords: ['type', 'tag', 'query', 'moduleid', 'status', 'name'], offsets: false}; | ||
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. 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. |
||
let searchQueryObj = searchQuery.parse(advancedQuery, options); | ||
Object.keys(searchQueryObj).forEach(function (key) { | ||
if (searchQueryObj[key]){ | ||
if (key === "text" || key === "query") { | ||
url += "&q=" + searchQueryObj[key]; | ||
} | ||
else if (key === "tag") { | ||
queryComponents["tag"] = value.toLowerCase(); | ||
else if (key === "type"){ | ||
url += "&type=" + searchQueryObj[key].toUpperCase(); | ||
} | ||
else if (key === "exclude"){ | ||
if (searchQueryObj[key].text){ | ||
url += "&exclude=" + searchQueryObj[key].text; | ||
} | ||
} | ||
else if (key === "status"){ | ||
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. you're handling "type" and "status" in the same way, can you combine these branches? |
||
url += "&status=" + searchQueryObj[key].toUpperCase(); | ||
} | ||
else { | ||
url += "&" + key + "=" + searchQueryObj[key]; | ||
} | ||
} | ||
else { | ||
tempQuery = tempQuery + ' ' + m; | ||
queryComponents["query"] = tempQuery; | ||
} | ||
}); | ||
} | ||
else{ | ||
queryComponents["query"] = advancedQuery; | ||
} | ||
this.setSplitQuery(queryComponents); | ||
} | ||
} | ||
|
||
doSearch() { | ||
if (this.state.splitQuery) { | ||
let url = "/search?"; | ||
|
||
if (this.state.splitQuery.type) { | ||
url += "type=" + this.state.splitQuery.type; | ||
} | ||
|
||
if (this.state.splitQuery.query) { | ||
url += "&q=" + this.state.splitQuery.query; | ||
}); | ||
} | ||
|
||
if (this.state.splitQuery.tag) { | ||
url += "&tag=" + this.state.splitQuery.tag; | ||
else { | ||
url += "&q=" + advancedQuery; | ||
} | ||
|
||
this.props.router.push(url); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,10 +31,16 @@ export default class SearchPage extends Component { | |
} | ||
|
||
doSearch() { | ||
const advancedQuery = this.props.location.query; | ||
const query = this.props.location.query.q; | ||
const type = this.props.location.query.type; | ||
const tag = this.props.location.query.tag; | ||
const searchKey = `type:${type ? type : "all"} query:${query ? query : ""} tag:${tag ? tag : ""}`; | ||
const tag = this.props.location.query.tag; | ||
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 commentThe 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 commentThe 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 :) |
||
|
||
const searchKey = `${type ? "type:" + type : ""} ${query ? "query:" + query : ""} ${tag ? "tag:" + tag : ""} ${exclude ? "-" + exclude : ""} ${name ? "name:" + name : ""} ${status ? "status:" + status : ""} ${moduleid ? "moduleId:" + moduleid : ""}`; | ||
|
||
if (this.state.latestSearch === searchKey) { | ||
return; | ||
|
@@ -43,15 +49,13 @@ export default class SearchPage extends Component { | |
this.handleStartSearch(searchKey); | ||
|
||
let url = "/api/v1/addon?"; | ||
if (type) { | ||
url += "type=" + type; | ||
} | ||
if (query) { | ||
url += "&q=" + query; | ||
} | ||
if (tag) { | ||
url += "&tag=" + tag; | ||
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 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. |
||
} | ||
|
||
Object.keys(advancedQuery).forEach(function (key) { | ||
if (advancedQuery[key]){ | ||
url += ("&" + key + "=" + advancedQuery[key]); | ||
} | ||
}); | ||
|
||
fetch(url) | ||
.then(response => { | ||
return response.json(); | ||
|
@@ -80,13 +84,18 @@ export default class SearchPage extends Component { | |
let simpleQuery = this.props.location.query.q; | ||
let moduleType = this.props.location.query.type; | ||
let moduleTag = this.props.location.query.tag; | ||
let moduleExclude = this.props.location.query.exclude; | ||
let moduleid = this.props.location.query.moduleid; | ||
let moduleName = this.props.location.query.name; | ||
let moduleStatus = this.props.location.query.status; | ||
|
||
if (this.state && this.state.error) { | ||
return <div>{this.state.error}</div> | ||
} | ||
else { | ||
return ( | ||
<div> | ||
<SearchBox initialQuery={`type:${moduleType ? moduleType : "all"} query:${simpleQuery ? simpleQuery : ""} tag:${moduleTag ? moduleTag : ""}`}/> | ||
<SearchBox initialQuery={`${moduleType ? "type:" + moduleType : ""} ${simpleQuery ? "query:" + simpleQuery : ""} ${moduleTag ? "tag:" + moduleTag : ""} ${moduleExclude ? "-" + moduleExclude : ""} ${moduleName ? "name:" + moduleName : ""} ${moduleStatus ? "status:" + moduleStatus : ""} ${moduleid ? "moduleId:" + moduleid : ""}`}/> | ||
|
||
{this.state.latestSearch ? | ||
<div>Searching for {this.state.latestSearch}</div> | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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