Add extra sorting and filtering options

This commit is contained in:
Aleksy Wroblewski 2021-03-23 20:17:03 +01:00
parent 74ed59921c
commit f3ee7e34ef
19 changed files with 428 additions and 28 deletions

2
server/.gitignore vendored
View File

@ -31,3 +31,5 @@ build/
### VS Code ### ### VS Code ###
.vscode/ .vscode/
.mvn*
mvnw*

View File

@ -11,7 +11,7 @@
<groupId>com.github</groupId> <groupId>com.github</groupId>
<artifactId>awrb</artifactId> <artifactId>awrb</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<name>awrb</name> <name>solr</name>
<description>ISI 2021 Lab 4</description> <description>ISI 2021 Lab 4</description>
<properties> <properties>
<java.version>1.8</java.version> <java.version>1.8</java.version>

View File

@ -2,12 +2,11 @@ package com.github.awrb;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration;
@SpringBootApplication @SpringBootApplication
public class AwrbApplication { public class SolrApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(AwrbApplication.class, args); SpringApplication.run(SolrApplication.class, args);
} }
} }

View File

@ -15,7 +15,7 @@ public class SolrFacade {
private SolrClient solrClient; private SolrClient solrClient;
public SolrFacade(@Value("${solr.address") String solrAddress) { public SolrFacade(@Value("${solr.address}") String solrAddress) {
this.solrClient = new HttpSolrClient.Builder(solrAddress).build(); this.solrClient = new HttpSolrClient.Builder(solrAddress).build();
} }

View File

@ -1,11 +1,13 @@
package com.github.awrb.solr.services; package com.github.awrb.solr.services;
import com.github.awrb.solr.SolrFacade; import com.github.awrb.solr.SolrFacade;
import com.github.awrb.solr.services.data.SolrQueryParams;
import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.params.SolrParams;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -13,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.io.IOException; import java.io.IOException;
@RestController @RestController
@CrossOrigin(origins = "http://localhost:3000")
public class SolrService { public class SolrService {
private SolrFacade solrFacade; private SolrFacade solrFacade;
@ -23,14 +26,7 @@ public class SolrService {
} }
@GetMapping("/search") @GetMapping("/search")
public SolrDocumentList search(@RequestParam("q") String query) throws IOException, SolrServerException { public SolrDocumentList search(SolrQueryParams params) throws IOException, SolrServerException {
SolrParams solrParams = solrParamsFromQuery(query); return solrFacade.query(params.toSolrParams()).getResults();
return solrFacade.query(solrParams).getResults();
}
private SolrParams solrParamsFromQuery(String query) {
ModifiableSolrParams params = new ModifiableSolrParams();
params.set("q", query);
return params;
} }
} }

View File

@ -0,0 +1,86 @@
package com.github.awrb.solr.services.data;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
public class SolrQueryParams {
private static final String MATCH_ALL = "*";
private static final int DEFAULT_ROW_COUNT = 10;
private String query;
private String reviewText = MATCH_ALL;
private String reviewerName = MATCH_ALL;
private String summary = MATCH_ALL;
private String asin = MATCH_ALL;
private Sort sort = Sort.DESC;
private int rows = DEFAULT_ROW_COUNT;
public void setQuery(String query) {
this.query = query;
}
public void setReviewText(String reviewText) {
this.reviewText = reviewText;
}
public void setRows(int rows) {
this.rows = rows;
}
public void setReviewerName(String reviewerName) {
this.reviewerName = reviewerName;
}
public void setSummary(String summary) {
this.summary = summary;
}
public void setAsin(String asin) {
this.asin = asin;
}
public void setSort(Sort sort) {
this.sort = sort;
}
public SolrParams toSolrParams() {
ModifiableSolrParams params = new ModifiableSolrParams();
params.set("q", buildQ());
params.set("rows", rows);
// params.set("sort", "overall " + sort.label);
System.out.println(params.toQueryString());
return params;
}
private String buildQ() {
StringBuilder sb = new StringBuilder();
sb.append("(reviewText:")
.append(reviewText)
.append(" reviewerName:")
.append(reviewerName)
.append(" summary:")
.append(summary)
.append(" asin:")
.append(asin)
.append(")");
// The query string ends up looking like this: (reviewText:* reviewerName:* summary:*)
// OR between parameters
System.out.println(sb.toString());
return sb.toString();
}
public enum Sort {
ASC("asc"), DESC("desc");
private String label;
Sort(String label) {
this.label = label;
}
}
}

