Solr faceted search client and react component pack
- 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
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.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import {
SolrFacetedSearch,
SolrClient
} from "solr-faceted-search-react";
// The search fields and filterable facets you want
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"}
];
// The sortable fields you want
const sortFields = [
{label: "Name", field: "koppelnaam_s"},
{label: "Date of birth", field: "birthDate_i"},
{label: "Date of death", field: "deathDate_i"}
];
document.addEventListener("DOMContentLoaded", () => {
// The client class
new SolrClient({
// The solr index url to be queried by the client
url: "http://localhost:8983/solr/gettingstarted/select",
searchFields: fields,
sortFields: sortFields,
// The change handler passes the current query- and result state for render
// as well as the default handlers for interaction with the search component
onChange: (state, handlers) =>
// Render the faceted search component
ReactDOM.render(
<SolrFacetedSearch
{...state}
{...handlers}
bootstrapCss={true}
onSelectDoc={(doc) => console.log(doc)}
/>,
document.getElementById("app")
)
}).initialize(); // this will send an initial search, fetching all results from solr
});
Instructions on building this example can be found here.
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:
// solr-reducer.js
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"
// Create a store for the reducer.
const store = createStore(solrReducer);
// (...)
// Construct the solr client api class
const solrClient = new SolrClient({
url: "http://localhost:8983/solr/gettingstarted/select",
searchFields: fields,
sortFields: sortFields,
// Delegate change callback to redux dispatcher
onChange: (state) => store.dispatch({type: "SET_SOLR_STATE", state: state})
});
// Register your app with the store
store.subscribe(() =>
// In stead of using the handlers passed along in the onChange callback of SolrClient
// use the .getHandlers() method to get the default click / change handlers
ReactDOM.render(
<SolrFacetedSearch
{...store.getState()}
{...solrClient.getHandlers()}
bootstrapCss={true}
onSelectDoc={(doc) => console.log(doc)}
/>,
document.getElementById("app")
)
);
document.addEventListener("DOMContentLoaded", () => {
// this will send an initial search initializing the app
solrClient.initialize();
});
To rebuild the webapp with redux follow the building the redux example.
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", // do not paginate, but dynamically load new results
idField: "id" // the field name in the index which is unique per indexed document.
// ...
});
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]}
]
// ...
});
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, // src/components/text-search/index.js
"list-facet": ListFacet, // src/components/list-facet/index.js
"range-facet": RangeFacet, // src/components/range-facet/index.js
container: SearchFieldContainer, // src/components/search-field-container.js
currentQuery: CurrentQuery // src/components/current-query/index.js
},
results: {
result: Result, // src/components/results/result.js
resultCount: CountLabel, // src/components/results/count-label.js
header: ResultHeader, // src/components/results/header.js
list: ResultList, // src/components/results/list.js
container: ResultContainer, // src/components/results/container.js
pending: ResultPending, // src/components/results/pending.js
paginate: ResultPagination // src/components/results/pagination.js
preloadIndicator: PreloadIndicator // src/components/results/preload-indicator.js
},
sortFields: {
menu: SortMenu // src/components/sort-menu/index.js
}
}
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";
// Custom class for the result component
class MyResult extends React.Component {
render() {
return (<li>
<a onClick={() => this.props.onSelect(this.props.doc)}>MyResult: {this.props.doc.name_t}</a>
</li>)
}
}
// Create a custom component pack from the default component pack
const myComponentPack = {
...defaultComponentPack,
results: {
...defaultComponentPack.results,
result: MyResult
}
}
// Render with the custom result component
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.
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")
);
{
url: "..." // the search url
searchFields: [{...}] // the search field configuration
sortFields: [{...}] // the sort field configuration,
onChange: (state, handlers) => {...} // the change handler for query and result state
rows: [1-9][0-9]+ // [optional] amount of results per page
pageStrategy: "paginate" // [optional, defaults to "paginate", other option is "cursor" - in which case you must provide a unique idField from the index]
idField: "id" // [optional, but required when using pageStrategy: "cursor"!]
facetLimit: [1-9][0-9]+ // [optional, sets the facet.limit param (defaults to -1 = all) ]
facetSort: "index" // [optional, how to sort the list facets by default (defaults to solr default)]
filters: [{...}] // [optional: static filters passed to search, not visible in user interface]
}
[
{
label: "All fields", // label of the field
field: "*", // field in index (asterisk indicates search in default text field)
type: "text" // renders a free input, sends a text filter query
},
{
label: ...
field: "name_t" // field in index
type: "text",
value: "jo*" // [optional: initial text search value]
},
{
label: ...
field: "deathDate_i",
type: "range-facet", // renders a range slider, sends a range query,
value: [1890, 1900] // [optional: initial range value of filter]
},
{
label: ...
field: "characteristics_ss",
type: "list-facet", // renders a facet list with checkboxes, sends a filter query
value: ["Publicist", "Bestuurslid vakvereniging"] // [optional: initial active filters]
facetSort: "index" // [optional: how to sort the list facet, options: "index" / "count"]
},
{
label: ...
field: "my-local-name",
lowerBound: "startDate_i",
upperBound: "endDate_i",
type: "period-range-facet", // renders a range slider, sends a range query over startDate_i and endDate_i
value: [1890, 1900] // [optional: initial range value of filter]
},
]
Search fields are presented in order of configuration array.
[
{
label: "Name", // Label of the field
field: "koppelnaam_s" // field in index
value: "asc" // [optional: presorts on this field ascendingly]
},
{
label: "Date of birth",
field: "birthDate_i",
value: "desc" // [optional: presorts on this field descendingly]
}
]
[
{
field: "name_t" // field in index
value: "jo*" // filter value
},
{
field: "deathDate_i",
type: "range",
value: [1890, 1900] // [optional: initial range value of filter]
}
]
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.
solrClient.setCurrentPage(3); // jump to page 3 (humans: 4) of the results
solrClient.setSearchFieldValue("characteristics_ss", ["Publicist", "Bestuurslid vakvereniging"]); // filter on exacly "Publicist" and "Bestuurslid vakvereniging"
solrClient.setSearchFieldValue("deathDate_i", [1890, 1900]); // filter on exactly the range 1890-1900
solrClient.setSearchFieldValue("name_t", "jo*"); // find all persons with a name starting with "jo"
solrClient.setSortFieldValue("name_t", "asc"); // sort by name ascendingly
solrClient.setSortFieldValue("birthDate_i", "desc"); // sort by birth date descendingly
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")
)
Download solr from the download page and extract the .tgz or .zip file.
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
<!-- enable CORS filters (only suitable for local testing, use a proxy for real world application) -->
<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>
<!--- /enable CORS filters -->
</web-app>
Start the solr server
$ bin/solr start -e cloud -noprompt
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
This completes the solr instruction. Back to quick start
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.
To run the redux integration example install redux:
$ npm i redux --save
And rebuild like this:
$
Back to the redux example