diff --git a/app/api/search/indexes/route.ts b/app/api/search/indexes/route.ts index 9222e50e..4febe72f 100644 --- a/app/api/search/indexes/route.ts +++ b/app/api/search/indexes/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server" // These should be the same indexes used in the search route // to ensure consistency -const allIndexes = ["blog", "projects"] +const allIndexes = ["blog", "projects", "research"] export async function GET() { try { diff --git a/app/api/search/route.ts b/app/api/search/route.ts index 34511481..53ecf931 100644 --- a/app/api/search/route.ts +++ b/app/api/search/route.ts @@ -1,36 +1,10 @@ -import algoliasearch from "algoliasearch" import { NextRequest, NextResponse } from "next/server" +import { getArticles, getProjects } from "@/lib/content" +import { searchArticles, searchProjects } from "@/lib/search" -// Cache search results for better performance -export const revalidate = 900 // Revalidate cache after 15 minutes +export const revalidate = 300 // 5 minutes -const appId = - process.env.ALGOLIA_APP_ID || process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "" -const apiKey = - process.env.ALGOLIA_SEARCH_API_KEY || - process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY || - "" -const additionalIndexes = ( - process.env.ALGOLIA_ADDITIONAL_INDEXES || - process.env.NEXT_PUBLIC_ALGOLIA_ADDITIONAL_INDEXES || - "" -) - .split(",") - .map((index) => index.trim()) - .filter(Boolean) - -const allIndexes = [...additionalIndexes].filter(Boolean) || [ - "blog", - "projects", -] -const searchClient = appId && apiKey ? algoliasearch(appId, apiKey) : null - -function transformQuery(query: string) { - if (query.toLowerCase().includes("intmax")) { - return query.replace(/intmax/i, "\"intmax\"") - } - return query -} +const allIndexes = ["blog", "projects", "research"] export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams @@ -46,79 +20,104 @@ export async function GET(request: NextRequest) { }) } - if (!searchClient) { - return NextResponse.json( - { - error: "Search client not initialized - missing Algolia credentials", - availableIndexes: [], - }, - { status: 500 } - ) - } - - try { - const transformedQuery = transformQuery(query) + const results = [] - // If an index is specified, search only that index - if (indexName && indexName.trim() !== "") { - const index = searchClient.initIndex(indexName) - const response = await index.search(transformedQuery, { hitsPerPage }) + // Search articles + if (!indexName || indexName === "blog") { + const articles = getArticles() + const matches = searchArticles(articles, query) + .slice(0, hitsPerPage) + .map((article: any) => ({ + objectID: article.id, + title: article.title, + content: article.tldr || article.content.slice(0, 200), + url: `/blog/${article.id}`, + })) - return NextResponse.json( - { - hits: response.hits, - status: "success", - availableIndexes: allIndexes, - }, - { - headers: { - "Cache-Control": - "public, s-maxage=900, stale-while-revalidate=1800", - }, - } - ) + if (matches.length > 0) { + results.push({ + indexName: "blog", + hits: matches, + }) } + } - // Otherwise search across all configured indexes - const searchPromises = allIndexes.map((idxName) => { - return searchClient! - .initIndex(idxName) - .search(transformedQuery, { hitsPerPage }) - .then((response) => ({ - indexName: idxName, - hits: response.hits, - })) - .catch((err) => { - console.error(`Search error for index ${idxName}:`, err) - return { indexName: idxName, hits: [] } - }) - }) + // Search projects (applications and devtools only) + if (!indexName || indexName === "projects") { + const allProjects = getProjects() + const projectsOnly = allProjects.filter( + (p: any) => + p.category?.toLowerCase() === "application" || + p.category?.toLowerCase() === "devtools" + ) + const matches = searchProjects(projectsOnly, query) + .slice(0, hitsPerPage) + .map((project: any) => ({ + objectID: project.id, + title: project.name || project.title, + description: project.description || project.tldr, + url: `/projects/${project.id}`, + })) - const indexResults = await Promise.all(searchPromises) - const nonEmptyResults = indexResults.filter( - (result) => result.hits && result.hits.length > 0 + if (matches.length > 0) { + results.push({ + indexName: "projects", + hits: matches, + }) + } + } + + // Search research (research category only) + if (!indexName || indexName === "research") { + const allProjects = getProjects() + const researchOnly = allProjects.filter( + (p: any) => p.category?.toLowerCase() === "research" ) + const matches = searchProjects(researchOnly, query) + .slice(0, hitsPerPage) + .map((project: any) => ({ + objectID: project.id, + title: project.name || project.title, + description: project.description || project.tldr, + url: `/projects/${project.id}`, + })) + if (matches.length > 0) { + results.push({ + indexName: "research", + hits: matches, + }) + } + } + + // If searching specific index, return single index format + if (indexName) { + const indexResult = results.find((r) => r.indexName === indexName) return NextResponse.json( { - results: nonEmptyResults, + hits: indexResult?.hits || [], status: "success", availableIndexes: allIndexes, }, { headers: { - "Cache-Control": "public, s-maxage=900, stale-while-revalidate=1800", + "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600", }, } ) - } catch (error: any) { - console.error("Global search error:", error) - return NextResponse.json( - { - error: error.message || "Search failed", - availableIndexes: [], - }, - { status: 500 } - ) } + + // Return multi-index format + return NextResponse.json( + { + results, + status: "success", + availableIndexes: allIndexes, + }, + { + headers: { + "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600", + }, + } + ) } diff --git a/app/layout.tsx b/app/layout.tsx index 0fca8fbf..371207c3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -158,10 +158,6 @@ export default function RootLayout({ children }: RootLayoutProps) { {/* External service optimization */} - - {/* Algolia search preconnect for faster search */} - - diff --git a/app/providers/ProjectsProvider.tsx b/app/providers/ProjectsProvider.tsx index 1661f0cd..076d8515 100644 --- a/app/providers/ProjectsProvider.tsx +++ b/app/providers/ProjectsProvider.tsx @@ -3,8 +3,8 @@ import { LABELS } from "@/app/labels" import { ProjectCategory, ProjectInterface } from "@/lib/types" import { uniq } from "@/lib/utils" +import { searchProjects as fuseSearchProjects } from "@/lib/search" import { useQuery } from "@tanstack/react-query" -import Fuse from "fuse.js" import { createContext, useContext, @@ -91,74 +91,43 @@ const filterProjects = ({ findAnyMatch?: boolean projects?: ProjectInterface[] }) => { - const projectList = projectListItems.map((project: any) => ({ + let projectList = projectListItems.map((project: any) => ({ ...project, id: project?.id?.toLowerCase(), })) - const keys = [ - "name", - "tldr", - "tags.themes", - "tags.keywords", - "tags.builtWith", - "projectStatus", - ] + const noActiveFilters = + Object.keys(activeFilters).length === 0 && searchPattern.length === 0 + if (noActiveFilters) return projectList + + // Apply tag filters first + projectList = projectList.filter((project: any) => { + return Object.entries(activeFilters).every(([filterKey, filterValues]) => { + if (!filterValues || filterValues.length === 0) return true - const tagsFiltersQuery: Record[] = [] + const projectTags = project.tags?.[filterKey] || [] - Object.entries(activeFilters).forEach(([key, values]) => { - values.forEach((value) => { - if (!value) return - tagsFiltersQuery.push({ - [`tags.${key}`]: value, + return filterValues.some((filterValue: string) => { + if (Array.isArray(projectTags)) { + return projectTags.some((tag: string) => + tag.toLowerCase() === filterValue.toLowerCase() + ) + } + return false }) }) }) - const noActiveFilters = - tagsFiltersQuery.length === 0 && searchPattern.length === 0 - if (noActiveFilters) return projectList - - let query: any = {} - - if (findAnyMatch) { - query = { - $or: [...tagsFiltersQuery, { name: searchPattern }], - } - } else if (searchPattern?.length === 0) { - query = { - $and: [...tagsFiltersQuery], - } - } else if (tagsFiltersQuery.length === 0) { - query = { - name: searchPattern, - } - } else { - query = { - $and: [ - { - $and: [...tagsFiltersQuery], - }, - { name: searchPattern }, - ], - } + // Apply text search + if (searchPattern.length > 0) { + projectList = fuseSearchProjects(projectList, searchPattern) } - const fuse = new Fuse(projectList, { - threshold: 0.3, - useExtendedSearch: true, - includeScore: true, - findAllMatches: true, - distance: 200, - keys, - }) - - const result = fuse.search(query)?.map(({ item, score }) => ({ - ...item, - score, + // Add score for sorting + return projectList.map((project: any) => ({ + ...project, + score: 0, })) - return result ?? [] } const sortProjectByFn = ({ diff --git a/components/blog/articles-list.tsx b/components/blog/articles-list.tsx index c08b7f25..ed338ded 100644 --- a/components/blog/articles-list.tsx +++ b/components/blog/articles-list.tsx @@ -6,6 +6,7 @@ import { ArticleInEvidenceCard } from "./article-in-evidance-card" import { ArticleListCard } from "./article-list-card" import { LABELS } from "@/app/labels" import { Article, ArticleTag } from "@/lib/content" +import { searchArticles } from "@/lib/search" import { useQuery } from "@tanstack/react-query" import { Search as SearchIcon } from "lucide-react" import { useRouter, useSearchParams } from "next/navigation" @@ -77,16 +78,7 @@ export const ArticlesList: React.FC = ({ if (searchQuery === "all") { otherArticles = articles } else if (searchQuery?.length > 0) { - otherArticles = articles.filter((article: Article) => { - const title = article.title.toLowerCase() - const content = article.content.toLowerCase() - const tags = - article.tags?.map((tag: ArticleTag) => tag.name.toLowerCase()) ?? [] - return ( - title.includes(searchQuery.toLowerCase()) || - tags.some((tag: string) => tag.includes(searchQuery.toLowerCase())) - ) - }) + otherArticles = searchArticles(articles, searchQuery) } const hasTag = tag !== undefined diff --git a/lib/search.ts b/lib/search.ts new file mode 100644 index 00000000..878b7bed --- /dev/null +++ b/lib/search.ts @@ -0,0 +1,123 @@ +import Fuse from "fuse.js" + +// Helper to get nested object values (e.g., "tags.name") +function getNestedValue(obj: any, path: string): any { + return path.split('.').reduce((current, key) => current?.[key], obj) +} + +// Post-filter results with word boundary matching +// This ensures "pir" matches "(pir)", "pir.", "pirate" but NOT "inspired" +function filterByWordBoundary(results: T[], query: string, keys: string[]): T[] { + if (!query.trim()) return results + + // Split query into individual words + const words = query.trim().split(/\s+/) + + // Create regex patterns for each word (word boundary matching) + const patterns = words.map(word => { + // Escape special regex characters + const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + // Match at word boundaries (start of word) + return new RegExp(`\\b${escaped}`, 'i') + }) + + return results.filter(item => { + // Check if ALL query words match at word boundaries in ANY searchable field + return patterns.every(pattern => { + return keys.some(key => { + const value = getNestedValue(item, key) + if (!value) return false + + // Handle arrays (like tags) + if (Array.isArray(value)) { + return value.some(v => { + const str = typeof v === 'object' ? JSON.stringify(v) : String(v) + return pattern.test(str) + }) + } + + return pattern.test(String(value)) + }) + }) + }) +} + +// Transform query for Fuse.js extended search +// Use include-match operator for fuzzy matching +function preprocessQuery(query: string): string { + const words = query.trim().split(/\s+/) + + // Use include-match (') for each word + const patterns = words.map(word => { + // If word already has extended search operators, don't modify it + if (word.startsWith('^') || word.endsWith('$') || + word.startsWith('=') || word.startsWith("'") || + word.startsWith('!')) { + return word + } + // Use include-match operator (fuzzy match anywhere) + return `'${word}` + }) + + return patterns.join(' ') +} + +// Search articles with proper field weights +export function searchArticles(articles: T[], query: string): T[] { + if (!query.trim()) return articles + + const fuse = new Fuse(articles, { + keys: [ + { name: "title", weight: 1.0 }, + { name: "tags.name", weight: 0.75 }, + { name: "tldr", weight: 0.5 }, + { name: "content", weight: 0.25 }, + ], + threshold: 0.1, + distance: 200, + includeScore: true, + ignoreLocation: true, + useExtendedSearch: true, + }) + + const processedQuery = preprocessQuery(query) + const fuseResults = fuse.search(processedQuery).map((result) => result.item) + + // Post-filter with word boundary matching + const searchKeys = ["title", "tags.name", "tldr", "content"] + return filterByWordBoundary(fuseResults, query, searchKeys) +} + +// Search projects with proper field weights +export function searchProjects(projects: T[], query: string): T[] { + if (!query.trim()) return projects + + const fuse = new Fuse(projects, { + keys: [ + { name: "name", weight: 1.0 }, + { name: "tags.themes", weight: 0.75 }, + { name: "tags.keywords", weight: 0.75 }, + { name: "tags.builtWith", weight: 0.75 }, + { name: "tldr", weight: 0.5 }, + { name: "description", weight: 0.5 }, + { name: "projectStatus", weight: 0.25 }, + { name: "content", weight: 0.25 }, + ], + threshold: 0.1, + distance: 200, + findAllMatches: true, + includeScore: true, + useExtendedSearch: true, + }) + + const processedQuery = preprocessQuery(query) + const fuseResults = fuse.search(processedQuery).map((result) => result.item) + + // Post-filter with word boundary matching + const searchKeys = [ + "name", "tags.themes", "tags.keywords", "tags.builtWith", + "tldr", "description", "projectStatus", "content" + ] + return filterByWordBoundary(fuseResults, query, searchKeys) +} + diff --git a/package.json b/package.json index c6f18779..1cca657d 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "@tw-classed/react": "^1.8.0", "@types/node": "^20.19.0", "@types/prismjs": "^1.26.5", - "algoliasearch": "^4", "autoprefixer": "^10.4.14", "class-variance-authority": "^0.4.0", "clsx": "^1.2.1", @@ -60,7 +59,6 @@ "react": "^18.2.0", "react-cookie": "^7.0.1", "react-dom": "^18.2.0", - "react-instantsearch-hooks-web": "^6.47.3", "react-markdown": "^8.0.7", "react-slick": "^0.30.3", "react-use": "^17.4.0", diff --git a/tests/api/api-test-suite.test.ts b/tests/api/api-test-suite.test.ts index 891c83cb..ca6e3f84 100644 --- a/tests/api/api-test-suite.test.ts +++ b/tests/api/api-test-suite.test.ts @@ -36,7 +36,7 @@ describe("API Test Coverage", () => { it("covers all critical API functionality", () => { const expectedTestCategories = [ "Content APIs (articles, projects)", - "Search functionality (Algolia integration)", + "Search functionality (local)", "External integrations (Discord, YouTube)", "RSS feed generation", "Error handling and validation", diff --git a/tests/api/search.test.ts b/tests/api/search.test.ts index 66530dc7..cbded817 100644 --- a/tests/api/search.test.ts +++ b/tests/api/search.test.ts @@ -1,33 +1,51 @@ import { GET } from "@/app/api/search/route" import { NextRequest } from "next/server" -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" - -// Mock algoliasearch -const mockSearch = vi.fn() -const mockInitIndex = vi.fn() - -vi.mock("algoliasearch", () => ({ - default: vi.fn((appId: string, apiKey: string) => - appId && apiKey - ? { - initIndex: mockInitIndex, - } - : null - ), +import { describe, it, expect, vi, beforeEach } from "vitest" + +// Mock content functions +const mockGetArticles = vi.fn() +const mockGetProjects = vi.fn() + +vi.mock("@/lib/content", () => ({ + getArticles: () => mockGetArticles(), + getProjects: () => mockGetProjects(), })) -// Mock environment variables -const originalEnv = process.env +// Mock search functions (uses real implementation) +vi.mock("@/lib/search", async () => { + const actual = await vi.importActual("@/lib/search") + return actual +}) describe("/api/search", () => { beforeEach(() => { vi.clearAllMocks() - process.env = { ...originalEnv } - mockInitIndex.mockReturnValue({ search: mockSearch }) - }) - - afterEach(() => { - process.env = originalEnv + mockGetArticles.mockReturnValue([ + { + id: "test-article", + title: "Test Article", + content: "This is test content", + tldr: "Test summary", + date: "2024-01-01", + tags: [{ id: "test", name: "Test" }], + }, + ]) + mockGetProjects.mockReturnValue([ + { + id: "test-project", + name: "Test Project", + title: "Test Project", + description: "Test project description", + category: "application", + }, + { + id: "test-research", + name: "Test Research", + title: "Test Research", + description: "Research project description", + category: "research", + }, + ]) }) const createMockRequest = (searchParams: Record = {}) => { @@ -38,19 +56,6 @@ describe("/api/search", () => { return new NextRequest(url.toString()) } - const mockSearchResults = { - hits: [ - { - objectID: "1", - title: "Test Article", - description: "Test description", - url: "/articles/test", - }, - ], - nbHits: 1, - page: 0, - } - describe("GET /api/search", () => { it("returns empty results when query is empty", async () => { const request = createMockRequest({ query: "" }) @@ -61,9 +66,8 @@ describe("/api/search", () => { expect(data).toEqual({ results: [], status: "empty", - availableIndexes: [], + availableIndexes: ["blog", "projects", "research"], }) - expect(mockSearch).not.toHaveBeenCalled() }) it("returns empty results when query is whitespace only", async () => { @@ -75,64 +79,304 @@ describe("/api/search", () => { expect(data).toEqual({ results: [], status: "empty", - availableIndexes: [], + availableIndexes: ["blog", "projects", "research"], }) - expect(mockSearch).not.toHaveBeenCalled() }) - it("returns error when Algolia credentials are missing", async () => { - // Clear environment variables to simulate missing credentials - process.env.ALGOLIA_APP_ID = "" - process.env.ALGOLIA_SEARCH_API_KEY = "" + it("searches articles by title", async () => { + const request = createMockRequest({ query: "test article" }) + const response = await GET(request) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.status).toBe("success") + expect(data.results.length).toBeGreaterThan(0) + const blogResult = data.results.find((r: any) => r.indexName === "blog") + expect(blogResult).toBeDefined() + expect(blogResult.hits[0].title).toBe("Test Article") + }) - const request = createMockRequest({ query: "test" }) + it("searches projects by description", async () => { + const request = createMockRequest({ query: "project", index: "projects" }) const response = await GET(request) const data = await response.json() - expect(response.status).toBe(500) - expect(data).toEqual({ - error: "Search client not initialized - missing Algolia credentials", - availableIndexes: [], - }) + expect(response.status).toBe(200) + expect(data.status).toBe("success") + expect(data.hits).toBeDefined() + expect(data.hits.length).toBeGreaterThan(0) + expect(data.hits[0].title).toBe("Test Project") }) - it("returns error for search when credentials are invalid", async () => { - // Test with invalid but present credentials - process.env.ALGOLIA_APP_ID = "" - process.env.ALGOLIA_SEARCH_API_KEY = "" + it("searches research index separately", async () => { + const request = createMockRequest({ query: "research", index: "research" }) + const response = await GET(request) + const data = await response.json() - const request = createMockRequest({ - query: "test query", - index: "blog", - }) + expect(response.status).toBe(200) + expect(data.status).toBe("success") + expect(data.hits).toBeDefined() + }) + + it("filters projects by category (application/devtools only)", async () => { + const request = createMockRequest({ query: "test", index: "projects" }) const response = await GET(request) const data = await response.json() - expect(response.status).toBe(500) - expect(data).toEqual({ - error: "Search client not initialized - missing Algolia credentials", - availableIndexes: [], - }) + expect(response.status).toBe(200) + // Should only return application project, not research + const researchHit = data.hits.find((h: any) => h.objectID === "test-research") + expect(researchHit).toBeUndefined() }) - it("handles search errors gracefully for specific index", async () => { - const error = new Error("Algolia search failed") + it("returns no results for non-matching query", async () => { + const request = createMockRequest({ query: "nonexistent" }) + const response = await GET(request) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.status).toBe("success") + expect(data.results).toHaveLength(0) + }) - // Set up valid credentials but mock will reject - process.env.ALGOLIA_APP_ID = "" - process.env.ALGOLIA_SEARCH_API_KEY = "" + it("respects hitsPerPage parameter", async () => { + mockGetArticles.mockReturnValue([ + { id: "1", title: "Article 1", content: "test", tldr: "test", date: "2024-01-01", tags: [] }, + { id: "2", title: "Article 2", content: "test", tldr: "test", date: "2024-01-02", tags: [] }, + { id: "3", title: "Article 3", content: "test", tldr: "test", date: "2024-01-03", tags: [] }, + ]) - const request = createMockRequest({ - query: "test", - index: "blog", - }) + const request = createMockRequest({ query: "article", hitsPerPage: "2" }) const response = await GET(request) const data = await response.json() - expect(response.status).toBe(500) - expect(data).toEqual({ - error: "Search client not initialized - missing Algolia credentials", - availableIndexes: [], + const blogResult = data.results.find((r: any) => r.indexName === "blog") + expect(blogResult?.hits.length).toBeLessThanOrEqual(2) + }) + + // New tests for word boundary matching + describe("Word Boundary Matching", () => { + it("matches words with word boundaries, not partial matches within words", async () => { + mockGetArticles.mockReturnValue([ + { + id: "pir-article", + title: "Understanding PIR (Private Information Retrieval)", + content: "PIR is important for privacy", + tldr: "Learn about PIR", + date: "2024-01-01", + tags: [], + }, + { + id: "inspired-article", + title: "Inspired by Innovation", + content: "This article was inspired by research", + tldr: "Get inspired", + date: "2024-01-02", + tags: [], + }, + ]) + + const request = createMockRequest({ query: "pir" }) + const response = await GET(request) + const data = await response.json() + + const blogResult = data.results.find((r: any) => r.indexName === "blog") + expect(blogResult).toBeDefined() + expect(blogResult.hits).toHaveLength(1) + expect(blogResult.hits[0].objectID).toBe("pir-article") + }) + + it("matches words followed by punctuation at boundaries", async () => { + mockGetProjects.mockReturnValue([ + { + id: "semaphore", + name: "Semaphore", + title: "Semaphore Protocol", + description: "Privacy protocol (Semaphore) for anonymous signaling.", + category: "application", + }, + ]) + + const request = createMockRequest({ query: "semaphore", index: "projects" }) + const response = await GET(request) + const data = await response.json() + + expect(data.hits.length).toBeGreaterThan(0) + expect(data.hits[0].objectID).toBe("semaphore") + }) + + it("performs case-insensitive word boundary matching", async () => { + mockGetArticles.mockReturnValue([ + { + id: "snark-article", + title: "SNARK Technology Overview", + content: "SNARKs are powerful cryptographic proofs", + tldr: "Learn about SNARKs", + date: "2024-01-01", + tags: [], + }, + ]) + + const request = createMockRequest({ query: "snark" }) + const response = await GET(request) + const data = await response.json() + + const blogResult = data.results.find((r: any) => r.indexName === "blog") + expect(blogResult).toBeDefined() + expect(blogResult.hits).toHaveLength(1) + expect(blogResult.hits[0].title).toBe("SNARK Technology Overview") + }) + + it("requires all words in multi-word query to match at word boundaries", async () => { + mockGetArticles.mockReturnValue([ + { + id: "crypto-zk", + title: "Zero Knowledge Cryptography", + content: "ZK proofs are used in crypto applications", + tldr: "ZK and crypto together", + date: "2024-01-01", + tags: [], + }, + { + id: "encryption-only", + title: "Cryptographic Methods", + content: "Various encryption techniques without zero knowledge", + tldr: "Encryption techniques", + date: "2024-01-02", + tags: [], + }, + ]) + + const request = createMockRequest({ query: "crypto zero" }) + const response = await GET(request) + const data = await response.json() + + const blogResult = data.results.find((r: any) => r.indexName === "blog") + expect(blogResult).toBeDefined() + expect(blogResult.hits).toHaveLength(1) + expect(blogResult.hits[0].objectID).toBe("crypto-zk") + }) + + it("returns no results when fuzzy matches don't satisfy word boundaries", async () => { + mockGetArticles.mockReturnValue([ + { + id: "inspiration", + title: "Inspiration for Developers", + content: "Get inspired by these innovative ideas", + tldr: "Developer inspiration", + date: "2024-01-01", + tags: [], + }, + ]) + + // "pir" should not match "inspiration" due to word boundary filtering + const request = createMockRequest({ query: "pir" }) + const response = await GET(request) + const data = await response.json() + + expect(data.status).toBe("success") + expect(data.results).toHaveLength(0) + }) + }) + + // New tests for tag matching + describe("Tag Search with Word Boundaries", () => { + it("searches tags with word boundary matching", async () => { + mockGetArticles.mockReturnValue([ + { + id: "privacy-article", + title: "Privacy Technologies (PIR)", + content: "Various privacy tools including PIR", + tldr: "Privacy overview with PIR", + tags: [{ id: "pir", name: "PIR" }], + date: "2024-01-01", + }, + { + id: "inspiration-article", + title: "Inspired Ideas", + content: "Inspiration for developers", + tldr: "Get inspired", + tags: [{ id: "inspiration", name: "Inspiration" }], + date: "2024-01-02", + }, + ]) + + const request = createMockRequest({ query: "pir" }) + const response = await GET(request) + const data = await response.json() + + const blogResult = data.results.find((r: any) => r.indexName === "blog") + expect(blogResult).toBeDefined() + expect(blogResult.hits).toHaveLength(1) + expect(blogResult.hits[0].objectID).toBe("privacy-article") + }) + + it("matches tags across multiple tag fields in projects", async () => { + mockGetProjects.mockReturnValue([ + { + id: "zk-project", + name: "ZK Application", + title: "ZK Application", + description: "Application using zero knowledge", + category: "application", + tags: { + themes: ["privacy"], + keywords: ["zk", "cryptography"], + builtWith: ["circom"], + }, + }, + ]) + + const request = createMockRequest({ query: "zk", index: "projects" }) + const response = await GET(request) + const data = await response.json() + + expect(data.hits).toHaveLength(1) + expect(data.hits[0].objectID).toBe("zk-project") + }) + }) + + // New tests for fuzzy matching + describe("Fuzzy Matching", () => { + it("performs fuzzy matching with low threshold for typos", async () => { + mockGetProjects.mockReturnValue([ + { + id: "semaphore", + name: "Semaphore", + title: "Semaphore", + description: "Privacy signaling protocol", + category: "application", + }, + ]) + + // "semaphor" (missing 'e') should match "Semaphore" with fuzzy matching + const request = createMockRequest({ query: "semaphor", index: "projects" }) + const response = await GET(request) + const data = await response.json() + + expect(data.hits.length).toBeGreaterThan(0) + expect(data.hits[0].objectID).toBe("semaphore") + }) + + it("handles special characters in search queries", async () => { + mockGetArticles.mockReturnValue([ + { + id: "regex-article", + title: "Understanding Regular Expressions (regex)", + content: "Learn about regex patterns", + tldr: "Regex tutorial", + date: "2024-01-01", + tags: [], + }, + ]) + + const request = createMockRequest({ query: "regex" }) + const response = await GET(request) + const data = await response.json() + + const blogResult = data.results.find((r: any) => r.indexName === "blog") + expect(blogResult).toBeDefined() + expect(blogResult.hits.length).toBeGreaterThan(0) }) }) }) diff --git a/yarn.lock b/yarn.lock index 0e5609ba..5b7789e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,181 +12,6 @@ __metadata: languageName: node linkType: hard -"@algolia/cache-browser-local-storage@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/cache-browser-local-storage@npm:4.25.2" - dependencies: - "@algolia/cache-common": "npm:4.25.2" - checksum: 10/ca0f39001e1d8d9b42adea349baadf1ff9f2ff43e87f4bc85928c0298cc533c1b061f393e1df40a87dbc365e4231dd85a6d8867fbdbd7e6de74c4a79745f307d - languageName: node - linkType: hard - -"@algolia/cache-common@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/cache-common@npm:4.25.2" - checksum: 10/706dca5d9c570490b4760646a94d5ed7b603cc846ae3b62c42fc5e7958cdd74b7bd37506be62bf4b97f4b6551526f7a64597c3240fe2c5e7477a8ece986c8d12 - languageName: node - linkType: hard - -"@algolia/cache-in-memory@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/cache-in-memory@npm:4.25.2" - dependencies: - "@algolia/cache-common": "npm:4.25.2" - checksum: 10/ffd66ff76cba41a1dd26e3575902881e7a5f685018c5d5b6ec2a972c682a56940b593ac3d33d59aac296db884328f0bd19a16e83281c69e3582ab121ed759d56 - languageName: node - linkType: hard - -"@algolia/client-account@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/client-account@npm:4.25.2" - dependencies: - "@algolia/client-common": "npm:4.25.2" - "@algolia/client-search": "npm:4.25.2" - "@algolia/transporter": "npm:4.25.2" - checksum: 10/a39e1be468aef039093f604872afee2cdf8ba17b1b6ed46fe4454a199b147c54a6daccef4efa830d1536385a3f0f5999bb37c52c9b4e09b943117b82d9bd5b3a - languageName: node - linkType: hard - -"@algolia/client-analytics@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/client-analytics@npm:4.25.2" - dependencies: - "@algolia/client-common": "npm:4.25.2" - "@algolia/client-search": "npm:4.25.2" - "@algolia/requester-common": "npm:4.25.2" - "@algolia/transporter": "npm:4.25.2" - checksum: 10/9954f32826d47cd99cb3bd34c7164d752ae5ff895e53ad5fddb4ac1949a98718ef0bb9c9e6bb0df1713438981d9bf038257137ab62df1a3bd72d6836f681b80e - languageName: node - linkType: hard - -"@algolia/client-common@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/client-common@npm:4.25.2" - dependencies: - "@algolia/requester-common": "npm:4.25.2" - "@algolia/transporter": "npm:4.25.2" - checksum: 10/b88e2abb9dede8fd457471f33f9592b2c0ad3866a95773a3075f8dd457ab400e336d68015f51f826a47aa31662750886ec579a20d45b8f90078269bcf0b5c0c3 - languageName: node - linkType: hard - -"@algolia/client-personalization@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/client-personalization@npm:4.25.2" - dependencies: - "@algolia/client-common": "npm:4.25.2" - "@algolia/requester-common": "npm:4.25.2" - "@algolia/transporter": "npm:4.25.2" - checksum: 10/5c2547f40de5ce65fb25685ccee830c98c8d868c4eba3d49710238fdd9b8b6880eaad51fe32831cba1ee04fd614efe8aa50acce492095c09e6d20c91d0595ae9 - languageName: node - linkType: hard - -"@algolia/client-search@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/client-search@npm:4.25.2" - dependencies: - "@algolia/client-common": "npm:4.25.2" - "@algolia/requester-common": "npm:4.25.2" - "@algolia/transporter": "npm:4.25.2" - checksum: 10/90a4df2ed4da8d4699801b74aaa1351fae098172cd5905afecd1b645294e7083080fff97722878a41135dc8a692623bc3ad69540d438150ed23ac534d7e1e63d - languageName: node - linkType: hard - -"@algolia/events@npm:^4.0.1": - version: 4.0.1 - resolution: "@algolia/events@npm:4.0.1" - checksum: 10/98d239899a9dac9398f751221369523f2d7706fc4b3bc3167b66a101773d57380fc52733467c0a12be36bce969577fd4010d6ccbd08c410f9c7adc088dadf4c6 - languageName: node - linkType: hard - -"@algolia/logger-common@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/logger-common@npm:4.25.2" - checksum: 10/56676de8131841cc966cd60f7804ff61d2266f56c1f8045ccb99680ce5b28eeecbb3db42b40add8750c17ac6bd3b0cee0eaa1f9ee38af38b17c4553b40bf2f22 - languageName: node - linkType: hard - -"@algolia/logger-console@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/logger-console@npm:4.25.2" - dependencies: - "@algolia/logger-common": "npm:4.25.2" - checksum: 10/f593e31479c41373112f89d7e38a0a2d28312b2c885f0e7286dc39685842098c3ac9a22ebbd9c6b965be09077c5184830a50405c3f32150ee961d9b60f86f564 - languageName: node - linkType: hard - -"@algolia/recommend@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/recommend@npm:4.25.2" - dependencies: - "@algolia/cache-browser-local-storage": "npm:4.25.2" - "@algolia/cache-common": "npm:4.25.2" - "@algolia/cache-in-memory": "npm:4.25.2" - "@algolia/client-common": "npm:4.25.2" - "@algolia/client-search": "npm:4.25.2" - "@algolia/logger-common": "npm:4.25.2" - "@algolia/logger-console": "npm:4.25.2" - "@algolia/requester-browser-xhr": "npm:4.25.2" - "@algolia/requester-common": "npm:4.25.2" - "@algolia/requester-node-http": "npm:4.25.2" - "@algolia/transporter": "npm:4.25.2" - checksum: 10/79d75c34bd24ac73a957a2450efcdc1024bae61c3394530f3c815d93b3a5aa3fefbec1ec5de6abd1b1d7454d75e3f8d5026d98f655cdb220898e69e7f08391e3 - languageName: node - linkType: hard - -"@algolia/requester-browser-xhr@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/requester-browser-xhr@npm:4.25.2" - dependencies: - "@algolia/requester-common": "npm:4.25.2" - checksum: 10/62f2caded45a1af6f4aa4bde925565f21d037010ff89bab0bb58289bda9bd0ecfa9dd5a451b74ef059970f280cadca98a8b294bd055f70fa75fd5034507d5c19 - languageName: node - linkType: hard - -"@algolia/requester-common@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/requester-common@npm:4.25.2" - checksum: 10/68ae6e4ff01f67807e1b61ea37433e4d0a39fdfbd1da5fcc35e3180757fd272cea7fd07bebe7510d7b7fcfd6dc1992535cb70595829b7e1cd12d516c2df523ff - languageName: node - linkType: hard - -"@algolia/requester-node-http@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/requester-node-http@npm:4.25.2" - dependencies: - "@algolia/requester-common": "npm:4.25.2" - checksum: 10/31a62aae0c041f49b2b5bf4dd794d88f6f5b356902b3a264061c2dc3a5273f6edfc69a06a5c40c0cdc6a25e4bbc66e0eb9f37ac3e65593a5f53293bab0f96560 - languageName: node - linkType: hard - -"@algolia/transporter@npm:4.25.2": - version: 4.25.2 - resolution: "@algolia/transporter@npm:4.25.2" - dependencies: - "@algolia/cache-common": "npm:4.25.2" - "@algolia/logger-common": "npm:4.25.2" - "@algolia/requester-common": "npm:4.25.2" - checksum: 10/4ad449c56b142806577e885edfbeb11c0219ce08cc8128c2d5283d72a00ad4c37173784e098b4c8aa6052667569f8886d6d096136cf9e526779551c130e82daf - languageName: node - linkType: hard - -"@algolia/ui-components-highlight-vdom@npm:^1.2.1": - version: 1.2.3 - resolution: "@algolia/ui-components-highlight-vdom@npm:1.2.3" - dependencies: - "@algolia/ui-components-shared": "npm:1.2.3" - "@babel/runtime": "npm:^7.0.0" - checksum: 10/20b3ac2c8dd34f50a77c3872f23b315cf31cb76f862a29de3df4f733a01b548fb78b4441bea0988ded8b3222ab76906317cf2fd30e359fdf6e5defd7337ab743 - languageName: node - linkType: hard - -"@algolia/ui-components-shared@npm:1.2.3, @algolia/ui-components-shared@npm:^1.2.1": - version: 1.2.3 - resolution: "@algolia/ui-components-shared@npm:1.2.3" - checksum: 10/bebed83507b1084d443dba435869348f8370c111b0fac224969f4dac197d157367c193f3a1ba36c8d2f0818028586c6a7f831cfb7990bff7ef0e5e3aa347bb78 - languageName: node - linkType: hard - "@alloc/quick-lru@npm:^5.2.0": version: 5.2.0 resolution: "@alloc/quick-lru@npm:5.2.0" @@ -385,7 +210,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.12.5": +"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.12.5": version: 7.28.2 resolution: "@babel/runtime@npm:7.28.2" checksum: 10/a0965fbdd6aaa40709290923bbe05e1c4314021f0cef608eb1d69f04f717c41829e50a53d79c4a0f461512b4be9b3c0190dc19387b219bcdaacdd793b2fe1b8a @@ -2425,13 +2250,6 @@ __metadata: languageName: node linkType: hard -"@types/dom-speech-recognition@npm:^0.0.1": - version: 0.0.1 - resolution: "@types/dom-speech-recognition@npm:0.0.1" - checksum: 10/9ac74dbfb1d28e90a052db858c9298f9987717674537f3f6eb86baf85bd691cb061b7f21bd43b3dd8d3fd101ad190fe77e5b4e1cfa15fb5ad12431ec29e32490 - languageName: node - linkType: hard - "@types/estree-jsx@npm:^1.0.0": version: 1.0.5 resolution: "@types/estree-jsx@npm:1.0.5" @@ -2448,13 +2266,6 @@ __metadata: languageName: node linkType: hard -"@types/google.maps@npm:^3.45.3": - version: 3.58.1 - resolution: "@types/google.maps@npm:3.58.1" - checksum: 10/3d5aaa901c0b5dcce45dc9f667912c04b99be0b4a8b541b5120b677697d17116684fddb457bea4955142755c9089993ea4b48b30705283c16935473b1818ecd1 - languageName: node - linkType: hard - "@types/hast@npm:^2.0.0": version: 2.3.10 resolution: "@types/hast@npm:2.3.10" @@ -2473,13 +2284,6 @@ __metadata: languageName: node linkType: hard -"@types/hogan.js@npm:^3.0.0": - version: 3.0.5 - resolution: "@types/hogan.js@npm:3.0.5" - checksum: 10/a2cc95b1a94bd321aa2fe0303005703a7e801cf463ee7b3ab5e2fae101ef426ace87bf9554bb995c8d3c60c2612b657d765d20d96faae3af03bd0e3a55357aba - languageName: node - linkType: hard - "@types/hoist-non-react-statics@npm:^3.3.5": version: 3.3.7 resolution: "@types/hoist-non-react-statics@npm:3.3.7" @@ -2592,13 +2396,6 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:^6.5.3": - version: 6.14.0 - resolution: "@types/qs@npm:6.14.0" - checksum: 10/1909205514d22b3cbc7c2314e2bd8056d5f05dfb21cf4377f0730ee5e338ea19957c41735d5e4806c746176563f50005bbab602d8358432e25d900bdf4970826 - languageName: node - linkType: hard - "@types/react-dom@npm:^18.2.4": version: 18.3.7 resolution: "@types/react-dom@npm:18.3.7" @@ -3171,13 +2968,6 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:1": - version: 1.1.1 - resolution: "abbrev@npm:1.1.1" - checksum: 10/2d882941183c66aa665118bafdab82b7a177e9add5eb2776c33e960a4f3c89cff88a1b38aba13a456de01d0dd9d66a8bea7c903268b21ea91dd1097e1e2e8243 - languageName: node - linkType: hard - "abbrev@npm:^3.0.0": version: 3.0.1 resolution: "abbrev@npm:3.0.1" @@ -3240,40 +3030,6 @@ __metadata: languageName: node linkType: hard -"algoliasearch-helper@npm:3.14.0": - version: 3.14.0 - resolution: "algoliasearch-helper@npm:3.14.0" - dependencies: - "@algolia/events": "npm:^4.0.1" - peerDependencies: - algoliasearch: ">= 3.1 < 6" - checksum: 10/5a3e1fda05a1688153577a0377fd38aaf84a3c095c63fc14877acd34449d41c274f6fb38dc8c6041e1f5c198425cd24653f9bac0f1e18a351ba7a3b500641aac - languageName: node - linkType: hard - -"algoliasearch@npm:^4": - version: 4.25.2 - resolution: "algoliasearch@npm:4.25.2" - dependencies: - "@algolia/cache-browser-local-storage": "npm:4.25.2" - "@algolia/cache-common": "npm:4.25.2" - "@algolia/cache-in-memory": "npm:4.25.2" - "@algolia/client-account": "npm:4.25.2" - "@algolia/client-analytics": "npm:4.25.2" - "@algolia/client-common": "npm:4.25.2" - "@algolia/client-personalization": "npm:4.25.2" - "@algolia/client-search": "npm:4.25.2" - "@algolia/logger-common": "npm:4.25.2" - "@algolia/logger-console": "npm:4.25.2" - "@algolia/recommend": "npm:4.25.2" - "@algolia/requester-browser-xhr": "npm:4.25.2" - "@algolia/requester-common": "npm:4.25.2" - "@algolia/requester-node-http": "npm:4.25.2" - "@algolia/transporter": "npm:4.25.2" - checksum: 10/d8d13ff04db7bceb6b5d01a0736f5b37538941188b6866f61457e78326d4f6e45a08c22478c8f86e8b178f98d94c07d5aaed74a4a435e841d7190650ebec4167 - languageName: node - linkType: hard - "ansi-escapes@npm:^7.0.0": version: 7.0.0 resolution: "ansi-escapes@npm:7.0.0" @@ -5972,18 +5728,6 @@ __metadata: languageName: node linkType: hard -"hogan.js@npm:^3.0.2": - version: 3.0.2 - resolution: "hogan.js@npm:3.0.2" - dependencies: - mkdirp: "npm:0.3.0" - nopt: "npm:1.0.10" - bin: - hulk: ./bin/hulk - checksum: 10/385784c5e61dafe019b01bad57b52cac27bc7509c1ad213dcbdd4bc39b001ef25f5af8af03dbb4e9885eaa2dad5462c87dd668e1eb6697fe71c1c8af6887b09e - languageName: node - linkType: hard - "hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" @@ -5993,13 +5737,6 @@ __metadata: languageName: node linkType: hard -"htm@npm:^3.0.0": - version: 3.1.1 - resolution: "htm@npm:3.1.1" - checksum: 10/cb862dc5c9eac532937af7a9e26edd1e0e7939fc78a06efde4ae10b3a145f9506e644ff084c871dd808c315342b56fd0baa174a2a2cdf6071a4130ee0abee9e0 - languageName: node - linkType: hard - "html-encoding-sniffer@npm:^4.0.0": version: 4.0.0 resolution: "html-encoding-sniffer@npm:4.0.0" @@ -6199,29 +5936,6 @@ __metadata: languageName: node linkType: hard -"instantsearch.js@npm:4.56.8": - version: 4.56.8 - resolution: "instantsearch.js@npm:4.56.8" - dependencies: - "@algolia/events": "npm:^4.0.1" - "@algolia/ui-components-highlight-vdom": "npm:^1.2.1" - "@algolia/ui-components-shared": "npm:^1.2.1" - "@types/dom-speech-recognition": "npm:^0.0.1" - "@types/google.maps": "npm:^3.45.3" - "@types/hogan.js": "npm:^3.0.0" - "@types/qs": "npm:^6.5.3" - algoliasearch-helper: "npm:3.14.0" - hogan.js: "npm:^3.0.2" - htm: "npm:^3.0.0" - preact: "npm:^10.10.0" - qs: "npm:^6.5.1 < 6.10" - search-insights: "npm:^2.6.0" - peerDependencies: - algoliasearch: ">= 3.1 < 6" - checksum: 10/ec7ee1fe8f54c92e27ffed1e4eecef14efbe36e2faeceb73c575e8051ba2f466a0d687a05c271e2874acc9eb583ee39f19535487c4077d5875985078517429b4 - languageName: node - linkType: hard - "internal-slot@npm:^1.1.0": version: 1.1.0 resolution: "internal-slot@npm:1.1.0" @@ -8346,13 +8060,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:0.3.0": - version: 0.3.0 - resolution: "mkdirp@npm:0.3.0" - checksum: 10/51b0010427561f044f3c2f453163c9e9452c4a26643d63defd8313674e314cde3866019f19e8e9fc7eefcff4c73666fa7ae8e4676b85bc15b5fddd850bffbed1 - languageName: node - linkType: hard - "mkdirp@npm:^3.0.1": version: 3.0.1 resolution: "mkdirp@npm:3.0.1" @@ -8487,7 +8194,6 @@ __metadata: "@vitejs/plugin-react": "npm:^4.7.0" "@vitest/coverage-v8": "npm:3.2.4" "@vitest/ui": "npm:^3.2.4" - algoliasearch: "npm:^4" autoprefixer: "npm:^10.4.14" class-variance-authority: "npm:^0.4.0" clsx: "npm:^1.2.1" @@ -8520,7 +8226,6 @@ __metadata: react: "npm:^18.2.0" react-cookie: "npm:^7.0.1" react-dom: "npm:^18.2.0" - react-instantsearch-hooks-web: "npm:^6.47.3" react-markdown: "npm:^8.0.7" react-slick: "npm:^0.30.3" react-use: "npm:^17.4.0" @@ -8649,17 +8354,6 @@ __metadata: languageName: node linkType: hard -"nopt@npm:1.0.10": - version: 1.0.10 - resolution: "nopt@npm:1.0.10" - dependencies: - abbrev: "npm:1" - bin: - nopt: ./bin/nopt.js - checksum: 10/4f01ad1e144883a190d70bd6003f26e2f3a899230fe1b0f3310e43779c61cab5ae0063a9209912cd52fc4c552b266b38173853aa9abe27ecb04acbdfdca2e9fc - languageName: node - linkType: hard - "nopt@npm:^8.0.0": version: 8.1.0 resolution: "nopt@npm:8.1.0" @@ -9123,13 +8817,6 @@ __metadata: languageName: node linkType: hard -"preact@npm:^10.10.0": - version: 10.27.0 - resolution: "preact@npm:10.27.0" - checksum: 10/440685d450349acb5802fbdba33b53b727e2aeb57a23bfa28b597a97b2e1f046f2e1abc0bd51662968ff3a45ebbcc33894b929fdc412b1644651669e2860dfac - languageName: node - linkType: hard - "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -9227,13 +8914,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.5.1 < 6.10": - version: 6.9.7 - resolution: "qs@npm:6.9.7" - checksum: 10/fb364b54bf4f092a095554968f5abf06036cfe359c9aba258a81b0c0366f625a46098fe1224b2a71ee2f88642470af391c7a8a1496508eca29c37093293f91a9 - languageName: node - linkType: hard - "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -9266,36 +8946,6 @@ __metadata: languageName: node linkType: hard -"react-instantsearch-hooks-web@npm:^6.47.3": - version: 6.47.3 - resolution: "react-instantsearch-hooks-web@npm:6.47.3" - dependencies: - "@babel/runtime": "npm:^7.1.2" - instantsearch.js: "npm:4.56.8" - react-instantsearch-hooks: "npm:6.47.3" - peerDependencies: - algoliasearch: ">= 3.1 < 5" - react: ">= 16.8.0 < 19" - react-dom: ">= 16.8.0 < 19" - checksum: 10/366f9088030c538a29ccf7f7860a652d962179a6b7b517e83d4a47569d0b3e53f077ddbe9549059b3bb6c3b86041cb6aa6fd497aec779c3064790755d4fa2d84 - languageName: node - linkType: hard - -"react-instantsearch-hooks@npm:6.47.3": - version: 6.47.3 - resolution: "react-instantsearch-hooks@npm:6.47.3" - dependencies: - "@babel/runtime": "npm:^7.1.2" - algoliasearch-helper: "npm:3.14.0" - instantsearch.js: "npm:4.56.8" - use-sync-external-store: "npm:^1.0.0" - peerDependencies: - algoliasearch: ">= 3.1 < 5" - react: ">= 16.8.0 < 19" - checksum: 10/65b4ff9c5aa9262df1d477d91d7bdb2e99fae387576bc2f144fbd42d5bfb8b8b85c7dffad359d78c122149101f992a520da44ae9af5d330ba614bb295836535b - languageName: node - linkType: hard - "react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -10014,13 +9664,6 @@ __metadata: languageName: node linkType: hard -"search-insights@npm:^2.6.0": - version: 2.17.3 - resolution: "search-insights@npm:2.17.3" - checksum: 10/7f2d7c5d317d84bb9bb745f3e5cd411c206fb72e453331515712bda855e3ee8af4b767231a4bc25693eadd34e2ffd58b6eebb7c407fc17eeb2932cc997442dff - languageName: node - linkType: hard - "section-matter@npm:^1.0.0": version: 1.0.0 resolution: "section-matter@npm:1.0.0" @@ -11602,7 +11245,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.2": +"use-sync-external-store@npm:^1.2.2": version: 1.5.0 resolution: "use-sync-external-store@npm:1.5.0" peerDependencies: