EMACS-DOCUMENT

=============>集思广益

查找各处org文件的内容

我经常使用org-mode.我用它写论文,记会议笔记,写推荐信,记文献笔记,维护项目的TODO列表和软件 的帮助文档,写演讲稿,甚至处理学生们的作业和保存联系人信息。我的一些文件存在Dropbox, GoogleDrive, Box 上,一些在git repos里。问题是,这样org文件就渐渐散布在我硬盘各处。如今我已经有了几千个org文件记录 着我五年来的工作。

像这样过上一段时间,查找文件就很困难了。确实有像recent-files, bookmarks, counsel-find-file, helm-for-files, counsel/helm-locate, helm/counsel-grep/ag/pt, projectile 这样的工具 可以用来搜索项目里的文件,有一大堆工具可以搜索打开的buffer,还有 recoll 等桌面搜索工具,当然还得有良好的 组织习惯等等。但五年过去,我终于找到了查找目标文件的更好的方法。如果说想找一份我一年前创建的文件,但 它现在既不在当前目录或项目,也不在我的org-agenda-files列表里呢?还有,我怎样才能得到一份包含所有文件的todo 的动态列表呢?或是怎么找到包含特定bibtex entry的文件或某个学生所写的所有文件呢?

起初,我用Switch-e来给org文件作索引, 这样搜索起来很容易,但它只能搜索headline或paragraph等等。 这个方法的问题是,由于Swish-e的局限我不得不每次重新生成数据库,因此作索引变得非常慢。后来我找到了 另一个方法--一个更好的数据库。在这篇文章里,我将会用 sqlite 来保存org文件中的headline和 link。

我的目标是每次打开或保存任何org文件时,它就会被添加进数据库或是更新数据库。这个数据库会保存headline, property,content... 这样我就能够高效地查询那些包含我曾访问过的todo headine,tagged headline, 以及各类link的文件。让我们来看看有没有效果吧。

1 数据库结构

我用 emacsql 来创建sqlite3数据库并与之交互。它能够用lisp来生成SQL query。我不会 在这讨论太多代码,你想了解的话可以看看这个: org-db.el 。数据库由一些包含filename, headline,tag,property,headline-content(可选),headline-tags,headline-property, 和link的表(table)组成。这里的lisp代码只是作为演示而非我日常使用的。这篇文章只是用来证明 这方法的效果。

当org文件被打开或保存时,我用hook来更新数据库(当然只有当文件与数据库中内容不同时才会更新, 基于md5码校验)。通常,这些函数先删除文件在数据库中的entry再把数据添加回库。我发现 这比用org-element来parse org文件更快,尤其是处理大文件时。因为这些都由hook来完成, 所以无论我在何时何地打开一个org 文件,它都会被加进数据库或是更新数据库,效果还可以。虽然这个 方法不能保证数据库永远保持准确(比如文件在emacs外被更改,如一次git pull),但它也并不需要 永远准确,大多数文件都不会经常改动,而数据库在每次打开文件时都会更新。你也可以手动让数据库为 文件重建索引。渐渐的你会找出合适的更新频率。

emacsql 允许你用lisp代码生成SQL送往数据库。下面是个例子:

(emacsql-flatten-sql [:select [name] :from main:sqlite_master :where (= type table)])
SELECT name FROM main.sqlite_master WHERE type = "table";

这里有些微小的差异,比如main:sqlite_master会被转换成main.sqlite_master。你可以用vector, keyword和sexps来设置命令。emacsql会把像filename-id这样的名字转换成filename_id。理解起来 并不困难,而且emacsql的作者很热心。我以后会回头看看这篇文章来回顾这些差异。

下面是一个数据库中表(table)的列表。有一些主表(primary table),还有一些表(table) 只保存标题里的tag,property和keyword。这是典型的emacsql代码:一条生成SQL的lisp表达式。 表达式中的org-db是个保存数据库连接(database connection)的变量。它由org-db.el创建。

(emacsql org-db [:select [name] :from main:sqlite_master :where (= type table)])
 files                     
---------------------------
 tags                      
---------------------------
 properties                