View File

@ -1,2 +1,2 @@
server.servlet.context-path=/api server.servlet.context-path=/api
solr.address=http://localhost:8983/solr/techproducts solr.address=http://localhost:8983/solr/reviews

View File

@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest @SpringBootTest
class AwrbApplicationTests { class SolrApplicationTests {
@Test @Test
void contextLoads() { void contextLoads() {

2
ui/solr/.gitignore vendored
View File

@ -21,3 +21,5 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
yarn.lock
package-lock.json

View File

@ -3,13 +3,16 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^5.11.4", "@material-ui/core": "4.11.3",
"@testing-library/react": "^11.1.0", "@material-ui/icons": "4.11.2",
"@testing-library/user-event": "^12.1.10", "@testing-library/jest-dom": "5.11.4",
"react": "^17.0.1", "@testing-library/react": "11.1.0",
"react-dom": "^17.0.1", "@testing-library/user-event": "12.1.10",
"axios": "0.21.1",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"web-vitals": "^1.0.1" "web-vitals": "1.0.1"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

11
ui/solr/src/api/solr.js Normal file
View File

@ -0,0 +1,11 @@
import axios from "axios";
const solr = axios.create({
baseURL: "http://localhost:8080/api/",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
export default solr;

View File

@ -0,0 +1,108 @@
import React, { useState } from "react";
import SearchBar from "./SearchBar";
import SearchResult from "./SearchResult";
import SortSelect from "./SortSelect";
import solr from "../api/solr";
import { List, CircularProgress, Grid } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import FilterSelect from "./FilterSelect";
import Constants from "../constants";
import PaginationSelect from "./PaginationSelect";
const useStyles = makeStyles({
loader: { margin: 20 },
select: { marginLeft: "3vw" },
});
const App = () => {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [filter, setFilter] = useState(Constants.REVIEW_TEXT);
const [term, setTerm] = useState("");
const [sort, setSort] = useState(Constants.DESC);
const [rows, setRows] = useState(10);
const classes = useStyles();
const getParams = () => {
const params = {
[Constants.REVIEWER_NAME]: "*",
[Constants.REVIEW_TEXT]: "*",
[Constants.SUMMARY]: "*",
[Constants.ASIN]: "*",
[Constants.ROWS]: rows,
[Constants.SORT]: sort,
};
if (filter === Constants.REVIEW_TEXT) {
params[Constants.REVIEW_TEXT] = term;
} else if (filter === Constants.REVIEWER_NAME) {
params[Constants.REVIEWER_NAME] = term;
} else if (filter === Constants.SUMMARY) {
params[Constants.SUMMARY] = term;
} else {
params[Constants.ASIN] = term;
}
return params;
};
const onSubmit = async () => {
setLoading(true);
const response = await solr.get("/search", { params: getParams() });
setResults(response.data);
setLoading(false);
};
const renderResults = () => {
return (
<List>
{results.map((result) => (
<SearchResult
key={result.id}
summary={result.summary[0]}
reviewText={result.reviewText[0]}
author={result.reviewerName[0]}
time={result.reviewTime[0]}
asin={result.asin}
rating={result.overall[0]}
/>
))}
</List>
);
};
return (
<React.Fragment>
<Grid container>
<Grid item>
<SearchBar
value={term}
onChange={(newTerm) => setTerm(newTerm)}
onSubmit={onSubmit}
/>
</Grid>
<Grid className={classes.select} item>
<FilterSelect
value={filter}
onChange={(filter) => setFilter(filter)}
/>
</Grid>
<Grid className={classes.select} item>
<SortSelect
onChange={(sort) => setSort(sort)}
value={sort}
className={classes.select}
/>
</Grid>
<Grid className={classes.select} item>
<PaginationSelect value={rows} onChange={(rows) => setRows(rows)} />
</Grid>
</Grid>
{loading && <CircularProgress className={classes.loader} />}
{!loading && results.length > 0 && renderResults()}
</React.Fragment>
);
};
export default App;

View File

@ -0,0 +1,28 @@
import { InputLabel, MenuItem, Select } from "@material-ui/core";
import React from "react";
import Constants from "../constants";
const FilterSelect = ({ onChange, value }) => {
const handleChange = (event) => {
onChange(event.target.value);
};
return (
<span>
<InputLabel id="filter-select">Filter By</InputLabel>
<Select
labelId="filter-select"
id="filter-select"
value={value}
onChange={handleChange}
>
<MenuItem value={Constants.REVIEW_TEXT}>Text</MenuItem>
<MenuItem value={Constants.REVIEWER_NAME}>Reviewer Name</MenuItem>
<MenuItem value={Constants.SUMMARY}>Summary</MenuItem>
<MenuItem value={Constants.ASIN}>ASIN</MenuItem>
</Select>
</span>
);
};
export default FilterSelect;

View File

@ -0,0 +1,27 @@
import { InputLabel, MenuItem, Select } from "@material-ui/core";
import React from "react";
const PaginationSelect = ({ onChange, value }) => {
const handleChange = (event) => {
onChange(event.target.value);
};
return (
<span>
<InputLabel id="pagination-select">Rows per page</InputLabel>
<Select
labelId="pagination-select"
id="pagination-select"
value={value}
onChange={handleChange}
>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={30}>30</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
</Select>
</span>
);
};
export default PaginationSelect;

View File

@ -0,0 +1,59 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import InputBase from "@material-ui/core/InputBase";
import IconButton from "@material-ui/core/IconButton";
import MenuIcon from "@material-ui/icons/Menu";
import SearchIcon from "@material-ui/icons/Search";
const useStyles = makeStyles((theme) => ({
root: {
padding: "2px 4px",
display: "flex",
alignItems: "center",
width: 400,
},
input: {
marginLeft: theme.spacing(1),
flex: 1,
},
iconButton: {
padding: 10,
},
}));
const SearchBar = ({ onSubmit, onChange, value }) => {
const classes = useStyles();
return (
<Paper
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
component="form"
className={classes.root}
>
<IconButton className={classes.iconButton} aria-label="menu">
<MenuIcon />
</IconButton>
<InputBase
value={value}
onChange={(e) => onChange(e.target.value)}
className={classes.input}
placeholder="Search"
inputProps={{ "aria-label": "search" }}
/>
<IconButton
type="submit"
className={classes.iconButton}
aria-label="search"
>
<SearchIcon />
</IconButton>
</Paper>
);
};
export default SearchBar;

View File

@ -0,0 +1,33 @@
import { Divider, ListItem, Paper, Typography } from "@material-ui/core";
import React from "react";
const SearchResult = ({
key,
summary,
reviewText,
author,
time,
asin,
rating,
}) => {
return (
<ListItem key={key}>
<Paper>
<Typography variant="h5">{summary}</Typography>
<Typography variant="body">{reviewText}</Typography>
<div style={{ marginTop: 30 }}>
<Divider />
<Typography variant="body">{author}</Typography>
<Divider />
<Typography variant="body">{time}</Typography>
<Divider />
<Typography variant="body">{`Rating ${rating}`}</Typography>
<Divider />
<Typography variant="body">{`ASIN ${asin}`}</Typography>
</div>
</Paper>
</ListItem>
);
};
export default SearchResult;

View File

@ -0,0 +1,26 @@
import { InputLabel, MenuItem, Select } from "@material-ui/core";
import React from "react";
import Constants from "../constants";
const SortSelect = ({ onChange, value }) => {
const handleChange = (event) => {
onChange(event.target.value);
};
return (
<span>
<InputLabel id="sort-select">Sort by rating</InputLabel>
<Select
labelId="sort-select"
id="sort-select"
value={value}
onChange={handleChange}
>
<MenuItem value={Constants.ASC}>Ascending</MenuItem>
<MenuItem value={Constants.DESC}>Descending</MenuItem>
</Select>
</span>
);
};
export default SortSelect;

21
ui/solr/src/constants.js Normal file
View File

@ -0,0 +1,21 @@
export const REVIEWER_NAME = "reviewerName";
export const REVIEW_TEXT = "reviewText";
export const SUMMARY = "summary";
export const ASIN = "asin";
export const ROWS = "rows";
export const ASC = "ASC";
export const DESC = "DESC";
export const SORT = "sort";
export const CONSTANTS = {
REVIEWER_NAME,
REVIEW_TEXT,
SUMMARY,
ASIN,
ROWS,
DESC,
ASC,
SORT,
};
export default CONSTANTS;

View File

@ -1,14 +1,13 @@
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom'; import ReactDOM from "react-dom";
import './index.css'; import App from "./components/App";
import App from './App'; import reportWebVitals from "./reportWebVitals";
import reportWebVitals from './reportWebVitals';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById("root")
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function