Solr faceted search client and react component pack
Solr faceted search client and react component pack
Table of Contents
- Quick start
- Redux integration
- Dynamically loaded result lists
- Using preset filters
- Injecting custom components
- Bootstrap CSS class names
- Using the SolrClient class
- Component Lego
Appendix A: Setting up Solr
Appendix B: Building the example webapp
Screenshot
Quick Start
This quick start assumes a solr installation as documented in the section on setting up solr.
Instructions on building a tiny web project from this example can be found here.
Installing this module:
$ npm i solr-faceted-search-react --save
The source below assumes succesfully setting up solr.
import React from "react";
import ReactDOM from "react-dom";
import {
SolrFacetedSearch,
SolrClient
} from "solr-faceted-search-react";
const fields = [
{label: "All text fields", field: "*", type: "text"},
{label: "Name", field: "name_t", type: "text"},
{label: "Characteristics", field: "characteristics_ss", type: "list-facet"},
{label: "Date of birth", field: "birthDate_i", type: "range-facet"},
{label: "Date of death", field: "deathDate_i", type: "range-facet"}
];
const sortFields = [
{label: "Name", field: "koppelnaam_s"},
{label: "Date of birth", field: "birthDate_i"},
{label: "Date of death", field: "deathDate_i"}
];
document.addEventListener("DOMContentLoaded", () => {
new SolrClient({
url: "http://localhost:8983/solr/gettingstarted/select",
searchFields: fields,
sortFields: sortFields,
onChange: (state, handlers) =>
ReactDOM.render(
<SolrFacetedSearch
{...state}
{...handlers}
bootstrapCss={true}
onSelectDoc={(doc) => console.log(doc)}
/>,
document.getElementById("app")
)
}).initialize();
});
Instructions on building this example can be found here.
Redux integration
In the quick start example, the SolrClient state is tightly coupled to the rendering of the SolrFacetedSearch component.
In many cases however, you might want to manage state yourself at application scope.
The example below illustrates how you can delegate state management to your own reducer/store using redux.
(To rebuild the webapp with redux follow the building the redux example.)
Given this reducer:
const initialState = {
query: {},
result: {}
}
export default function(state=initialState, action) {
switch (action.type) {
case "SET_SOLR_STATE":
console.log("In reducer: ", action.state);
return {...state, ...action.state}
}
}
The quick start example can be modified to look like this:
import solrReducer from "./solr-reducer";
import { createStore } from "redux"
const store = createStore(solrReducer);
const solrClient = new SolrClient({
url: "http://localhost:8983/solr/gettingstarted/select",
searchFields: fields,
sortFields: sortFields,
onChange: (state) => store.dispatch({type: "SET_SOLR_STATE", state: state})
});
store.subscribe(() =>
ReactDOM.render(
<SolrFacetedSearch
{...store.getState()}
{...solrClient.getHandlers()}
bootstrapCss={true}
onSelectDoc={(doc) => console.log(doc)}
/>,
document.getElementById("app")
)
);
document.addEventListener("DOMContentLoaded", () => {
solrClient.initialize();
});
To rebuild the webapp with redux follow the building the redux example.
Dynamically loaded result lists
If you want to change the way to paginate through the result list initialize the SolrClient with pageStrategy set to "cursor".
In this case you must also provide a unique idField, which is explained here.
const solrClient = new SolrClient({
pageStrategy: "cursor",
idField: "id"
});
Using preset filters
If you want to show only a subset of the index, filtered by a specific field, you can via the the filters prop of the SolrClient:
const solrClient = new SolrClient({
filters: [
{field: "name_t", value "maria"},
{field: "birthDate_i", type: "range", value [1880, 1890]}
]
});
Injecting custom components
The SolrFacetedSearch component provides the facility of overriding its inner components.
The default components are exposed through the defaultComponentPack, which has the following structure:
{
searchFields: {
text: TextSearch,
"list-facet": ListFacet,
"range-facet": RangeFacet,
container: SearchFieldContainer,
currentQuery: CurrentQuery
},
results: {
result: Result,
resultCount: CountLabel,
header: ResultHeader,
list: ResultList,
container: ResultContainer,
pending: ResultPending,
paginate: ResultPagination
preloadIndicator: PreloadIndicator
},
sortFields: {
menu: SortMenu
}
}
To override a default component an altered version of the defaultComponentPack can be passed to the SolrFacetedSearch component
via the customComponents prop:
import {
SolrFacetedSearch,
SolrClient,
defaultComponentPack
} from "solr-faceted-search-react";
class MyResult extends React.Component {
render() {
return (<li>
<a onClick={() => this.props.onSelect(this.props.doc)}>MyResult: {this.props.doc.name_t}</a>
</li>)
}
}
const myComponentPack = {
...defaultComponentPack,
results: {
...defaultComponentPack.results,
result: MyResult
}
}
ReactDOM.render(
<SolrFacetedSearch
{...state}
{...handlers}
bootstrapCss={true}
customComponents={myComponentPack}
onSelectDoc={(doc) => console.log(doc)}
/>,
document.getElementById("app")
);
When overriding a component it is worthwhile to look at the prop signature (and usage) in the source of the default component.
Bootstrap CSS class names
The SolrFacetedSearch component and its default components optionally add bootstrap class names to the rendered dom elements.
To turn this off, render the SolrFacetedSearch component with the property bootstrapCss set to false.
ReactDOM.render(
<SolrFacetedSearch
{...state}
{...handlers}
bootstrapCss={false}
onSelectDoc={(doc) => console.log(doc)}
/>,
document.getElementById("app")
);
Using the SolrClient class
Settings passed to the constructor:
{
url: "..."
searchFields: [{...}]
sortFields: [{...}]
onChange: (state, handlers) => {...}
rows: [1-9][0-9]+
pageStrategy: "paginate"
idField: "id"
facetLimit: [1-9][0-9]+
facetSort: "index"
filters: [{...}]
}
Layout of searchFields:
[
{
label: "All fields",
field: "*",
type: "text"
},
{
label: ...
field: "name_t"
type: "text",
value: "jo*"
},
{
label: ...
field: "deathDate_i",
type: "range-facet",
value: [1890, 1900]
},
{
label: ...
field: "characteristics_ss",
type: "list-facet",
value: ["Publicist", "Bestuurslid vakvereniging"]
facetSort: "index"
},
{
label: ...
field: "my-local-name",
lowerBound: "startDate_i",
upperBound: "endDate_i",
type: "period-range-facet",
value: [1890, 1900]
},
]
Search fields are presented in order of configuration array.
Layout of sortFields:
[
{
label: "Name",
field: "koppelnaam_s"
value: "asc"
},
{
label: "Date of birth",
field: "birthDate_i",
value: "desc"
}
]
Layout of filters:
[
{
field: "name_t"
value: "jo*"
},
{
field: "deathDate_i",
type: "range",
value: [1890, 1900]
}
]
Methods
The SolrClient class exposes a number of methods to manipulate its state directly.
Invoking the methods below will trigger a new solr search (and rerender of the SolrFacetedSearch component).
These methods should be called after .initialize() has been invoked.
Changing the result page:
solrClient.setCurrentPage(3);
Setting active filters on a searchField
solrClient.setSearchFieldValue("characteristics_ss", ["Publicist", "Bestuurslid vakvereniging"]);
solrClient.setSearchFieldValue("deathDate_i", [1890, 1900]);
solrClient.setSearchFieldValue("name_t", "jo*");
Setting sortations
solrClient.setSortFieldValue("name_t", "asc");
solrClient.setSortFieldValue("birthDate_i", "desc");
Component Lego
The SolrFacetedSearch component is not actually needed as a wrapper around the components.
If the container classes in the defaultComponentPack do not provide enough flexibility for moving around components,
they can be used in a standalone manner.
Note, however, that prop management based on state.query and state.results is then up to the app developer.
Minor example:
const TextSearch = defaultComponentPack.searchFields.text;
ReactDOM.render(
<div>
<TextSearch
bootstrapCss={false}
field="name_t"
label="Standalone name field"
onChange={solrClient.getHandlers().onSearchFieldChange}
value={state.query.searchFields.find((sf) => sf.field === "name_t").value }
/>
{state.results.docs.map((doc, i) => <div key={i}>{doc.name_t}</div>)}
</div>,
document.getElementById("app")
)
Setting up solr
Install solr
Download solr from the download page and extract the .tgz or .zip file.
Start solr with CORS
Navigate to the solr dir (assuming solr-6.1.0).
$ cd solr-6.1.0
Edit the file server/etc/webdefault.xml and add these lines just above the last closing tag
<filter>
<filter-name>cross-origin</filter-name>
<filter-class>org.eclipse.jetty.servlets.CrossOriginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cross-origin</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
Start the solr server
$ bin/solr start -e cloud -noprompt
Load sample data
Get sample data from this project
$ wget https://raw.githubusercontent.com/HuygensING/solr-faceted-search-react/master/solr-sample-data.json
Load the sample data into the gettingstarted index of solr
$ bin/post -c gettingstarted solr-sample-data.json
Check whether the data was succesfully indexed by navigation to http://localhost:8983/solr/gettingstarted/select?q=:&wt=json
Done
This completes the solr instruction. Back to quick start
Building the example webapp
These are just some minimal steps for building a webapp from the quick start with browserify.
Install react
$ npm i react react-dom --save
For this example install
$ npm i browserify babelify babel-preset-react babel-preset-react babel-preset-es2015 babel-preset-stage-2 --save-dev
Run browserify
$ ./node_modules/.bin/browserify index.js \
--require react \
--require react-dom \
--transform [ babelify --presets [ react es2015 stage-2 ] ] \
--standalone FacetedSearch \
-o web.js
Load this index.html in a browser
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="web.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" />
<style type="text/css">
a {
cursor: pointer;
}
.list-facet ul {
overflow-y: auto;
max-height: 200px;
}
.list-facet ul li {
cursor: pointer
}
.list-facet ul li:hover {
text-decoration: underline;
}
.facet-range-slider {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-user-drag: none;
user-drag: none;
cursor: pointer;
width: 100%;
stroke: #f1ebe6;
fill: #f1ebe6;
}
.facet-range-slider .range-line {
stroke-width: 8;
}
.facet-range-slider .range-line circle {
stroke-width: 0;
}
.facet-range-slider .range-line circle.hovering,
.facet-range-slider .range-line circle:hover {
fill: #bda47e;
}
.facet-range-slider .range-line path.hovering,
.facet-range-slider .range-line path:hover {
stroke: #bda47e;
}
.current-query label,
.solr-search-results ul label {
display: inline-block;
margin: 0 20px 0 0;
width: 120px;
color: #666;
overflow: hidden;
white-space: nowrap;
vertical-align: bottom;
text-overflow: ellipsis
}
.solr-search-results ul li ul {
list-style: none;
padding: 0;
}
svg.search-icon {
stroke: #595959;
fill: #595959;
}
.current-query .label {
display: inline-block;
padding: 4px;
cursor: pointer;
margin-left: 4px;
}
.current-query .label:hover a {
color: #d08989;
}
.current-query .label a {
color: white;
margin-left: 4px;
}
.range-facet header h5 {
max-width: calc(100% - 75px)
}
.facet-item-amount {
display:inline-block;
float:right;
color: #aaa;
}
.list-facet > .list-group {
box-shadow: none;
}
.list-facet > .list-group > .list-group-item {
border: none;
}
.list-facet > input {
width: calc(100% - 125px)
}
</style>
</head>
<body>
<div id="app"></div>
</body>
</html>
This is enough for the quick start.
Building the redux example
To run the redux integration example install redux:
$ npm i redux --save
And rebuild like this:
$
Back to the redux example