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
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
33accff
Advanced Search Initial Support
reubenvarghese1 Jun 13, 2019
a381e4a
Advanced Search Initial Support
reubenvarghese1 Jun 13, 2019
03ead9f
Advanced Search Initial Support
reubenvarghese1 Jun 14, 2019
6e88bf2
Advanced Search Initial Support
reubenvarghese1 Jun 14, 2019
3102de9
Advanced Search Initial Support
reubenvarghese1 Jun 14, 2019
e9ec7fe
Advanced Search Initial Support
reubenvarghese1 Jun 14, 2019
0706d4c
Advanced Search Initial Support
reubenvarghese1 Jun 14, 2019
fb9d5b6
Advanced Search Initial Support
reubenvarghese1 Jun 14, 2019
efdcda1
Advanced Search added regex checks
reubenvarghese1 Jun 14, 2019
613134d
Advanced Search added regex checks
reubenvarghese1 Jun 14, 2019
c289432
Retaining Advanced Query search
reubenvarghese1 Jun 14, 2019
d226200
Advanced Query search Fixes
reubenvarghese1 Jun 14, 2019
3259cd0
Advanced Query search Fixes
reubenvarghese1 Jun 14, 2019
ca0ca4d
Advanced Query search Fixes
reubenvarghese1 Jun 15, 2019
00f7783
Advanced Query Search Frontend Changes
reubenvarghese1 Jul 2, 2019
bdfcd55
Backend for Advanced Query Search
reubenvarghese1 Jul 3, 2019
f24fe19
Changes implemented partially
reubenvarghese1 Jul 8, 2019
0a5cca6
Changes implemented partially
reubenvarghese1 Jul 9, 2019
a6904ca
Trial
reubenvarghese1 Jul 11, 2019
0804487
Trial
reubenvarghese1 Jul 11, 2019
f635bc5
Trial
reubenvarghese1 Jul 11, 2019
a71f7f7
Trial
reubenvarghese1 Jul 11, 2019
c6cc91c
Trial
reubenvarghese1 Jul 12, 2019
80a93b4
Trial
reubenvarghese1 Jul 12, 2019
1a30516
Trial
reubenvarghese1 Jul 12, 2019
d8a3c47
Trial with case sensitivity
reubenvarghese1 Jul 12, 2019
4f30974
Trial with case sensitivity
reubenvarghese1 Jul 12, 2019
b4c6a57
Trial with case sensitivity
reubenvarghese1 Jul 12, 2019
828ec46
Trial with case sensitivity
reubenvarghese1 Jul 13, 2019
51755a1
Trial with case sensitivity
reubenvarghese1 Jul 13, 2019
069ed9f
Trial with case sensitivity
reubenvarghese1 Jul 13, 2019
62725a5
Trial with case sensitivity
reubenvarghese1 Jul 13, 2019
bf095a1
Fixed moduleid and uid error
reubenvarghese1 Jul 13, 2019
51e8adb
Fixed moduleid and uid error
reubenvarghese1 Jul 13, 2019
d41064c
Fixed moduleid and uid error
reubenvarghese1 Jul 13, 2019
f3cc839
Trial with case sensitivity
reubenvarghese1 Aug 8, 2019
bff4db5
Trial with case sensitivity
reubenvarghese1 Aug 8, 2019
418f919
Trial with case sensitivity
reubenvarghese1 Aug 8, 2019
db524ac
Trial with case sensitivity
reubenvarghese1 Aug 8, 2019
3b2ef2a
Trial with case sensitivity
reubenvarghese1 Aug 8, 2019
534f175
Trial with case sensitivity
reubenvarghese1 Aug 8, 2019
514b05f
Moved Parsing to SearchPage
reubenvarghese1 Aug 8, 2019
df696ba
Moved Parsing to SearchPage
reubenvarghese1 Aug 8, 2019
88cb630
Moved Parsing to SearchPage
reubenvarghese1 Aug 8, 2019
0e39fa3
Moved Parsing to SearchPage
reubenvarghese1 Aug 8, 2019
7ec8a3d
Moved Parsing to SearchPage
reubenvarghese1 Aug 8, 2019
f5fae33
Moved Parsing to SearchPage
reubenvarghese1 Aug 8, 2019
45506f9
Moved Parsing to SearchPage
reubenvarghese1 Aug 8, 2019
77e0a78
Moved Parsing to SearchPage
reubenvarghese1 Aug 8, 2019
f250080
Moved Parsing to SearchPage
reubenvarghese1 Aug 8, 2019
05d4ab8
Moved Parsing to SearchPage
reubenvarghese1 Aug 9, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ public String findModules(
@RequestParam(value = "openmrs_version", required = false) String openmrsVersion,
@RequestParam(value = "excludeModule", required = false) List<String> excludeModuleIds
) throws Exception {
Collection<AddOnInfoSummary> results = index.search(AddOnType.OMOD, query, null);
Collection<AddOnInfoSummary> results = index.search(AddOnType.OMOD, query, null, null, null,
null, null, null);

LegacyFindModulesResponse response = new LegacyFindModulesResponse();
response.setsEcho(sEcho);
Expand Down
14 changes: 11 additions & 3 deletions src/main/java/org/openmrs/addonindex/rest/AddOnController.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@

import org.openmrs.addonindex.domain.AddOnInfoAndVersions;
import org.openmrs.addonindex.domain.AddOnInfoSummary;
import org.openmrs.addonindex.domain.AddOnStatus;
import org.openmrs.addonindex.domain.AddOnType;
import org.openmrs.addonindex.domain.AddOnVersion;

import org.openmrs.addonindex.service.Index;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -38,9 +41,14 @@ public AddOnController(Index index) {

@RequestMapping(method = RequestMethod.GET, value = "/api/v1/addon")
public Collection<AddOnInfoSummary> search(@RequestParam(value = "type", required = false) AddOnType type,
@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.

@RequestParam(value = "tag", required = false) String tag,
@RequestParam(value = "uid", required = false) String uid,
@RequestParam(value = "moduleid", required = false) String moduleId,
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "exclude", required = false) String excludeText,
@RequestParam(value = "status", required = false) AddOnStatus status) throws Exception {
return index.search(type, query, tag, uid, moduleId, name, excludeText, status);
}

@RequestMapping(method = RequestMethod.GET, value = "/api/v1/addon", params = "modulePackage")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@
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.AddOnStatus;
import org.openmrs.addonindex.domain.AddOnType;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -70,12 +73,14 @@ public void setUp() throws IOException {
} else {
// need to create the index
logger.info("Creating new ES index: " + AddOnInfoAndVersions.ES_INDEX);
handleError(client.execute(new CreateIndex.Builder(AddOnInfoAndVersions.ES_INDEX).build()));
handleError(client.execute(new CreateIndex.Builder(AddOnInfoAndVersions.ES_INDEX).settings(
loadResource("elasticsearch/addOnInfoAndVersions-settings.json")).build()));
}
logger.info("Updating mappings on ES index");
handleError(client.execute(new PutMapping.Builder(AddOnInfoAndVersions.ES_INDEX,
AddOnInfoAndVersions.ES_TYPE,
loadResource("elasticsearch/addOnInfoAndVersions-mappings.json")).build()));
loadResource("elasticsearch/addOnInfoAndVersions-mappings.json"))
.build()));
}

