接下來要讓 Nextjs 專案串接 Typsense 的搜尋功能,主要會是利用 Algolia 出品的 Instasearch 元件來串。
Typsense 另外製作了一個轉接器串接 Instasearch ,蠻方便的。
首先安裝套件。
pnpm i algoliasearch react-instantsearch typesense-instantsearch-adapter
再來另外開個新頁面來測試。
// src/app/books/page.tsx
"use client";
import { type LiteClient } from "algoliasearch/lite";
import { Hits, InstantSearch, SearchBox } from "react-instantsearch";
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "xyz", // Be sure to use the search-only-api-key
nodes: [
{
host: "localhost",
port: 8108,
protocol: "http",
},
],
},
// The following parameters are directly passed to Typesense's search API endpoint.
// So you can pass any parameters supported by the search endpoint below.
// query_by is required.
additionalSearchParameters: {
query_by: "title,authors",
},
});
const searchClient = typesenseInstantsearchAdapter.searchClient as LiteClient;
function Hit({
hit,
}: {
hit: {
title: string;
authors: string;
};
}) {
return (
<article>
<h1>{hit.title}</h1>
</article>
);
}
const Page = () => {
return (
<>
<h1>{"Books"}</h1>
<InstantSearch searchClient={searchClient} indexName="books">
<SearchBox />
<Hits hitComponent={Hit} />
</InstantSearch>
</>
);
};
export default Page;
這邊的 Typesense api key 如果繼續用建置服務時的無限制 api key 的話蠻危險的,得另外發一隻有限權限的才行。
typesense-dashboard 也有提供 api key 的管理畫面,可以在這裡輕鬆的建立一支新的金鑰。
用 JSON 表示金鑰的名稱跟權限,這邊只允許 books 的搜尋功能。
{
"description": "Read books",
"actions": [
"documents:search"
],
"collections": [
"books"
]
}
新增好後記下金鑰貼回 Nextjs 這邊,就能看到搜尋輸入跟搜尋結果了。
為了方便客製化 Instasearch 沒有預設任何的樣式,而是另外提供 Algolia 跟 Satellite 的樣式主題方便引入後套用,但是這邊不打算用那個,而是套用 MUI。
要替換整個元件的話就要利用個元件的 hook,例如 useSearchBox 。
最後整合的結果範例如下:
"use client";
import { type LiteClient } from "algoliasearch/lite";
import {
InstantSearch,
type SearchBoxProps,
useHits,
type UseHitsProps,
useSearchBox,
} from "react-instantsearch";
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "xyz", // Be sure to use the search-only-api-key
nodes: [
{
host: "localhost",
port: 8108,
protocol: "http",
},
],
},
// The following parameters are directly passed to Typesense's search API endpoint.
// So you can pass any parameters supported by the search endpoint below.
// query_by is required.
additionalSearchParameters: {
query_by: "title,authors",
},
});
const searchClient = typesenseInstantsearchAdapter.searchClient as LiteClient;
type Book = {
title: string;
authors: string[];
};
const CustomSearchBox = ({
hitsProps,
...props
}: SearchBoxProps & {
hitsProps?: UseHitsProps<Book>;
}) => {
const { query, refine, clear } = useSearchBox(props);
const { items, results, banner, sendEvent } = useHits<Book>(hitsProps);
return (
<Box width={360} p={1}>
<Autocomplete
options={items}
getOptionLabel={(option) => option.title}
onInputChange={(event, newInputValue) => {
refine(newInputValue);
}}
renderInput={(params) => <TextField {...params} />}
/>
</Box>
);
};
const Page = () => {
return (
<>
<h1>{"Books"}</h1>
<InstantSearch searchClient={searchClient} indexName="books">
<CustomSearchBox />
</InstantSearch>
</>
);
};
export default Page;
這樣就能有一個方便統一樣式的搜尋功能了。
在多加一個標註匹配字串的功能,利用 Instasearch 的 Highlight 元件。
hit 都會內涵 highlight 資訊,可以用 Highlight 元件將 hit 換成用 <mark>
標記好匹配字的字串,所以可以針對 <mark>
做樣式改變。
import {
Highlight,
useSearchBox,
type SearchBoxProps,
useHits,
type UseHitsProps,
} from "react-instantsearch";
const CustomSearchBox = ({
hitsProps,
...props
}: SearchBoxProps & {
hitsProps?: UseHitsProps<Book>;
}) => {
const { query, refine, clear } = useSearchBox(props);
const { items, results, banner, sendEvent } = useHits<Book>(hitsProps);
return (
<Box width={360} p={1}>
<Autocomplete
options={items}
getOptionLabel={(option) => option.title}
onInputChange={(event, newInputValue) => {
refine(newInputValue);
}}
renderInput={(params) => <TextField {...params} />}
renderOption={(props, option) => (
<Box
component="li"
{...props}
sx={{
"& mark": {
backgroundColor: "unset",
color: "red",
},
}}
>
<Highlight attribute="title" hit={option} />
</Box>
)}
/>
</Box>
);
};
自己做這種搜尋輸入框還挺頭痛的,有這些方便的元件真是太好了。