From f2a544e64cd81bb4e4e7fef158680234b4ea84bb Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Thu, 29 Nov 2018 12:43:45 -0800 Subject: [PATCH] Use algolia for search --- src/App.js | 3 ++- src/algolia.js | 27 +++++++++++++++++++++++++++ src/redux/actionCreators.js | 4 ++++ src/redux/epics.js | 26 ++++++++++++++++++++++++++ src/redux/selectors.js | 8 ++------ src/redux/store.js | 19 +++++++++++++++---- 6 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 src/algolia.js create mode 100644 src/redux/epics.js diff --git a/src/App.js b/src/App.js index f9c87ab8..18de495a 100644 --- a/src/App.js +++ b/src/App.js @@ -7,6 +7,7 @@ import Skin from "./Skin"; import FocusedSkin from "./FocusedSkin"; import * as Utils from "./utils"; import * as Selectors from "./redux/selectors"; +import * as Actions from "./redux/actionCreators"; import { SKIN_WIDTH, SKIN_HEIGHT, SKIN_RATIO } from "./constants"; const OVERSCAN_ROWS_LEADING = 10; @@ -166,7 +167,7 @@ const mapDispatchToProps = dispatch => ({ dispatch({ type: "SELECT_SKIN", hash, position }); }, setSearchQuery(query) { - dispatch({ type: "SET_SEARCH_QUERY", query }); + dispatch(Actions.searchQueryChanged(query)); } }); export default connect( diff --git a/src/algolia.js b/src/algolia.js new file mode 100644 index 00000000..918a7350 --- /dev/null +++ b/src/algolia.js @@ -0,0 +1,27 @@ +import algoliasearch from "algoliasearch"; +var client = algoliasearch("HQ9I5Z6IM5", "6466695ec3f624a5fccf46ec49680e51"); + +var index = client.initIndex("Skins"); + +export function search(query) { + return new Promise((resolve, reject) => { + index.search( + { + query, + attributes: ["objectID"], + attributesToHighlight: [], + hitsPerPage: 1000, + // https://www.algolia.com/doc/api-reference/api-parameters/typoTolerance/ + // min: Retrieve records with the smallest number of typos. + typoTolerance: "min" + }, + (err, content) => { + if (err != null) { + reject(err); + return; + } + resolve(content); + } + ); + }); +} diff --git a/src/redux/actionCreators.js b/src/redux/actionCreators.js index e28684aa..4bd0ccac 100644 --- a/src/redux/actionCreators.js +++ b/src/redux/actionCreators.js @@ -1,3 +1,7 @@ export function closeModal() { return { type: "CLOSE_MODAL" }; } + +export function searchQueryChanged(query) { + return { type: "SEARCH_QUERY_CHANGED", query }; +} diff --git a/src/redux/epics.js b/src/redux/epics.js new file mode 100644 index 00000000..40b16159 --- /dev/null +++ b/src/redux/epics.js @@ -0,0 +1,26 @@ +import { combineEpics } from "redux-observable"; +import { of, from } from "rxjs"; +import { filter, switchMap, map } from "rxjs/operators"; +import { search } from "../algolia"; + +const searchEpic = actions => + actions.pipe( + filter(action => action.type === "SEARCH_QUERY_CHANGED"), + switchMap(({ query }) => { + if (query.length === 0) { + return of({ + type: "GOT_NEW_MATCHING_HASHES", + matchingHashes: null + }); + } + + return from(search(query)).pipe( + map(content => { + const matchingHashes = new Set(content.hits.map(hit => hit.objectID)); + return { type: "GOT_NEW_MATCHING_HASHES", matchingHashes }; + }) + ); + }) + ); + +export default combineEpics(searchEpic); diff --git a/src/redux/selectors.js b/src/redux/selectors.js index 03c08918..72f5af8b 100644 --- a/src/redux/selectors.js +++ b/src/redux/selectors.js @@ -22,14 +22,10 @@ export function getSearchQuery(state) { export function getMatchingSkinHashes(state) { const hashes = Object.keys(skins); const searchQuery = getSearchQuery(state); - if (searchQuery == null) { + if (searchQuery == null || state.matchingHashes == null) { return hashes; } - const normalizedSearchQuery = searchQuery.toLowerCase(); - return hashes.filter(hash => { - const { fileName } = skins[hash]; - return fileName.toLowerCase().includes(normalizedSearchQuery); - }); + return hashes.filter(hash => state.matchingHashes.has(hash)); } export function getUrl(state) { diff --git a/src/redux/store.js b/src/redux/store.js index 91ef4049..d6880a91 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -1,10 +1,13 @@ -import { createStore as createReduxStore } from "redux"; +import { createStore as createReduxStore, applyMiddleware } from "redux"; +import { createEpicMiddleware } from "redux-observable"; import * as Selectors from "./selectors"; +import rootEpic from "./epics"; const defaultState = { searchQuery: null, selectedSkinHash: null, - selectedSkinPosition: null + selectedSkinPosition: null, + matchingHashes: null }; function reducer(state = defaultState, action) { @@ -35,17 +38,25 @@ function reducer(state = defaultState, action) { }; } return { ...defaultState, searchQuery }; - case "SET_SEARCH_QUERY": + case "SEARCH_QUERY_CHANGED": return { ...state, searchQuery: action.query }; + case "GOT_NEW_MATCHING_HASHES": + return { + ...state, + matchingHashes: action.matchingHashes + }; default: return state; } } export function createStore() { - const store = createReduxStore(reducer); + const epicMiddleware = createEpicMiddleware(); + + const store = createReduxStore(reducer, applyMiddleware(epicMiddleware)); + epicMiddleware.run(rootEpic); let lastUrl = null; store.subscribe(() => { const state = store.getState();