From 68a28c5524e6fb4c1ccd4d98082197c9e85e1b12 Mon Sep 17 00:00:00 2001 From: Raff Viglianti Date: Thu, 7 Nov 2024 17:05:22 -0500 Subject: [PATCH] initial static search --- gatsby-config.ts | 107 +++++++++++++++++++++++++++++- package-lock.json | 100 ++++++++++++++++++++++++++++ package.json | 3 + src/components/FacetAccordion.tsx | 5 +- src/components/Header.tsx | 3 - src/pages/search.tsx | 72 ++++++++++++++++---- 6 files changed, 272 insertions(+), 18 deletions(-) diff --git a/gatsby-config.ts b/gatsby-config.ts index bb1f25834..3a145c445 100644 --- a/gatsby-config.ts +++ b/gatsby-config.ts @@ -72,7 +72,112 @@ const config: GatsbyConfig = { }, ] } - } + }, + { + resolve: 'gatsby-plugin-local-search', + options: { + // A unique name for the search index. This should be descriptive of + // what the index contains. This is required. + name: 'collections', + + // Set the search engine to create the index. This is required. + // The following engines are supported: flexsearch, lunr + engine: 'flexsearch', + + // Provide options to the engine. This is optional and only recommended + // for advanced users. + // + // Note: Only the flexsearch engine supports options. + engineOptions: 'speed', + + // GraphQL query used to fetch all data for the search index. This is + // required. + query: ` + { + allAirtableScdItems( + filter: {data: {scd_publish_status: {nin: ["duplicate-record-do-not-display", "do-not-display"]}}} + ) { + nodes { + data { + collection_id + scd_publish_status + record_type + collection_content_category + collection_title + collection_description + collection_holder_category + collection_holder_name + collection_holder_city + collection_holder_state + collection_holder_country + content_types + dates + extent + historical_relevance + subjects + creators + physical_formats + access_statement + finding_aid_url + collection_catalog_url + supporting_documentation + languages + inventory_description + } + } + } + } + `, + + // Field used as the reference value for each document. + // Default: 'id'. + ref: 'collection_id', + + // List of keys to index. The values of the keys are taken from the + // normalizer function below. + // Default: all fields + // index: ['title', 'body'], + + // List of keys to store and make available in your UI. The values of + // the keys are taken from the normalizer function below. + // Default: all fields + store: ['collection_id'], + + // Function used to map the result from the GraphQL query. This should + // return an array of items to index in the form of flat objects + // containing properties to index. The objects must contain the `ref` + // field above (default: 'id'). This is required. + normalizer: ({ data }: {data: Queries.qCollectionsQuery}) => + data.allAirtableScdItems.nodes.map((node) => { + const d = node.data! + return ({ + collection_id: d.collection_id, + scd_publish_status: d.scd_publish_status, + record_type: d.record_type, + collection_content_category: d.collection_content_category, + collection_title: d.collection_title, + collection_description: d.collection_description, + collection_holder_category: d.collection_holder_category, + collection_holder_name: d.collection_holder_name, + collection_holder_city: d.collection_holder_city, + collection_holder_state: d.collection_holder_state, + collection_holder_country: d.collection_holder_country, + content_types: d.content_types, + dates: d.dates, + extent: d.extent, + historical_relevance: d.historical_relevance, + subjects: d.subjects, + creators: d.creators, + physical_formats: d.physical_formats, + access_statement: d.access_statement, + finding_aid_url: d.finding_aid_url, + collection_catalog_url: d.collection_catalog_url, + supporting_documentation: d.supporting_documentation, + languages: d.languages, + inventory_description: d.inventory_description + })}), + }, + }, ] }; diff --git a/package-lock.json b/package-lock.json index 5718654d6..1fa9bcad0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "1.0.0", "dependencies": { "autoprefixer": "^10.4.20", + "flexsearch": "^0.7.43", "gatsby": "^5.13.7", "gatsby-plugin-image": "^3.13.1", + "gatsby-plugin-local-search": "^2.0.1", "gatsby-plugin-postcss": "^6.13.1", "gatsby-plugin-sharp": "^5.13.1", "gatsby-source-airtable": "^2.4.3", @@ -20,6 +22,7 @@ "postcss": "^8.4.47", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-use-flexsearch": "^0.1.1", "tailwindcss": "^3.4.14" }, "devDependencies": { @@ -8362,6 +8365,12 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==" }, + "node_modules/flexsearch": { + "version": "0.7.43", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.7.43.tgz", + "integrity": "sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==", + "license": "Apache-2.0" + }, "node_modules/follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", @@ -9032,6 +9041,30 @@ } } }, + "node_modules/gatsby-plugin-local-search": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/gatsby-plugin-local-search/-/gatsby-plugin-local-search-2.0.1.tgz", + "integrity": "sha512-qrApdH2IYfHL+dSmcwSzhDPVxlkt13N0IfEkKxfWf0gITmBwObOJBYAMnYiYUmP0dpYmSV9anJE//SLZBSsisA==", + "license": "MIT", + "dependencies": { + "flexsearch": "^0.6.32", + "lodash": "^4.17.19", + "lunr": "^2.3.8", + "pascal-case": "^3.1.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "gatsby": ">= 2.20.0" + } + }, + "node_modules/gatsby-plugin-local-search/node_modules/flexsearch": { + "version": "0.6.32", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.6.32.tgz", + "integrity": "sha512-EF1BWkhwoeLtbIlDbY/vDSLBen/E5l/f1Vg7iX5CDymQCamcx1vhlc3tIZxIDplPjgi0jhG37c67idFbjg+v+Q==", + "license": "Apache-2.0" + }, "node_modules/gatsby-plugin-page-creator": { "version": "5.13.1", "resolved": "https://registry.npmjs.org/gatsby-plugin-page-creator/-/gatsby-plugin-page-creator-5.13.1.tgz", @@ -11673,6 +11706,12 @@ "es5-ext": "~0.10.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "license": "MIT" + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -14683,6 +14722,24 @@ "node": ">=0.4.0" } }, + "node_modules/react-use-flexsearch": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/react-use-flexsearch/-/react-use-flexsearch-0.1.1.tgz", + "integrity": "sha512-UDRDB26HPcAo0gXNkUYYkcjoYCW4FSWr7Ich4adgVr7bqefJG7fnjlcqnwsKQkbZuteRLYzzox+1FKRTt3Z5vg==", + "license": "MIT", + "dependencies": { + "flexsearch": "^0.6.22" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-use-flexsearch/node_modules/flexsearch": { + "version": "0.6.32", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.6.32.tgz", + "integrity": "sha512-EF1BWkhwoeLtbIlDbY/vDSLBen/E5l/f1Vg7iX5CDymQCamcx1vhlc3tIZxIDplPjgi0jhG37c67idFbjg+v+Q==", + "license": "Apache-2.0" + }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -24017,6 +24074,11 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==" }, + "flexsearch": { + "version": "0.7.43", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.7.43.tgz", + "integrity": "sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==" + }, "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", @@ -24664,6 +24726,24 @@ "prop-types": "^15.8.1" } }, + "gatsby-plugin-local-search": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/gatsby-plugin-local-search/-/gatsby-plugin-local-search-2.0.1.tgz", + "integrity": "sha512-qrApdH2IYfHL+dSmcwSzhDPVxlkt13N0IfEkKxfWf0gITmBwObOJBYAMnYiYUmP0dpYmSV9anJE//SLZBSsisA==", + "requires": { + "flexsearch": "^0.6.32", + "lodash": "^4.17.19", + "lunr": "^2.3.8", + "pascal-case": "^3.1.1" + }, + "dependencies": { + "flexsearch": { + "version": "0.6.32", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.6.32.tgz", + "integrity": "sha512-EF1BWkhwoeLtbIlDbY/vDSLBen/E5l/f1Vg7iX5CDymQCamcx1vhlc3tIZxIDplPjgi0jhG37c67idFbjg+v+Q==" + } + } + }, "gatsby-plugin-page-creator": { "version": "5.13.1", "resolved": "https://registry.npmjs.org/gatsby-plugin-page-creator/-/gatsby-plugin-page-creator-5.13.1.tgz", @@ -26328,6 +26408,11 @@ "es5-ext": "~0.10.2" } }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -28349,6 +28434,21 @@ } } }, + "react-use-flexsearch": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/react-use-flexsearch/-/react-use-flexsearch-0.1.1.tgz", + "integrity": "sha512-UDRDB26HPcAo0gXNkUYYkcjoYCW4FSWr7Ich4adgVr7bqefJG7fnjlcqnwsKQkbZuteRLYzzox+1FKRTt3Z5vg==", + "requires": { + "flexsearch": "^0.6.22" + }, + "dependencies": { + "flexsearch": { + "version": "0.6.32", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.6.32.tgz", + "integrity": "sha512-EF1BWkhwoeLtbIlDbY/vDSLBen/E5l/f1Vg7iX5CDymQCamcx1vhlc3tIZxIDplPjgi0jhG37c67idFbjg+v+Q==" + } + } + }, "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", diff --git a/package.json b/package.json index 482141f75..626add794 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,10 @@ }, "dependencies": { "autoprefixer": "^10.4.20", + "flexsearch": "^0.7.43", "gatsby": "^5.13.7", "gatsby-plugin-image": "^3.13.1", + "gatsby-plugin-local-search": "^2.0.1", "gatsby-plugin-postcss": "^6.13.1", "gatsby-plugin-sharp": "^5.13.1", "gatsby-source-airtable": "^2.4.3", @@ -29,6 +31,7 @@ "postcss": "^8.4.47", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-use-flexsearch": "^0.1.1", "tailwindcss": "^3.4.14" }, "devDependencies": { diff --git a/src/components/FacetAccordion.tsx b/src/components/FacetAccordion.tsx index 5c47eaad7..359352b15 100644 --- a/src/components/FacetAccordion.tsx +++ b/src/components/FacetAccordion.tsx @@ -79,7 +79,10 @@ const FacetAccordion: React.FC = ({lab return
  • {active - ? <>{item.label} handleRemoveFacet(e, item)} href="#" className="text-gray-500 font-bold pl-2 text-[0.6rem] align-bottom hover:text-rose-800">[remove] + ? <> + handleRemoveFacet(e, item)} href="#" className="text-gray-500 font-bold pr-2 text-[0.6rem] align-bottom hover:text-rose-800">[remove] + {item.label} + : handleItemClick(e, item)}>{item.label} } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 53c2d4ce1..4357f85cd 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -17,9 +17,6 @@ const Header = () => { -
    - {/* This will hold the search bar */} -
    ) } diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 8fafa05a4..5fa31daca 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -1,5 +1,6 @@ import * as React from "react" import { graphql, Link, type HeadFC, type PageProps } from "gatsby" +import { useFlexSearch } from 'react-use-flexsearch' import Layout from "../components/Layout" import Button from "../components/Button" import Pagination from "../components/Pagination" @@ -75,9 +76,10 @@ const SearchPage: React.FC = ({data}) => { const [resultsPerPage, setResultsPerPage] = React.useState(20) const [sortOrder, setSortOrder] = React.useState("asc"); const [facets, setFacets] = React.useState([]); - - const facetsFromAirTable = (data as Queries.qSearchPageQuery).allAirtableScdFacets.nodes || [] - const fieldsFromAirTable = (data as Queries.qSearchPageQuery).allAirtableScdFields.nodes || [] + + const d = data as Queries.qSearchPageQuery + const facetsFromAirTable = d.allAirtableScdFacets.nodes || [] + const fieldsFromAirTable = d.allAirtableScdFields.nodes || [] const facetData = facetsFromAirTable.map(f => [f.data!.scd_field_label_revised, f.data!.Fields!.replace(/-/g, '_')] as [string, string] @@ -98,8 +100,15 @@ const SearchPage: React.FC = ({data}) => { return false }) : results; + const [searchQuery, setSearchQuery] = React.useState() + const searchData = useFlexSearch(searchQuery, d.localSearchCollections.index, d.localSearchCollections.store) + const searchResultIds = searchData.map((item: {collection_id: string}) => item.collection_id) + + // Apply search results + const searchResults = searchQuery ? facetedResults.filter(r => searchResultIds.includes(r.data!.collection_id)) : facetedResults; + // sort then paginate - (facetedResults as DeepWritable).sort((a, b) => { + (searchResults as DeepWritable).sort((a, b) => { if (sortOrder === "asc") { return a.data!.collection_title!.localeCompare(b.data!.collection_title!) } else { @@ -107,15 +116,20 @@ const SearchPage: React.FC = ({data}) => { } }) - const totalPages = Math.ceil(facetedResults.length / resultsPerPage) + const totalPages = Math.ceil(searchResults.length / resultsPerPage) const startIndex = (currentPage - 1) * resultsPerPage - const endIndex = Math.min(startIndex + resultsPerPage, facetedResults.length) + const endIndex = Math.min(startIndex + resultsPerPage, searchResults.length) - const paginatedResults = facetedResults.slice(startIndex, endIndex) + const paginatedResults = searchResults.slice(startIndex, endIndex) // Update component with existing query parameters on load React.useEffect(() => { const urlParams = new URLSearchParams(window.location.search) + // Search Query + const q = urlParams.get("q") + if (q) { + setSearchQuery(q) + } // Facets const newFacets: Facet[] = [] for (const facet of facetFields.keys()) { @@ -244,13 +258,13 @@ const SearchPage: React.FC = ({data}) => { history.pushState(null, '', '?' + urlParams.toString()); } } - return
    {prev}{startIndex+1} – {endIndex} of {facetedResults.length}{next}
    + return endIndex > 0 ?
    {prev}{startIndex+1} – {endIndex} of {searchResults.length}{next}
    : "" } // Function to extract facets const extractFacet = (field: string) => { const f = field as keyof Queries.qSearchPageQuery["allAirtableScdItems"]["nodes"][0]["data"] - return results.reduce((acc: { label: string; count: number, action: () => null }[], item) => { + return results.reduce((acc: { label: string; count: number, inSearchCount: number, action: () => null }[], item) => { // If facet value is not present, skip if (!item.data![f]) return acc; const values = Array.isArray(item.data![f]) ? item.data![f] : [item.data![f] as string] @@ -258,13 +272,16 @@ const SearchPage: React.FC = ({data}) => { values.forEach(type => { // Find if the facet value is already in the accumulator const existing = acc.find(entry => entry.label === type); + const inSearch = !searchQuery ? true : searchResultIds.includes(item.data!.collection_id); if (existing) { // If found, increment the count existing.count += 1; + if (inSearch) existing.inSearchCount += 1; } else { // If not found, add a new entry with count 1 - acc.push({ label: type || "", count: 1, action: () => null }); + acc.push({ label: type || "", count: 1, inSearchCount: inSearch ? 1 : 0, action: () => null }); } + }); return acc; @@ -272,11 +289,33 @@ const SearchPage: React.FC = ({data}) => { .sort((a, b) => b.count - a.count); } + const handleSearchChange = (e: React.ChangeEvent) => { + const q = e.target.value + setSearchQuery(q) + if (q === "") { + quietlyRemoveUrlSearch(["q"]) + } else { + quietlyUpdateUrlSearch([{param: "q", val: e.target.value}]) + } + } + return (
    -
    +
    +
    + +
    +
    + +
    + +
    +
    +
    @@ -304,7 +343,8 @@ const SearchPage: React.FC = ({data}) => {
    -
    + {searchResults.length > 0 + ? <>
    Sort by: handleSortChange("asc")}, @@ -319,7 +359,9 @@ const SearchPage: React.FC = ({data}) => {
    - + + :
    No results.
    + }
    @@ -371,6 +413,10 @@ export const query = graphql` } } } + localSearchCollections { + store + index + } } `