【问题标题】:Ideal functional language for implementing a full text search with .NET [closed]使用 .NET 实现全文搜索的理想功能语言 [关闭]
【发布时间】:2012-11-05 14:09:25
【问题描述】:

在我学习计算机科学的过程中,我接触了一些函数式语言,比如 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,但它似乎已经很成熟了。

欢迎任何见解!

【问题讨论】:

  • 这里根本帮不了你,但是...... Prolog 通常不被认为是一种函数式语言,而是一种逻辑编程语言,或者一种演绎语言。
  • C#、Ruby、JavaScript 是函数式语言。序言不是。我怀疑,LINQ(也非常实用)在你的情况下应该足够了。
  • C#、Ruby 和 JavaScript 不是函数式语言。它们是支持一些函数抽象的语言,但对函数式编程的语言支持很少。
  • @SK-logic,所以all of these are functional(包括Java和C)?
  • 大家好,您可以考虑在函数式语言讨论中打开一个单独的线程。在这里,它基本上使我的问题的答案难以讨论。感谢 Dax 分享您的见解并直言不讳!

标签: .net scala haskell clojure f#


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

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

  • 性能非常重要。目前我的算法经常需要两秒搜索一次(不算sql),很亲切 好的,但应该改进。我们的服务器有 16 个处理器,所以 并行处理将受到欢迎。因为我们得到了大约一次搜索 每秒请求并且当前实现是单线程的, 处理器时间仍然可用。

假设您从功能优先且不可变的实现开始,那么并行化您的应用程序应该很容易。此外,asynchronous workflow 是对像您这样的 IO 密集型应用程序的祝福。

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

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

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

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

阅读有关 F# 的文章让我百感交集,因为它似乎想要成为 能够做几乎所有事情,而我倾向于更多 给定任务的“纯”数学方法。但也许那是 F# 也可以,但我还不知道。

F# 3.0 引入type providers 允许用户以类型安全的方式访问非结构化数据;您可能想查看this "F# 3.0 - Information Rich Programming" video 以获得更多见解。如果你想使用 F# 作为数据挖掘的编程语言,我已经问了一个相关的问题,并得到了很好的回复 here

也就是说,您对 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")))

【讨论】:

  • 嗨垫,感谢您的快速答复。我有点希望不必使用仅限 Microsoft 的语言,但如果它是适合这项工作的工具,没问题。不过,在我在这里做出决定之前,我会等待一些 pro clojure 或 scala 参数。
  • 我应该提到 F# 是一种open source 语言。最近,在 F# 社区中出现了一些讨论,以进一步推动对 Mono 的 F# 支持。所以不要担心你会被一种仅限微软的语言所束缚:)。
  • 嗨,majnu,这是一个有趣的评论。我也有自己的理由不想只与 MSFT 绑定。 (必须获得 VS 需要 n 级授权,我们的集群是 linux 和其他东西的混合)。你的是什么?我倾向于认为 msft 在追求开放语言方面不会松懈,因为它会扩大它们的采用范围,而且他们仍然会有最优秀的人来研究它。 .net 框架似乎是一项非常好的技术,应该更多地使用......
  • 在 F# 的情况下,提到的问题已经“解决”了,因为你有 emacs,单开发 IDE 以及单声道运行时。我已经为 F#2.0 尝试过单声道它工作正常。 F#3.0 我认为是在管道中
  • @pad:感谢您提供代码示例!它给了我一个关于 F# 将如何处理任务的好主意。我开始阅读 Chris Smith 的书,并且非常喜欢这门语言。最后,我决定将 Dax 的回复标记为答案,因为这是我现在要采取的方式。我不知道 C# 能带我走多远。
【解决方案2】:

我很好奇您为什么不考虑选择 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 运算符链来功能性地做事(或者如果您对非功能性方法很好,则如果/否则),模式匹配在不同的情况下进行条件操作原始数据类型的变化更加简洁。这可能在您计算精确匹配、前缀匹配和模糊匹配排名时很有用,但是 C# 中的 b?x:y 链 var alg = x.match == exact ? alg1 : x.match == prefix ? alg2 : alg3 在这个简单的情况下会非常清晰——当匹配变得更加复杂时,语言集成模式匹配变得更有价值。

有趣的是,我认为 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 示例here 进行简化)。

【讨论】:

  • 嗨,Dax,首先感谢您的慷慨回答!我确实想将当前的算法从一个循环的、改变状态的怪物转换为功能性的、声明性的代码,它更多地说明了结果应该是什么样子,而不是为了到达那里需要做的所有事情。不仅为了美观,而且为了可维护性和可扩展性。我使用 LINQ 来折叠和映射集合,从而避免了自写循环。但是,我首先需要更深入地研究 LINQ,以便了解如何在这里使用它。会尽快回复您!
  • 使用 LINQ 的最大缺点是可扩展性。正如您自己所说,C# 缺乏一流的功能、模式匹配、强大的类型系统等。随着应用程序的发展,您将受到宿主语言的限制。对我来说,使用真正的函数式编程语言也意味着范式转变。它们迫使我以 声明性 的方式思考,而我在 C# 中很容易偏离这种方式。
  • @pad,我同意 100%,你不能在 LINQ 中编写整个应用程序。但是考虑到他的要求,如果时间和金钱是有限的,而他只是想清理一些查询/聚合代码,LINQ 也可以完成这项工作,而且在我看来会更加务实。有了更多的时间和范围,F# 肯定会成为一种体验,但学习曲线会更陡峭,可能更适合特定工作,也可能不适合。无论如何,只是想将选项添加到他的注意事项列表中。
  • 嗨,Dax,感谢您提供的精彩代码示例!是的,这是必须的,但比我现在拥有的要优雅得多。我需要看看这种方法是否能够满足我现在(以及未来的一些)的所有要求,并会就此回复您。也许我需要一两天,因为我现在正忙于其他事情......
  • @pad:我不知道这是否是一个小问题,但您愿意大致展示一下您将如何在 F# 中实现此代码吗?只是为了得到一个想法。
猜你喜欢
  • 1970-01-01
  • 2018-08-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-24
  • 2010-09-07
相关资源
最近更新 更多