7

在我学习计算机科学的过程中,我接触了一些函数式语言,比如 Prolog,但现在我在过去 10 年里只做 C#、Ruby JavaScript 和 Java 等命令式语言。目前,我正在为一家在线商店创建一个全文搜索引擎,而且我已经走得很远了。但是偶然发现了一些函数式语言,比如 Clojure 的 Haskell,很明显,函数式范式更适合,而命令式的方式并不是这项工作的正确工具。

所以我们有大约 1000 万条记录的全文索引。每个记录基本上都包含一个单词出现,以及它起源的记录的 id 和文本位置。

当用户输入搜索字符串时,它会被解析为表达式树。例如,搜索字符串“transformer 100 W”的结果类似于

AND('transformer%', OR(NEAR('100', 'W'), NEAR('100', 'watts'), '100W', '0.1kW'))

这里有一些额外的“智能”,但这与这个问题无关。

然后递归地评估表达式树并生成几个 sql 查询,这些查询可以以 .NET-DataTables 的形式返回多达 100,000 行。然后将它们读入集合或字典,并根据谓词应用交集和并集,以找到与整个搜索表达式匹配的所有结果。对于 NEAR 评估,还比较了已发现事件的位置索引。但这一切都是命令式的,有很多 for 循环。

此外,还有一个排名功能,可以将找到的单词出现的分数相加。仅作为前缀或模糊匹配(由数据库服务器完成)找到的单词得分低于精确匹配。

对于每个结果项,我还需要获取所有匹配的单词的列表,以便在结果页面中突出显示这些单词。

所以大致评估算法是一个类似的函数

expression tree, full text index -> 
resulting items that match the expressin tree, 
each with a ranking sum 
and a list of all found word occurrences for this item

我只是在这里给出一个粗略的概述,但我希望你能得到足够的图片。

现在“现实世界”的约束:

  • 整个应用程序(到目前为止)都是用 C# 编写的,因此与 .NET 的轻松集成至关重要。
  • 大量数据被读入 .NET-DataTables,然后需要进行评估和转换。结果应包含在 .NET 类型中(字典、集合、数组等...)。
  • 性能非常重要。目前我的算法搜索通常需要两秒钟(不包括 sql),这还可以,但应该改进。我们的服务器有 16 个处理器,因此欢迎并行处理。由于我们每秒收到一个搜索请求,并且当前实现是单线程的,因此处理器时间仍然可用。
  • 语言(和编译器)应该是成熟的。

由于我需要继续使用 .NET,因此我正在研究 Clojure-CLR、F# 和 Scala 的 .NET。

我非常喜欢 Clojure 的概念,但现在我无法评估它是否能胜任这项工作。阅读有关 F# 的文章让我感觉很复杂,因为它似乎希望能够做几乎所有事情,而对于给定的任务,我倾向于使用更“纯”的数学方法。但也许这对于 F# 也是可能的,我还没有意识到这一点。我还没有深入研究 Scala,但它似乎已经很成熟了。

欢迎任何见解!

4

2 回答 2

15
  • 整个应用程序(到目前为止)都是用 C# 编写的,因此与 .NET 的轻松集成至关重要。
  • 大量数据被读入 .NET-DataTables,然后需要进行评估和转换。结果应包含在 .NET 类型中(字典、集合、数组等...)。

F# 应该是更好的选择。作为 Visual Studio 中的一流语言,F# 与 C# 的互操作性非常好。

  • 性能非常重要。目前我的算法搜索通常需要两秒钟(不包括 sql),这还可以,但应该改进。我们的服务器有 16 个处理器,因此欢迎并行处理。由于我们每秒收到一个搜索请求,并且当前实现是单线程的,因此处理器时间仍然可用。

假设您从功能优先且不可变的实现开始,那么并行化您的应用程序应该很容易。此外,异步工作流对于像您这样的 IO 密集型应用程序来说是一大福音。

  • 语言(和编译器)应该是成熟的。

我不会将 F# 与 JVM 上的 Clojure 和 Scala 进行比较,但 F# 比 .NET 上的 Clojure CLR 和 Scala 成熟得多。在选择 F# 时,您一定会得到 Microsoft 的长期承诺和不断发展的 F# 社区的帮助。

当用户输入搜索字符串时,它会被解析为表达式树。