private String loadResource(String name) throws IOException {
Expand All @@ -98,8 +103,25 @@ 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 moduleId,
String name, String exclude, AddOnStatus status) throws IOException {
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?

}
if (uid != null) {
//Exact match on uid. This is different from moduleId
boolQB.filter(QueryBuilders.matchQuery("uid", uid));
}
if (moduleId != null) {
//Exact match on moduleID
boolQB.filter(QueryBuilders.matchQuery("moduleId", moduleId));
}
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));
Expand Down Expand Up @@ -127,6 +149,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);
Expand Down
11 changes: 7 additions & 4 deletions src/main/java/org/openmrs/addonindex/service/Index.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@

package org.openmrs.addonindex.service;

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.AddOnStatus;
import org.openmrs.addonindex.domain.AddOnType;

import java.util.Collection;
import java.util.List;


/**
* Interface for accessing the datastore where we have indexed information about add-ons and versions.
*/
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 moduleId,
String name, String exclude, AddOnStatus status) throws Exception;

Collection<AddOnInfoAndVersions> getAllByType(AddOnType type) throws Exception;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
"type": "keyword"
},
"name": {
"type": "text"
"type": "text",
"fields": {
"raw": {
"type": "keyword",
"normalizer": "case_normalizer"
}
}
},
"description": {
"type": "text"
Expand Down
Original file line number Diff line number Diff line change
@@ -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

"analysis": {
"normalizer": {
"case_normalizer": {
"type": "custom",
"filter": ["lowercase"]
}
}
}

}
65 changes: 40 additions & 25 deletions src/main/ui/app/component/SearchBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -21,43 +22,57 @@ class SearchBox extends Component {

setQuery(query) {
this.setState({
query: query
});
query: query
});
}

doSearch() {
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

//Basic Regex Matching to fix query inconsistencies
//Removing all extra spaces i.e. two or more
query = query.replace(/\s+/g,' ').trim();
//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()?

let url = "/search?";
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

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

if (searchQueryObj[key]){
if (key === "text" || key === "query") {
url += "&q=" + searchQueryObj[key];
}
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"){
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?

url += "&status=" + searchQueryObj[key].toUpperCase();
}
else {
url += "&" + key + "=" + searchQueryObj[key];
}
}
});
}
if (this.state.query) {
url += "&q=" + this.state.query;
}
if (this.state.tag) {
url += "&tag=" + this.state.tag;
else {
url += "&q=" + query;
}

this.props.router.push(url);
}
}

formatType(type) {
if (type === "OMOD") {
return "Module (OMOD)";
}
else if (type === "OWA") {
return "Open Web App (OWA)";
}
else {
return "All Types";
}
}

render() {
const title = (
<span>{this.formatType(this.state.type)}</span>
);
return (
<div className="row pushdown">
<Form onSubmit={(evt) => {
Expand Down
26 changes: 12 additions & 14 deletions src/main/ui/app/route/SearchPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@ export default class SearchPage extends Component {
}

doSearch() {
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 query = this.props.location.query;
const searchKey = `${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 : ""}`;

if (this.state.latestSearch === searchKey) {
return;
Expand All @@ -43,15 +41,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;
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.

}

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?

}
});

fetch(url)
.then(response => {
return response.json();
Expand All @@ -77,13 +73,15 @@ export default class SearchPage extends Component {
}

render() {
let query = this.props.location.query;

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?


{this.state.latestSearch ?
<div>Searching for {this.state.latestSearch}</div>
Expand Down
Loading