solrkit
Version:
 
1,186 lines (1,013 loc) • 30.4 kB
text/typescript
import * as Immutable from 'seamless-immutable';
import {
SolrResponse
} from './Data';
import * as fetchJsonp from 'fetch-jsonp';
import * as _ from 'lodash';
import { FacetValue } from '../component/facet/FacetTypes';
function escape(value: QueryParam): string {
if (value === '*') {
return value;
}
return (
(value.toString().indexOf(' ') > 0) ? (
'"' + value.toString().replace(/ /g, '%20') + '"'
) : (
value.toString()
)
);
}
function escapeNoQuote(value: QueryParam): string {
if (value === '*') {
return value;
}
return (
(value.toString().indexOf(' ') > 0) ? (
value.toString().replace(/ /g, '%20')
) : (
value.toString()
)
);
}
// Note that the stored versions of these end up namespaced and/or aliased
enum UrlParams {
ID = 'id',
QUERY = 'query',
FQ = 'fq',
START = 'start',
TYPE = 'type',
HL = 'highlight'
}
interface SavedSearch {
type?: 'QUERY' | 'MLT' | 'DETAILS';
query?: string;
boost?: string;
sort?: string[];
facets?: { [ key: string ]: string[] };
}
interface SearchParams extends SavedSearch {
start?: number;
}
type QueryParam = string | number;
type NamespacedUrlParam = [UrlParams, QueryParam];
type UrlFragment = [UrlParams | NamespacedUrlParam, QueryParam | [string, QueryParam[]]] | null; // k, v
class QueryBeingBuilt {
solrUrlFragment: string;
appUrlFragment: UrlFragment;
constructor(solrUrlFragment: string, appUrlFragment: UrlFragment) {
this.solrUrlFragment = solrUrlFragment;
// TODO, these need to support the following:
// aliasing multiple parameters (i.e. any of the following values could be a 'q')
// this supports renames over time
// urls in the path or the query
// this is for seo
// aliasing groups of parameters (this is like the named search example)
// numeric indexes - e.g. fq
// facets should have some special handling;
// facets can be arrays
// facets can be hierarchies (probably there are different implementations of this)
// "named" values that wrap sets of parameters - e.g named sort fields or enum queries
// need to be able to namespace the output of this to be unique to support multiple searches on a site
// this needs to work 100% bidirectional (i.e. url -> objects, objects -> url)
this.appUrlFragment = appUrlFragment;
}
}
class SolrQueryBuilder<T> {
searchResponse?: SolrResponse<T>;
previous?: SolrQueryBuilder<T>;
op: () => QueryBeingBuilt;
constructor(op: () => QueryBeingBuilt, previous?: SolrQueryBuilder<T>) {
this.op = op;
this.previous = previous;
}
get(id: QueryParam) {
return new SolrQueryBuilder<T>(
() => new QueryBeingBuilt('get?id=' + id, [UrlParams.ID, id]),
this
);
}
select() {
return new SolrQueryBuilder<T>(
() => new QueryBeingBuilt('select?facet=true', [UrlParams.TYPE, 'QUERY']),
this
);
}
moreLikeThis(handler: string, col: string, id: QueryParam, props: MoreLikeThisProps) {
function joinProp(useProps: MoreLikeThisProps, propertyName: string): string | null {
if (!useProps || !useProps[propertyName]) {
return null;
} else {
return 'mlt.' + propertyName + '=' + useProps[propertyName].join(',');
}
}
function prop(useProps: MoreLikeThisProps, propertyName: string): string | null {
if (!useProps || !useProps[propertyName]) {
return null;
} else {
return 'mlt.' + propertyName + '=' + useProps[propertyName];
}
}
return new SolrQueryBuilder<T>(
() => new QueryBeingBuilt(
handler + '?q=' + col + ':' + id + '&' +
[
joinProp(props, 'fl'),
prop(props, 'mintf'),
prop(props, 'mindf'),
prop(props, 'maxdf'),
prop(props, 'minwl'),
prop(props, 'maxwl'),
prop(props, 'maxqt'),
prop(props, 'maxntp'),
prop(props, 'boost'),
joinProp(props, 'qf')
].filter( (x) => !!x ).join(','),
[UrlParams.ID, id]
),
this
);
}
start(start: number) {
return new SolrQueryBuilder<T>(
() => new QueryBeingBuilt('start=' + start, [UrlParams.START, start]),
this
);
}
jsonp(callback: string) {
return new SolrQueryBuilder<T>(
() => new QueryBeingBuilt('wt=json&json.wrf=' + callback, null),
this
);
}
export() {
return new SolrQueryBuilder<T>(
() => new QueryBeingBuilt('wt=csv', null),
this
);
}
qt(qt: string) {
return new SolrQueryBuilder<T>(
// TODO: tv.all here is probably wrong
() => new QueryBeingBuilt('tv.all=true&qt=' + qt, null),
this
);
}
q(searchFields: string[], value: QueryParam) {
return new SolrQueryBuilder<T>(
() =>
new QueryBeingBuilt(
'defType=edismax&q=' +
searchFields.map(
(field) => field + ':' + escape(value)
).join('%20OR%20'),
[UrlParams.QUERY, value]
)
,
this
);
}
bq(query: string) {
return new SolrQueryBuilder<T>(
() => {
return new QueryBeingBuilt(
'bq=' + query,
null
);
},
this
);
}
fq(field: string, values: QueryParam[]) {
return new SolrQueryBuilder<T>(
() => {
return new QueryBeingBuilt(
'fq=' +
'{!tag=' + field + '_q}' +
values.map(escape).map((v) => field + ':' + v).join('%20OR%20'),
[UrlParams.FQ, [field, values]]
);
},
this
);
}
highlight(query: HighlightQuery) {
if (query.fields.length > 0) {
return new SolrQueryBuilder<T>(
() => {
let params: string[] = [
'method',
'fields',
'query',
'qparser',
'requireFieldMatch',
'usePhraseHighlighter',
'highlightMultiTerm',
'snippets',
'fragsize',
'encoder',
'maxAnalyzedChars'].map(
(key: string) => query[key] ? (
'hl.' + key + '=' + query[key]
) : ''
).filter(
(key) => key !== ''
);
if (query.pre) {
params.push('hl.tag.pre=' + query.pre);
}
if (query.post) {
params.push('hl.tag.post=' + query.pre);
}
return new QueryBeingBuilt(
'hl=true&' +
'hl.fl=' + query.fields.join(',') + (
params.length > 0 ? ( '&' + params.join('&') ) : ''
),
// Not saving these, because I'm assuming these will be
// configured as part of the search engine, rather than
// changed by user behavior
null
);
}
);
} else {
return null;
}
}
fl(fields: QueryParam[]) {
return new SolrQueryBuilder<T>(
() =>
new QueryBeingBuilt(
'fl=' + fields.join(','),
null
),
this
);
}
requestFacet(field: string) {
return new SolrQueryBuilder<T>(
() =>
new QueryBeingBuilt(
`facet.field={!ex=${field}_q}${field}&` +
`facet.mincount=1&` +
`facet.${field}.limit=50&` +
`facet.${field}.sort=count`,
null
),
this
);
}
rows(rows: number) {
return new SolrQueryBuilder<T>(
() =>
new QueryBeingBuilt(
'rows=' + rows,
null
),
this
);
}
sort(fields: string[]) {
return new SolrQueryBuilder<T>(
() =>
new QueryBeingBuilt(
'sort=' + fields.map(escapeNoQuote).join(','),
// TODO: I think this should add a 'named sort' to the URL, because
// this could have a large amount of Solr specific stuff in it - i.e.
// add(field1, mul(field2, field3)) and you don't want to expose the
// internals to external search engines, or they'll index the site in
// a way that would make this hard to change
null
),
this
);
}
schema() {
return new SolrQueryBuilder<T>(
() =>
new QueryBeingBuilt(
'admin/luke?',
null
)
);
}
construct(): [string, Array<UrlFragment>] {
let start: [string, Array<UrlFragment>] = ['', []];
if (this.previous) {
start = this.previous.construct();
}
if (start[0] !== '') {
start[0] = start[0] + '&';
}
const output = this.op();
return [
start[0] + output.solrUrlFragment,
start[1].concat([output.appUrlFragment])
];
}
buildCurrentParameters(): SearchParams {
const initialParams = this.construct()[1];
const result: Array<UrlFragment> = initialParams;
const searchParams: SearchParams = {
};
result.map(
(p: UrlFragment) => {
if (p !== null) {
if (p[0] === UrlParams.FQ) {
const facet = p[1][0];
const values = p[1][1];
if (!searchParams.facets) {
searchParams.facets = {};
}
searchParams.facets[facet] = values;
} else {
searchParams[p[0] as string] = p[1];
}
}
}
);
return searchParams;
}
buildSolrUrl() {
return this.construct()[0];
}
}
interface PaginationData {
numFound: number;
start: number;
pageSize: number;
}
type QueryEvent<T> = (object: T[], paging: PaginationData) => void;
type FacetEvent = (object: FacetValue[]) => void;
type ErrorEvent = (error: object) => void;
type GetEvent<T> = (object: T) => void;
type MoreLikeThisEvent<T> = (object: T[]) => void;
interface SolrGet<T> {
doGet: (id: string | number) => void;
onGet: (cb: GetEvent<T>) => void;
}
interface SolrUpdate {
doUpdate: (id: string | number, attr: string, value: string) => void;
}
interface SolrQuery<T> {
doQuery: (q: GenericSolrQuery) => void;
doExport: (q: GenericSolrQuery) => void;
onQuery: (cb: QueryEvent<T>) => void;
registerFacet: (facet: string[]) => (cb: FacetEvent) => void;
refine: (q: GenericSolrQuery) => SolrQuery<T>;
}
interface SolrHighlight<T> {
onHighlight: (cb: QueryEvent<T>) => void;
}
interface SolrMoreLikeThis<T> {
doMoreLikeThis: (id: string | number, props: MoreLikeThisProps) => void;
onMoreLikeThis: (cb: MoreLikeThisEvent<T>) => void;
}
interface SolrTransitions {
clearEvents: () => void;
getCoreConfig: () => SolrConfig;
getNamespace: () => string;
getCurrentParameters: () => SearchParams;
stateTransition: (v: SearchParams) => void;
}
type SolrSchemaFieldDefinition = {
type: string;
schema: string;
dynamicBase: string;
docs: number;
};
function mergeQuery(
q1?: GenericSolrQuery,
q2?: GenericSolrQuery
): GenericSolrQuery {
if (!q1) {
return q2 || { query: '*:* /* q2 */'};
}
if (!q2) {
return q1 || { query: '*:* /* q1 */'};
}
return {
query: '(' + q1.query + ') AND (' + q2.query + ')',
rows: q1.rows || q2.rows,
boost: q1.boost || q2.boost,
sort: q1.sort || q2.sort
};
}
type SolrSchemaDefinition = {
responseHeader: { status: number; QTime: number };
index: {
numDocs: number;
maxDoc: number,
deletedDocs: number,
indexHeapUsageBytes: number;
version: number;
segmentCount: number;
current: boolean;
hasDeletions: boolean;
directory: string;
segmentsFile: string;
segmentsFileSizeInBytes: number;
userData: {
commitTimeMSec: string;
commitCommandVer: string;
};
lastModified: string;
},
fields: { [key: string]: SolrSchemaFieldDefinition};
info: object;
};
interface SolrSchema {
getSchema: () => SolrSchemaDefinition;
}
// TODO - this needs a lot more definition to be useful
interface GenericSolrQuery {
query: string;
boost?: string;
sort?: string[];
rows?: number;
}
/**
* See: https://lucene.apache.org/solr/guide/6_6/highlighting.html
*/
interface HighlightQuery {
method?: 'unified' | 'original' | 'fastVector' | 'postings';
fields: string[];
query?: string;
qparser?: string;
requireFieldMatch?: boolean;
usePhraseHighlighter?: boolean;
highlightMultiTerm?: boolean;
snippets?: number;
fragsize?: number;
pre?: string;
post?: string;
encoder?: string;
maxAnalyzedChars?: number;
}
interface SolrConfig {
url: string;
core: string;
primaryKey: string;
defaultSearchFields: string[];
fields: string[];
pageSize: number;
prefix: string;
fq?: [string, string];
qt?: string;
}
// This needs to be a global in case you trigger
// a bunch of JSONP requests all at once
let requestId: number = 0;
interface MoreLikeThisProps {
fl: string[];
mintf: number;
mindf: number;
maxdf: number;
minwl: number;
maxwl: number;
maxqt: number;
maxntp: number;
boost: number;
qf: string[];
}
class SolrCore<T> implements SolrTransitions {
solrConfig: SolrConfig;
private events: {
query: QueryEvent<T>[],
error: ErrorEvent[],
get: GetEvent<T>[],
mlt: MoreLikeThisEvent<T>[],
facet: { [key: string]: FacetEvent[] };
};
private currentParameters: SearchParams = {};
private query?: GenericSolrQuery;
private getCache = {};
private mltCache = {};
private refinements: SolrCore<T>[] = [];
constructor(solrConfig: SolrConfig) {
this.solrConfig = solrConfig;
this.clearEvents();
// this.onGet = this.memoize(this.onGet);
}
clearEvents() {
this.events = {
query: [],
error: [],
get: [],
mlt: [],
facet: {}
};
if (_.keys(this.getCache).length > 100) {
this.getCache = {};
}
if (_.keys(this.mltCache).length > 100) {
this.mltCache = {};
}
}
getCoreConfig() {
return this.solrConfig;
}
onQuery(op: QueryEvent<T>) {
this.events.query.push(op);
}
refine(query: GenericSolrQuery) {
// TODO - decide if the events should proxy
// https://stackoverflow.com/questions/728360/how-do-i-correctly-clone-a-javascript-object#728694
const obj: SolrCore<T> = this;
if (null == obj || 'object' !== typeof obj) {
return obj;
}
const copy: SolrCore<T> = new SolrCore<T>(this.solrConfig);
for (let attr in obj) {
if (
obj.hasOwnProperty(attr) &&
attr !== 'events' &&
attr !== 'refinements'
) {
copy[attr] = obj[attr];
}
}
copy.clearEvents();
copy.query = mergeQuery(query, this.query);
this.refinements.push(copy);
return copy;
}
registerFacet(facetNames: string[]) {
const events = this.events.facet;
return function facetBind(cb: FacetEvent) {
// this works differently than the other event types because
// you may not know in advance what all the facets should be
facetNames.map(
(facetName) => {
events[facetName] =
(events[facetName] || []);
events[facetName].push(cb);
}
);
};
}
onError(op: ErrorEvent) {
this.events.error.push(op);
}
doMoreLikeThis(id: string | number, mltProps: MoreLikeThisProps) {
this.prefetchMoreLikeThis(id, mltProps, true);
}
getNamespace() {
return '';
}
prefetchMoreLikeThis(id: string | number, mltProps: MoreLikeThisProps, prefetch: boolean) {
const self = this;
if (!self.mltCache[id]) {
const callback = 'cb_' + requestId++;
const qb =
new SolrQueryBuilder(
() => new QueryBeingBuilt('', null)
).moreLikeThis(
'mlt', // TODO - configurable
self.solrConfig.primaryKey,
id,
mltProps
).fl(self.solrConfig.fields).jsonp(
callback
);
const url = self.solrConfig.url + self.solrConfig.core + '/' + qb.buildSolrUrl();
fetchJsonp(url, {
jsonpCallbackFunction: callback
}).then(
(data) => {
data.json().then(
(responseData) => {
const mlt = responseData.response.docs;
if (prefetch) {
self.events.mlt.map(
(event) => {
event(Immutable(mlt));
}
);
mlt.map(
(doc) => {
self.prefetchMoreLikeThis(
doc[this.solrConfig.primaryKey],
mltProps,
false
);
}
);
}
responseData.response.docs.map(
(doc) => self.getCache[id] = responseData.doc
);
self.mltCache[id] = responseData.response.docs;
}
).catch(
(error) => {
self.events.error.map(
(event) => event(error)
);
}
);
}
);
} else {
self.events.mlt.map(
(event) => {
event(Immutable(this.mltCache[id]));
}
);
}
}
onMoreLikeThis(op: (v: T[]) => void) {
this.events.mlt.push(op);
}
onGet(op: GetEvent<T>) {
this.events.get.push(op);
}
doGet(id: string | number) {
const self = this;
const callback = 'cb_' + requestId++;
const qb =
new SolrQueryBuilder(() => new QueryBeingBuilt('', null)).get(
id
).fl(this.solrConfig.fields).jsonp(
callback
);
const url = this.solrConfig.url + this.solrConfig.core + '/' + qb.buildSolrUrl();
if (!this.getCache[id]) {
fetchJsonp(url, {
jsonpCallbackFunction: callback
}).then(
(data) => {
data.json().then(
(responseData) => {
self.events.get.map(
(event) => event(Immutable(responseData.doc))
);
this.getCache[id] = responseData.doc;
}
).catch(
(error) => {
self.events.error.map(
(event) => event(error)
);
}
);
}
);
} else {
self.events.get.map(
(event) => event(Immutable(this.getCache[id]))
);
}
}
doQuery(desiredQuery: GenericSolrQuery, cb?: (qb: SolrQueryBuilder<{}>) => SolrQueryBuilder<{}>) {
const self = this;
// this lets you make one master datastore and refine off it,
// and if no controls need it, it won't be run
if (self.events.query.length > 0) {
const callback = 'cb_' + (requestId++);
// this lets the provided query override rows/boost
const query: GenericSolrQuery =
mergeQuery(desiredQuery, this.query);
let qb =
new SolrQueryBuilder(
() => new QueryBeingBuilt('', null),
);
qb = qb.select().q(
this.solrConfig.defaultSearchFields,
query.query
);
if (this.solrConfig.qt) {
qb = qb.qt(this.solrConfig.qt);
}
qb = qb.fl(this.solrConfig.fields).rows(
query.rows || this.solrConfig.pageSize
);
if (this.solrConfig.fq) {
qb = qb.fq(this.solrConfig.fq[0], [this.solrConfig.fq[1]]);
}
if (query.boost) {
qb = qb.bq(query.boost);
}
if (query.sort) {
qb = qb.sort(query.sort);
}
_.map(
this.events.facet,
(v, k) => {
qb = qb.requestFacet(k);
}
);
if (cb) {
qb = cb(qb);
}
qb = qb.jsonp(
callback
);
const url = this.solrConfig.url + this.solrConfig.core + '/' + qb.buildSolrUrl();
fetchJsonp(url, {
jsonpCallbackFunction: callback,
timeout: 30000
}).then(
(data) => {
this.currentParameters = qb.buildCurrentParameters();
data.json().then(
(responseData) => {
self.events.query.map(
(event) =>
event(
Immutable(responseData.response.docs),
Immutable({
numFound: responseData.response.numFound,
start: responseData.response.start,
pageSize: query.rows || 10
})
)
);
const facetCounts = responseData.facet_counts;
if (facetCounts) {
const facetFields = facetCounts.facet_fields;
if (facetFields) {
_.map(
self.events.facet,
(events, k) => {
if (facetFields[k]) {
const previousValues = (this.currentParameters.facets || {})[k];
const facetLabels = facetFields[k].filter( (v, i) => i % 2 === 0 );
const facetLabelCount = facetFields[k].filter( (v, i) => i % 2 === 1 );
const facetSelections = facetLabels.map(
(value) => _.includes(previousValues, value)
);
events.map(
(event) => {
event(
_.zipWith(facetLabels, facetLabelCount, facetSelections).map(
(facetData: [string, number, boolean]) => {
return {
value: facetData[0],
count: facetData[1],
checked: facetData[2]
};
}
));
}
);
}
}
);
}
}
}
).catch(
(error) => {
self.events.error.map(
(event) => event(error)
);
}
);
}
);
}
this.refinements.map(
(refinement) => refinement.doQuery(desiredQuery, cb)
);
}
doUpdate(id: string | number, attr: string, value: string) {
const self = this;
const op = {
id: id,
};
op[attr] = { set: value };
const url = this.solrConfig.url + this.solrConfig.core + '/' + 'update?commit=true';
const http = new XMLHttpRequest();
const params = JSON.stringify(op);
http.open('POST', url, true);
http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
delete this.getCache[id];
http.onreadystatechange = function() {
if (http.readyState === 4) {
// trigger UI re-render - don't care if success or failure
// because we might have succeeded but got a CORS error
self.doGet(id);
}
};
http.send(params);
}
doExport() {
const query = this.getCurrentParameters();
let qb =
new SolrQueryBuilder(
() => new QueryBeingBuilt('', null),
).select().q(
this.solrConfig.defaultSearchFields,
query.query || '*'
).fl(this.solrConfig.fields)
.rows(
2147483647
);
if (this.solrConfig.fq) {
qb = qb.fq(this.solrConfig.fq[0], [this.solrConfig.fq[1]]);
}
_.map(
this.events.facet,
(v, k) => {
qb = qb.requestFacet(k);
}
);
qb = qb.export();
const url = this.solrConfig.url + this.solrConfig.core + '/' + qb.buildSolrUrl();
window.open(url, '_blank');
}
next(op: (event: SolrQueryBuilder<T>) => SolrQueryBuilder<T>) {
const self = this;
const qb =
op(
new SolrQueryBuilder<T>(
() => new QueryBeingBuilt('', null)
)
).fl(self.solrConfig.fields);
const url = self.solrConfig.url + self.solrConfig.core + '/select?' + qb.buildSolrUrl();
fetchJsonp(url).then(
(data) => {
data.json().catch(
(error) => {
self.events.error.map(
(event) => event(error)
);
}
).then(
(responseData) => {
self.events.get.map(
(event) => event(Immutable(responseData.response.docs))
);
}
);
}
);
}
stateTransition(newState: SearchParams) {
if (newState.type === 'QUERY') {
this.doQuery(
{
rows: this.solrConfig.pageSize,
query: newState.query || '*',
boost: newState.boost,
sort: newState.sort
},
(qb: SolrQueryBuilder<{}>) => {
let response = qb.start(
newState.start || 0
);
_.map(
newState.facets,
(values: string[], k: string) => {
if (values.length > 0) {
response =
response.fq(
k, values
);
}
}
);
return response;
}
);
} else {
throw 'INVALID STATE TRANSITION: ' + JSON.stringify(newState);
}
}
getCurrentParameters(): SearchParams {
return this.currentParameters;
}
}
// TODO: I want a way to auto-generate these from Solr management APIs
// TODO: This thing should provide some reflection capability so the
// auto-registered version can be used to bind controls through
// a properties picker UI
class DataStore {
cores: { [ keys: string ]: SolrCore<object> } = {};
clearEvents() {
_.map(
this.cores,
(v: SolrCore<object>, k) => v.clearEvents()
);
}
registerCore<T extends object>(config: SolrConfig): SolrCore<T> {
// Check if this exists - Solr URL + core should be enough
let key = config.url;
if (!_.endsWith(key, '/')) {
key += '/';
}
key += config.core;
if (!this.cores[key]) {
this.cores[key] = new SolrCore<T>(config);
}
return (this.cores[key] as SolrCore<T>);
}
}
class AutoConfiguredDataStore extends DataStore {
private core: SolrCore<object> & SolrGet<object> & SolrQuery<object>;
private facets: string[];
private fields: string[];
getCore() {
return this.core;
}
getFacets() {
return this.facets;
}
getFields() {
return this.fields;
}
autoconfigure<T extends object>(
config: SolrConfig,
complete: () => void,
useFacet: (facet: string) => boolean
): void {
const callback = 'cb_autoconfigure_' + config.core;
useFacet = useFacet || ((facet: string) => true);
let qb =
new SolrQueryBuilder(
() => new QueryBeingBuilt('', null),
).schema().jsonp(
callback
);
const url = config.url + config.core + '/' + qb.buildSolrUrl();
fetchJsonp(url, {
jsonpCallbackFunction: callback
}).then(
(data) => {
data.json().then(
(responseData: SolrSchemaDefinition) => {
// TODO cache aggressively
const fields =
_.toPairs(responseData.fields).filter(
([fieldName, fieldDef]) => {
return fieldDef.docs > 0 && fieldDef.schema.match(/I.S............../);
}
).map(
([fieldName, fieldDef]) => fieldName
);
const defaultSearchFields =
_.toPairs(responseData.fields).filter(
([fieldName, fieldDef]) => {
return fieldDef.docs > 0 && fieldDef.schema.match(/I................/);
}
).map(
([fieldName, fieldDef]) => fieldName
);
const coreConfig = _.extend(
{},
{
primaryKey: 'id',
fields: fields,
defaultSearchFields: defaultSearchFields,
pageSize: 50,
prefix: config.core
},
config
);
this.core = new SolrCore<T>(coreConfig);
this.fields = fields;
this.facets = fields.filter(useFacet);
this.core.registerFacet(this.facets)(_.identity);
complete();
}
);
}
);
}
}
type SingleComponent<T> =
(data: T, index?: number) => object | null | undefined;
export {
MoreLikeThisProps,
ErrorEvent,
UrlParams,
QueryParam,
HighlightQuery,
NamespacedUrlParam,
UrlFragment,
PaginationData,
SavedSearch,
SearchParams,
QueryBeingBuilt,
SolrQueryBuilder,
SingleComponent,
MoreLikeThisEvent,
GetEvent,
GenericSolrQuery,
QueryEvent,
FacetEvent,
SolrConfig,
SolrGet,
SolrUpdate,
SolrMoreLikeThis,
SolrQuery,
SolrHighlight,
SolrTransitions,
SolrCore,
SolrSchemaFieldDefinition,
SolrSchemaDefinition,
SolrSchema,
DataStore,
AutoConfiguredDataStore
};