Могу ли я легко реализовать полнотекстовый поиск на статическом веб-сайте на стороне клиента без базы данных?

#javascript #npm #full-text-search #refactoring #static-site

Вопрос:

Я где-то нашел этот сценарий… возможно, он был в исходном коде npm на самом деле… не уверен, все, что я знаю, это то, что я не писал это сам.. но, глядя на это, я не могу не задаться вопросом, может ли он или аналогичный рефакторинг следующего кода позволить быстрый обход статического сайта и вернуть список URL-адресов, которые ведут к страницам, которые имеют наибольшее количество просмотров по поисковому запросу… Мне не нужно ничего необычного, такого как нечеткий поиск, и я не прошу кого-либо написать код для меня так сильно, как мне бы хотелось, чтобы вторая (или третья) пара глаз посмотрела на этот код и решила, есть ли в этом какой-либо потенциал для реализации простого полнотекстового поиска.

 
const fs = require("fs");
const path = require("path");
const npm = require("./npm.js");
const color = require("ansicolors");
const output = require("./utils/output.js");
const usageUtil = require("./utils/usage.js");
const { promisify } = require("util");
const glob = promisify(require("glob"));
const readFile = promisify(fs.readFile);
const didYouMean = require("./utils/did-you-mean.js");
const { cmdList } = require("./utils/cmd-list.js");

const usage = usageUtil("help-search", "npm help-search <text>");
const completion = require("./utils/completion/none.js");

const npmUsage = require("./utils/npm-usage.js");

const cmd = (args, cb) =>
  helpSearch(args)
    .then(() => cb())
    .catch(cb);

const helpSearch = async (args) => {
  if (!args.length) throw usage;

  const docPath = path.resolve(__dirname, "..", "docs/content");

  const files = await glob(`${docPath}/*/*.md`);
  const data = await readFiles(files);
  const results = await searchFiles(args, data, files);
  // if only one result, then just show that help section.
  if (results.length === 1) {
    return npm.commands.help([path.basename(results[0].file, ".md")], (er) => {
      if (er) throw er;
    });
  }

  const formatted = formatResults(args, results);
  if (!formatted.trim()) npmUsage(false);
  else {
    output(formatted);
    output(didYouMean(args[0], cmdList));
  }
};

const readFiles = async (files) => {
  const res = {};
  await Promise.all(
    files.map(async (file) => {
      res[file] = (await readFile(file, "utf8"))
        .replace(/^---n(.*n)*?---n/, "")
        .trim();
    })
  );
  return res;
};

const searchFiles = async (args, data, files) => {
  const results = [];
  for (const [file, content] of Object.entries(data)) {
    const lowerCase = content.toLowerCase();
    // skip if no matches at all
    if (!args.some((a) => lowerCase.includes(a.toLowerCase()))) continue;

    const lines = content.split(/n /);

    // if a line has a search term, then skip it and the next line.
    // if the next line has a search term, then skip all 3
    // otherwise, set the line to null.  then remove the nulls.
    for (let i = 0; i < lines.length; i  ) {
      const line = lines[i];
      const nextLine = lines[i   1];
      let match = false;
      if (nextLine) {
        match = args.some((a) =>
          nextLine.toLowerCase().includes(a.toLowerCase())
        );
        if (match) {
          // skip over the next line, and the line after it.
          i  = 2;
          continue;
        }
      }

      match = args.some((a) => line.toLowerCase().includes(a.toLowerCase()));

      if (match) {
        // skip over the next line
        i  ;
        continue;
      }

      lines[i] = null;
    }

    // now squish any string of nulls into a single null
    const pruned = lines.reduce((l, r) => {
      if (!(r === null amp;amp; l[l.length - 1] === null)) l.push(r);

      return l;
    }, []);

    if (pruned[pruned.length - 1] === null) pruned.pop();

    if (pruned[0] === null) pruned.shift();

    // now count how many args were found
    const found = {};
    let totalHits = 0;
    for (const line of pruned) {
      for (const arg of args) {
        const hit =
          (line || "").toLowerCase().split(arg.toLowerCase()).length - 1;

        if (hit > 0) {
          found[arg] = (found[arg] || 0)   hit;
          totalHits  = hit;
        }
      }
    }

    const cmd = "npm help "   path.basename(file, ".md").replace(/^npm-/, "");
    results.push({
      file,
      cmd,
      lines: pruned,
      found: Object.keys(found),
      hits: found,
      totalHits,
    });
  }

  // sort results by number of results found, then by number of hits
  // then by number of matching lines

  // coverage is ignored here because the contents of results are
  // nondeterministic due to either glob or readFiles or Object.entries
  return results
    .sort(
      /* istanbul ignore next */ (a, b) =>
        a.found.length > b.found.length
          ? -1
          : a.found.length < b.found.length
          ? 1
          : a.totalHits > b.totalHits
          ? -1
          : a.totalHits < b.totalHits
          ? 1
          : a.lines.length > b.lines.length
          ? -1
          : a.lines.length < b.lines.length
          ? 1
          : 0
    )
    .slice(0, 10);
};