您可以使用有区别的联合来表示表达式树。通过在 F# 3.0 中引入查询表达式,您可以轻松地将逻辑转换为 SQL 查询。您甚至可以通过为您的域定义类似的查询语言来进一步推动它。

阅读有关 F# 的文章让我感觉很复杂,因为它似乎希望能够做几乎所有事情,而对于给定的任务,我倾向于使用更“纯”的数学方法。但也许这对于 F# 也是可能的,我还没有意识到这一点。

F# 3.0 引入了类型提供程序,允许用户以类型安全的方式访问非结构化数据;您可能想观看此“F# 3.0 - 信息丰富的编程”视频以获得更多见解。如果你想使用 F# 作为数据挖掘的编程语言,我已经问了一个相关的问题,并在这里得到了很好的回答。

也就是说,您对 F# 的第一感觉可能并不正确。根据我的经验,你总是可以尽可能地接近功能性和不可变的一面。鉴于您已经有了一个有趣的应用程序,我建议您亲自动手了解一下 F# 是否适合您的目的。

更新:

下面是一个展示这个想法的 F# 原型:

/// You start by modeling your domain with a set of types.
/// FullText is a sequence of Records, which is processed on demand.
type Word = string
and Freq = int
and Record = {Occurrences: (Word * Freq) list; Id: string}
and FullText = Record seq

/// Model your expression tree by following the grammar closely.
type Expression =
    | Occur of Word
    | Near of Word * Word
    | And of Expression * Expression 
    | Or of Expression * Expression

/// Find wether a word w occurs in the occurrence list.
let occur w {Occurrences = xs} = xs |> Seq.map fst |> Seq.exists ((=) w)

/// Check whether two words are near each other.
/// Note that type annotation is only needed for the stub implementation.
let near (w1: Word) (w2: Word) (r: Record): bool = failwith "Not implemented yet"

/// Evaluate an expression tree.
/// The code is succinct and clear thanks to pattern matching. 
let rec eval expr r = 
    match expr with
    | Occur w -> occur w r
    | Near(w1, w2) -> near w1 w2 r
    | And(e1, e2) -> eval e1 r && eval e2 r
    | Or(e1, e2) -> eval e1 r || eval e2 r

/// Utility function which returns second element in a 3-tuple
let inline snd3 (_, x, _) = x

/// Get the rank of the record by adding up frequencies on the whole database.
let rank (r: Record) (ft: FullText): Freq = failwith "Not implemented yet"

/// Retrieve all records which match the expression tree.
let retrieve expr fullText =
    fullText |> Seq.filter (eval expr)
             |> Seq.map (fun r -> r, rank r fullText, r.Occurrences)
             |> Seq.sortBy snd3

/// An example query
let query = 
    And (Occur "transformer%", 
         Or (Or (Near ("100", "W"), Near ("100", "watts")), 
             Or (Occur "100W", Occur "0.1kW")))
于 2012-11-05T14:30:01.117 回答
7

我很好奇您为什么不考虑选择 LINQ。它似乎满足您的所有标准。注意我没有使用 Scala 的经验,所以我不能对此发表评论。

  • 整个应用程序(到目前为止)都是用 C# 编写的,因此与 .NET 的轻松集成至关重要。
  • 大量数据被读入 .NET-DataTables,然后需要进行评估和转换。结果应包含在 .NET 类型中(字典、集合、数组等...)。

在这里,LINQ > F# > Clojure-CLR。如果一切都已经在 C# 中,那么 LINQ 将是最容易集成的。Visual Studio 对智能感知和函数定义导航之类的支持在纯 C# 程序中似乎要好得多。从 C# 调用 Clojure 可能很可怕——理论上它应该可以正常工作,但在实践中,请准备好花费数周时间找出为什么事情没有按照您期望的方式工作。它真的被设计成顶级的东西;你从 Clojure 调用 C#,相反的方向在 Clojure-CLR 开发人员的优先级列表中并不高;有基本的支持,但你得到你得到的。

  • 性能非常重要。目前我的算法搜索通常需要两秒钟(不包括 sql),这还可以,但应该改进。我们的服务器有 16 个处理器,因此欢迎并行处理。由于我们每秒收到一个搜索请求,并且当前实现是单线程的,因此处理器时间仍然可用。

