尽管我最近开始用VNote做读书或读在线文档的笔记,但更多的时候,我把经验型知识都记录在一个名为my_note的Git仓库中。这个仓库中有许多.org文件:

  1. TeX.org,记录与LaTeX相关的问题和解决方法;
  2. asm.org,记录的是与编写汇编语言程序相关的问题和解决办法;
  3. cl.org,记录的是与编写Common Lisp代码相关的问题和解决办法;

这些内容被我称为FAQ。尽管不同的文件记载着不同方面的内容,但它们的格式是一致的:

  1. 每个文件都以org-mode的语法书写;
  2. 文件中只有一级条目,没有嵌套;
  3. 每一个条目的标题就是一个问题的表述,下方的文字则是这个问题的答案。

A picture is worth a thousand words

这些问题都比较常见(不然怎么叫FAQ呢——也许上图的不算常见吧),回过头来查找的机率很高。显然,在纷繁复杂的文字中凭肉眼寻找关键字是低效的,即使是祭出grep,用正则表达式这样的大杀器来查找也不是特别称手——因为并不知道怎样的正则表达式可以匹配到寻找的内容——也许多写了关键词,也许少写了,也许顺序不对。

对于搜索这类非结构的文字资料来说,全文检索是一个更好的选择,因此,我是把这些内容丢进ElasticSearch里再查找的。

解析并导入到ElasticSearch

FAQ中的每一个条目,都对应ElasticSearch中的一个文档,它们都存储在索引faq中。一个文档有如下的字段:

  1. answer,即问题的答案;
  2. path,文件绝对路径,表示文档来自于哪一个文件中的条目;
  3. question,即问题的描述;
  4. questionLineNum,即问题存在于文件的第几行。

解析这些文件的逻辑也很简单:每当读入行首为星号的一行后(这一行即为问题),便继续读入后续的每一行直到再次遇到行首为星号的行为止,这些后续读入的行组成了这个问题的答案。有了问题和答案,便可以导入到ElasticSearch中。最终的脚本如下

const request = require('co-request');

const fs = require('fs');

function parseFaqOrg(path) {
  const content = fs.readFileSync(path).toString('utf-8');
  const lines = content.split('\n');
  const qas = [];
  let answer = [];
  let lineNum = 0;
  let mode;
  let question;
  let questionLineNum;
  for (const line of lines) {
    lineNum += 1;
    if (line.startsWith('*')) {
      if (mode === 'answer') {
        // 在遇到星号的时候模式已经处于answer中,说明在此之前还有未处理的QA
        qas.push({
          answer: answer.join('\n'),
          path,
          question,
          questionLineNum
        });
        answer = [];
        question = null;
      }
      mode = 'question';
    } else {
      mode = 'answer';
    }
    if (mode === 'answer') {
      answer.push(line);
    } else {
      question = line;
      questionLineNum = lineNum;
    }
  }
  if (question) {
    qas.push({
      answer: answer.join('\n'),
      question
    });
  }
  // console.log(JSON.stringify(qas, null, 2));
  return qas;
}

async function dropFaq() {
  await request({
    method: 'delete',
    url: 'http://localhost:9200/faq'
  });
}

/**
 * 重建faq索引并写入全量的笔记数据
 */
async function main() {
  console.log(new Date().toLocaleString());
  await dropFaq();
  const dir = '/Users/liutos/Documents/Projects/my_note/faq/';
  const basenames = fs.readdirSync(dir);
  for (const basename of basenames) {
    const path = dir + basename;
    const type = basename.match(/(.*)\.org/)[1];
    const qas = parseFaqOrg(path);
    for (const qa of qas) {
      await request({
        body: qa,
        json: true,
        method: 'post',
        url: 'http://localhost:9200/faq/_doc'
      });
    }
    console.log(`文件${path}处理完毕`);
  }
  console.log(new Date().toLocaleString());
}

main();

后记

如果想知道我是如何在Emacs中查询这些FAQ的,可以参见《在Emacs中搭建笔记查阅系统的尝试》这篇文章。

阅读原文


用户bPGfS
169 声望3.7k 粉丝