用PHP构建自定义搜索引擎

Posted on 2011-04-27 11:55 PHP博客 阅读(195) 评论(0)  编辑 收藏 引用 网摘
在 Internet 时代,人们希望信息能够像快餐一样被打包起来:能够快速无障碍使用,并且分为很小的单位(或者是以字节 大小为单位?)。实际上,为了满足急躁而又渴求信息的用户的需求,甚至最普通的 Web 站点现在都要求具有快速浏览样式的各种菜单:

    * RSS 是比萨快递员,会把新鲜出炉的比萨送上门。
    * 网络日志是当地的中餐馆,为您献上喜爱的风味菜肴。
    * 论坛是家常便饭(或者可能更恰当地说,“动物屋” 中抢夺食物的场景)。
    * 而搜索就像在当地的餐厅吃自助晚餐一样:不断将想吃的食物填满盘子就行,只要您的食道 —— 还有您的椅子 —— 撑得住。

幸运的是,PHP 开发人员可以找到各种 RSS、blog 和论坛软件来创建或者改进站点。而且,虽然 Google 和其他搜索站点几乎无所不能并且执行过滤通信,但是搜索引擎并不一定会良好地适应各个站点。

例如,如果 Web 站点提供成百上千的全新和翻新的保时捷汽车零件,Google 可能通过诸如 “Carrera parts” 之类的广义搜索找到您的站点,但是对于更具体的 “used 1991 Porsche 911 Targa headlight bezel” 查询,它可能不会得到精确结果。

如果站点内容高度专业化,或者访问者期望搜索功能与现实工作流类似,那么最好在 Web 的全局搜索引擎基础上增加一个为您的站点量身定做的本地搜索系统(有关专业化搜索的更多实例,请参阅 “A needle in a billion haystacks”)。

通过本文了解如何向 PHP 站点中添加一个快速、高效、开源和免费的搜索引擎。本文没有开发可见的 Web 站点。相反,重点讨论交付有效搜索结果所需的组件:数据库、索引、搜索引擎和 PHP 应用程序编程接口 (API)。

访问优秀的 sphinx

要为站点提供自定义搜索功能,您必须有数据源和搜索该数据源的功能。对于 Web 应用程序,数据源通常是一个关系数据库,其中内置了一些搜索功能(Equality 是一个简单的搜索运算符,与 SQL 运算符 LIKE 一样)。但是,一些搜索可能比数据库可以执行的搜索更加具体,或者搜索可能过于复杂,而导致固有的 SQL JOIN 反应迟钝。
海底捞针

许多站点提供特定于某一个行业、职业或者娱乐的内容,例如医药、法律、音乐和汽车维修。深入研究这些内容可能要求使用特殊工具或者培训,或者仅需要使用一个索引来生成相关的实用结果。

下面是一些需要定制搜索系统的常见搜索场景:

    * 查找 Joe Hockey 所撰写的关于斯坦利杯 (Stanley Cup) 的所有文章。
    * 查找 HP LaserJet 3015 All-in-One 打印机的最新驱动程序。
    * 查找 Dinosaur Jr. 参与大卫深夜脱口秀节目的电视片段。

要加速搜索,您可以重新安排表,并由此简化底层查询(表和 SQL 查询优化高度依赖于模式和引擎。可通过在线搜索查找有关数据库性能的各种文章和书籍)。此外,您可以添加一个专门化的搜索引擎。应用哪种形式的搜索引擎还依赖于数据的形式(和数量)和预算。有许多选择可用:您可以将一个 Google 工具连接到您的网络中,购买 Endeca 或其他大型商业搜索产品,或者尝试 Lucene。但是在很多情况下,使用商业产品都有点小题大做,或者浪费运营预算,并且 Lucene 在 2007 年 7 月编写时并未提供 PHP API。

作为一个备选方案,考虑一下 Sphinx,它是一种开源和免费的搜索引擎,可以非常快速地搜索文本。例如,在一个几乎有 300,000 行及五个索引列(每列包含大约 15 个单词)的活动数据库中,Sphinx 可以在 1/100 秒内得到 “这些单词中任何一个单词” 的搜索结果(在运行 Debian Linux® Sarge 的 2-GHz AMD Opteron 处理器、1 GB RAM 的计算机上)。

Sphinx 提供了大量功能,包括:

    * 它可以为能够表示为字符串的所有数据建立索引。
    * 它可以以各种方式为相同数据建立索引。对于多个索引,每个索引都针对特定目的而定制,您可以选择最适当的索引来优化搜索结果。
    * 它可以把属性与每条索引数据关联起来。然后您可以使用一个或多个属性来进一步过滤搜索结果。
    * 它支持词法,因此搜索单词 “cats” 还会找到词根 “cat”。
    * 您可以在许多计算机中分发 Sphinx 索引,从而提供故障恢复功能。
    * 它可以创建任意长度的单词前缀索引和可变长度的中缀子字符串的索引。例如,一个零件号可以是 10 个字符宽。前缀索引将匹配位于字符串开头处的所有可能的子字符串。中缀索引将匹配在字符串内任意位置的子字符串。
    * 您可以在 MySQL V5 内将其作为存储引擎运行,降低使用其他守护程序的需求(通常被视为另一个故障点)。

