65.9K
CodeProject 正在变化。 阅读更多。
Home

如何使用 C# 编译和使用 Xapian

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2010 年 4 月 9 日

CPOL

9分钟阅读

viewsIcon

69377

downloadIcon

746

本文探讨了如何在 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.dllzlib1.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.xxapian-core-x.x.xxapian-omega-x.x.x

将 Flax 的 Win32 构建脚本解压到 xapian-code 目录(它应该解压到 win32 子目录中)。

安装 ActivePerl(32 位即可,使用 MSI)。C:\perl 是一个不错的安装位置。

编译 zlib

解压 zlib 源代码(例如到 c:\zlibsrc)。浏览到源代码中的 projects\visualc6 目录。打开 zlib.dsw 文件。您可能会被要求转换项目,选择“全部是”。添加一个 x64 构建目标(点击 Win32 下拉菜单,选择“配置管理器”,在“活动解决方案平台”下,点击 ,选择 x64,点击“确定”,然后关闭)。从下拉菜单中选择“LIB Release”项目并构建它。从下拉菜单中选择“DLL Release”项目并构建它。

创建一个 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 版本。
© . All rights reserved.