标签 搜索 下的文章

Google API 可以凸显出有关 Google 如何对网站进行分类的线索,以及如何调整内容以改进搜索结果的方法。

 title=

作为一名技术性的搜索引擎优化人员,我一直在寻找以新颖的方式使用数据的方法,以更好地了解 Google 如何对网站进行排名。我最近研究了 Google 的 自然语言 API 能否更好地揭示 Google 是如何分类网站内容的。

尽管有 开源 NLP 工具,但我想探索谷歌的工具,前提是它可能在其他产品中使用同样的技术,比如搜索。本文介绍了 Google 的自然语言 API,并探究了常见的自然语言处理(NLP)任务,以及如何使用它们来为网站内容创建提供信息。

了解数据类型

首先,了解 Google 自然语言 API 返回的数据类型非常重要。

实体

实体 Entities 是可以与物理世界中的某些事物联系在一起的文本短语。 命名实体识别 Named Entity Recognition (NER)是 NLP 的难点,因为工具通常需要查看关键字的完整上下文才能理解其用法。例如, 同形异义字 homographs 拼写相同,但是具有多种含义。句子中的 “lead” 是指一种金属:“铅”(名词),使某人移动:“牵领”(动词),还可能是剧本中的主要角色(也是名词)?Google 有 12 种不同类型的实体,还有第 13 个名为 “UNKNOWN”(未知)的统称类别。一些实体与维基百科的文章相关,这表明 知识图谱 对数据的影响。每个实体都会返回一个显著性分数,即其与所提供文本的整体相关性。

 title=

情感

情感 Sentiment ,即对某事的看法或态度,是在文件和句子层面以及文件中发现的单个实体上进行衡量。情感的 得分 score 范围从 -1.0(消极)到 1.0(积极)。 幅度 magnitude 代表情感的 非归一化 non-normalized 强度;它的范围是 0.0 到无穷大。

 title=

语法

语法 Syntax 解析包含了大多数在较好的库中常见的 NLP 活动,例如 词形演变 lemmatization 词性标记 part-of-speech tagging 依赖树解析 dependency-tree parsing 。NLP 主要处理帮助机器理解文本和关键字之间的关系。语法解析是大多数语言处理或理解任务的基础部分。

 title=

分类

分类 Categories 是将整个给定内容分配给特定行业或主题类别,其 置信度 confidence 得分从 0.0 到 1.0。这些分类似乎与其他 Google 工具使用的受众群体和网站类别相同,如 AdWords。

 title=

提取数据

现在,我将提取一些示例数据进行处理。我使用 Google 的 搜索控制台 API 收集了一些搜索查询及其相应的网址。Google 搜索控制台是一个报告人们使用 Google Search 查找网站页面的术语的工具。这个 开源的 Jupyter 笔记本 可以让你提取有关网站的类似数据。在此示例中,我在 2019 年 1 月 1 日至 6 月 1 日期间生成的一个网站(我没有提及名字)上提取了 Google 搜索控制台数据,并将其限制为至少获得一次点击(而不只是 曝光 impressions )的查询。

该数据集包含 2969 个页面和在 Google Search 的结果中显示了该网站网页的 7144 条查询的信息。下表显示,绝大多数页面获得的点击很少,因为该网站侧重于所谓的长尾(越特殊通常就更长尾)而不是短尾(非常笼统,搜索量更大)搜索查询。

 title=

为了减少数据集的大小并仅获得效果最好的页面,我将数据集限制为在此期间至少获得 20 次曝光的页面。这是精炼数据集的按页点击的柱状图,其中包括 723 个页面:

 title=

在 Python 中使用 Google 自然语言 API 库

要测试 API,在 Python 中创建一个利用 google-cloud-language 库的小脚本。以下代码基于 Python 3.5+。

首先,激活一个新的虚拟环境并安装库。用环境的唯一名称替换 <your-env>

virtualenv <your-env>
source <your-env>/bin/activate
pip install --upgrade google-cloud-language
pip install --upgrade requests

该脚本从 URL 提取 HTML,并将 HTML 提供给自然语言 API。返回一个包含 sentimententitiescategories 的字典,其中这些键的值都是列表。我使用 Jupyter 笔记本运行此代码,因为使用同一内核注释和重试代码更加容易。

# Import needed libraries
import requests
import json

from google.cloud import language
from google.oauth2 import service_account
from google.cloud.language import enums
from google.cloud.language import types

# Build language API client (requires service account key)
client = language.LanguageServiceClient.from_service_account_json('services.json')

# Define functions
def pull_googlenlp(client, url, invalid_types = ['OTHER'], **data):
   
        html = load_text_from_url(url, **data)
   
        if not html:
        return None
   
        document = types.Document(
        content=html,
        type=language.enums.Document.Type.HTML )

        features = {'extract_syntax': True,
                'extract_entities': True,
                'extract_document_sentiment': True,
                'extract_entity_sentiment': True,
                'classify_text': False
                }
   
        response = client.annotate_text(document=document, features=features)
        sentiment = response.document_sentiment
        entities = response.entities
   
        response = client.classify_text(document)
        categories = response.categories
         
        def get_type(type):
        return client.enums.Entity.Type(entity.type).name
   
        result = {}
   
        result['sentiment'] = []    
        result['entities'] = []
        result['categories'] = []

        if sentiment:
        result['sentiment'] = [{ 'magnitude': sentiment.magnitude, 'score':sentiment.score }]
         
        for entity in entities:
        if get_type(entity.type) not in invalid_types:
                result['entities'].append({'name': entity.name, 'type': get_type(entity.type), 'salience': entity.salience, 'wikipedia_url': entity.metadata.get('wikipedia_url', '-')  })
         
        for category in categories:
        result['categories'].append({'name':category.name, 'confidence': category.confidence})
         
         
        return result


def load_text_from_url(url, **data):

        timeout = data.get('timeout', 20)
   
        results = []
   
        try:
         
        print("Extracting text from: {}".format(url))
        response = requests.get(url, timeout=timeout)

        text = response.text
        status = response.status_code

        if status == 200 and len(text) > 0:
                return text
         
        return None
         

        except Exception as e:
        print('Problem with url: {0}.'.format(url))
        return None

要访问该 API,请按照 Google 的 快速入门说明 在 Google 云主控台中创建一个项目,启用该 API 并下载服务帐户密钥。之后,你应该拥有一个类似于以下内容的 JSON 文件:

 title=

命名为 services.json,并上传到项目文件夹。

然后,你可以通过运行以下程序来提取任何 URL(例如 Opensource.com)的 API 数据:

url = "https://opensource.com/article/19/6/how-ssh-running-container"
pull_googlenlp(client,url)

如果设置正确,你将看到以下输出:

 title=

为了使入门更加容易,我创建了一个 Jupyter 笔记本,你可以下载并使用它来测试提取网页的实体、类别和情感。我更喜欢使用 JupyterLab,它是 Jupyter 笔记本的扩展,其中包括文件查看器和其他增强的用户体验功能。如果你不熟悉这些工具,我认为利用 Anaconda 是开始使用 Python 和 Jupyter 的最简单途径。它使安装和设置 Python 以及常用库变得非常容易,尤其是在 Windows 上。

处理数据

使用这些函数,可抓取给定页面的 HTML 并将其传递给自然语言 API,我可以对 723 个 URL 进行一些分析。首先,我将通过查看所有页面中返回的顶级分类的数量来查看与网站相关的分类。

分类

 title=

这似乎是该特定站点的关键主题的相当准确的代表。通过查看一个效果最好的页面进行排名的单个查询,我可以比较同一查询在 Google 搜索结果中的其他排名页面。

  • URL 1 |顶级类别:/法律和政府/与法律相关的(0.5099999904632568)共 1 个类别。
  • 未返回任何类别。
  • URL 3 |顶级类别:/互联网与电信/移动与无线(0.6100000143051147)共 1 个类别。
  • URL 4 |顶级类别:/计算机与电子产品/软件(0.5799999833106995)共有 2 个类别。
  • URL 5 |顶级类别:/互联网与电信/移动与无线/移动应用程序和附件(0.75)共有 1 个类别。
  • 未返回任何类别。
  • URL 7 |顶级类别:/计算机与电子/软件/商业与生产力软件(0.7099999785423279)共 2 个类别。
  • URL 8 |顶级类别:/法律和政府/与法律相关的(0.8999999761581421)共 3 个类别。
  • URL 9 |顶级类别:/参考/一般参考/类型指南和模板(0.6399999856948853)共有 1 个类别。
  • 未返回任何类别。

上方括号中的数字表示 Google 对页面内容与该分类相关的置信度。对于相同分类,第八个结果比第一个结果具有更高的置信度,因此,这似乎不是定义排名相关性的灵丹妙药。此外,分类太宽泛导致无法满足特定搜索主题的需要。

通过排名查看平均置信度,这两个指标之间似乎没有相关性,至少对于此数据集而言如此:

 title=

这两种方法对网站进行规模审查是有意义的,以确保内容类别易于理解,并且样板或销售内容不会使你的页面与你的主要专业知识领域无关。想一想,如果你出售工业用品,但是你的页面返回 “Marketing(销售)” 作为主要分类。似乎没有一个强烈的迹象表明,分类相关性与你的排名有什么关系,至少在页面级别如此。

情感

我不会在情感上花很多时间。在所有从 API 返回情感的页面中,它们分为两个区间:0.1 和 0.2,这几乎是中立的情感。根据直方图,很容易看出情感没有太大价值。对于新闻或舆论网站而言,测量特定页面的情感到中位数排名之间的相关性将是一个更加有趣的指标。

 title=

实体

在我看来,实体是 API 中最有趣的部分。这是在所有页面中按 显著性 salience (或与页面的相关性)选择的顶级实体。请注意,对于相同的术语(销售清单),Google 会推断出不同的类型,可能是错误的。这是由于这些术语出现在内容中的不同上下文中引起的。

 title=

然后,我分别查看了每个实体类型,并一起查看了该实体的显著性与页面的最佳排名位置之间是否存在任何关联。对于每种类型,我匹配了与该类型匹配的顶级实体的显著性(与页面的整体相关性),按显著性排序(降序)。