您可以在 Sphinx 源代码附带的 README 文件中或通过在线资料找到完整的功能列表。Sphinx Web 站点还列出了已经部署了 Sphinx 的若干个项目。

Sphinx 是用 C++ 编写、用 GNU 编译器构建、支持 64 位支持平台,并在 Linux、UNIX®、Microsoft® Windows® 和 Mac OS X 上运行。构建 Sphinx 十分简单:下载并解压缩代码,然后运行 ./configure && make && make install 命令。

默认情况下,Sphinx 实用程序将被安装到 /usr/local/bin/ 中,并且所有 Sphinx 组件的配置文件都位于 /usr/local/etc/sphinx.conf 中。

Sphinx 有三个组件:索引生成器、搜索引擎和命令行 search 实用程序:

    * 索引生成器被称为索引器。它将查询数据库,为结果的每行中的每列建立索引,并且将每个索引条目绑定到行的主键上。
    * 搜索引擎是名为 searchd 的守护程序。该守护程序将接收搜索词和其他参数,快速遍历一个或多个索引,并返回结果。如果找到匹配,searchd 将返回一个主键数组。对于这些键,应用程序可以针对相关数据库运行查询来查找包含匹配的完整记录。Searchd 将在端口 3312 上通过套接字连接与应用程序进行通信。
    * 便捷的 search 实用程序使您可以从命令行构造搜索而无需编写代码。如果 searchd 返回匹配,则 search 将查询数据库并显示匹配集中的行。search 实用程序对于调试 Sphinx 配置和执行临时搜索十分有用。

此外,Sphinx 的作者 Andrew Aksyonoff 和其他贡献者为 PHP、Perl、C/C++ 和其他编程语言提供了 API。

回页首

搜索车身零件

假定 Body-Parts.com 出售车身零件 —— 挡泥板、铬、缓冲器等 —— 用于珍贵且值得收藏的汽车。正如在现实世界中,Body Parts 站点的访问者很可能按制造商(比如保时捷或制造同类零件的第三方制造商)、零件号、产地、车型、年份、条件(二手、全新、翻新)以及描述或者这些属性的某种组合来搜索零件。

要构建 Body Parts 搜索功能,让我们使用 MySQL V5.0 作为数据存储并使用 Sphinx search 守护程序来提供快速而精确的文本搜索。MySQL V5.0 是一个功能强大的数据库,但是它的增强型全文本搜索功能并不特别丰富。实际上,它仅限于 MyISAM 表 —— 不支持外键的一种表格式,因此使用有限。债务追讨

清单 1 至清单 4 显示了与此示例相关的 Body Parts 模式的部分代码。您将分别看到 Model(清单 1)、Assembly(清单 2)、Inventory(清单 3)和 Schematic(清单 4)表。

Model 表

清单 1 中所示的 Model 表十分简单:label 列将列举车型的名称 (“Corvette”);description 使用客户友好方式进行描述(“两门跑车;第一年引入”);而 begin_production 和 end_production 分别表示开始生产和结束生产该车型的年份。由于前述列中的值并不惟一,因此使用一个独立 ID 表示每四个这样的元素(label、description、begin_production、end_production),并且是其他表中的外键。人人
清单 1. 车身零件 Model 表

