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

Root-198: add search-as-you-type suggestion query functionality #7

Merged
merged 16 commits into from
Mar 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
4,229 changes: 2,241 additions & 1,988 deletions build/index.js

Large diffs are not rendered by default.

22 changes: 21 additions & 1 deletion src/api/server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import xhr from "xhr";
import solrQuery from "./solr-query";
import solrQuery, { solrSuggestQuery } from "./solr-query";

const MAX_INT = 2147483647;

Expand Down Expand Up @@ -33,6 +33,26 @@ server.submitQuery = (query, callback) => {
});
};

server.submitSuggestQuery = (suggestQuery, callback) => {
callback({type: "SET_SUGGESTIONS_PENDING"});

server.performXhr({
url: suggestQuery.url,
data: solrSuggestQuery(suggestQuery),
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded",
...(suggestQuery.userpass ? {"Authorization": "Basic " + suggestQuery.userpass} : {}),
}
}, (err, resp) => {
if (resp.statusCode >= 200 && resp.statusCode < 300) {
callback({type: "SET_SUGGESTIONS", data: JSON.parse(resp.body)});
} else {
console.log("Server error: ", resp.statusCode);
}
});
};

server.fetchCsv = (query, callback) => {
server.performXhr({
url: query.url,
Expand Down
41 changes: 41 additions & 0 deletions src/api/solr-client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import queryReducer from "../reducers/query";
import resultReducer from "../reducers/results";
import suggestionReducer from "../reducers/suggestions";
import suggestQueryReducer from "../reducers/suggestQuery";
// import { submitQuery, fetchCsv } from "./server";
import server from "./server";

Expand Down Expand Up @@ -84,6 +86,39 @@ class SolrClient {
});
}

setSuggestQuery(query, autocomplete, value) {
const {searchFields} = query;
// Add the current text field value to the searchFields array.
const newFields = searchFields
.map((searchField) => searchField.field === query.mainQueryField ? {...searchField, value: value} : searchField);
const payload = {
type: "SET_SUGGEST_QUERY",
suggestQuery: {
searchFields: newFields,
sortFields: query.sortFields,
filters: query.filters,
userpass: query.userpass,
mainQueryField: query.mainQueryField,
start: 0,
mode: autocomplete.mode,
url: autocomplete.url,
rows: autocomplete.suggestionRows || 5,
appendWildcard: autocomplete.appendWildcard || false,
value
}
};
this.sendSuggestQuery(suggestQueryReducer(this.state.suggestQuery, payload));
}

sendSuggestQuery(suggestQuery = this.state.suggestQuery) {
this.state.suggestQuery = suggestQuery;
server.submitSuggestQuery(suggestQuery, (action) => {
this.state.suggestions = suggestionReducer(this.state.suggestions, action);
this.state.suggestQuery = suggestQueryReducer(this.state.suggestQuery, action);
this.onChange(this.state, this.getHandlers());
});
}

sendNextCursorQuery() {
server.submitQuery(this.state.query, (action) => {
this.state.results = resultReducer(this.state.results, {
Expand Down Expand Up @@ -132,6 +167,11 @@ class SolrClient {
const payload = {type: "SET_SEARCH_FIELDS", newFields: newFields};

this.sendQuery(queryReducer(this.state.query, payload));
// Enable the the autosuggest input to be cleared cleared
// but only if autcomplete has been configured.
if (Object.hasOwnProperty.call(this.state, "suggestQuery")) {
this.state.suggestQuery = suggestQueryReducer(this.state.suggestQuery, payload);
}
}

setFacetSort(field, value) {
Expand Down Expand Up @@ -172,6 +212,7 @@ class SolrClient {

getHandlers() {
return {
onTextInputChange: this.setSuggestQuery.bind(this),
onSortFieldChange: this.setSortFieldValue.bind(this),
onSearchFieldChange: this.setSearchFieldValue.bind(this),
onFacetSortChange: this.setFacetSort.bind(this),
Expand Down
85 changes: 65 additions & 20 deletions src/api/solr-query.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,30 +84,18 @@ const buildFormat = (format) => Object.keys(format)
.join("&");

const buildMainQuery = (fields, mainQueryField) => {
let qs = "q=";
let params = fields.filter(function (searchField) {
return searchField.field === mainQueryField;
}).map(function (searchField) {
return fieldToQueryFilter(searchField);
return searchField.value;
});
// If there are multiple main query fields, join them.
if (params.length > 1) {
qs += params.join("&");
}
// If there is only one main query field, add only it.
else if (params.length === 1) {
if (params[0] !== null) {
qs += params[0];
} else {
// If query field exists but is null send the wildcard query.
qs += "*:*";
}
}
// If there are no main query fields, send the wildcard query.
else {
qs += "*:*";
// Add value of the mainQueryField to the q param, if there is one.
if (params[0]) {
return `q=${params[0]}`;
}
return qs;

// If query field exists but is null/empty/undefined send the wildcard query.
return "q=*:*";
};

const buildHighlight = (highlight) => {
Expand Down Expand Up @@ -185,6 +173,61 @@ const solrQuery = (query, format = {wt: "json"}) => {

export default solrQuery;

const buildSuggestQuery = (fields, mainQueryField, appendWildcard) => {
let qs = "q=";
let params = fields.filter(function (searchField) {
return searchField.field === mainQueryField;
}).map(function (searchField) {
// Remove spaces on either end of the value.
const trimmed = searchField.value.trim();
// One method of supporting search-as-you-type is to append a wildcard '*'
// to match zero or more additional characters at the end of the users search term.
// @see: https://lucene.apache.org/solr/guide/6_6/the-standard-query-parser.html#TheStandardQueryParser-WildcardSearches
// @see: https://opensourceconnections.com/blog/2013/06/07/search-as-you-type-with-solr/
if (appendWildcard && trimmed.length > 0) {
// Split into word chunks.
const words = trimmed.split(" ");
// If there are multiple chunks, join them with "+", repeat the last word + append "*".
if (words.length > 1) {
return `${words.join("+")}+${words.pop()}*`;
}
// If there is only 1 word, repeat it an append "*".
return `${words}+${words}*`;
}
// If we are not supposed to append a wildcard, just return the value.
// ngram tokens/filters should be set up in solr config for
// the autocomplete endpoint request handler.
return trimmed;
});

if (params[0]) {
qs += params[0];
}

return qs;
};

const solrSuggestQuery = (suggestQuery, format = {wt: "json"}) => {
const {
rows,
searchFields,
filters,
appendWildcard,
} = suggestQuery;

const mainQueryField = Object.hasOwnProperty.call(suggestQuery, "mainQueryField") ? suggestQuery.mainQueryField : null;

const queryFilters = (filters || []).map((filter) => ({...filter, type: filter.type || "text"}));
const mainQuery = buildSuggestQuery(searchFields.concat(queryFilters), mainQueryField, appendWildcard);
const queryParams = buildQuery(searchFields.concat(queryFilters), mainQueryField);
const facetFieldParam = facetFields(searchFields);

return mainQuery +
`${queryParams.length > 0 ? `&${queryParams}` : ""}` +
`${facetFieldParam.length > 0 ? `&${facetFieldParam}` : ""}` +
`&rows=${rows}` +
`&${buildFormat(format)}`;
};

export {
rangeFacetToQueryFilter,
Expand All @@ -194,9 +237,11 @@ export {
fieldToQueryFilter,
buildQuery,
buildMainQuery,
buildSuggestQuery,
buildHighlight,
facetFields,
facetSorts,
buildSort,
solrQuery
solrQuery,
solrSuggestQuery
};
35 changes: 35 additions & 0 deletions src/reducers/suggestQuery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const initialState = {};

const setSuggestQuery = (state, action) => {
return {
...action.suggestQuery
};
};

const setSuggestQueryField = (state, action) => {
// Clear the suggestQueryField data only if the search field has been cleared.
if (action.newFields.filter(field => field.field === "tm_rendered_item" && field.value ==="").length) {
return Object.assign({},
...state,
{
suggestQuery: {
value: ""
}
},
);
}
return {
...state
};
};

export default function (state = initialState, action) {
switch (action.type) {
case "SET_SUGGEST_QUERY":
return setSuggestQuery(state, action);
case "SET_SEARCH_FIELDS":
return setSuggestQueryField(state, action);
}

return state;
}
22 changes: 22 additions & 0 deletions src/reducers/suggestions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const initialState = {
suggestionsPending: false,
docs: []
};

export default function (state = initialState, action) {
switch (action.type) {
case "SET_SUGGESTIONS":
return {
...state,
docs: action.data.response ? action.data.response.docs : [],
suggestionsPending: false
};

case "SET_SUGGESTIONS_PENDING":
return {
...state, suggestionsPending: true
};
}

return state;
}
Loading