如何使用 C# 编译和使用 Xapian
本文探讨了如何在 Windows 上编译和使用 Xapian 搜索技术,以及其中的陷阱。
注意:本文内容全部基于 Xapian 1.0.18 版本。未来版本中,内容(例如文件中的位置)可能会有所不同。
引言
如果您正在为您的网站或应用程序构建搜索功能,市面上有很多选择。Xapian 是其中之一,从表面上看,它似乎是一个相当不错的选择,因为它的功能列表很有吸引力且完整。它还包括一个索引器(omega),可以索引并将各种文档格式添加到 Xapian 数据库中,这在您开始实际构建索引和搜索组件时极具吸引力。
然而,所有的文档和支持似乎都围绕着各种 *nix 平台构建(无论是编译还是从预编译包中获取库 [取决于发行版])。有 C# 的绑定,也有预编译的 C# 绑定可供下载。本文讨论了如何开始使用这些绑定,如何编译 Xapian 包的其余部分 (omega),以及该库在 Windows 环境中的陷阱。
背景
搜索技术通常有三个组成部分。
第一个组件通常被称为文档索引,这个名称有些不准确。它实际是从文档(PDF、网站、Office 文档等)中提取文本的过程。在 Windows 领域,有多种技术可以完成这项工作。典型的方法是使用 IFilter
(COM 对象)。对于大多数格式,也有命令行工具(稍后会详细介绍)。
第二个组件是这些文档中文本的实际索引。关于如何最好地分离文档中的单词、如何存储它们以及词干提取等语言技巧,有很多理论。这就是 Xapian 等工具非常重要的原因,因为自行构建索引器所需的精力非常大。
第三个组件是搜索组件——如何实际从创建的索引中检索存储的信息和文档。该组件通常与索引组件紧密集成,因为它将针对创建的索引进行搜索。同样,在大多数情况下,像 Xapian 这样的工具远比自行开发的工具更受欢迎,因为查询理论相当复杂。
入门
第一步是下载 C# 绑定。有两个重要的文件:XapianCSharp.dll(Xapian C++ DLL 的实际 C# 绑定)和 _XapianSharp.dll(C++ Xapian 核心功能)。
您还需要下载 zlib。您将需要从此次下载中获取 zlib1.dll。
在 Visual Studio 中创建一个新的命令行项目。添加对 XapianCSharp.dll 的引用。将 _XapianSharp.dll 和 zlib1.dll 添加到项目中,并确保它们在编译期间设置为复制到输出目录。
添加一个新类(SearchManager.cs),它将作为 Xapian 调用的中介。添加一个 OpenWriteDatabase
方法以写入模式打开 Xapian 数据库。添加一个 AddDocument
方法,该方法将文档添加到索引中,并存储有关该文档的一些信息,这些信息稍后在搜索时可以使用。
public class SearchManager
{
private const string DB_PATH = @"c:\temp\xap.db";
private static WritableDatabase OpenWriteDatabase()
{
return new WritableDatabase(DB_PATH,
Xapian.Xapian.DB_CREATE_OR_OPEN);
}
/// <summary>
/// Adds a document to the search index
/// </summary>
/// the application specific id for
/// the particular item we're storing (ie. uploadId)
/// the type of object we're storing (upload, client, etc.)
/// the text to store
/// <returns>the index document id</returns>
public static int AddDocument( int id, string type, string body )
{
// since the Xapian wrapper is PInvoking
// into a C++ dll, we need to be pretty strict
// on our memory management, so let's
// "using" everything that it's touching.
using( var db = OpenWriteDatabase() )
using( var indexer = new TermGenerator() )
using( var stemmer = new Stem("english") )
using( var doc = new Document())
{
// set the data on the document. Xapian ignores
// this data, but you can use it when you get a
// document returned to you from a search
// to do something useful (like build a link)
doc.SetData(string.Format( "{0}_{1}", type, id));
// the indexer actually is what will build the terms
// in the document so Xapian can search and find the document.
indexer.SetStemmer(stemmer);
indexer.SetDocument(doc);
indexer.IndexText(body);
// Add the document to the index
return (int)db.AddDocument(doc);
}
}
}
向您的项目添加另一个类 SearchResult.cs,以处理查询结果。
public class SearchResult
{
public int Id { get; set; }
public string Type { get; set; }
public int ResultRank { get; set; }
public int ResultPercentage { get; set; }
public SearchResult( string combinedId )
{
var parts = combinedId.Split('_');
if ( parts.Length == 2 )
{
Type = parts[0];
int i;
if ( !int.TryParse( parts[1], out i ))
throw new ApplicationException(string.Format(
"CombinedId ID part incorrectly formatted: {0}",
combinedId));
Id = i;
return;
}
throw new ApplicationException( string.Format(
"CombinedId incorrectly formatted: {0}", combinedId ));
}
}
现在,添加一个 Search(string query)
方法来搜索索引。
private static Database OpenQueryDatabase()
{
return new Database(DB_PATH);
}
/// <summary>
/// Search the index for the given querystring,
/// returning the set of results specified
/// </summary>
/// the user inputted string
/// the zero indexed record to start from
/// the number of results to return
/// <returns>a list of SearchResult records</returns>
public static IEnumerable<searchresult> Search( string queryString,
int beginIndex, int count )
{
var results = new List<searchresult>();
using( var db = OpenQueryDatabase() )
using( var enquire = new Enquire( db ) )
using( var qp = new QueryParser() )
using( var stemmer = new Stem("english") )
{
qp.SetStemmer(stemmer);
qp.SetDatabase(db);
qp.SetStemmingStrategy(QueryParser.stem_strategy.STEM_SOME);
var query = qp.ParseQuery(queryString);
enquire.SetQuery(query);
using (var matches = enquire.GetMSet((uint)beginIndex, (uint)count))
{
var m = matches.Begin();
while (m != matches.End())
{
results.Add(
new SearchResult(m.GetDocument().GetData())
{
ResultPercentage = m.GetPercent(),
ResultRank = (int)m.GetRank()
}
);
m++;
}
}
}
return results;
}
编辑主函数以添加一些数据,然后对其进行查询。
var docId = SearchManager.AddDocument(1, "upload", "this is my upload");
Console.WriteLine( "added: " + docId );
docId = SearchManager.AddDocument(2, "upload",
"This will eventually be the contents of a PDF");
Console.WriteLine("added: " + docId);
docId = SearchManager.AddDocument(1, "client", "McAdams Enterprises");
Console.WriteLine("added: " + docId);
docId = SearchManager.AddDocument(1, "Message",
"I think MSFT is wincakes!");
Console.WriteLine("added: " + docId);
var results = SearchManager.Search("upload", 0, 10);
foreach( var result in results )
Console.WriteLine( result.Id + " " + result.Type);
results = SearchManager.Search("MSFT", 0, 10);
foreach (var result in results)
Console.WriteLine(result.Id + " " + result.Type);
results = SearchManager.Search("PDF", 0, 10);
foreach (var result in results)
Console.WriteLine(result.Id + " " + result.Type);
编译程序,跳到 shell,然后尝试运行它。如果幸运的话,它会直接工作。如果运气不佳(就像我一样),那么它就无法工作。
出了什么问题?
如果您像现在大多数人一样,您很可能正在运行 64 位版本的 Windows。如果您不在开发计算机上,您的服务器很可能正在运行 64 位版本的操作系统。一旦您尝试调用 Xapian,您将得到以下友好的提示:
Unhandled Exception:
System.TypeInitializationException: The type initializer
for 'Xapian.XapianPINVOKE' threw an exception.
---> System.TypeInitializationException: The type initializer
---> for 'SWIGExceptionHelper' threw an exception.
---> System.BadImageFormatException: An attempt was made to load a program
---> with an incorrect format. (Exception from HRESULT: 0x8007000B)
问题在于,在 64 位操作系统的计算机上,针对“任何 CPU”(默认设置)编译的 ASP.NET/C# 代码将无法使用 PInvoke(Xapian 包装器的工作方式)调用 32 位 DLL。
这意味着什么?
这意味着您要么必须以 x86(32 位)模式编译您的代码,要么获取 Xapian 的 64 位二进制文件。不幸的是,您之前下载的文件没有 64 位绑定。更糟糕的是,您还需要 64 位版本的 zlib1.dll(他们也没有提供)。在 64 位服务器上运行 32 位编译的 ASP.NET 应用程序很麻烦(它能工作,但由于在 WoW64 模式下运行,您会损失很多优势)。
为 64 位操作编译 Xapian 和 zlib
因此,如果您真的想在 Windows 64 位环境中使用 Xapian,您将不得不亲自动手。而且,我不能保证不会出现任何问题,因为您会收到大量关于精度损失的编译器警告。
必备组件
您需要安装了 C++ 的 Visual Studio .NET 2005 或 2008(如果您像我一样,从未想过会需要它,所以没有安装它)。现在去安装它。
获取 zlib 的源代码。
从 Flax 托管网站获取构建文件(一个 zip 包)和源代码(三个 gzip 压缩包)。
将源代码解压到一个公共位置(我建议使用 c:\xapian 以便您的操作更方便)。在此目录下,您应该有三个目录,分别用于 xapian-bindings-x.x.x、xapian-core-x.x.x 和 xapian-omega-x.x.x。
将 Flax 的 Win32 构建脚本解压到 xapian-code 目录(它应该解压到 win32 子目录中)。
安装 ActivePerl(32 位即可,使用 MSI)。C:\perl 是一个不错的安装位置。
编译 zlib
解压 zlib 源代码(例如到 c:\zlibsrc)。浏览到源代码中的 projects\visualc6 目录。打开 zlib.dsw 文件。您可能会被要求转换项目,选择“全部是”。添加一个 x64 构建目标(点击 Win32 下拉菜单,选择“配置管理器”,在“活动解决方案平台”下,点击
创建一个 zlib 目录,用于构建 Xapian(例如 c:\zlib)。将 zlib source\projects\visualc6\win32_dll_release 目录中的所有内容复制到 zlib 目录。在 zlib 目录中创建一个 include 文件夹。将 zlib 源代码复制到该 include 目录。在 zlib 目录中创建一个 lib 目录。将 zlib source\projects\visualc6\win32_lib_release 中的所有内容复制到该 lib 目录。在此目录中,复制 zlib.lib 文件并将其重命名为 zdll.lib。
编译 Xapian
编辑 xapian-core\win32\config.mak 文件(使用记事本)。编辑以下行:
- (第 32 行):将其设置为您的 Perl 安装的适当目录
- (第 155 行):将其设置为您的 Visual Studio 安装的适当目录
- (第 166 行):将其设置为上面创建的 zlib 目录(c:\zlib)
- (第 212 行):移除 -D "_USE_32BIT_TIME_T"
编辑 xapian-core\win32\makedepend\makedepend.mak 文件(使用记事本)。编辑以下行:
- (第 36 行):将 /machine:I386 更改为 /machine:AMD64
编辑 xapian-core\common\utils.h 并添加以下行:
- (在第 58 行插入)
/// Convert a 64 bit integer to a string
string om_tostring(unsigned __int64 a);
编辑 xapian-core\common\utils.cc 并添加以下行:
- (在第 85 行插入)
string
om_tostring(unsigned __int64 val)
{
// Avoid a format string warning from GCC - mingw uses the MS C runtime DLL
// which does understand "%I64d", but GCC doesn't know that.
static const char fmt[] = { '%', 'I', '6', '4', 'd', 0 };
CONVERT_TO_STRING(fmt)
}
打开“Visual Studio 2005 x64 Win64 Command Prompt”(位于“开始”菜单的 Visual Studio 工具下)。将目录更改为 c:\xapian\xapian-code.x.x.x\win32。运行“nmake”。如果一切按计划进行,一段时间后,以及大量关于数据可能丢失的编译器警告之后,应该完成编译。
现在运行“nmake COPYMAKFILES”(这将把 mak 文件复制到适当的位置)。
在 x64 绑定编译的代码中存在一个“bug”。运行“set libpath”,如果结果以分号 (;) 结尾,则需要重置 libpath 才能编译绑定。在我的情况下,我需要运行“set LIBPATH=C:\windows\Microsoft.NET\Framework64\v2.0.50727”。
将目录更改为 c:\xapian\xapian-bindings.x.x.x\csharp 并运行“nmake”。这应该会编译绑定。绑定最终位于 c:\xapian\xapian-core-x.x.x\win32\Release\CSharp。将这些文件复制到您上面创建的项目中,它应该可以在 64 位计算机上运行(您需要删除/重新添加对 XapianCSharp.dll 的引用,因为它将以不同的方式签名,然后重新编译)。
将目录更改为 c:\xapian\xapian-omega.x.x.x 并运行“nmake”。这将编译 Xapian 的 omega 组件。
Omega 和文档处理如何?
Xapian 的 功能页面有点名不副实。它声称“提供的索引器可以索引 HTML、PHP、PDF、PostScript、OpenOffice/StarOffice、OpenDocument、Microsoft Word/Excel/PowerPoint/Works、Word Perfect、AbiWord、RTF、DVI、Perl POD 文档和纯文本。”然而,一旦您深入研究 Omega 文档,就会发现它依赖其他组件才能实际解析其他文档类型:
- HTML (.html, .htm, .shtml)
- PHP (.php) - 我们的 HTML 解析器知道忽略 PHP 代码
- 文本文件 (.txt, .text)
- PDF (.pdf) 如果 pdftotext 可用 (随 xpdf 提供)
- PostScript (.ps, .eps, .ai) 如果 ps2pdf (来自 ghostscript) 和 pdftotext (随 xpdf 提供) 可用
- OpenOffice/StarOffice 文档 (.sxc, .stc, .sxd, .std, .sxi, .sti, .sxm, .sxw, .sxg, .stw) 如果 unzip 可用
- OpenDocument 格式文档 (.odt, .ods, .odp, .odg, .odc, .odf, .odb, .odi, .odm, .ott, .ots, .otp, .otg, .otc, .otf, .oti, .oth) 如果 unzip 可用
- MS Word 文档 (.doc, .dot) 如果 antiword 可用
- MS Excel 文档 (.xls, .xlb, .xlt) 如果 xls2csv 可用 (随 catdoc 提供)
- MS Powerpoint 文档 (.ppt, .pps) 如果 catppt 可用, (随 catdoc 提供)
- MS Office 2007 文档 (.docx, .dotx, .xlsx, .xlst, .pptx, .potx, .ppsx) 如果 unzip 可用
- Wordperfect 文档 (.wpd) 如果 wpd2text 可用 (随 libwpd 提供)
- MS Works 文档 (.wps, .wpt) 如果 wps2text 可用 (随 libwps 提供)
- AbiWord 文档 (.abw)
- 压缩的 AbiWord 文档 (.zabw) 如果 gzip 可用
- 富文本格式文档 (.rtf) 如果 unrtf 可用
- Perl POD 文档 (.pl, .pm, .pod) 如果 pod2text 可用
- TeX DVI 文件 (.dvi) 如果 catdvi 可用
- DjVu 文件 (.djv, .djvu) 如果 djvutxt 可用
- XPS 文件 (.xps) 如果 unzip 可用
因此,对于绝大多数您感兴趣的文档解析,Omega 将需要其他第三方应用程序——这些应用程序可能在 Windows 环境中可用,也可能不可用。此外,Omega 似乎会调用这些外部应用程序并读取其输出来解析文档文本。
虽然这不一定是完成从文档中提取文本的坏方法,但它可能成为一个潜在的故障点,而且将极难追踪,因为在调用外部应用程序时发生的错误几乎没有(如果有的话)日志记录。
Xapian 的替代品
在 C# 世界中,Lucene.NET 是最常用的搜索“引擎”。它也有自己的问题(没有最近的官方版本、糟糕的文档、由于在托管代码中运行而对大型数据集的性能担忧等),但它也必须进行评估。它不提供像 Omega 这样的工具,因此您将负责从文档中提取数据(通过 IFilter
或与 Omega 相同的外部程序)。
历史
- 2010 年 4 月 8 日:首次发布。
- 2010 年 4 月 9 日:添加了说明,指出本文适用于 Xapian 1.0.18 版本。