const formatResults = (args, results) => {
  const cols = Math.min(process.stdout.columns || Infinity, 80)   1;

  const out = results
    .map((res) => {
      const out = [res.cmd];
      const r = Object.keys(res.hits)
        .map((k) => `${k}:${res.hits[k]}`)
        .sort((a, b) => (a > b ? 1 : -1))
        .join(" ");

      out.push(
        " ".repeat(Math.max(1, cols - out.join(" ").length - r.length - 1))
      );
      out.push(r);

      if (!npm.flatOptions.long) return out.join("");

      out.unshift("nn");
      out.push("n");
      out.push("-".repeat(cols - 1)   "n");
      res.lines.forEach((line, i) => {
        if (line === null || i > 3) return;

        if (!npm.color) {
          out.push(line   "n");
          return;
        }
        const hilitLine = [];
        for (const arg of args) {
          const finder = line.toLowerCase().split(arg.toLowerCase());
          let p = 0;
          for (const f of finder) {
            hilitLine.push(line.substr(p, f.length));
            const word = line.substr(p   f.length, arg.length);
            const hilit = color.bgBlack(color.red(word));
            hilitLine.push(hilit);
            p  = f.length   arg.length;
          }
        }
        out.push(hilitLine.join("")   "n");
      });

      return out.join("");
    })
    .join("n");

  const finalOut =
    results.length amp;amp; !npm.flatOptions.long
      ? "Top hits for "  
        args.map(JSON.stringify).join(" ")  
        "n"  
        "—".repeat(cols - 1)  
        "n"  
        out  
        "n"  
        "—".repeat(cols - 1)  
        "n"  
        "(run with -l or --long to see more context)"
      : out;

  return finalOut.trim();
};

module.exports = Object.assign(cmd, { usage, completion });
 

Комментарии:

1. Я не женат на этом сценарии… На самом деле я еще не нашел времени, чтобы полностью понять его…. Он просто служит заполнителем для решения с одним файлом, которое я имею в виду. Очевидно, что если он может возвращать список результатов поиска в формате json, то манипуляция dom для отображения результатов будет проще по сравнению… нет необходимости подробно останавливаться на этой части процесса.

Ответ №1:

В зависимости от того, как структурирован и создан ваш сайт, я не понимаю, почему текстовый поиск на стороне клиента не будет работать. Я бы не рекомендовал сканировать сайт на стороне клиента, поэтому, вероятно, было бы лучше создать файл данных во время сборки, а затем основывать поиск на этом.

Если ваш статический сайт создан с помощью генератора статических сайтов, вы можете заставить генератор статических сайтов создать файл JSON со всем содержимым. В противном случае, если это просто статические ресурсы, вы, вероятно, могли бы создать сценарий для чтения вашего контента и создания файла данных таким образом.

Существует также множество доступных библиотек, которые выполняют поиск объекта JSON, например fuse.js.

Основная проблема при поиске на стороне клиента-это объем текста для поиска. Если у вас много контента, клиенту придется загружать все в память, что может вызвать беспокойство, хотя вам придется протестировать все для вашего конкретного случая использования.