Функциональность поиска — одна из наиболее распространенных функций, которые вы видите в любом цифровом продукте. Я бы не стал использовать продукт, который не содержит панели поиска (учитывая, что панель поиска необходима). Однако создание такой большой поисковой системы, как Google, потребует много времени и энергии и может быть не под силу разработчику-одиночке. Итак, здесь я продемонстрирую простой способ создания поисковой системы для продуктов малого и среднего размера.
Стек
Прежде чем приступить к кодированию, позвольте мне познакомить вас с техническим стеком. Я буду использовать JavaScript как для внешнего, так и для внутреннего интерфейса, а также LunrJS для индексации и поиска по текстовому контенту.
Если вы не слышали о LunrJS, это библиотека полнотекстового поиска, которая немного похожа на Solr, но намного меньше и не такая яркая. Библиотека, написанная на JavaScript как для клиентской, так и для серверной части. LunrJS индексирует текстовый контент в документ JSON. Рабочий пакет LunrJS имеет размер 8,2 КБ, что делает его также подходящим для внешнего интерфейса.
Некоторые из альтернатив Lunr: js-search, flexsearch, fuse, wade.
Поток
Чтобы интегрировать функцию поиска в веб-сайт, нам нужны некоторые данные. Мы будем искать конкретную информацию из этого озера данных (ну, пока что это небольшое озеро). Для хранения данных мы можем использовать любую из доступных баз данных в зависимости от потребностей проекта. Для этой демонстрации я использую MongoDB (через Mongoose ORM).
Вот как инициализировать соединение с базой данных с помощью Mongoose в бессерверной среде:
import mongoose from "mongoose";
let mongoDBConn: mongoose.Connection | null = null;
const connectionStr = process.env.DATABASE_URI;
if (typeof connectionStr !== `string`) {
throw new Error(`database uri: not a string`);
process.exit(1);
}
if (!mongoDBConn) {
mongoose
.connect(connectionStr)
.then((m) => (mongoDBConn = m.connection))
.catch(console.error);
}
Вы можете заметить необычный способ инициализации объекта подключения к базе данных. Я кэширую его внутри переменной. Таким образом, последующий бессерверный вызов сможет повторно использовать его.
function getBlogSchema() {
const BlogCollection = new mongoose.Schema({
title: { type: String, required: true, unique: true },
// rest of the document fields
});
BlogCollection.index({ url: 1, title: 1, description: 1 });
const model = mongoose.model(`Blog`, BlogCollection);
model.syncIndexes();
return model;
}
export const blogModel = mongoose.models.Blog
? mongoose.models.Blog
: getBlogSchema();
Опять же, еще один нетрадиционный способ создания модели базы данных, все благодаря serverless. Поскольку мы кэшировали базу данных в переменную, мы должны проверить, существует ли модель в кеше. Мы не можем воссоздать модель в Mongoose. Попытка сделать это вызовет ошибку.
Двигаясь дальше, мы должны установить пакет lunr
, запустив yarn add lunr
. После этого пришло время настроить lunr
. Начнем с import
s.
import fs from "fs";
import lunr from "lunr";
import { blogModal } from "./path/to/blogModel";
Затем я собираюсь написать несколько вспомогательных функций. Эти функции помогут нам систематически выполнять поиск.
buildSearchIndex
: Как следует из названия, эта функция создаст поисковый индекс. Поисковый индекс — это набор данных, хранящихся на диске или в памяти. Эта коллекция анализируется и хранится таким образом, чтобы запросы к ней становились проще и эффективнее.
export async function buildSearchIndex(): Promise<lunr.Index> {
try {
const docs = await blogModel?.find();
const index = lunr((builder) => {
builder.ref(`_id`);
builder.field(`title`);
// additional fields if any
for (let i = 0; i < docs.length; i++) {
const d = docs[i];
builder.add(d);
}
});
return index;
} catch (err) {
console.log(err);
throw err;
}
}
Давайте разбираться, что происходит. Сначала мы вызываем функцию lunr
, которая принимает обратный вызов. Первый параметр обратного вызова — это объект с именем builder
(передается автоматически lunr
).
Метод builder.ref
используется для ссылки на исходный документ. Назначьте ему уникальное поле. _id
, например.
Метод builder.field
сообщает builder
какое поле индексировать. Добавьте все поля, в которых вы хотите искать.
Наконец, мы добавляем документы в индекс, вызывая метод builder.add
внутри цикла for
. Наконец, мы добавляем документы в индекс, вызывая метод builder.add
внутри цикла for
.
saveSearchIndex
: эта функция сохраняет заданный поисковый индекс в файловой системе. Когда размер озера данных увеличивается, становится неэффективно создавать индекс при каждом вызове API. В таких случаяхlunr
может загрузить заранее созданный индекс с диска.
export function saveSearchIndex(index: lunr.Index) {
try {
fs.writeFileSync(INDEX_PATH, JSON.stringify(index, null, 2), {
encoding: "utf-8",
});
} catch (err) {
console.log(err);
}
}
loadSearchIndex
: эта функция загружает индекс в память, чтобыlunr
мог выполнять над ним операции.
export function loadSearchIndex(): lunr.Index {
try {
const content = fs.readFileSync(INDEX_PATH, {
encoding: `utf-8`,
});
return lunr.Index.load(JSON.parse(content));
} catch (err) {
console.log(err);
throw err;
}
}
deleteSearchIndex
иhasSearchIndex
: эти функции используются для удаления существующего поискового индекса с диска и проверки существования данного индекса.
export function deleteSearchIndexFile() {
return fs.unlinkSync(INDEX_PATH);
}
export function hasSearchIndex() {
return fs.existsSync(INDEX_PATH);
}
Теперь, когда у нас есть все вспомогательные функции, мы можем приступить к реализации этой функции. Внутри файла конечной точки API мы собираемся инициализировать индекс lunr
.
Стоит отметить, что мы должны обновлять индекс через определенный период. В противном случае в индекс будут включены не все данные из базы данных.
let searchIndex: lunr.Index;
let indexBuiltAt: Date;
const TEN_MIN_IN_MILI = 600000;
В приведенном выше фрагменте кода я объявил несколько переменных. Переменная indexBuiltAt
хранит отметку времени самой последней сборки. Основываясь на этой метке времени, я буду обновлять файл index.
function createSearchIndex() {
buildSearchIndex()
.then((index) => {
searchIndex = index;
saveSearchIndex(index);
indexBuiltAt = new Date();
})
.catch(console.log);
}
Приведенная выше функция создает индекс поиска и сохраняет его в объявленных ранее переменных.
Наконец, пришло время склеить все вместе и сделать это рабочим решением.
Следующий кодовый блок в значительной степени объясняет сам себя. Я использовал setImmediate
, чтобы он не блокировал основной цикл событий.
setImmediate(() => {
if (hasSearchIndex()) {
searchIndex = loadSearchIndex();
} else {
createSearchIndex();
}
setInterval(() => {
// reload search index at every 10 mins
if (
indexBuiltAt &&
indexBuiltAt?.getTime() + TEN_MIN_IN_MILI < new Date().getTime()
) {
if (hasSearchIndex()) {
searchIndex = loadSearchIndex();
} else {
createSearchIndex();
}
}
}, 30 * 1000);
});
На данный момент все сделано. И мы готовы выполнять запросы по этому индексу. Чтобы выполнить запрос с использованием lunr
, мы должны вызвать метод search
.
const ids = [];
const result = searchIndex.search(`*${search.split(` `).join(`*`)}*`);
for (let i = 0; i < result.length; i++) {
const doc = result[i];
mongoose.isValidObjectId(doc.ref) && ids.push(doc.ref);
}
Я собираю все совпадающие id
в массив. Используя эти id
, я получу фактические документы и отправлю их в качестве ответа API.
Заключение
Эта настройка идеальна, если ваш продукт относительно небольшой (и у вас нет большого объема данных для выполнения операций). Я использовал ту же настройку в одном из проектов, которые я построил. Это можно значительно улучшить. Например, вы можете создавать поисковый индекс каждый раз, когда в базе данных появляется новая запись.
Для получения дополнительной информации о lunr
посетите официальный сайт. В него встроено много других полезных вещей.
Первоначально опубликовано на https://www.abdus.net.
Создавайте компонуемые приложения
Не создавайте веб-монолиты. Используйте Bit для создания и компоновки несвязанных программных компонентов — в ваших любимых фреймворках, таких как React или Node. Создавайте масштабируемые и модульные приложения с мощными и приятными возможностями разработки.
Перенесите свою команду в Bit Cloud, чтобы совместно размещать и совместно работать над компонентами, а также значительно ускорить, масштабировать и стандартизировать разработку в команде. Начните с компонуемых интерфейсов, таких как Design System или Micro Frontends, или исследуйте компонуемый сервер. Попробуйте →