---------------------------
 keywords                  
---------------------------
 headlines                 
---------------------------
 headline_content          
---------------------------
 headline_content_content  
---------------------------
 headline_content_segments 
---------------------------
 headline_content_segdir   
---------------------------
 headline_content_docsize  
---------------------------
 headline_content_stat     
---------------------------
 headline_tags             
---------------------------
 headline_properties       
---------------------------
 file_keywords             
---------------------------
 links                     

这是文件表(files table)里各列的描述:

(emacsql org-db [:pragma (funcall table_info files)])
 0 

还有标题表(headlines table)里的各列:

(emacsql org-db [:pragma (funcall table_info headlines)])
 0 

标题里的标签和属性保存在 headline-tags 和 headline-properties 两张表里。

如果只保存标题和链接(不保存内容)的话,数据库不会很大。而保存了内容后,它会达到500MB而且 变得有点慢。所以这篇文章里,我就不讨论包含内容的表了。

du -hs ~/org-db/org-db.sqlite
 56M | /Users/jkitchin/org-db/org-db.sqlite 

我们可以看看数据库里有多少文件。这些只是我Dropbox文件夹里的org文件。除此之外还有很多!如果 我把我所有的研究和教学项目的org文件包括进来,这个数字会达到10,000! 你是不会想要对这些文件 运行org-map-entries的。注意这也包括了所有的org_archive文件。

(emacsql org-db [:select (funcall count) :from files])
 1569 

这是标题总数。你可以看到根本不可能记住这些标题都在哪里!

(emacsql org-db [:select (funcall count) :from headlines])
 38587 

还有链接总数。有如此多链接!

(emacsql org-db [:select (funcall count) :from links])
 303739 

这真是一堆数目可观的链接。

2 查询链接表

让我们看看有多少引用链接(cite link):

(emacsql org-db [:select (funcall count) :from links :where (= type "cite")])
 14766 

哇,这些链接也好多!我以写proposal和paper为生,我用org-ref来简化工作,所以也许这个数字 没什么奇怪的。我们可以在链接表(link table)里搜索引用了"kitchin-2015-examp"的文件。链接表 里只有filename-id,所以我们把它和文件表合并来得出有用的信息。这里我们可以看到引用了 "kitchin-2015-examp"的文件列表。里面有手稿,企划,展示,文档和笔记。