CREATE TABLE Model (
  id int(10) unsigned NOT NULL auto_increment,
  label varchar(7) NOT NULL,
  description varchar(256) NOT NULL,
  begin_production int(4) NOT NULL,
  end_production int(4) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB;

下面是 Model 表的一些样例数据:

INSERT INTO Model
  (`id`, `label`, `description`, `begin_production`, `end_production`)
VALUES
  (1,'X Sedan','Four-door performance sedan',1998,1999),
  (3,'X Sedan','Four door performance sedan, 1st model year',1995,1997),
  (4,'J Convertible','Two-door roadster, metal retracting roof',2002,2005),
  (5,'J Convertible','Two-door roadster',2000,2001),
  (7,'W Wagon','Four-door, all-wheel drive sport station wagon',2007,0);

Assembly 表

assembly 是一个子系统,例如汽车上安装的传动装置或所有玻璃。车主使用部件图及相关零件列表来查找备件。清单 2 中所示的 Assembly 表也十分简单:它将把一个惟一 ID 与部件标签和描述关联起来。
清单 2. Assembly 表

CREATE TABLE Assembly (
  id int(10) unsigned NOT NULL auto_increment,
  label varchar(7) NOT NULL,
  description varchar(128) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB;

继续示例,下面是 Assembly 表的一些样例数据:

INSERT INTO Assembly
  (`id`, `label`, `description`)
VALUES
  (1,'5-00','Seats'),
  (2,'4-00','Electrical'),什么是海运提单
  (3,'3-00','Glasses'),
  (4,'2-00','Frame'),
  (5,'1-00','Engine'),
  (7,'101-00','Accessories');

Inventory 表

Inventory 表是汽车零件的典范列表。零件 —— 例如螺钉或灯泡 —— 可能用于每辆汽车和多个部件中,但是零件只在 Inventory 表中显示一次。Inventory 表中的每行包含:

    * 使用了惟一的 32 位整数 serialno 标识行。
    * 字母数字零件号(此零件号惟一并且可以用作主键。但是,由于它可以包含字母数字字符,因此它不适于与 Sphinx 结合使用,Sphinx 要求索引的每条记录都有一个惟一的 32 位整型键)。
    * 文本描述。
    * 价格。

Inventory 表的规范如清单 3 中所示:
清单 3. Inventory 表

CREATE TABLE Inventory (
  id int(10) unsigned NOT NULL auto_increment,
  partno varchar(32) NOT NULL,
  description varchar(256) NOT NULL,
  price float unsigned NOT NULL default '0',
  PRIMARY KEY (id),
  UNIQUE KEY partno USING BTREE (partno)
) ENGINE=InnoDB;

零件的(部分)列表可能如下面所示:

INSERT INTO `Inventory`
  (`id`, `partno`, `description`, `price`)
VALUES
  (1,'WIN408','Portal window',423),
  (2,'ACC711','Jack kit',110),
  (3,'ACC43','Rear-view mirror',55),
  (4,'ACC5409','Cigarette lighter',20),
  (5,'WIN958','Windshield, front',500),
  (6,'765432','Bolt',0.1),
  (7,'ENG001','Entire engine',10000),
  (8,'ENG088','Cylinder head',55),
  (9,'ENG976','Large cylinder head',65);

Schematic 表

Schematic 表将把零件与部件和车型版本绑定在一起。因此,将使用 Schematic 表来查找组装 1979 J Class 敞篷车引擎的所有零件。Schematic 表中的每行都有一个惟一 ID,一个引用 Inventory 表行的外键,一个标识部件的外键,以及用于引用 Model 表中特定型号和版本的另一个键。各行如清单 4 所示:
清单 4. Schematic 表

CREATE TABLE Schematic (
  id int(10) unsigned NOT NULL auto_increment,
  partno_id int(10) unsigned NOT NULL,
  assembly_id int(10) unsigned NOT NULL,
  model_id int(10) unsigned NOT NULL,
  PRIMARY KEY (id),
  KEY partno_index USING BTREE (partno_id),
  KEY assembly_index USING BTREE (assembly_id),
  KEY model_index USING BTREE (model_id),
  FOREIGN KEY (partno_id) REFERENCES Inventory(id),
  FOREIGN KEY (assembly_id) REFERENCES Assembly(id),
  FOREIGN KEY (model_id) REFERENCES Model(id)
) ENGINE=InnoDB;

为了强化表的意图,下面是 Schematic 中的一张小型行列表:

INSERT INTO `Schematic`
  (`id`, `partno_id`, `assembly_id`, `model_id`)
VALUES
  (1,6,5,1),
  (2,8,5,1),
  (3,1,3,1),
  (4,5,3,1),
  (5,8,5,7),
  (6,6,5,7),
  (7,4,7,3),
  (8,9,5,3);

搜索表

定义了这些表后,就可以轻松地响应很多搜索:

    * 显示特定型号的所有版本
    * 列出装配特殊型号和版本所需的所有部件
    * 显示构成特定型号和版本的特殊部件的所有零件

但是很多搜索代价较大:

    * 查找所有模型和版本中出现零件号开头为 “WIN” 的所有零件
    * 查找描述中有 “lacquer” 或 “paint” 的那些零件
    * 查找描述中有 “black leather” 的所有零件
    * 查找描述中有 “paint” 的所有 2002 J 系列零件

这些搜索中的每个搜索都要求使用长篇的 JOIN 子句或代价高昂的 LIKE 子句,尤其是在 Inventory 表和 Schematic 表十分大时更是如此。而且,复杂的文本搜索完全超出了 MySQL 的能力。要搜索大量文本数据,请考虑构建和使用 Sphinx 索引。

只有注册用户登录后才能发表评论。
网站导航:

posts - 139, comments - 0, trackbacks - 0, articles - 0

Copyright © PHP博客