Создание поисковой системы, устойчивой к опечаткам, эффективной и результативной, является чрезвычайно сложной задачей. Опечатка может привести к неудаче поиска, даже если нужный элемент находится в базе данных. Устранив необходимость создавать поисковую систему с нуля, Typesense может сэкономить много времени и усилий. Пользователи также смогут успешно использовать поисковый инструмент приложения, что приведет к положительному пользовательскому опыту. Typesense — это бесплатная поисковая система с открытым исходным кодом для программистов, устойчивая к опечаткам, цель которой — сократить время, необходимое для выполнения эффективного и результативного поиска. Чтобы узнать больше о typesense =>.Что такое Typesense, и почему это такой замечательный инструмент?
В этом руководстве вы узнаете, как установить Typesense, как создать приложение Typesense с нуля, как предварительно настроить клиент Typesense и многое другое. В этой статье также показано, как создать коллекцию Typesense. Наконец, мы запустим нашу программу, добавим новый элемент в коллекцию и выполним поиск по индексированным данным/коллекции.
Давайте начнем. Целью этой статьи является создание приложения типа мгновенного поиска, также известного как «поиск по мере ввода», что означает, что когда вы что-то набираете, результаты появляются мгновенно, обеспечивая приятный пользовательский опыт. Итак, в предыдущей статье мы создали простое javascript-приложение Typesense Booksearch, а в этой статье мы создадим Anime search
, но с использованием набора данных Animes, а также с помощью react.js, с целью просто показать вам, как это сделать, используя самый популярный фреймворк или библиотеку пользовательского интерфейса.Итак, давайте начнем с нашего React js
приложения с Javascript
. Для этого просто следуйте приведенным ниже инструкциям.
Настройка нашего приложения React
Мы начнем с использования create-react-app
для настройки нашего фронтенда. Мы создадим пользовательский интерфейс и его функции с нуля. Давайте сразу же приступим к работе над нашим приложением.
Настройка загрузки приложения react с помощью CRA
Давайте начнем с части react и приступим к его сборке. Первое, что вам нужно сделать, это установить Node.js
, если он еще не установлен на вашем компьютере. Итак, перейдите на официальный сайт Node.js и скачайте последнюю версию. Node js необходим для того, чтобы использовать менеджер пакетов node, известный как npm
. Теперь откройте папку в выбранном вами редакторе кода. Мы будем использовать редактор кода VScode для этой статьи-учебника. Затем откройте встроенный терминал и введите npx create-react-app
. Эта команда создаст приложение react в текущей директории.
Обычно на его установку уходит всего несколько минут. Обычно мы используем npm для получения пакетов в проект, но в данном случае мы воспользуемся npx, программой для запуска пакетов, которая загрузит и настроит все за нас, чтобы мы могли сразу же приступить к работе с отличным шаблоном. Пришло время запустить наш сервер разработки, поэтому запустите npm start
, и браузер мгновенно откроет react-app.
Вот так сразу появляется шаблон boilerplate. Теперь пришло время исследовать структуру файлов и папок, предоставляемую create-react-app. Есть папка node module, которая содержит все наши зависимости от node. Затем есть папка public, где единственное, что имеет значение, это файл index.html. Это стандартный HTML-файл с тегами head, body и meta. Вы заметите div с id root внутри нашего тега body, за которым следует тег fallback noscript, который будет виден, только если в браузере пользователя отключен javascript.
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React practice</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
Вы, наверное, задаетесь вопросом, откуда берется контент. Помните, что весь наш исходный код содержится в папке source или src, и react будет внедрять его в корневой элемент div. Давайте посмотрим на нашу папку src, которая содержит некоторые таблицы стилей, файлы javascript и файлы SVG.
Теперь перейдите к нашему файлу App.js.
// App.js
import logo from "./logo.svg";
import "./App.css";
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
В данном случае мы просто импортируем react из react и logo из нашего logo с помощью стандартного javascript. После этого у нас есть обычная функция javascript под названием APP, а эта функция в react известна как функциональный компонент, и эта функция возвращает react-элемент, который выглядит как HTML, но на самом деле является jsx, как вы можете видеть, здесь есть тег div с className APP, и мы не можем сказать class сам по себе, потому что class является зарезервированным словом в javascript, поэтому в jsx мы должны использовать className. После этого у нас есть заголовок, а затем изображение, и обратите внимание на источник изображения, что у нас есть наш логотип, который на самом деле является переменной javascript, которую мы импортировали сверху, поэтому, чтобы использовать javascript в JSX, мы должны окружить его фигурными скобками, а затем у нас есть параграф, тег якоря, и это все для этого компонента.
ПРИМЕЧАНИЕ: Благодаря экспорту мы можем извлечь компонент и разместить его на веб-странице. Export появляется в нижней части файла app.js, указывая на то, что мы экспортируем функцию App.
Итак, теперь давайте посмотрим на файл index.js.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
reportWebVitals();
Итак, в данном случае мы снова импортируем react из react, и на этот раз мы также импортируем react-dom, затем мы импортируем файл таблицы стилей CSS, и, наконец, мы импортируем App из App.js, который является файлом, о котором мы только что говорили, и в нем есть service worker, который используется для того, чтобы ваше приложение работало полностью автономно. Затем мы вызываем ReactDom.render, который принимает два параметра. Первый параметр — это объект jsx, а в jsx мы можем включать наши пользовательские компоненты, поэтому react strict mode — это компонент, определенный реактом, а App — это компонент, определенный пользователем, а второй параметр — document.getElementById(‘root’), который указывает на корневой div в нашем файле index.html и является способом доступа к содержимому нашей веб-страницы.
Примечание: ReactDom рендерит наш контент в корневой div, расположенный в нашем файле index.html.
Очистка файлов React boilerplate
Сначала мы должны привести в порядок наши проекты, удалив некоторые файлы, предоставляемые create-react-app, прежде чем мы сможем приступить к их созданию. После очистки файлов и папок они должны выглядеть следующим образом.
Добавление и установка некоторых пакетов
Нам понадобится установить несколько сторонних пакетов для этого проекта. Поэтому скопируйте и вставьте в терминал следующую команду
Установка typesense
Это будет наш основной пакет typesense.
npm install typesense
Установка typesense-instantsearch-adapter
Этот пакет позволит нам использовать пользовательский интерфейс instantsearch, а этот адаптер, по сути, подключит его к typesense, поскольку instantsearch.js
создан algolia, а typesense создал этот адаптер, чтобы перенести его функциональность и возможности в сам пакет typesense
.
npm install typesense-instantsearch-adapter
Установка styled-components
Этот пакет позволит вам писать актуальные CSS внутри вашего JavaScript проекта.
npm install styled-components
Установка instantsearch.css
Вместо того чтобы создавать все с нуля, этот пакет предоставит готовые стили, такие как окно поиска и множество стилей пользовательского интерфейса.
npm install instantsearch.css
Установка react-instantsearch-dom
Этот пакет представляет собой React-версию библиотеки instantsearch.js
от Algolia, которая предоставит нам компоненты, необходимые для рендеринга в наших проектах.
Этот пакет будет
npm install react-instantsearch-dom
Наконец, ваш файл package.json
должен выглядеть следующим образом после установки всех зависимостей вашего проекта.
{
"name": "anime-searchapp-reactjs-typesense",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",
"instantsearch.css": "^7.4.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-instantsearch-dom": "^6.22.0",
"react-scripts": "5.0.0",
"styled-components": "^5.3.3",
"typesense": "^1.1.3",
"typesense-instantsearch-adapter": "^2.3.0",
"web-vitals": "^2.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": ["react-app", "react-app/jest"]
},
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Теперь, когда наш проект и зависимости установлены, мы можем приступить к работе. Во-первых, нам нужно импортировать данные animes
, поэтому создайте папку dataset и внутри нее создайте свой собственный файл anime list json, заполнив его всей необходимой информацией о продукте, или скачайте anime dataset отсюда. В итоге структура папок и набор данных должны выглядеть примерно так.
Итак, чтобы заполнить наш индекс Typesense, мы должны сначала запустить наш сервер Typesense, и есть несколько способов сделать это. Есть старый программный способ, который требует, чтобы вы установили все, запустили его и предоставили конфигурацию с помощью кода, или есть один потрясающий метод, который является предпочтительным методом, который очень устойчив и, как известно, работает, и который используется в образе docker и запускает контейнер docker, который в основном связывает и дает ему том, который будет подключаться к месту, где вы будете хранить данные, и это все, что есть в этом.Так что это по существу то, что мы будем использовать в этой статье.
Давайте начнем с создания нового скрипта и папки под названием scripts
, потому что здесь у нас будет несколько скриптов, которые позволят нам либо индексировать данные, либо запускать сервер typesense или контейнер docker, поэтому вы можете поместить их в тег script run в package.json
. Чтобы каждый раз, когда вы захотите запустить сервер, вы могли просто открыть командную строку и запустить команду npm.
Внутри папки scripts
создайте новый файл runServer.js
. Итак, этот скрипт по сути будет выполнять команду docker, но перед этим мы должны его настроить. Структура папок должна выглядеть примерно так.
Самый первый шаг — это выполнение команды из дочернего процесса node.js.
// runServer.js
const { exec } = require("child_process");
Далее давайте настроим команду. Мы будем использовать docker -run и detach для запуска docker в фоновом режиме, а затем назначим порт. Самым важным разделом этой команды является раздел тома. Здесь мы привязываем том, который по сути является способом хранения данных typesense, которые по умолчанию находятся внутри самого контейнера. Таким образом, typesense будет хранить данные в папке данных контейнера под названием /data
, и он будет раскрывать и связывать эту папку /data
в папку, которую мы назначим в нашей системе, в данном случае это будет каталог tmp
. Такой подход поможет сохранить данные последовательными и обеспечит их устойчивость, и мы всегда будем сохранять данные, так что если контейнер docker будет удален, остановлен или произойдет что-то подобное, мы просто сохраним наши данные в безопасном месте. Указывая том -v
, мы просто указываем, где мы хотим хранить данные. После этого нам нужно указать, какой образ должен быть запущен внутри нашего контейнера docker, поэтому мы будем использовать typesense/typesense:0.22.2
, но вы можете использовать свой собственный образ, а затем указать каталог данных и api ключ, который вы можете указать как угодно, и передать порт прослушивания, и, наконец, включить CORS, чтобы у нас не было проблем, связанных с CORS
. Вот как должен выглядеть ваш код.
// runServer.js
const { exec } = require("child_process");
const command = `docker run -d -p 8108:8108 -v/tmp/typesense-server-data/:/data typesense/typesense:0.22.2 --data-dir /data --api-key=animesearch --listen-port 8108 --enable-cors`;
Наконец, мы должны запустить команду и просто создать обработчик ошибок на случай, если во время выполнения команд произойдет ошибка. В результате ваш окончательный код «runServer» должен выглядеть следующим образом.
// runServer.js
const { exec } = require("child_process");
const command = `docker run -d -p 8108:8108 -v/tmp/typesense-server-data/:/data typesense/typesense:0.22.2 --data-dir /data --api-key=animesearch --listen-port 8108 --enable-cors`;
exec(command, (err) => {
if (!err) console.log("Typesense Server is up and running...✰✨");
if (err) {
console.log("Error running server: ", err);
}
});
Теперь, когда у нас есть готовый сценарий runServer.js
, мы можем просто обновить тег сценария в нашем файле package.json
. Наконец, после обновления сценария ваш файл package.json
должен выглядеть следующим образом.
// package.json
{
"name": "anime-searchapp-reactjs-typesense",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",
"instantsearch.css": "^7.4.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-instantsearch-dom": "^6.22.0",
"react-scripts": "5.0.0",
"styled-components": "^5.3.3",
"typesense": "^1.1.3",
"typesense-instantsearch-adapter": "^2.3.0",
"web-vitals": "^2.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"start-server": "node scripts/runServer.js"
},
"eslintConfig": {
"extends": ["react-app", "react-app/jest"]
},
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Наконец, запустите сервер, набрав npm run start-server
, и ваш docker запустится, после чего вы сможете перейти к следующему шагу индексирования данных/коллекции.
Следующим шагом будет фактическое заполнение или индексирование данных на сервере typesense, так что теперь, когда у нас есть запущенный сервер и куча данных аниме, это набор данных, который мы хотим заполнить на сервере typesense, чтобы позже в нашем пользовательском интерфейсе мы могли запросить сервер typesense и выполнить мгновенный поиск, который будет невероятно быстрым.Итак, чтобы сделать это, давайте начнем писать скрипты импорта данных. Начнем с создания файла loadData.js
в папке scripts
, которую мы создали ранее, в котором мы инициализируем клиента typesense.
Индексирование данных в typesense: пошаговое руководство
Первый шаг: Нам необходимо импортировать библиотеку Typesense
в наш проект.
// loadData.js
const Typesense = require("typesense");
Второй шаг: Давайте сделаем самоисполняющуюся функцию, которая будет запускаться всякий раз, когда мы запускаем скрипт, и сделаем ее асинхронной, чтобы мы могли использовать функцию async await. Просто создайте ‘module.export’ и экспортируйте в него самоисполняющуюся функцию и сделайте ее асинхронной, чтобы мы могли сделать скрипт, который считывает данные и выполняет сбор, управляет сервером typesense и индексирует данные. Итак, первое, что нам нужно сделать, это настроить клиент typesense, чтобы мы могли подключиться к серверу и начать управлять, индексировать и извлекать данные.
Поэтому сначала создайте переменную конфигурации typesense и передайте ей свойства nodes
. Это позволяет вам иметь несколько узлов для одного сервера, например, клиент может подключаться к нескольким узлам, а узлы — это, по сути, просто серверы, поэтому конкретный nodes
— это массив, который содержит фактическую конфигурацию для каждого сервера, к которому вы хотите подключиться и к которому вы хотите, чтобы клиент имел доступ, поэтому в настоящее время у нас работает только один сервер, поэтому мы будем использовать только один nodes
. Далее, внутри массива nodes
укажите хост, порт typesense и протокол, который он использует, а также api ключ.
// loadData.js
const Typesense = require("typesense");
module.exports = (async () => {
const TYPESENSE_CONFIG = {
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
apiKey: "animesearch",
};
})();
Третий шаг: Давайте используем конфигурацию typesense для создания клиента Typesense.
// loadData.js
const Typesense = require("typesense");
module.exports = (async () => {
const TYPESENSE_CONFIG = {
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
apiKey: "animesearch",
};
console.log("Config: ", TYPESENSE_CONFIG);
const typesense = new Typesense.Client(TYPESENSE_CONFIG);
})();
Четвертый этап: Это решающий этап, поскольку именно здесь мы предоставляем schema
, по которой наши данные будут индексироваться в реальной базе данных typesense, поэтому schema
является довольно критичным. Это довольно простой синтаксис, и с ним очень просто работать. Схема — это то, где вы описываете, как будут сохранены ваши данные. Для нашей schema
у нас есть название, синопсис, жанр, эфир, популярность, рейтинг, оценка, img url и ссылки. Поэтому в schema
нужно поместить только те поля, которые вы хотите индексировать. Если вы знакомы с базами данных nosql, особенно с mongodb, то этот подход во многом вдохновлен этим. Если вы знакомы, например, с mongoose ORM (Object Relational Model)
: как она работает и как вы можете получать данные и коллекции, она в основном работает так же, и typesense имеет именно эту особенность. Так что это в основном как база данных nosql. Она немного сложнее, но вы можете думать о ней так, чтобы получить общее впечатление о ней и понять, как поступают данные, и как все это организовано вместе.Начните с того, что дайте схеме имя и убедитесь, что количество документов установлено на ноль. Затем добавьте поля, которые будут представлять собой массив объектов, содержащих все поля, которые мы хотим индексировать и хранить в нашей базе данных, поэтому укажите имя, тип и фасет. Если вам интересно, что такое фасет, то это функция, которая позволяет определять категории на основе подмножества атрибутов, чтобы пользователи могли сузить результаты поиска. Вот как должна выглядеть ваша схема.
// loadData.js
const Typesense = require("typesense");
module.exports = (async () => {
const TYPESENSE_CONFIG = {
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
apiKey: "animesearch",
};
console.log("Config: ", TYPESENSE_CONFIG);
const typesense = new Typesense.Client(TYPESENSE_CONFIG);
const schema = {
name: "animes",
num_documents: 0,
fields: [
{
name: "title",
type: "string",
facet: false,
},
{
name: "synopsis",
type: "string",
facet: false,
},
{
name: "genre",
type: "auto",
facet: true,
},
{
name: "genre.lvl0",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl1",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl2",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl3",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl4",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl5",
type: "auto",
facet: true,
optional: true,
},
{
name: "aired",
type: "string",
facet: true,
},
{
name: "popularity",
type: "float",
facet: true,
},
{
name: "ranked",
type: "float",
facet: true,
},
{
name: "score",
type: "string",
facet: true,
},
{
name: "img_url",
type: "string",
facet: true,
},
{
name: "link",
type: "string",
facet: true,
},
],
default_sorting_field: "popularity",
};
})();
Итак, если вы хотите проиндексировать все данные в массиве жанров, например, вам нужно будет хранить каждый уровень массива по своему собственному полю.
Давайте перейдем к нашему набору данных и посмотрим на раздел жанров. Как вы можете видеть на рисунке ниже, в этом массиве четыре элемента, поэтому мы сделаем каждое поле уровнем для каждого из этих элементов.
Пятый шаг: Начнем с чтения фильмов из json-файлов, а затем импортируем набор данных. Теперь самое время вызвать клиент Typesense и установить соединение со схемой внутри него.
// loadData.js
const Typesense = require("typesense");
module.exports = (async () => {
const TYPESENSE_CONFIG = {
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
apiKey: "animesearch",
};
console.log("Config: ", TYPESENSE_CONFIG);
const typesense = new Typesense.Client(TYPESENSE_CONFIG);
const schema = {
name: "animes",
num_documents: 0,
fields: [
{
name: "title",
type: "string",
facet: false,
},
{
name: "synopsis",
type: "string",
facet: false,
},
{
name: "genre",
type: "auto",
facet: true,
},
{
name: "genre.lvl0",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl1",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl2",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl3",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl4",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl5",
type: "auto",
facet: true,
optional: true,
},
{
name: "aired",
type: "string",
facet: true,
},
{
name: "popularity",
type: "float",
facet: true,
},
{
name: "ranked",
type: "float",
facet: true,
},
{
name: "score",
type: "string",
facet: true,
},
{
name: "img_url",
type: "string",
facet: true,
},
{
name: "link",
type: "string",
facet: true,
},
],
default_sorting_field: "popularity",
};
const animes = require("../dataset/animes.json");
try {
const collection = await typesense.collections("animes").retrieve();
console.log("Found existing collection of animes");
console.log(JSON.stringify(collection, null, 2));
} catch (err) {
console.error(err);
}
})();
Шестой шаг: Если при загрузке данных возникает ошибка (ошибка дублирования данных), просто добавьте следующий фрагмент кода в файл loadData.js
перед созданием schema
, потому что он просто удалит существующие данные и заполнит их новыми.
if (collection.num_documents !== animes.length) {
console.log("Collection has diff number of docs than data");
console.log("Deleting collection");
await typesense.collections("animes").delete();
}
Седьмой шаг: Создание коллекции с именем animes
. В Typesense коллекция — это набор связанных документов, который функционирует аналогично таблице в реляционной базе данных. Мы даем коллекции имя и описываем поля, которые будут индексироваться при добавлении документа в коллекцию, когда мы ее создаем.
Ваш окончательный код внутри файла loadData.js
должен выглядеть следующим образом.
// loadData.js
const Typesense = require("typesense");
module.exports = (async () => {
const TYPESENSE_CONFIG = {
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
apiKey: "animesearch",
};
console.log("Config: ", TYPESENSE_CONFIG);
const typesense = new Typesense.Client(TYPESENSE_CONFIG);
const schema = {
name: "animes",
num_documents: 0,
fields: [
{
name: "title",
type: "string",
facet: false,
},
{
name: "synopsis",
type: "string",
facet: false,
},
{
name: "genre",
type: "auto",
facet: true,
},
{
name: "genre.lvl0",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl1",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl2",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl3",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl4",
type: "auto",
facet: true,
optional: true,
},
{
name: "genre.lvl5",
type: "auto",
facet: true,
optional: true,
},
{
name: "aired",
type: "string",
facet: true,
},
{
name: "popularity",
type: "float",
facet: true,
},
{
name: "ranked",
type: "float",
facet: true,
},
{
name: "score",
type: "string",
facet: true,
},
{
name: "img_url",
type: "string",
facet: true,
},
{
name: "link",
type: "string",
facet: true,
},
],
default_sorting_field: "popularity",
};
const animes = require("../dataset/animes.json");
try {
const collection = await typesense.collections("animes").retrieve();
console.log("Found existing collection of animes");
console.log(JSON.stringify(collection, null, 2));
if (collection.num_documents !== animes.length) {
console.log("Collection has diff number of docs than data");
console.log("Deleting collection");
await typesense.collections("animes").delete();
}
} catch (err) {
console.error(err);
}
console.log("Creating schema...");
console.log(JSON.stringify(schema, null, 2));
await typesense.collections().create(schema);
console.log("Populating collection data...");
try {
const returnData = await typesense
.collections("animes")
.documents()
.import(animes);
console.log("Return data: ", returnData);
} catch (err) {
console.error(err);
}
})();
Теперь, когда у нас есть готовый сценарий loadData.js
, мы можем просто обновить тег сценария в нашем файле package.json
. Наконец, после обновления сценария ваш файл package.json
должен выглядеть следующим образом.
// package.json
{
"name": "anime-searchapp-reactjs-typesense",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",
"instantsearch.css": "^7.4.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-instantsearch-dom": "^6.22.0",
"react-scripts": "5.0.0",
"styled-components": "^5.3.3",
"typesense": "^1.1.3",
"typesense-instantsearch-adapter": "^2.3.0",
"web-vitals": "^2.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"start-server": "node scripts/runServer.js",
"indexer": "node scripts/loadData.js"
},
"eslintConfig": {
"extends": ["react-app", "react-app/jest"]
},
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Наконец, запустите индексацию данных на сервере typesense, набрав npm run indexer
, и ваши данные начнут заполняться на сервере typesense.
Создание пользовательского интерфейса и получение данных
Давайте начнем с создания пользовательского интерфейса. Наш фронтенд, или пользовательский интерфейс, должен будет подключаться к серверу typesense и выполнять запросы. Сервер Typesense является стандартным и гибким, как и любой другой HTTP-сервер. Вы можете отправить HTTP-запрос, который мы также будем использовать здесь, и клиент просто отправит обычный HTTP-запрос, и он выполнит поиск за вас. В принципе, именно так будет происходить взаимодействие между клиентом и сервером typesense.
Итак, прежде чем выполнять любую работу с пользовательским интерфейсом, отображать или рендерить какие-либо компоненты, мы должны сначала подключиться к серверу и предоставить конфигурацию, которую мы выполнили ранее. Теперь мы можем окончательно настроить проект для использования Typesense. У нас есть наш экземпляр typesense, запущенный в фоновом режиме. Чтобы заставить React использовать адаптер Typesense, откройте файл src/app.js
и сначала создайте соединение. Внутри него создайте объект TypesenseInstantsearchAdapter
и добавьте server
в качестве ключа. Внутри него передайте apiKey
и nodes
, а внутри nodes
укажите host
, port
и protocol
. Наконец, добавьте к нему дополнительный параметр поиска и передайте query
и queryByWeight
, в соответствии с которыми вы хотите отобразить проиндексированный документ/данные. (Помните, что эти параметры передаются непосредственно в конечную точку API поиска Typesense. Таким образом, через нее можно передавать любые параметры, поддерживаемые конечной точкой поиска).
Ниже приведен пример того, как должен выглядеть ваш код.
import React, { useState } from "react";
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "animesearch",
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
},
additionalSearchParameters: {
queryBy: "titles,synopsis,genre",
queryByWeights: "4,2,1",
numTypos: 3,
typoTokensThreshold: 1,
},
});
const App = () => {
return (
<>
<div>App</div>
</>
);
};
export default App;
Теперь, когда мы завершили настройку, давайте перейдем к созданию интерфейса для нашего приложения. Для этого сначала импортируйте компонент InstantSearch
из библиотеки react-instantsearch-dom
и передайте indexName
и searchClient
в качестве props этому компоненту.
// app.js
import React, { useState } from "react";
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
import { InstantSearch } from "react-instantsearch-dom";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "animesearch",
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
},
additionalSearchParameters: {
queryBy: "titles,synopsis,genre",
queryByWeights: "4,2,1",
numTypos: 3,
typoTokensThreshold: 1,
},
});
const App = () => {
return (
<>
<InstantSearch
indexName="animes"
searchClient={typesenseInstantsearchAdapter.searchClient}
></InstantSearch>
</>
);
};
export default App;
Если вы запустите свое приложение » react.js», оно покажется пустым.
Прежде чем мы погрузимся в интеграцию поисковой панели, давайте оформим наше приложение и выделим секцию поиска, а также добавим немного стиля в наш интерфейс, поэтому просто следуйте приведенному ниже коду и оберните его внутри компонента InstantSearch
.
const App = () => {
return (
<>
<InstantSearch
indexName="animes"
searchClient={typesenseInstantsearchAdapter.searchClient}
>
<div className="search-container">
<aside className="results-section"></aside>
<main>Search/result section</main>
</div>
</InstantSearch>
</>
);
};
Теперь давайте добавим компоненты SearchBox
и Hits
из библиотеки react-instantsearch-dom
, чтобы мы могли напрямую внедрить эти компоненты в наше приложение.
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
import ResultTemplate from "./components/ResultTemplate";
import "./App.css";
import {
InstantSearch,
SearchBox,
Hits,
Configure,
Pagination,
SortBy,
Panel,
RefinementList,
} from "react-instantsearch-dom";
import "instantsearch.css/themes/satellite.css";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "animesearch",
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
},
additionalSearchParameters: {
queryBy: "title,synopsis,genre",
queryByWeights: "4,2,1",
numTypos: 3,
typoTokensThreshold: 1,
},
});
const App = () => {
return (
<>
<h1 className="super-title">
?????Anime search application built using react???
</h1>
<InstantSearch
indexName="animes"
searchClient={typesenseInstantsearchAdapter.searchClient}
>
<Configure hitsPerPage={12} />
<div className="search-container">
<aside className="results-section"></aside>
<main>
<SearchBox />
<div className="searchbox-gap"></div>
<Hits />
</main>
</div>
</InstantSearch>
</>
);
};
export default App;
Просто повторно запустите приложение после исправления, и теперь ваше приложение должно выглядеть следующим образом.
На данный момент наши данные отображаются в формате json, как и в нашем индексе. Давайте представим данные в более привлекательном виде, поэтому создадим новую папку с компонентами и внутри этой папки создадим новый файл под названием ResultTemplate.js
и передадим ему реквизит hit
. Наконец, покажите название аниме, просто передав {hit.title}
и обернув его в простой тег div
.
// components/ResultTemplate
import React from "react";
const ResultTemplate = ({ hit }) => {
return (
<>
<div>{hit.title}</div>
</>
);
};
export default ResultTemplate;
После того как вы закончили создание компонента, просто импортируйте его в файл App.js и передайте компоненту Hit.
<Hits hitComponent={ResultTemplate} />
Просто повторно запустите приложение после исправления, и оно должно выглядеть следующим образом.
Итак, давайте добавим немного стилизации в наше приложение, чтобы результаты отображались в формате сетки, поэтому перейдите на главную страницу приложения и проверьте там элемент.
Концепция этой библиотеки reactInstanceSearch заключается в том, что эти элементы имеют предопределенные имена классов, как вы можете видеть, каждый элемент имеет ais-Hits
, ais-Hits-lists
и затем у вас есть ais-Hits-items
. поэтому нам просто нужно изменить стили, и поскольку мы используем ванильный подход css, мы будем использовать сетку из четырех колонок, поэтому давайте сохраним приложение и просто запустим его снова.
/* App.css */
.ais-Hits-list {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1rem;
}
Также не забудьте внести некоторые изменения в стили ResultTemplate
перед повторным запуском приложения.
// components/ResultTemplate
import React from "react";
const ResultTemplate = ({ hit }) => {
return (
<>
<div className="anime-container">
<h3 className="anime-wrapper">{hit.name}</h3>
</div>
</>
);
};
export default ResultTemplate;
.anime-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 2rem;
}
.anime-wrapper {
border-radius: 1rem;
}
и теперь приложение должно выглядеть примерно так.
Пришло время отобразить некоторые изображения наших аниме-фильмов, поэтому мы воспользуемся тегом «img» и просто передадим{hit.image_url}
в качестве источника изображения, а затем стили.
// components/ResultTemplate
import React from "react";
const ResultTemplate = ({ hit }) => {
return (
<>
<div className="anime-container">
<div className="anime-wrapper">
<img className="anime-image" src={hit.img_url} alt="movie" />
</div>
<h3>{hit.name}</h3>
</div>
</>
);
};
export default ResultTemplate;
Стили
/* App.css */
.anime-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 2rem;
}
.anime-wrapper {
border-radius: 1rem;
}
.anime-image {
width: 100%;
height: 150px;
object-fit: cover;
}
.ais-Hits-list {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1rem;
}
Пришло время настроить список хитов
, который будет отображаться на вашей странице, просто добавьте компонент Configure и укажите опцию hitsPerPage
, чтобы настроить список хитов
, который вы хотите отображать на вашей странице. Наконец, используйте виджеты Pagination
, предлагаемые библиотекой react-instantsearch-dom
, чтобы добавить пагинацию. Также попробуем добавить фасеты. Для этого добавим виджет, который в библиотеке react-instanctsearch-dom
называется RefinementList
, затем определим атрибут, который мы хотим взять, в нашем случае это будет genre
, а также для разметки добавим компонент Panel
. Наконец, снова запустите приложение. В результате ваш готовый код должен выглядеть следующим образом…
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
import ResultTemplate from "./components/ResultTemplate";
import "./App.css";
import {
InstantSearch,
SearchBox,
Hits,
Configure,
Pagination,
Panel,
RefinementList,
} from "react-instantsearch-dom";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "animesearch",
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
},
additionalSearchParameters: {
queryBy: "title,synopsis,genre",
queryByWeights: "4,2,1",
numTypos: 3,
typoTokensThreshold: 1,
},
});
const App = () => {
return (
<>
<h1 className="super-title">
?????Anime search application built using react???
</h1>
<InstantSearch
indexName="animes"
searchClient={typesenseInstantsearchAdapter.searchClient}
>
<Configure hitsPerPage={12} />
<div className="search-container">
<aside className="results-section">
<Panel header="Popularity"></Panel>
<Panel header="Genre">
<RefinementList
attribute="genre"
transformItems={(items) =>
items.map((item) => ({
...item,
label: item.label.slice(2, -2),
}))
}
searchable={true}
showMore={true}
limit={10}
showMoreText="Show more"
showLessText="Show less"
/>
</Panel>
<Panel header="Aired">
<RefinementList attribute="aired" />
</Panel>
</aside>
<main>
<SearchBox />
<div className="searchbox-gap"></div>
<Hits hitComponent={ResultTemplate} />
<Pagination />
</main>
</div>
</InstantSearch>
</>
);
};
export default App;
и следующим образом должно выглядеть ваше приложение:
Наконец, используя те же процедуры, что и раньше, мы можем добавить в приложение возможность сортировки: Добавьте items
с меткой default со значением animes
в виджет/компонент SortBy
из react-instantsearch-dom
, а затем создайте еще одну метку под названием ranked (asc)
со значением animes/sort/popularity: asc
, и еще одну метку ranked (desc)
со значением animes/sort/popularity:desc
.
<SortBy
items={[
{ label: "Default", value: "animes" },
{
label: "ranked (asc)",
value: "animes/sort/popularity:asc",
},
{
label: "ranked (desc)",
value: "animes/sort/popularity:desc",
},
]}
defaultRefinement="animes"
/>
Наконец, обновим шаблон для информации, которую мы хотим отображать в нашем приложении (например, title, img_url и genres), вот как должен выглядеть ваш код.
// components/ResultTemplate
import React from "react";
const ResultTemplate = ({ hit }) => {
return (
<>
<div className="anime-container">
<div className="anime-wrapper">
<a href={hit.link} target="_blank">
<img className="anime-image" src={hit.img_url} alt="movie" />
</a>
</div>
<a href={hit.link} target="_blank">
<h2 className="anime-title">{hit.title}</h2>
</a>
<h3 className="anime-genre">Genres: {hit.genre}</h3>
<p>{hit.synopsis}</p>
</div>
</>
);
};
export default ResultTemplate;
Добавьте в проект еще несколько стилей, чтобы он выглядел еще лучше, поэтому стили приложения должны выглядеть следующим образом.
/* App.css */
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600&family=Poppins:ital,wght@0,100;0,200;0,300;1,100;1,200&display=swap");
* {
font-family: "Poppins", sans-serif;
}
.super-title {
display: flex;
justify-content: center;
}
.search-container {
display: flex;
padding-right: 10px;
}
.results-section {
height: 100vh;
padding-left: 5rem;
padding-right: 1rem;
padding-top: 5rem;
}
.anime-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 2rem;
}
.anime-wrapper {
border-radius: 1rem;
}
.anime-image {
width: 100%;
height: 150px;
object-fit: cover;
}
.searchbox-gap {
padding: 10px;
}
.ais-Hits-list {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1rem;
}
a:link,
a:visited {
text-align: center;
text-decoration: none;
display: inline-block;
}
Наконец, используйте стиль instantsearch.css
библиотеки react instantsearch и добавьте его непосредственно в проект. Таким образом, после того, как вы все интегрировали, исходный код вашего проекта должен выглядеть следующим образом.
// App.js
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
import ResultTemplate from "./components/ResultTemplate";
import "./App.css";
import {
InstantSearch,
SearchBox,
Hits,
Configure,
Pagination,
SortBy,
Panel,
RefinementList,
} from "react-instantsearch-dom";
import "instantsearch.css/themes/satellite.css";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "animesearch",
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
},
additionalSearchParameters: {
queryBy: "title,synopsis,genre",
queryByWeights: "4,2,1",
numTypos: 3,
typoTokensThreshold: 1,
},
});
const App = () => {
return (
<>
<h1 className="super-title">
?????Anime search application built using react???
</h1>
<InstantSearch
indexName="animes"
searchClient={typesenseInstantsearchAdapter.searchClient}
>
<Configure hitsPerPage={12} />
<div className="search-container">
<aside className="results-section">
<Panel header="Popularity">
<SortBy
items={[
{ label: "Default", value: "animes" },
{
label: "ranked (asc)",
value: "animes/sort/popularity:asc",
},
{
label: "ranked (desc)",
value: "animes/sort/popularity:desc",
},
]}
defaultRefinement="animes"
/>
</Panel>
<Panel header="Genre">
<RefinementList
attribute="genre"
transformItems={(items) =>
items.map((item) => ({
...item,
label: item.label.slice(2, -2),
}))
}
searchable={true}
showMore={true}
limit={10}
showMoreText="Show more"
showLessText="Show less"
/>
</Panel>
<Panel header="Aired">
<RefinementList attribute="aired" />
</Panel>
</aside>
<main>
<SearchBox />
<div className="searchbox-gap"></div>
<Hits hitComponent={ResultTemplate} />
<Pagination />
</main>
</div>
</InstantSearch>
</>
);
};
export default App;
Давайте посмотрим на финальную версию нашего приложения для поиска по Аниме, интегрированного в typesense.
Полный исходный код приложения можно найти здесь
Присоединяйтесь к сообществу Aviyel, чтобы узнать больше о проекте с открытым исходным кодом, получить советы о том, как внести свой вклад, и присоединиться к активным группам разработчиков. Aviyel — это платформа для совместной работы, которая помогает сообществам проектов с открытым исходным кодом в монетизации и долгосрочной устойчивости. Чтобы узнать больше, посетите Aviyel.com и найдите отличные блоги и мероприятия, такие как это! Подпишитесь сейчас, чтобы получить ранний доступ, и не забудьте следить за нами в наших социальных сетях!
Следите за @aviyelHQ или зарегистрируйтесь на Aviyel для получения раннего доступа, если вы являетесь сопровождающим проекта, соавтором или просто энтузиастом Open Source.
Присоединяйтесь к Aviyel’s Discord => Aviyel’s world
Twitter =>https://twitter.com/AviyelHq