有些实体类型在所有示例中返回的显著性为零,因此我从下面的图表中省略了这些结果。

 title=

“Consumer Good(消费性商品)” 实体类型具有最高的正相关性, 皮尔森相关度 Pearson correlation 为 0.15854,尽管由于较低编号的排名更好,所以 “Person” 实体的结果最好,相关度为 -0.15483。这是一个非常小的样本集,尤其是对于单个实体类型,我不能对数据做太多的判断。我没有发现任何具有强相关性的值,但是 “Person” 实体最有意义。网站通常都有关于其首席执行官和其他主要雇员的页面,这些页面很可能在这些查询的搜索结果方面做得好。

继续,当从整体上看站点,根据实体名称和实体类型,出现了以下主题。

 title=

我模糊了几个看起来过于具体的结果,以掩盖网站的身份。从主题上讲,名称信息是在你(或竞争对手)的网站上局部查看其核心主题的一种好方法。这样做仅基于示例网站的排名网址,而不是基于所有网站的可能网址(因为 Search Console 数据仅记录 Google 中展示的页面),但是结果会很有趣,尤其是当你使用像 Ahrefs 之类的工具提取一个网站的主要排名 URL,该工具会跟踪许多查询以及这些查询的 Google 搜索结果。

实体数据中另一个有趣的部分是标记为 “CONSUMER\_GOOD” 的实体倾向于 “看起来” 像我在看到 “ 知识结果 Knowledge Results ”的结果,即页面右侧的 Google 搜索结果。

 title=

在我们的数据集中具有三个或三个以上关键字的 “Consumer Good(消费性商品)” 实体名称中,有 5.8% 的知识结果与 Google 对该实体命名的结果相同。这意味着,如果你在 Google 中搜索术语或短语,则右侧的框(例如,上面显示 Linux 的知识结果)将显示在搜索结果页面中。由于 Google 会 “挑选” 代表实体的示例网页,因此这是一个很好的机会,可以在搜索结果中识别出具有唯一特征的机会。同样有趣的是,5.8% 的在 Google 中显示这些知识结果名称中,没有一个实体的维基百科 URL 从自然语言 API 中返回。这很有趣,值得进行额外的分析。这将是非常有用的,特别是对于传统的全球排名跟踪工具(如 Ahrefs)数据库中没有的更深奥的主题。

如前所述,知识结果对于那些希望自己的内容在 Google 中被收录的网站所有者来说是非常重要的,因为它们在桌面搜索中加强高亮显示。假设,它们也很可能与 Google Discover 的知识库主题保持一致,这是一款适用于 Android 和 iOS 的产品,它试图根据用户感兴趣但没有明确搜索的主题为用户浮现内容。

总结

本文介绍了 Google 的自然语言 API,分享了一些代码,并研究了此 API 对网站所有者可能有用的方式。关键要点是:

  • 学习使用 Python 和 Jupyter 笔记本可以为你的数据收集任务打开到一个由令人难以置信的聪明和有才华的人建立的不可思议的 API 和开源项目(如 Pandas 和 NumPy)的世界。
  • Python 允许我为了一个特定目的快速提取和测试有关 API 值的假设。
  • 通过 Google 的分类 API 传递网站页面可能是一项很好的检查,以确保其内容分解成正确的主题分类。对于竞争对手的网站执行此操作还可以提供有关在何处进行调整或创建内容的指导。
  • 对于示例网站,Google 的情感评分似乎并不是一个有趣的指标,但是对于新闻或基于意见的网站,它可能是一个有趣的指标。
  • Google 发现的实体从整体上提供了更细化的网站的主题级别视图,并且像分类一样,在竞争性内容分析中使用将非常有趣。
  • 实体可以帮助定义机会,使你的内容可以与搜索结果或 Google Discover 结果中的 Google 知识块保持一致。我们将 5.8% 的结果设置为更长的(字计数)“Consumer Goods(消费商品)” 实体,显示这些结果,对于某些网站来说,可能有机会更好地优化这些实体的页面显著性分数,从而有更好的机会在 Google 搜索结果或 Google Discovers 建议中抓住这个重要作用的位置。

via: https://opensource.com/article/19/7/python-google-natural-language-api

作者:JR Oakes 选题:lujun9972 译者:stevenzdg988 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Equinix 推出“时间即服务”

许多行业和云服务都依赖于精确、可靠和安全的时间同步。传统的、基于天线的 GPS 授时基础设施解决方案可能难以安装,并会带来安全漏洞。同时,从互联网上利用公开 NTP 服务器授时也有网络安全风险。

数字基础设施公司 Equinix 正在推出的 Precision Time 服务旨在解决这个挑战。Equinix 管理和维护一个由 GPS 天线、带有原子钟保持器的时间服务器及主时钟组成的冗余堆栈。新服务支持 NTP 和 PTP 等网络授时协议。该服务提供 50 微秒的 SLA,要比大多数时间同步精度要求高得多。

连时间都能成为服务,听起来有趣,但是其实有一定的必要性。

2020 年三分之二的 Google 搜索没有带来进一步点击

2019 年的一项研究发现,50.33% 的 Google 搜索没有导致外链点击。2020 年这一比例进一步提高到三分之二左右。这一数据来自 SimilarWeb 对 2020 年 51 万亿次 Google 搜索的分析。在这些搜索中,33.59% 点击了搜索结果,1.59% 点击了付费搜索结果,其余 64.82% 则是零点击。

看来通用搜索引擎的有效性越来越差了,难怪现在有很多垂直搜索引擎出现。

Canonical 将谷歌的 Flutter 视为未来桌面和移动应用的默认选择

Flutter 是一个使用 Dart 语言的框架,可以开发跨 Android、iOS、Windows、Linux、macOS 和 Web 的应用,不过 macOS 和 Linux 版本尚处于测试阶段。尽管如此,为了扩大 Linux 应用生态系统,Ubuntu 的开发商 Canonical 上个月还是宣布将用 Flutter 构建新的桌面安装程序,并向开发者表示“Flutter 是 Canonical 未来创建的桌面和移动应用的默认选择”。

Flutter 的性能比 Electron 等替代品更好,虽然它的性能不如原生开发平台,但它跨操作系统开发的能力超过了这种差异。跨平台开发不仅仅意味着 Linux 开发者可以为其他平台编写应用,还意味着任何其他平台的 Flutter 开发者都可以将他们的应用贡献给 Linux,从而扩大 Linux 应用生态系统。

这是一个看起来不错的选择,但是 Flutter 并不是第一个以跨平台为主要卖点的框架,而之前的类似框架并未真正给 Linux 带来多少重要应用,所以将来的发展需要拭目以待。

Linux 管理员一天都不能离开搜索文件,因为这是他们的日常活动。了解一些搜索的东西是不错的,因为这能帮助你在命令行服务器中工作。这些命令记忆起来不复杂,因为它们使用的是标准语法。

可以通过四个 Linux 命令啦执行此操作,每个命令都有自己独特的功能。

方法 1:使用 find 命令在 Linux 中搜索文件和文件夹

find 命令被广泛使用,并且是在 Linux 中搜索文件和文件夹的著名命令。它搜索当前目录中的给定文件,并根据搜索条件递归遍历其子目录。

它允许用户根据大小、名称、所有者、组、类型、权限、日期和其他条件执行所有类型的文件搜索。

运行以下命令以在系统中查找给定文件。

# find / -iname "sshd_config"
/etc/ssh/sshd_config

运行以下命令以查找系统中的给定文件夹。要在 Linux 中搜索文件夹,我们需要使用 -type 参数。

# find / -type d -iname "ssh"
/usr/lib/ssh
/usr/lib/go/src/cmd/vendor/golang.org/x/crypto/ssh
/usr/lib/go/pkg/linux_amd64/cmd/vendor/golang.org/x/crypto/ssh
/etc/ssh

使用通配符搜索系统上的所有文件。我们将搜索系统中所有以 .config 为扩展名的文件。

# find / -name "*.config"
/usr/lib/mono/gac/avahi-sharp/1.0.0.0__4d116c78973743f5/avahi-sharp.dll.config
/usr/lib/mono/gac/avahi-ui-sharp/0.0.0.0__4d116c78973743f5/avahi-ui-sharp.dll.config
/usr/lib/python2.7/config/Setup.config
/usr/share/git/mw-to-git/t/test.config
/var/lib/lightdm/.config
/home/daygeek/.config
/root/.config
/etc/skel/.config

使用以下命令格式在系统中查找空文件和文件夹。

# find / -empty

使用以下命令组合查找 Linux 上包含特定文本的所有文件。

# find / -type f -exec grep "Port 22" '{}' \; -print
# find / -type f -print | xargs grep "Port 22"
# find / -type f | xargs grep 'Port 22'
# find / -type f -exec grep -H 'Port 22' {} \;

方法 2:使用 locate 命令在 Linux 中搜索文件和文件夹

locate 命令比 find 命令运行得更快,因为它使用 updatedb 数据库,而 find 命令在真实系统中搜索。

它使用数据库而不是搜索单个目录路径来获取给定文件。

locate 命令未在大多数发行版中预安装,因此,请使用你的包管理器进行安装。

数据库通过 cron 任务定期更新,但我们可以通过运行以下命令手动更新它。

$ sudo updatedb

只需运行以下命令即可列出给定的文件或文件夹。在 locate 命令中不需要指定特定选项来打印文件或文件夹。

在系统中搜索 ssh 文件夹。

# locate --basename '\ssh'
/etc/ssh
/usr/bin/ssh
/usr/lib/ssh
/usr/lib/go/pkg/linux_amd64/cmd/vendor/golang.org/x/crypto/ssh
/usr/lib/go/src/cmd/go/testdata/failssh/ssh
/usr/lib/go/src/cmd/vendor/golang.org/x/crypto/ssh

在系统中搜索 ssh_config 文件。

# locate --basename '\sshd_config'
/etc/ssh/sshd_config

方法 3:在 Linux 中搜索文件使用 which 命令

which 返回在终端输入命令时执行的可执行文件的完整路径。

当你想要为可执行文件创建桌面快捷方式或符号链接时,它非常有用。