LINQ ~= F# > Clojure。我在其他地方读到,对于大多数惯用编写的算法,LINQ 的性能可以证明比 F#好,但它们已经足够接近了,这无关紧要。PLINQ 使并行性变得容易。Clojure-CLR 的启动时间非常慢,而且运行时开销也会减慢速度。

  • 语言(和编译器)应该是成熟的。

LINQ >= F# > Clojure。并不是说 F# 完全不成熟但 Visual Studio 的支持有点落后,而且世界上基于 LINQ 的生产代码(以及更多堆栈溢出答案)比 F# 多得多。

阅读有关 F# 的文章让我感觉很复杂,因为它似乎希望能够做几乎所有事情,而对于给定的任务,我倾向于使用更“纯”的数学方法。但也许这对于 F# 也是可能的,我还没有意识到这一点。

没有一种语言像 Haskell 那样是纯纯的,但就编写非纯代码的难度而言,我将其列为:LINQ > Clojure > F# > Scala。LINQ 只能通过调用不纯的方法而变得不纯。Clojure 有 refs 和 atom,F# 任何东西都可以被指定为可变的,而 Scala(根据我的理解)实际上只是带有功能特性的 Java。

F# 和 Scala 的功能特性是对模式匹配的语言支持。在 C# 中,您需要某种继承层次结构或 b?x:y 运算符链以功能性地执行操作(或者如果您对非功能性方法感到满意,则使用 if/else),模式匹配在不同的情况下进行条件操作原始数据类型的变化更加简洁。这可能在您计算精确匹配、前缀匹配和模糊匹配排名时很有用,但是var alg = x.match == exact ? alg1 : x.match == prefix ? alg2 : alg3C# 中的 ab?x:y 链在这种简单情况下会非常清晰——当匹配变得更加复杂时,语言集成模式匹配变得更加复杂有价值的。

有趣的是,我认为 F# 证明比 LINQ 更有用的工具包的一个方面不是查询,LINQ 本身的名称应该表明它可以处理,而是将搜索字符串解析为表达式树。 这是函数式语言和模式匹配真正擅长的一个领域,添加 FsLex 和 FsYacc 等工具可以让您抢占先机。

综上所述,我认为决定取决于你想去哪里。如果您只想清理搜索算法并完成它,我建议您使用 LINQ 方法。但是,如果您想为整个程序逐步采用更注重功能的风格(并且您的公司愿意为您投入的时间付费),那么不妨看看 F#选项。无论哪种方式,我都会先执行 LINQ 选项,因为这对您来说可能更直接,并且一旦您开始走这条路,就会帮助引导您的 F# 在功能上更具惯用性。

简单地说,这就是您想要的,只需填写您的 Near 和 Equal 提取器的函数,以及 GetRank 和 GetStrings 函数,然后使用下面的

static IEnumerable<Record> FetchRecords(this Tree tree) {
    return tree.Op == "OR"    ? tree.Args.SelectMany(FetchRecords).Distinct() :
           tree.Op == "AND"   ? tree.Args.Select(FetchRecords).Aggregate((intersect, current) => intersect.Intersect(current)) :
           tree.Op == "NEAR"  ? FetchValsNear(tree.Args[0].Op, tree.Args[1].Op) :
                                FetchValsEqual(tree.Op);
}

static IEnumerable<Record> FetchValsEqual(string s) {
    throw new NotImplementedException();
}

static IEnumerable<Record> FetchValsNear(string s1, string s2) {
    throw new NotImplementedException();
}

static IEnumerable<Tuple<Record, double, string[]>> OrderByRank(this IEnumerable<Record> vals) {
    return from val in vals
           let rank = GetRank(val)
           orderby rank
           let strings = GetStringsIn(val)
           select Tuple.Create(val, rank, strings);
}

static string[] GetStringsIn(Record val) {
    throw new NotImplementedException();
}

static double GetRank(Record val) {
    throw new NotImplementedException();
}

class Tree {
    public string Op;
    public Tree[] Args;
}

struct Record {/*your record here--use struct so Distinct and Intersect above work naturally (or use class and override Equals)*/}

像这样:

foreach (var tuple in myTree.FetchRecords().AsParallel().OrderByRank().Take(30)) {
    // add to datagrid or whatever
}

这为您提供了简单的并行性和惰性,因此该GetStringsIn函数仅在您获取的记录上执行(在本例中为前 30 个)。(请注意,可以使用此处的任何示例AND来简化选择器)。IntersectAll

于 2012-11-05T15:44:10.373 回答