(emacsql org-db [:select :distinct [files:filename]
                         :from links :inner :join files :on (= links:filename-id files:rowid) 
                         :where (and (= type "cite") (like path "%kitchin-2015-examp%"))])
 /Users/jkitchin/Dropbox/CMU/manuscripts/2015/                              
 Research_Data_Publishing_Paper/manuscript.org                              
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/manuscripts/2015/                              
 Research_Data_Publishing_Paper/manuscript-2015-06-29/manuscript.org        
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/manuscripts/2015/                              
 Research_Data_Publishing_Paper/manuscript-2015-10-10/manuscript.org        
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/manuscripts/2015/                              
 Research_Data_Publishing_Paper/manuscript-2016-03-09/manuscript.org        
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/manuscripts/2015/                              
 Research_Data_Publishing_Paper/manuscript-2016-04-18/manuscript.org        
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/manuscripts/2015/human-readable-data/          
 manuscript.org                                                             
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/manuscripts/@archive/2015/                     
 Research_Data_Publishing_Paper/manuscript.org                              
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/manuscripts/@archive/2015/                     
 Research_Data_Publishing_Paper/manuscript-2015-06-29/manuscript.org        
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/manuscripts/@archive/2015/                     
 Research_Data_Publishing_Paper/manuscript-2015-10-10/manuscript.org        
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/manuscripts/@archive/2015/                     
 Research_Data_Publishing_Paper/manuscript-2016-03-09/manuscript.org        
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/manuscripts/@archive/2015/                     
 Research_Data_Publishing_Paper/manuscript-2016-04-18/manuscript.org        
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/manuscripts/@archive/2015/human-readable-data/ 
 manuscript.org                                                             
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/meetings/@archive/2015/BES-2015/               
 doe-bes-wed-data-briefing/doe-bes-wed-data-sharing.org                     
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/meetings/@archive/2015/NIST-july-2015/         
 data-sharing.org                                                           
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/meetings/@archive/2015/UD-webinar/             
 ud-webinar.org                                                             
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/meetings/@archive/2016/AICHE/data-sharing/     
 data-sharing.org                                                           
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/meetings/@archive/2016/Spring-ACS/data-sharing 
 /data-sharing.org                                                          
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/projects/DOE-Early-Career/annual-reports/      
 final-report/kitchin-DESC0004031-final-report.org                          
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/proposals/@archive/2015/DOE-renewal/           
 proposal-v2.org                                                            
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/proposals/@archive/2015/DOE-renewal/archive/   
 proposal.org                                                               
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/proposals/@archive/2016/DOE-single-atom-alloy/ 
 proposal.org                                                               
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/proposals/@archive/2016/MRSEC/                 
 MRSEC-IRG-metastable-materials-preproposal/IRG-concept.org                 
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/proposals/@archive/2016/ljaf-open-science/     
 kitchin-proposal.org                                                       
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/proposals/@archive/2016/nsf-germination/       
 project-description.org                                                    
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/proposals/@archive/2016/nsf-reu-supplement/    
 project-description.org                                                    
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/CMU/proposals/@archive/2016/                       
 proctor-and-gamble-education/proposal.org                                  
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/bibliography/notes.org                             
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/kitchingroup/jmax/org-ref/citeproc/readme.org      
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/kitchingroup/jmax/org-ref/citeproc/                
 readme-unsrt.org                                                           
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/kitchingroup/jmax/org-ref/citeproc/                
 readme-author-year.org                                                     
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/kitchingroup/jmax/org-ref/tests/test-1.org         
----------------------------------------------------------------------------
 /Users/jkitchin/Dropbox/kitchingroup/jmax/org-ref/tests/sandbox/elpa/      
 org-ref-20160122.1725/citeproc/readme.org                                  

很明显,我们可以用这种方式生成helm和ivy这类工具的候选条目。

(ivy-read "Open: " (emacsql org-db [:select [files:filename links:begin]
                                            :from links :inner :join files :on (= links:filename-id files:rowid) 
                                            :where (and (= type "cite") (like path "%kitchin-2015-examp%"))])
          :action '(1 ("o"
                       (lambda (c)
                         (find-file (car c))
                         (goto-char (nth 1 c))
                         (org-show-entry)))))
/Users/jkitchin/Dropbox/CMU/manuscripts/2015/human-readable-data/manuscript.org

现在,你可以找到引用任何bibtex key的所有文件。因为SQL是查询语言(query language), 你应该可以写出更复杂的查询(query),比如过滤出多次引用(multiple citation)和 不同引用等等。

3 查询标题

每个标题,连同它的位置,标签和属性都被保存了下来。我们可以用数据库找到带有标签或特定属性的标题。 你可以看到我的数据库里有293个标签。

(emacsql org-db [:select (funcall count) :from tags])
 293 

这里我们查找带有electrolyte标签的标题。我有时用它来标记一些相关的文献。

(emacsql org-db [:select :distinct [files:filename headlines:title]
                         :from headlines :inner :join headline-tags :on (=  headlines:rowid headline-tags:headline-id)
                         :inner :join tags :on (= tags:rowid headline-tags:tag-id)
                         :inner :join files :on (= headlines:filename-id files:rowid)
                         :where (= tags:tag "electrolyte") :limit 5])
 /Users/jkitchin/Dropbox/ 
 org-mode/                
 prj-doe-early-career.org 

这里我们可以看到有很多带EMAIL属性的entry,这些可以作为邮件联系人。

(emacsql org-db [:select [(funcall count)] :from
                         headlines :inner :join headline-properties :on (=  headlines:rowid headline-properties:headline-id)
                         :inner :join properties :on (= properties:rowid headline-properties:property-id)
                         :where (and (= properties:property "EMAIL") (not (null headline-properties:value)))])
 7452 

如果你想看匹配"jkitchin"的标题,它们在这。