which 命令搜索当前用户而不是所有用户的 $PATH 环境变量中列出的目录。我的意思是,当你登录自己的帐户时,你无法搜索 root 用户文件或目录。

运行以下命令以打印 vim 可执行文件的完整路径。

# which vi
/usr/bin/vi

或者,它允许用户一次执行多个文件搜索。

# which -a vi sudo
/usr/bin/vi
/bin/vi
/usr/bin/sudo
/bin/sudo

方法 4:使用 whereis 命令在 Linux 中搜索文件

whereis 命令用于搜索给定命令的二进制、源码和手册页文件。

# whereis vi
vi: /usr/bin/vi /usr/share/man/man1/vi.1p.gz /usr/share/man/man1/vi.1.gz

via: https://www.2daygeek.com/four-easy-ways-to-search-or-find-files-and-folders-in-linux/

作者:Prakash Subramanian 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

如何在超过 500 万篇文章的 Wikipedia 上找到与你研究相关的文章?

如何在超过 20 亿用户的 Facebook 中找到你的朋友(并且还拼错了名字)?

谷歌如何在整个因特网上搜索你的模糊的、充满拼写错误的查询?

在本教程中,我们将带你探索如何配置我们自己的全文搜索应用程序(与上述问题中的系统相比,它的复杂度要小很多)。我们的示例应用程序将提供一个 UI 和 API 去从 100 部经典文学(比如,《彼得·潘》 、 《弗兰肯斯坦》 和 《金银岛》)中搜索完整的文本。

你可以在这里(https://search.patricktriest.com)预览该教程应用的完整版本。

preview webapp

这个应用程序的源代码是 100% 开源的,可以在 GitHub 仓库上找到它们 —— https://github.com/triestpa/guttenberg-search

在应用程序中添加一个快速灵活的全文搜索可能是个挑战。大多数的主流数据库,比如,PostgreSQLMongoDB,由于受其查询和索引结构的限制只能提供一个非常基础的文本搜索功能。为实现高质量的全文搜索,通常的最佳选择是单独的数据存储。Elasticsearch 是一个开源数据存储的领导者,它专门为执行灵活而快速的全文搜索进行了优化。

我们将使用 Docker 去配置我们自己的项目环境和依赖。Docker 是一个容器化引擎,它被 UberSpotifyADP 以及 Paypal 使用。构建容器化应用的一个主要优势是,项目的设置在 Windows、macOS、以及 Linux 上都是相同的 —— 这使我写这个教程快速又简单。如果你还没有使用过 Docker,不用担心,我们接下来将经历完整的项目配置。

我也会使用 Node.js (使用 Koa 框架)和 Vue.js,用它们分别去构建我们自己的搜索 API 和前端 Web 应用程序。

1 - Elasticsearch 是什么?

全文搜索在现代应用程序中是一个有大量需求的特性。搜索也可能是最难的一项特性 —— 许多流行的网站的搜索功能都不合格,要么返回结果太慢,要么找不到精确的结果。通常,这种情况是被底层的数据库所局限:大多数标准的关系型数据库局限于基本的 CONTAINSLIKE SQL 查询上,它仅提供最基本的字符串匹配功能。

我们的搜索应用程序将具备:

  1. 快速 - 搜索结果将快速返回,为用户提供一个良好的体验。
  2. 灵活 - 我们希望能够去修改搜索如何执行的方式,这是为了便于在不同的数据库和用户场景下进行优化。
  3. 容错 - 如果所搜索的内容有拼写错误,我们将仍然会返回相关的结果,而这个结果可能正是用户希望去搜索的结果。
  4. 全文 - 我们不想限制我们的搜索只能与指定的关键字或者标签相匹配 —— 我们希望它可以搜索在我们的数据存储中的任何东西(包括大的文本字段)。

Elastic Search Logo

为了构建一个功能强大的搜索功能,通常最理想的方法是使用一个为全文搜索任务优化过的数据存储。在这里我们使用 Elasticsearch,Elasticsearch 是一个开源的内存中的数据存储,它是用 Java 写的,最初是在 Apache Lucene 库上构建的。

这里有一些来自 Elastic 官方网站 上的 Elasticsearch 真实使用案例。

  • Wikipedia 使用 Elasticsearch 去提供带高亮搜索片断的全文搜索功能,并且提供按类型搜索和 “did-you-mean” 建议。
  • Guardian 使用 Elasticsearch 把社交网络数据和访客日志相结合,为编辑去提供新文章的公众意见的实时反馈。
  • Stack Overflow 将全文搜索和地理查询相结合,并使用 “类似” 的方法去找到相关的查询和回答。
  • GitHub 使用 Elasticsearch 对 1300 亿行代码进行查询。

与 “普通的” 数据库相比,Elasticsearch 有什么不一样的地方?

Elasticsearch 之所以能够提供快速灵活的全文搜索,秘密在于它使用 反转索引 inverted index

“索引” 是数据库中的一种数据结构,它能够以超快的速度进行数据查询和检索操作。数据库通过存储与表中行相关联的字段来生成索引。在一种可搜索的数据结构(一般是 B 树)中排序索引,在优化过的查询中,数据库能够达到接近线性的时间(比如,“使用 ID=5 查找行”)。

Relational Index

我们可以将数据库索引想像成一个图书馆中老式的卡片式目录 —— 只要你知道书的作者和书名,它就会告诉你书的准确位置。为加速特定字段上的查询速度,数据库表一般有多个索引(比如,在 name 列上的索引可以加速指定名字的查询)。

反转索引本质上是不一样的。每行(或文档)的内容是分开的,并且每个独立的条目(在本案例中是单词)反向指向到包含它的任何文档上。

Inverted Index

这种反转索引数据结构可以使我们非常快地查询到,所有出现 “football” 的文档。通过使用大量优化过的内存中的反转索引,Elasticsearch 可以让我们在存储的数据上,执行一些非常强大的和自定义的全文搜索。

2 - 项目设置

2.0 - Docker

我们在这个项目上使用 Docker 管理环境和依赖。Docker 是个容器引擎,它允许应用程序运行在一个独立的环境中,不会受到来自主机操作系统和本地开发环境的影响。现在,许多公司将它们的大规模 Web 应用程序主要运行在容器架构上。这样将提升灵活性和容器化应用程序组件的可组构性。

Docker Logo

对我来说,使用 Docker 的优势是,它对本教程的作者非常方便,它的本地环境设置量最小,并且跨 Windows、macOS 和 Linux 系统的一致性很好。我们只需要在 Docker 配置文件中定义这些依赖关系,而不是按安装说明分别去安装 Node.js、Elasticsearch 和 Nginx,然后,就可以使用这个配置文件在任何其它地方运行我们的应用程序。而且,因为每个应用程序组件都运行在它自己的独立容器中,它们受本地机器上的其它 “垃圾” 干扰的可能性非常小,因此,在调试问题时,像“它在我这里可以工作!”这类的问题将非常少。

2.1 - 安装 Docker & Docker-Compose

这个项目只依赖 Dockerdocker-compose,docker-compose 是 Docker 官方支持的一个工具,它用来将定义的多个容器配置 组装 成单一的应用程序栈。

2.2 - 设置项目主目录

为项目创建一个主目录(名为 guttenberg_search)。我们的项目将工作在主目录的以下两个子目录中。

  • /public - 保存前端 Vue.js Web 应用程序。
  • /server - 服务器端 Node.js 源代码。

2.3 - 添加 Docker-Compose 配置

接下来,我们将创建一个 docker-compose.yml 文件来定义我们的应用程序栈中的每个容器。

  1. gs-api - 后端应用程序逻辑使用的 Node.js 容器
  2. gs-frontend - 前端 Web 应用程序使用的 Ngnix 容器。
  3. gs-search - 保存和搜索数据的 Elasticsearch 容器。
version: '3'

services:
  api: # Node.js App
    container_name: gs-api
    build: .
    ports:
      - "3000:3000" # Expose API port
      - "9229:9229" # Expose Node process debug port (disable in production)
    environment: # Set ENV vars
     - NODE_ENV=local
     - ES_HOST=elasticsearch
     - PORT=3000
    volumes: # Attach local book data directory
      - ./books:/usr/src/app/books

  frontend: # Nginx Server For Frontend App
    container_name: gs-frontend
    image: nginx
    volumes: # Serve local "public" dir
      - ./public:/usr/share/nginx/html
    ports:
      - "8080:80" # Forward site to localhost:8080

  elasticsearch: # Elasticsearch Instance
    container_name: gs-search
    image: docker.elastic.co/elasticsearch/elasticsearch:6.1.1
    volumes: # Persist ES data in seperate "esdata" volume
      - esdata:/usr/share/elasticsearch/data
    environment:
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - discovery.type=single-node
    ports: # Expose Elasticsearch ports
      - "9300:9300"
      - "9200:9200"

volumes: # Define seperate volume for Elasticsearch data
  esdata:

这个文件定义了我们全部的应用程序栈 —— 不需要在你的本地系统上安装 Elasticsearch、Node 和 Nginx。每个容器都将端口转发到宿主机系统(localhost)上,以便于我们在宿主机上去访问和调试 Node API、Elasticsearch 实例和前端 Web 应用程序。

2.4 - 添加 Dockerfile

对于 Nginx 和 Elasticsearch,我们使用了官方预构建的镜像,而 Node.js 应用程序需要我们自己去构建。

在应用程序的根目录下定义一个简单的 Dockerfile 配置文件。

# Use Node v8.9.0 LTS
FROM node:carbon

# Setup app working directory
WORKDIR /usr/src/app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install app dependencies
RUN npm install

# Copy sourcecode
COPY . .

# Start app
CMD [ "npm", "start" ]

这个 Docker 配置扩展了官方的 Node.js 镜像、拷贝我们的应用程序源代码、以及在容器内安装 NPM 依赖。

我们也增加了一个 .dockerignore 文件,以防止我们不需要的文件拷贝到容器中。

node_modules/
npm-debug.log
books/
public/
请注意:我们之所以不拷贝 node_modules 目录到我们的容器中 —— 是因为我们要在容器构建过程里面运行 npm install。从宿主机系统拷贝 node_modules 到容器里面可能会引起错误,因为一些包需要为某些操作系统专门构建。比如说,在 macOS 上安装 bcrypt 包,然后尝试将这个模块直接拷贝到一个 Ubuntu 容器上将不能工作,因为 bcyrpt 需要为每个操作系统构建一个特定的二进制文件。

2.5 - 添加基本文件

为了测试我们的配置,我们需要添加一些占位符文件到应用程序目录中。

public/index.html 文件中添加如下内容。

<html><body>Hello World From The Frontend Container</body></html>

接下来,在 server/app.js 中添加 Node.js 占位符文件。

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx, next) => {
  ctx.body = 'Hello World From the Backend Container'
})

const port = process.env.PORT || 3000

app.listen(port, err => {
  if (err) console.error(err)
  console.log(`App Listening on Port ${port}`)
})

最后,添加我们的 package.json Node 应用配置。

{
  "name": "guttenberg-search",
  "version": "0.0.1",
  "description": "Source code for Elasticsearch tutorial using 100 classic open source books.",
  "scripts": {
    "start": "node --inspect=0.0.0.0:9229 server/app.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/triestpa/guttenberg-search.git"
  },
  "author": "[email protected]",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/triestpa/guttenberg-search/issues"
  },
  "homepage": "https://github.com/triestpa/guttenberg-search#readme",
  "dependencies": {
    "elasticsearch": "13.3.1",
    "joi": "13.0.1",
    "koa": "2.4.1",
    "koa-joi-validate": "0.5.1",
    "koa-router": "7.2.1"
  }
}

这个文件定义了应用程序启动命令和 Node.js 包依赖。

注意:不要运行 npm install —— 当它构建时,依赖会在容器内安装。

2.6 - 测试它的输出

现在一切新绪,我们来测试应用程序的每个组件的输出。从应用程序的主目录运行 docker-compose build,它将构建我们的 Node.js 应用程序容器。

docker build output

接下来,运行 docker-compose up 去启动整个应用程序栈。

docker compose output

这一步可能需要几分钟时间,因为 Docker 要为每个容器去下载基础镜像。以后再次运行,启动应用程序会非常快,因为所需要的镜像已经下载完成了。

在你的浏览器中尝试访问 localhost:8080 —— 你将看到简单的 “Hello World” Web 页面。

frontend sample output

访问 localhost:3000 去验证我们的 Node 服务器,它将返回 “Hello World” 信息。

backend sample output

最后,访问 localhost:9200 去检查 Elasticsearch 运行状态。它将返回类似如下的内容。