(emacsql org-db [:select :distinct [headlines:title headline-properties:value] :from
                         headlines :inner :join headline-properties :on (=  headlines:rowid headline-properties:headline-id)
                         :inner :join properties :on (= properties:rowid headline-properties:property-id)
                         :where (and (= properties:property "EMAIL") (like headline-properties:value "%jkitchin%"))])
 John Kitchin  

这是一个查找有多少deadline是2017年的标题。看来我很忙啊!

(emacsql org-db [:select (funcall count) :from
                         headlines :inner :join headline-properties :on (=  headlines:rowid headline-properties:headline-id)
                         :inner :join properties :on (= properties:rowid headline-properties:property-id)
                         :where (and (= properties:property "DEADLINE") (glob headline-properties:value "*2017*"))])
 50 

4 查询关键字

我们也保存了文件关键字,这样就能搜索文档标题,作者等等。这里是五篇title长度超过35个字符的 文档,按照降序排列。

(emacsql org-db [:select :distinct [value] :from
                         file-keywords :inner :join keywords :on (= file-keywords:keyword-id keywords:rowid)
                         :where (and (> (funcall length value) 35) (= keywords:keyword "TITLE"))
                         :order :by value :desc
                         :limit 5])
 pycse - Python3 Computations in Science and Engineering                    
----------------------------------------------------------------------------
 org-show - simple presentations in org-mode                                
----------------------------------------------------------------------------
 org-mode - A Human Readable, Machine Addressable Approach to Data          
 Archiving and Sharing in Science and Engineering                           
----------------------------------------------------------------------------
 modifying emacs to make typing easier.                                     
----------------------------------------------------------------------------
 jmax - John's customizations to maximize Emacs                             

我们也可以搜索作者或别的东西。我的便签(memo)带有#+SUBJECT关键字,所以我可以找到某个 subject的便签。这里我可以轻松地找到所有带LATEX_CLASS关键字的cmu-memo:

(emacsql org-db [:select [(funcall count)] :from
                         file-keywords :inner :join keywords :on (= file-keywords:keyword-id keywords:rowid)
                         :where (and (= value "cmu-memo") (= keywords:keyword "LATEX_CLASS"))
                         :limit 5])
 119 

有119条符合条件的便签。能够找出它们真是不错。

5 全文搜索

理论上,数据库里有标题内容(headline content)的表, 它也完全是可搜索的。我发现加上这些表 的话数据库变得有点慢,体积也增长到500GB,所以现在我把它们略去不谈。

6 总结

真正有趣的地方在这。当所有表合并在一起时,查询写起来有点繁琐。但是其中一些查询可以 包装起来成为函数。尽管把所有的概念对应地转变成SQL中概念要费点功夫,但我喜欢lisp式的查询。 一个包装起来的函数可能像下面这样:

(org-db-query (and (= properties:property "DEADLINE") (glob headline-properties:value "*2017*")))

使用tag或property来匹配的话就像下面这样。有时要写类似上面那样的代码的话,字符串不得不展开。 我不清楚这有多难。用上 a recursive descent parser (由emacsql作者编写) 应该就容易多了。

(org-db-query "DEADLINE={2017}")

数据库的效果还可以。对于大型的org文件,更新数据库时有明显的停顿,因为更新时Emacs 处于block的状态。我可以通过计时器 (timer) 来按顺序更新数据库甚至定时更新。数据库并不 需要保持实时更新,因为下次搜索时它们也不一定完全准确。至少目前来说这还不是个大问题。我 关注过 xapian (一个搜索引擎函数库:Search Engine Library)因为mu4e使用它。利用外部 函数库而非emacs来parse org文件也许会好一点。但这似乎是个大工程,可能得到下个暑假才能 弄完。

用外部函数库的另一个好处是“忽略模式”(ignore pattern)或是一些防止被建索引的文件特性。 比如我用org-mode维护一个加密的密码文件,但是当我打开它时,数据库就会建立它的纯文本的索引。 正如你在目录间跳转时会尽量避开 .dropbox.cache 这样的目录,这样不加筛选地建立索引可是个 大问题,不解决好的话,这个方法就还不够完善。