{
  "name" : "SLTcfpI",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "iId8e0ZeS_mgh9ALlWQ7-w",
  "version" : {
    "number" : "6.1.1",
    "build_hash" : "bd92e7f",
    "build_date" : "2017-12-17T20:23:25.338Z",
    "build_snapshot" : false,
    "lucene_version" : "7.1.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

如果三个 URL 都显示成功,祝贺你!整个容器栈已经正常运行了,接下来我们进入最有趣的部分。

3 - 连接到 Elasticsearch

我们要做的第一件事情是,让我们的应用程序连接到我们本地的 Elasticsearch 实例上。

3.0 - 添加 ES 连接模块

在新文件 server/connection.js 中添加如下的 Elasticsearch 初始化代码。

const elasticsearch = require('elasticsearch')

// Core ES variables for this project
const index = 'library'
const type = 'novel'
const port = 9200
const host = process.env.ES_HOST || 'localhost'
const client = new elasticsearch.Client({ host: { host, port } })

/** Check the ES connection status */
async function checkConnection () {
  let isConnected = false
  while (!isConnected) {
    console.log('Connecting to ES')
    try {
      const health = await client.cluster.health({})
      console.log(health)
      isConnected = true
    } catch (err) {
      console.log('Connection Failed, Retrying...', err)
    }
  }
}

checkConnection()

现在,我们重新构建我们的 Node 应用程序,我们将使用 docker-compose build 来做一些改变。接下来,运行 docker-compose up -d 去启动应用程序栈,它将以守护进程的方式在后台运行。

应用程序启动之后,在命令行中运行 docker exec gs-api "node" "server/connection.js",以便于在容器内运行我们的脚本。你将看到类似如下的系统输出信息。

{ cluster_name: 'docker-cluster',
  status: 'yellow',
  timed_out: false,
  number_of_nodes: 1,
  number_of_data_nodes: 1,
  active_primary_shards: 1,
  active_shards: 1,
  relocating_shards: 0,
  initializing_shards: 0,
  unassigned_shards: 1,
  delayed_unassigned_shards: 0,
  number_of_pending_tasks: 0,
  number_of_in_flight_fetch: 0,
  task_max_waiting_in_queue_millis: 0,
  active_shards_percent_as_number: 50 }

继续之前,我们先删除最下面的 checkConnection() 调用,因为,我们最终的应用程序将调用外部的连接模块。

3.1 - 添加函数去重置索引

server/connection.js 中的 checkConnection 下面添加如下的函数,以便于重置 Elasticsearch 索引。

/** Clear the index, recreate it, and add mappings */
async function resetIndex (index) {
  if (await client.indices.exists({ index })) {
    await client.indices.delete({ index })
  }

  await client.indices.create({ index })
  await putBookMapping()
}

3.2 - 添加图书模式

接下来,我们将为图书的数据模式添加一个 “映射”。在 server/connection.js 中的 resetIndex 函数下面添加如下的函数。

/** Add book section schema mapping to ES */
async function putBookMapping () {
  const schema = {
    title: { type: 'keyword' },
    author: { type: 'keyword' },
    location: { type: 'integer' },
    text: { type: 'text' }
  }

  return client.indices.putMapping({ index, type, body: { properties: schema } })
}

这是为 book 索引定义了一个映射。Elasticsearch 中的 index 大概类似于 SQL 的 table 或者 MongoDB 的 collection。我们通过添加映射来为存储的文档指定每个字段和它的数据类型。Elasticsearch 是无模式的,因此,从技术角度来看,我们是不需要添加映射的,但是,这样做,我们可以更好地控制如何处理数据。

比如,我们给 titleauthor 字段分配 keyword 类型,给 text 字段分配 text 类型。之所以这样做的原因是,搜索引擎可以区别处理这些字符串字段 —— 在搜索的时候,搜索引擎将在 text 字段中搜索可能的匹配项,而对于 keyword 类型字段,将对它们进行全文匹配。这看上去差别很小,但是它们对在不同的搜索上的速度和行为的影响非常大。

在文件的底部,导出对外发布的属性和函数,这样我们的应用程序中的其它模块就可以访问它们了。

module.exports = {
  client, index, type, checkConnection, resetIndex
}

4 - 加载原始数据

我们将使用来自 古登堡项目 的数据 —— 它致力于为公共提供免费的线上电子书。在这个项目中,我们将使用 100 本经典图书来充实我们的图书馆,包括《福尔摩斯探案集》、《金银岛》、《基督山复仇记》、《环游世界八十天》、《罗密欧与朱丽叶》 和《奥德赛》。

Book Covers

4.1 - 下载图书文件

我将这 100 本书打包成一个文件,你可以从这里下载它 —— https://cdn.patricktriest.com/data/books.zip

将这个文件解压到你的项目的 books/ 目录中。

你可以使用以下的命令来完成(需要在命令行下使用 wgetThe Unarchiver)。

wget https://cdn.patricktriest.com/data/books.zip
unar books.zip

4.2 - 预览一本书

尝试打开其中的一本书的文件,假设打开的是 219-0.txt。你将注意到它开头是一个公开访问的协议,接下来是一些标识这本书的书名、作者、发行日期、语言和字符编码的行。

Title: Heart of Darkness

Author: Joseph Conrad

Release Date: February 1995 [EBook #219]
Last Updated: September 7, 2016

Language: English

Character set encoding: UTF-8

*** START OF THIS PROJECT GUTENBERG EBOOK HEART OF DARKNESS *** 这些行后面,是这本书的正式内容。

如果你滚动到本书的底部,你将看到类似 *** END OF THIS PROJECT GUTENBERG EBOOK HEART OF DARKNESS *** 信息,接下来是本书更详细的协议版本。

下一步,我们将使用程序从文件头部来解析书的元数据,提取 *** START OF***END OF 之间的内容。

4.3 - 读取数据目录

我们将写一个脚本来读取每本书的内容,并将这些数据添加到 Elasticsearch。我们将定义一个新的 Javascript 文件 server/load_data.js 来执行这些操作。

首先,我们将从 books/ 目录中获取每个文件的列表。

server/load_data.js 中添加下列内容。

const fs = require('fs')
const path = require('path')
const esConnection = require('./connection')

/** Clear ES index, parse and index all files from the books directory */
async function readAndInsertBooks () {
  try {
    // Clear previous ES index
    await esConnection.resetIndex()

    // Read books directory
    let files = fs.readdirSync('./books').filter(file => file.slice(-4) === '.txt')
    console.log(`Found ${files.length} Files`)

    // Read each book file, and index each paragraph in elasticsearch
    for (let file of files) {
      console.log(`Reading File - ${file}`)
      const filePath = path.join('./books', file)
      const { title, author, paragraphs } = parseBookFile(filePath)
      await insertBookData(title, author, paragraphs)
    }
  } catch (err) {
    console.error(err)
  }
}

readAndInsertBooks()

我们将使用一个快捷命令来重构我们的 Node.js 应用程序,并更新运行的容器。

运行 docker-compose up -d --build 去更新应用程序。这是运行 docker-compose builddocker-compose up -d 的快捷命令。

docker build output

为了在容器中运行我们的 load_data 脚本,我们运行 docker exec gs-api "node" "server/load_data.js" 。你将看到 Elasticsearch 的状态输出 Found 100 Books

这之后,脚本发生了错误退出,原因是我们调用了一个没有定义的辅助函数(parseBookFile)。

docker exec output

4.4 - 读取数据文件

接下来,我们读取元数据和每本书的内容。

server/load_data.js 中定义新函数。

/** Read an individual book text file, and extract the title, author, and paragraphs */
function parseBookFile (filePath) {
  // Read text file
  const book = fs.readFileSync(filePath, 'utf8')

  // Find book title and author
  const title = book.match(/^Title:\s(.+)$/m)[1]
  const authorMatch = book.match(/^Author:\s(.+)$/m)
  const author = (!authorMatch || authorMatch[1].trim() === '') ? 'Unknown Author' : authorMatch[1]

  console.log(`Reading Book - ${title} By ${author}`)

  // Find Guttenberg metadata header and footer
  const startOfBookMatch = book.match(/^\*{3}\s*START OF (THIS|THE) PROJECT GUTENBERG EBOOK.+\*{3}$/m)
  const startOfBookIndex = startOfBookMatch.index + startOfBookMatch[0].length
  const endOfBookIndex = book.match(/^\*{3}\s*END OF (THIS|THE) PROJECT GUTENBERG EBOOK.+\*{3}$/m).index

  // Clean book text and split into array of paragraphs
  const paragraphs = book
    .slice(startOfBookIndex, endOfBookIndex) // Remove Guttenberg header and footer
    .split(/\n\s+\n/g) // Split each paragraph into it's own array entry
    .map(line => line.replace(/
/g, ' ').trim()) // Remove paragraph line breaks and whitespace
    .map(line => line.replace(/_/g, '')) // Guttenberg uses "_" to signify italics.  We'll remove it, since it makes the raw text look messy.
    .filter((line) => (line && line.length !== '')) // Remove empty lines

  console.log(`Parsed ${paragraphs.length} Paragraphs\n`)
  return { title, author, paragraphs }
}

这个函数执行几个重要的任务。

  1. 从文件系统中读取书的文本。
  2. 使用正则表达式(关于正则表达式,请参阅 这篇文章 )解析书名和作者。
  3. 通过匹配 “古登堡项目” 的头部和尾部,识别书的正文内容。
  4. 提取书的内容文本。
  5. 分割每个段落到它的数组中。
  6. 清理文本并删除空白行。

它的返回值,我们将构建一个对象,这个对象包含书名、作者、以及书中各段落的数组。

再次运行 docker-compose up -d --builddocker exec gs-api "node" "server/load_data.js",你将看到输出同之前一样,在输出的末尾有三个额外的行。

docker exec output

成功!我们的脚本从文本文件中成功解析出了书名和作者。脚本再次以错误结束,因为到现在为止,我们还没有定义辅助函数。

4.5 - 在 ES 中索引数据文件

最后一步,我们将批量上传每个段落的数组到 Elasticsearch 索引中。

load_data.js 中添加新的 insertBookData 函数。

/** Bulk index the book data in Elasticsearch */
async function insertBookData (title, author, paragraphs) {
  let bulkOps = [] // Array to store bulk operations

  // Add an index operation for each section in the book
  for (let i = 0; i < paragraphs.length; i++) {
    // Describe action
    bulkOps.push({ index: { _index: esConnection.index, _type: esConnection.type } })

    // Add document
    bulkOps.push({
      author,
      title,
      location: i,
      text: paragraphs[i]
    })

    if (i > 0 && i % 500 === 0) { // Do bulk insert in 500 paragraph batches
      await esConnection.client.bulk({ body: bulkOps })
      bulkOps = []
      console.log(`Indexed Paragraphs ${i - 499} - ${i}`)
    }
  }

  // Insert remainder of bulk ops array
  await esConnection.client.bulk({ body: bulkOps })
  console.log(`Indexed Paragraphs ${paragraphs.length - (bulkOps.length / 2)} - ${paragraphs.length}\n\n\n`)
}

这个函数将使用书名、作者和附加元数据的段落位置来索引书中的每个段落。我们通过批量操作来插入段落,它比逐个段落插入要快的多。

我们分批索引段落,而不是一次性插入全部,是为运行这个应用程序的内存稍有点小(1.7 GB)的服务器 search.patricktriest.com 上做的一个重要优化。如果你的机器内存还行(4 GB 以上),你或许不用分批上传。

运行 docker-compose up -d --builddocker exec gs-api "node" "server/load_data.js" 一次或多次 —— 现在你将看到前面解析的 100 本书的完整输出,并插入到了 Elasticsearch。这可能需要几分钟时间,甚至更长。

data loading output

5 - 搜索

现在,Elasticsearch 中已经有了 100 本书了(大约有 230000 个段落),现在我们尝试搜索查询。

5.0 - 简单的 HTTP 查询

首先,我们使用 Elasticsearch 的 HTTP API 对它进行直接查询。

在你的浏览器上访问这个 URL - http://localhost:9200/library/_search?q=text:Java&pretty

在这里,我们将执行一个极简的全文搜索,在我们的图书馆的书中查找 “Java” 这个词。

你将看到类似于下面的一个 JSON 格式的响应。

{
  "took" : 11,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 13,
    "max_score" : 14.259304,
    "hits" : [
      {
        "_index" : "library",
        "_type" : "novel",
        "_id" : "p_GwFWEBaZvLlaAUdQgV",
        "_score" : 14.259304,
        "_source" : {
          "author" : "Charles Darwin",
          "title" : "On the Origin of Species",
          "location" : 1080,
          "text" : "Java, plants of, 375."
        }
      },
      {
        "_index" : "library",
        "_type" : "novel",
        "_id" : "wfKwFWEBaZvLlaAUkjfk",
        "_score" : 10.186235,
        "_source" : {
          "author" : "Edgar Allan Poe",
          "title" : "The Works of Edgar Allan Poe",
          "location" : 827,
          "text" : "After many years spent in foreign travel, I sailed in the year 18-- , from the port of Batavia, in the rich and populous island of Java, on a voyage to the Archipelago of the Sunda islands. I went as passenger--having no other inducement than a kind of nervous restlessness which haunted me as a fiend."
        }
      },
      ...
    ]
  }
}

用 Elasticseach 的 HTTP 接口可以测试我们插入的数据是否成功,但是如果直接将这个 API 暴露给 Web 应用程序将有极大的风险。这个 API 将会暴露管理功能(比如直接添加和删除文档),最理想的情况是完全不要对外暴露它。而是写一个简单的 Node.js API 去接收来自客户端的请求,然后(在我们的本地网络中)生成一个正确的查询发送给 Elasticsearch。

5.1 - 查询脚本

我们现在尝试从我们写的 Node.js 脚本中查询 Elasticsearch。

创建一个新文件,server/search.js

const { client, index, type } = require('./connection')

module.exports = {
  /** Query ES index for the provided term */
  queryTerm (term, offset = 0) {
    const body = {
      from: offset,
      query: { match: {
        text: {
          query: term,
          operator: 'and',
          fuzziness: 'auto'
        } } },
      highlight: { fields: { text: {} } }
    }

    return client.search({ index, type, body })
  }
}

我们的搜索模块定义一个简单的 search 函数,它将使用输入的词 match 查询。

这是查询的字段分解 -

  • from - 允许我们分页查询结果。默认每个查询返回 10 个结果,因此,指定 from: 10 将允许我们取回 10-20 的结果。
  • query - 这里我们指定要查询的词。
  • operator - 我们可以修改搜索行为;在本案例中,我们使用 and 操作去对查询中包含所有字元(要查询的词)的结果来确定优先顺序。
  • fuzziness - 对拼写错误的容错调整,auto 的默认为 fuzziness: 2。模糊值越高,结果越需要更多校正。比如,fuzziness: 1 将允许以 Patricc 为关键字的查询中返回与 Patrick 匹配的结果。
  • highlights - 为结果返回一个额外的字段,这个字段包含 HTML,以显示精确的文本字集和查询中匹配的关键词。

你可以去浏览 Elastic Full-Text Query DSL,学习如何随意调整这些参数,以进一步自定义搜索查询。

6 - API

为了能够从前端应用程序中访问我们的搜索功能,我们来写一个快速的 HTTP API。

6.0 - API 服务器

用以下的内容替换现有的 server/app.js 文件。

const Koa = require('koa')
const Router = require('koa-router')
const joi = require('joi')
const validate = require('koa-joi-validate')
const search = require('./search')

const app = new Koa()
const router = new Router()

// Log each request to the console
app.use(async (ctx, next) => {
  const start = Date.now()
  await next()
  const ms = Date.now() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}`)
})

// Log percolated errors to the console
app.on('error', err => {
  console.error('Server Error', err)
})

// Set permissive CORS header
app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', '*')
  return next()
})

// ADD ENDPOINTS HERE

const port = process.env.PORT || 3000

app
  .use(router.routes())
  .use(router.allowedMethods())
  .listen(port, err => {
    if (err) throw err
    console.log(`App Listening on Port ${port}`)
  })

这些代码将为 Koa.js Node API 服务器导入服务器依赖,设置简单的日志,以及错误处理。

6.1 - 使用查询连接端点

接下来,我们将在服务器上添加一个端点,以便于发布我们的 Elasticsearch 查询功能。

server/app.js 文件的 // ADD ENDPOINTS HERE 下面插入下列的代码。

/**
 * GET /search
 * Search for a term in the library
 */
router.get('/search', async (ctx, next) => {
    const { term, offset } = ctx.request.query
    ctx.body = await search.queryTerm(term, offset)
  }
)

使用 docker-compose up -d --build 重启动应用程序。之后在你的浏览器中尝试调用这个搜索端点。比如,http://localhost:3000/search?term=java 这个请求将搜索整个图书馆中提到 “Java” 的内容。

结果与前面直接调用 Elasticsearch HTTP 界面的结果非常类似。

{
    "took": 242,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 93,
        "max_score": 13.356944,
        "hits": [{
            "_index": "library",
            "_type": "novel",
            "_id": "eHYHJmEBpQg9B4622421",
            "_score": 13.356944,
            "_source": {
                "author": "Charles Darwin",
                "title": "On the Origin of Species",
                "location": 1080,
                "text": "Java, plants of, 375."
            },
            "highlight": {
                "text": ["<em>Java</em>, plants of, 375."]
            }
        }, {
            "_index": "library",
            "_type": "novel",
            "_id": "2HUHJmEBpQg9B462xdNg",
            "_score": 9.030668,
            "_source": {
                "author": "Unknown Author",
                "title": "The King James Bible",
                "location": 186,
                "text": "10:4 And the sons of Javan; Elishah, and Tarshish, Kittim, and Dodanim."
            },
            "highlight": {
                "text": ["10:4 And the sons of <em>Javan</em>; Elishah, and Tarshish, Kittim, and Dodanim."]
            }
        }
        ...
      ]
   }
}

6.2 - 输入校验

这个端点现在还很脆弱 —— 我们没有对请求参数做任何的校验,因此,如果是无效的或者错误的值将使服务器出错。

我们将添加一些使用 JoiKoa-Joi-Validate 库的中间件,以对输入做校验。

/**
 * GET /search
 * Search for a term in the library
 * Query Params -
 * term: string under 60 characters
 * offset: positive integer
 */
router.get('/search',
  validate({
    query: {
      term: joi.string().max(60).required(),
      offset: joi.number().integer().min(0).default(0)
    }
  }),
  async (ctx, next) => {
    const { term, offset } = ctx.request.query
    ctx.body = await search.queryTerm(term, offset)
  }
)

现在,重启服务器,如果你使用一个没有搜索关键字的请求(http://localhost:3000/search),你将返回一个带相关消息的 HTTP 400 错误,比如像 Invalid URL Query - child "term" fails because ["term" is required]

如果想从 Node 应用程序中查看实时日志,你可以运行 docker-compose logs -f api

7 - 前端应用程序

现在我们的 /search 端点已经就绪,我们来连接到一个简单的 Web 应用程序来测试这个 API。

7.0 - Vue.js 应用程序

我们将使用 Vue.js 去协调我们的前端。

添加一个新文件 /public/app.js,去控制我们的 Vue.js 应用程序代码。

const vm = new Vue ({
  el: '#vue-instance',
  data () {
    return {
      baseUrl: 'http://localhost:3000', // API url
      searchTerm: 'Hello World', // Default search term
      searchDebounce: null, // Timeout for search bar debounce
      searchResults: [], // Displayed search results
      numHits: null, // Total search results found
      searchOffset: 0, // Search result pagination offset

      selectedParagraph: null, // Selected paragraph object
      bookOffset: 0, // Offset for book paragraphs being displayed
      paragraphs: [] // Paragraphs being displayed in book preview window
    }
  },
  async created () {
    this.searchResults = await this.search() // Search for default term
  },
  methods: {
    /** Debounce search input by 100 ms */
    onSearchInput () {
      clearTimeout(this.searchDebounce)
      this.searchDebounce = setTimeout(async () => {
        this.searchOffset = 0
        this.searchResults = await this.search()
      }, 100)
    },
    /** Call API to search for inputted term */
    async search () {
      const response = await axios.get(`${this.baseUrl}/search`, { params: { term: this.searchTerm, offset: this.searchOffset } })
      this.numHits = response.data.hits.total
      return response.data.hits.hits
    },
    /** Get next page of search results */
    async nextResultsPage () {
      if (this.numHits > 10) {
        this.searchOffset += 10
        if (this.searchOffset + 10 > this.numHits) { this.searchOffset = this.numHits - 10}
        this.searchResults = await this.search()
        document.documentElement.scrollTop = 0
      }
    },
    /** Get previous page of search results */
    async prevResultsPage () {
      this.searchOffset -= 10
      if (this.searchOffset < 0) { this.searchOffset = 0 }
      this.searchResults = await this.search()
      document.documentElement.scrollTop = 0
    }
  }
})

这个应用程序非常简单 —— 我们只定义了一些共享的数据属性,以及添加了检索和分页搜索结果的方法。为防止每次按键一次都调用 API,搜索输入有一个 100 毫秒的除颤功能。

解释 Vue.js 是如何工作的已经超出了本教程的范围,如果你使用过 Angular 或者 React,其实一些也不可怕。如果你完全不熟悉 Vue,想快速了解它的功能,我建议你从官方的快速指南入手 —— https://vuejs.org/v2/guide/

7.1 - HTML

使用以下的内容替换 /public/index.html 文件中的占位符,以便于加载我们的 Vue.js 应用程序和设计一个基本的搜索界面。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Elastic Library</title>
  <meta name="description" content="Literary Classic Search Engine.">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css" rel="stylesheet" type="text/css" />
  <link href="https://cdn.muicss.com/mui-0.9.20/css/mui.min.css" rel="stylesheet" type="text/css" />
  <link href="https://fonts.googleapis.com/css?family=EB+Garamond:400,700|Open+Sans" rel="stylesheet">
  <link href="styles.css" rel="stylesheet" />
</head>
<body>
<div class="app-container" id="vue-instance">
    <!-- Search Bar Header -->
    <div class="mui-panel">
      <div class="mui-textfield">
        <input v-model="searchTerm" type="text" v-on:keyup="onSearchInput()">
        <label>Search</label>
      </div>
    </div>

    <!-- Search Metadata Card -->
    <div class="mui-panel">
      <div class="mui--text-headline">{{ numHits }} Hits</div>
      <div class="mui--text-subhead">Displaying Results {{ searchOffset }} - {{ searchOffset + 9 }}</div>
    </div>

    <!-- Top Pagination Card -->
    <div class="mui-panel pagination-panel">
        <button class="mui-btn mui-btn--flat" v-on:click="prevResultsPage()">Prev Page</button>
        <button class="mui-btn mui-btn--flat" v-on:click="nextResultsPage()">Next Page</button>
    </div>

    <!-- Search Results Card List -->
    <div class="search-results" ref="searchResults">
      <div class="mui-panel" v-for="hit in searchResults" v-on:click="showBookModal(hit)">
        <div class="mui--text-title" v-html="hit.highlight.text[0]"></div>
        <div class="mui-divider"></div>
        <div class="mui--text-subhead">{{ hit._source.title }} - {{ hit._source.author }}</div>
        <div class="mui--text-body2">Location {{ hit._source.location }}</div>
      </div>
    </div>

    <!-- Bottom Pagination Card -->
    <div class="mui-panel pagination-panel">
        <button class="mui-btn mui-btn--flat" v-on:click="prevResultsPage()">Prev Page</button>
        <button class="mui-btn mui-btn--flat" v-on:click="nextResultsPage()">Next Page</button>
    </div>

    <!-- INSERT BOOK MODAL HERE -->
</div>
<script src="https://cdn.muicss.com/mui-0.9.28/js/mui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.17.0/axios.min.js"></script>
<script src="app.js"></script>
</body>
</html>

7.2 - CSS

添加一个新文件 /public/styles.css,使用一些自定义的 UI 样式。

body { font-family: 'EB Garamond', serif; }

.mui-textfield > input, .mui-btn, .mui--text-subhead, .mui-panel > .mui--text-headline {
  font-family: 'Open Sans', sans-serif;
}

.all-caps { text-transform: uppercase; }
.app-container { padding: 16px; }
.search-results em { font-weight: bold; }
.book-modal > button { width: 100%; }
.search-results .mui-divider { margin: 14px 0; }

.search-results {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: space-around;
}

.search-results > div {
  flex-basis: 45%;
  box-sizing: border-box;
  cursor: pointer;
}

@media (max-width: 600px) {
  .search-results > div { flex-basis: 100%; }
}

.paragraphs-container {
  max-width: 800px;
  margin: 0 auto;
  margin-bottom: 48px;
}

.paragraphs-container .mui--text-body1, .paragraphs-container .mui--text-body2 {
  font-size: 1.8rem;
  line-height: 35px;
}

.book-modal {
  width: 100%;
  height: 100%;
  padding: 40px 10%;
  box-sizing: border-box;
  margin: 0 auto;
  background-color: white;
  overflow-y: scroll;
  position: fixed;
  top: 0;
  left: 0;
}

.pagination-panel {
  display: flex;
  justify-content: space-between;
}

.title-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
}

@media (max-width: 600px) {
  .title-row{ 
    flex-direction: column; 
    text-align: center;
    align-items: center
  }
}

.locations-label {
  text-align: center;
  margin: 8px;
}

.modal-footer {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  display: flex;
  justify-content: space-around;
  background: white;
}

7.3 - 尝试输出

在你的浏览器中打开 localhost:8080,你将看到一个简单的带结果分页功能的搜索界面。在顶部的搜索框中尝试输入不同的关键字来查看它们的搜索情况。

preview webapp

你没有必要重新运行 docker-compose up 命令以使更改生效。本地的 public 目录是装载在我们的 Nginx 文件服务器容器中,因此,在本地系统中前端的变化将在容器化应用程序中自动反映出来。

如果你尝试点击任何搜索结果,什么反应也没有 —— 因为我们还没有为这个应用程序添加进一步的相关功能。

8 - 分页预览

如果能点击每个搜索结果,然后查看到来自书中的内容,那将是非常棒的体验。

8.0 - 添加 Elasticsearch 查询

首先,我们需要定义一个简单的查询去从给定的书中获取段落范围。

server/search.js 文件中添加如下的函数到 module.exports 块中。

/** Get the specified range of paragraphs from a book */
getParagraphs (bookTitle, startLocation, endLocation) {
  const filter = [
    { term: { title: bookTitle } },
    { range: { location: { gte: startLocation, lte: endLocation } } }
  ]

  const body = {
    size: endLocation - startLocation,
    sort: { location: 'asc' },
    query: { bool: { filter } }
  }

  return client.search({ index, type, body })
}

这个新函数将返回给定的书的开始位置和结束位置之间的一个排序后的段落数组。

8.1 - 添加 API 端点

现在,我们将这个函数链接到 API 端点。

添加下列内容到 server/app.js 文件中最初的 /search 端点下面。

/**
 * GET /paragraphs
 * Get a range of paragraphs from the specified book
 * Query Params -
 * bookTitle: string under 256 characters
 * start: positive integer
 * end: positive integer greater than start
 */
router.get('/paragraphs',
  validate({
    query: {
      bookTitle: joi.string().max(256).required(),
      start: joi.number().integer().min(0).default(0),
      end: joi.number().integer().greater(joi.ref('start')).default(10)
    }
  }),
  async (ctx, next) => {
    const { bookTitle, start, end } = ctx.request.query
    ctx.body = await search.getParagraphs(bookTitle, start, end)
  }
)

8.2 - 添加 UI 功能

现在,我们的新端点已经就绪,我们为应用程序添加一些从书中查询和显示全部页面的前端功能。

/public/app.js 文件的 methods 块中添加如下的函数。

    /** Call the API to get current page of paragraphs */
    async getParagraphs (bookTitle, offset) {
      try {
        this.bookOffset = offset
        const start = this.bookOffset
        const end = this.bookOffset + 10
        const response = await axios.get(`${this.baseUrl}/paragraphs`, { params: { bookTitle, start, end } })
        return response.data.hits.hits
      } catch (err) {
        console.error(err)
      }
    },
    /** Get next page (next 10 paragraphs) of selected book */
    async nextBookPage () {
      this.$refs.bookModal.scrollTop = 0
      this.paragraphs = await this.getParagraphs(this.selectedParagraph._source.title, this.bookOffset + 10)
    },
    /** Get previous page (previous 10 paragraphs) of selected book */
    async prevBookPage () {
      this.$refs.bookModal.scrollTop = 0
      this.paragraphs = await this.getParagraphs(this.selectedParagraph._source.title, this.bookOffset - 10)
    },
    /** Display paragraphs from selected book in modal window */
    async showBookModal (searchHit) {
      try {
        document.body.style.overflow = 'hidden'
        this.selectedParagraph = searchHit
        this.paragraphs = await this.getParagraphs(searchHit._source.title, searchHit._source.location - 5)
      } catch (err) {
        console.error(err)
      }
    },
    /** Close the book detail modal */
    closeBookModal () {
      document.body.style.overflow = 'auto'
      this.selectedParagraph = null
    }

这五个函数提供了通过页码从书中下载和分页(每次十个段落)的逻辑。

现在,我们需要添加一个 UI 去显示书的页面。在 /public/index.html<!-- INSERT BOOK MODAL HERE --> 注释下面添加如下的内容。

    <!-- Book Paragraphs Modal Window -->
    <div v-if="selectedParagraph" ref="bookModal" class="book-modal">
      <div class="paragraphs-container">
        <!-- Book Section Metadata -->
        <div class="title-row">
          <div class="mui--text-display2 all-caps">{{ selectedParagraph._source.title }}</div>
          <div class="mui--text-display1">{{ selectedParagraph._source.author }}</div>
        </div>
        <br>
        <div class="mui-divider"></div>
        <div class="mui--text-subhead locations-label">Locations {{ bookOffset - 5 }} to {{ bookOffset + 5 }}</div>
        <div class="mui-divider"></div>
        <br>

        <!-- Book Paragraphs -->
        <div v-for="paragraph in paragraphs">
          <div v-if="paragraph._source.location === selectedParagraph._source.location" class="mui--text-body2">
            <strong>{{ paragraph._source.text }}</strong>
          </div>
          <div v-else class="mui--text-body1">
            {{ paragraph._source.text }}
          </div>
          <br>
        </div>
      </div>

      <!-- Book Pagination Footer -->
      <div class="modal-footer">
        <button class="mui-btn mui-btn--flat" v-on:click="prevBookPage()">Prev Page</button>
        <button class="mui-btn mui-btn--flat" v-on:click="closeBookModal()">Close</button>
        <button class="mui-btn mui-btn--flat" v-on:click="nextBookPage()">Next Page</button>
      </div>
    </div>

再次重启应用程序服务器(docker-compose up -d --build),然后打开 localhost:8080。当你再次点击搜索结果时,你将能看到关键字附近的段落。如果你感兴趣,你现在甚至可以看这本书的剩余部分。

preview webapp book page

祝贺你!你现在已经完成了本教程的应用程序。

你可以去比较你的本地结果与托管在这里的完整示例 —— https://search.patricktriest.com/

9 - Elasticsearch 的缺点

9.0 - 耗费资源

Elasticsearch 是计算密集型的。官方建议 运行 ES 的机器最好有 64 GB 的内存,强烈反对在低于 8 GB 内存的机器上运行它。Elasticsearch 是一个 内存中 数据库,这样使它的查询速度非常快,但这也非常占用系统内存。在生产系统中使用时,他们强烈建议在一个集群中运行多个 Elasticsearch 节点,以实现高可用、自动分区和一个节点失败时的数据冗余。

我们的这个教程中的应用程序运行在一个 $15/月 的 GCP 计算实例中( search.patricktriest.com),它只有 1.7 GB 的内存,它勉强能运行这个 Elasticsearch 节点;有时候在进行初始的数据加载过程中,整个机器就 ”假死机“ 了。在我的经验中,Elasticsearch 比传统的那些数据库,比如,PostgreSQL 和 MongoDB 耗费的资源要多很多,这样会使托管主机的成本增加很多。

9.1 - 与数据库的同步

对于大多数应用程序,将数据全部保存在 Elasticsearch 并不是个好的选择。可以使用 ES 作为应用程序的主要事务数据库,但是一般不推荐这样做,因为在 Elasticsearch 中缺少 ACID,如果大量读取数据的时候,它能导致写操作丢失。在许多案例中,ES 服务器更多是一个特定的角色,比如做应用程序中的一个文本搜索功能。这种特定的用途,要求它从主数据库中复制数据到 Elasticsearch 实例中。

比如,假设我们将用户信息保存在一个 PostgreSQL 表中,但是用 Elasticsearch 去提供我们的用户搜索功能。如果一个用户,比如,“Albert”,决定将他的名字改成 “Al”,我们将需要把这个变化同时反映到我们主要的 PostgreSQL 数据库和辅助的 Elasticsearch 集群中。

正确地集成它们可能比较棘手,最好的答案将取决于你现有的应用程序栈。这有多种开源方案可选,从 用一个进程去关注 MongoDB 操作日志 并自动同步检测到的变化到 ES,到使用一个 PostgresSQL 插件 去创建一个定制的、基于 PSQL 的索引来与 Elasticsearch 进行自动沟通。

如果没有有效的预构建选项可用,你可能需要在你的服务器代码中增加一些钩子,这样可以基于数据库的变化来手动更新 Elasticsearch 索引。最后一招,我认为是一个最后的选择,因为,使用定制的业务逻辑去保持 ES 的同步可能很复杂,这将会给应用程序引入很多的 bug。

让 Elasticsearch 与一个主数据库同步,将使它的架构更加复杂,其复杂性已经超越了 ES 的相关缺点,但是当在你的应用程序中考虑添加一个专用的搜索引擎的利弊得失时,这个问题是值的好好考虑的。

总结

在很多现在流行的应用程序中,全文搜索是一个非常重要的功能 —— 而且是很难实现的一个功能。对于在你的应用程序中添加一个快速而又可定制的文本搜索,Elasticsearch 是一个非常好的选择,但是,在这里也有一个替代者。Apache Solr 是一个类似的开源搜索平台,它是基于 Apache Lucene 构建的,与 Elasticsearch 的核心库是相同的。Algolia 是一个搜索即服务的 Web 平台,它已经很快流行了起来,并且它对新手非常友好,很易于上手(但是作为折衷,它的可定制性较小,并且使用成本较高)。

“搜索” 特性并不是 Elasticsearch 唯一功能。ES 也是日志存储和分析的常用工具,在一个 ELK(Elasticsearch、Logstash、Kibana)架构配置中通常会使用它。灵活的全文搜索功能使得 Elasticsearch 在数据量非常大的科学任务中用处很大 —— 比如,在一个数据集中正确的/标准化的条目拼写,或者为了类似的词组搜索一个文本数据集。

对于你自己的项目,这里有一些创意。

  • 添加更多你喜欢的书到教程的应用程序中,然后创建你自己的私人图书馆搜索引擎。
  • 利用来自 Google Scholar 的论文索引,创建一个学术抄袭检测引擎。
  • 通过将字典中的每个词索引到 Elasticsearch,创建一个拼写检查应用程序。
  • 通过将 Common Crawl Corpus 加载到 Elasticsearch 中,构建你自己的与谷歌竞争的因特网搜索引擎(注意,它可能会超过 50 亿个页面,这是一个成本极高的数据集)。
  • 在 journalism 上使用 Elasticsearch:在最近的大规模泄露的文档中搜索特定的名字和关键词,比如, Panama PapersParadise Papers

本教程中应用程序的源代码是 100% 公开的,你可以在 GitHub 仓库上找到它们 —— https://github.com/triestpa/guttenberg-search

我希望你喜欢这个教程!你可以在下面的评论区,发表任何你的想法、问题、或者评论。


作者简介:

全栈工程师,数据爱好者,学霸,“构建强迫症患者”,探险爱好者。


via: https://blog.patricktriest.com/text-search-docker-elasticsearch/

作者:Patrick Triest 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

使用简单的命令在 Linux 下基于类型、内容等快速查找文件。

如果你是 Windows 或 OSX 的非资深用户,那么可能使用 GUI 来查找文件。你也可能发现界面受限,令人沮丧,或者两者兼而有之,并学会了组织文件并记住它们的确切顺序。你也可以在 Linux 中做到这一点 —— 但你不必这样做。

Linux 的好处之一是它提供了多种方式来处理。你可以打开任何文件管理器或按下 Ctrl+F,你也可以使用程序手动打开文件,或者你可以开始输入字母,它会过滤当前目录列表。

 title=

使用 Ctrl+F 在 Linux 中查找文件的截图

但是如果你不知道你的文件在哪里,又不想搜索整个磁盘呢?对于这个以及其他各种情况,Linux 都很合适。

按命令名查找程序位置

如果你习惯随心所欲地放文件,Linux 文件系统看起来会让人望而生畏。对我而言,最难习惯的一件事是找到程序在哪里。

例如,which bash 通常会返回 /bin/bash,但是如果你下载了一个程序并且它没有出现在你的菜单中,那么 which 命令就是一个很好的工具。

一个类似的工具是 locate 命令,我发现它对于查找配置文件很有用。我不喜欢输入程序名称,因为像 locate php 这样的简单程序通常会提供很多需要进一步过滤的结果。

有关 locatewhich 的更多信息,请参阅 man 页面:

  • man which
  • man locate

find

find 工具提供了更先进的功能。以下是我安装在许多服务器上的脚本示例,我用于确保特定模式的文件(也称为 glob)仅存在五天,并且所有早于此的文件都将被删除。 (自上次修改以来,分数用于保留最多 240 分钟的偏差)

find ./backup/core-files*.tar.gz -mtime +4.9 -exec rm {} \;

find 工具有许多高级用法,但最常见的是对结果执行命令,而不用链式地按照类型、创建日期、修改日期过滤文件。

find 的另一个有趣用处是找到所有有可执行权限的文件。这有助于确保没有人在你昂贵的服务器上安装比特币挖矿程序或僵尸网络。

find / -perm /+x

有关 find 的更多信息,请使用 man find 参考 man 页面。

grep

想通过内容中查找文件? Linux 已经实现了。你可以使用许多 Linux 工具来高效搜索符合模式的文件,但是 grep 是我经常使用的工具。

假设你有一个程序发布代码引用和堆栈跟踪的错误消息。你要在日志中找到这些。 grep 不总是最好的方法,但如果文件是一个给定的值,我经常使用 grep -R

越来越多的 IDE 正在实现查找功能,但是如果你正在访问远程系统或出于任何原因没有 GUI,或者如果你想在当前目录递归查找,请使用:grep -R {searchterm} 或在支持 egrep 别名的系统上,只需将 -e 标志添加到命令 egrep -r {regex-pattern}

我在去年给 Raspbian 中的 dhcpcd5 打补丁时使用了这种技术,这样我就可以在树莓派基金会发布新的 Debian 时继续操作网络接入点了。

哪些提示可帮助你在 Linux 上更有效地搜索文件?


via: https://opensource.com/article/18/4/how-find-files-linux

作者:Lewis Cowles 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 Linux 系统上搜索文件的方法有很多,有的命令很简单,有的很详细。我们的目标是:缩小搜索范围,找到您正在寻找的文件,又不受其他文件的干扰。在今天的文章中,我们将研究一些对文件搜索最有用的命令和选项。我们将涉及:

  • 快速搜索
  • 更复杂的搜索条件
  • 组合条件
  • 反转条件
  • 简单和详细的回应
  • 寻找重复的文件

有很多有用的命令可以搜索文件,find 命令可能是其中最有名的,但它不是唯一的命令,也不一定总是找到目标文件的最快方法。

快速搜索命令:which 和 locate

搜索文件的最简单的命令可能就是 whichlocate 了,但二者都有一些局限性。which 命令只会在系统定义的搜索路径中,查找可执行的文件,通常用于识别命令。如果您对输入 which 时会运行哪个命令感到好奇,您可以使用命令 which which,它会指出对应的可执行文件。

$ which which
/usr/bin/which

which 命令会显示它找到的第一个以相应名称命名的可执行文件(也就是使用该命令时将运行的那个文件),然后停止。

locate 命令更大方一点,它可以查找任意数量的文件,但它也有一个限制:仅当文件名被包含在由 updatedb 命令构建的数据库时才有效。该文件可能会存储在某个位置,如 /var/lib/mlocate/mlocate.db,但不能用 locate 以外的任何命令读取。这个文件的更新通常是通过每天通过 cron 运行的 updatedb 进行的。

简单的 find 命令没有太多限制,不过它需要指定搜索的起点和搜索条件。最简单的 find 命令:按文件名搜索文件。如下所示:

$ find . -name runme
./bin/runme

如上所示,通过文件名搜索文件系统的当前位置将会搜索所有子目录,除非您指定了搜索深度。

不仅仅是文件名

find 命令允许您搜索除文件名以外的多种条件,包括文件所有者、组、权限、大小、修改时间、缺少所有者或组,和文件类型等。除了查找文件外,您还可以删除文件、对其进行重命名、更改所有者、更改权限和对找到的文件运行几乎任何命令。

下面两条命令会查找:在当前目录中 root 用户拥有的文件,以及不被指定用户(在本例中为 shs)所拥有的文件。在这个例子中,两个输出是一样的,但并不总是如此。

$ find . -user root -ls
 396926 0 lrwxrwxrwx 1 root root 21 Sep 21 09:03 ./xyz -> /home/peanut/xyz
$ find . ! -user shs -ls
 396926 0 lrwxrwxrwx 1 root root 21 Sep 21 09:03 ./xyz -> /home/peanut/xyz

感叹号 ! 字符代表“非”:反转跟随其后的条件。

下面的命令将查找具有特定权限的文件:

$ find . -perm 750 -ls
 397176 4 -rwxr-x--- 1 shs shs 115 Sep 14 13:52 ./ll
 398209 4 -rwxr-x--- 1 shs shs 117 Sep 21 08:55 ./get-updates
 397145 4 drwxr-x--- 2 shs shs 4096 Sep 14 15:42 ./newdir

接下来的命令显示具有 777 权限的非符号链接文件:

$ sudo find /home -perm 777 ! -type l -ls
 397132 4 -rwxrwxrwx 1 shs shs 18 Sep 15 16:06 /home/shs/bin/runme
 396949 4 -rwxrwxrwx 1 root root 558 Sep 21 11:21 /home/oops

以下命令将查找大小超过千兆字节的文件。请注意,我们找到了一个非常有趣的文件。它以 ELF core 文件格式表示了该系统的物理内存。

$ sudo find / -size +1G -ls
 4026531994 0 -r-------- 1 root root 140737477881856 Sep 21 11:23 /proc/kcore
 1444722 15332 -rw-rw-r-- 1 shs shs 1609039872 Sep 13 15:55 /home/shs/Downloads/ubuntu-17.04-desktop-amd64.iso

只要您知道 find 命令是如何描述文件类型的,就可以通过文件类型来查找文件。

b = 块设备文件
c = 字符设备文件
d = 目录
p = 命名管道
f = 常规文件
l = 符号链接
s = 套接字
D = 门(仅限 Solaris)

在下面的命令中,我们要寻找符号链接和套接字:

$ find . -type l -ls
 396926 0 lrwxrwxrwx 1 root root 21 Sep 21 09:03 ./whatever -> /home/peanut/whatever
$ find . -type s -ls
 395256 0 srwxrwxr-x 1 shs shs 0 Sep 21 08:50 ./.gnupg/S.gpg-agent

您还可以根据 inode 号来搜索文件:

$ find . -inum 397132 -ls
 397132 4 -rwx------ 1 shs shs 18 Sep 15 16:06 ./bin/runme

另一种通过 inode 搜索文件的方法是使用 debugfs 命令。在大的文件系统上,这个命令可能比 find 快得多,您可能需要安装 icheck。

$ sudo debugfs -R 'ncheck 397132' /dev/sda1
debugfs 1.42.13 (17-May-2015)
Inode Pathname
397132 /home/shs/bin/runme

在下面的命令中,我们从主目录(~)开始,限制搜索的深度(即我们将搜索子目录的层数),并只查看在最近一天内创建或修改的文件(mtime 设置)。

$ find ~ -maxdepth 2 -mtime -1 -ls
 407928 4 drwxr-xr-x 21 shs shs 4096 Sep 21 12:03 /home/shs
 394006 8 -rw------- 1 shs shs 5909 Sep 21 08:18 /home/shs/.bash_history
 399612 4 -rw------- 1 shs shs 53 Sep 21 08:50 /home/shs/.Xauthority
 399615 4 drwxr-xr-x 2 shs shs 4096 Sep 21 09:32 /home/shs/Downloads

不仅仅是列出文件

使用 -exec 选项,在您使用 find 命令找到文件后可以以某种方式更改文件。您只需参照 -exec 选项即可运行相应的命令。

$ find . -name runme -exec chmod 700 {} \;
$ find . -name runme -ls
 397132 4 -rwx------ 1 shs shs 18 Sep 15 16:06 ./bin/runme

在这条命令中,{} 代表文件名。此命令将更改当前目录和子目录中任何名为 runme 的文件的权限。

把您想运行的任何命令放在 -exec 选项之后,并使用类似于上面命令的语法即可。

其他搜索条件

如上面的例子所示,您还可以通过其他条件进行搜索:文件的修改时间、所有者、权限等。以下是一些示例。

根据用户查找文件

$ sudo find /home -user peanut
/home/peanut
/home/peanut/.bashrc
/home/peanut/.bash_logout
/home/peanut/.profile
/home/peanut/examples.desktop

根据权限查找文件

$ sudo find /home -perm 777
/home/shs/whatever
/home/oops

根据修改时间查找文件

$ sudo find /home -mtime +100
/home/shs/.mozilla/firefox/krsw3giq.default/gmp-gmpopenh264/1.6/gmpopenh264.info
/home/shs/.mozilla/firefox/krsw3giq.default/gmp-gmpopenh264/1.6/libgmpopenh264.so

通过比较修改时间查找文件

像这样的命令可以让您找到修改时间较近的文件。

$ sudo find /var/log -newer /var/log/syslog
/var/log/auth.log

寻找重复的文件

如果您正在清理磁盘空间,则可能需要删除较大的重复文件。确定文件是否真正重复的最好方法是使用 fdupes 命令。此命令使用 md5 校验和来确定文件是否具有相同的内容。使用 -r(递归)选项,fdupes 将在一个目录下并查找具有相同校验和而被确定为内容相同的文件。

如果以 root 身份运行这样的命令,您可能会发现很多重复的文件,但是很多文件都是创建时被添加到主目录的启动文件。

# fdupes -rn /home > /tmp/dups.txt
# more /tmp/dups.txt
/home/jdoe/.profile
/home/tsmith/.profile
/home/peanut/.profile
/home/rocket/.profile

/home/jdoe/.bashrc
/home/tsmith/.bashrc
/home/peanut/.bashrc
/home/rocket/.bashrc

同样,您可能会在 /usr 中发现很多重复的但不该删除的配置文件。所以,请谨慎利用 fdupes 的输出。

fdupes 命令并不总是很快,但是要记住,它正在对许多文件运行校验和来做比较,你可能会意识到它是多么有效。

总结

有很多方法可以在 Linux 系统上查找文件。如果您可以描述清楚您正在寻找什么,上面的命令将帮助您找到目标。


via: https://www.networkworld.com/article/3227075/linux/mastering-file-searches-on-linux.html

作者:Sandra Henry-Stocker 译者:jessie-pang 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出