【问题标题】:How to properly type a multi-keys index using maps如何使用地图正确键入多键索引
【发布时间】:2020-05-20 01:24:31
【问题描述】:

我正在寻找一个很好的解决方案来构建和查询文档的索引存储,在我的例子中是“带有元数据的页面”。

我的页面有各种元数据字段,例如“标签”、“作者”等。

这样的页面可以这样建模

data Page = 
  Page { content :: Text
       , tags :: [Text]
       , authors :: [Text]
       , score :: Int
       , online :: Bool }

软件处理这样一个Pages的集合,需要通过这个集合进行查询。我当前的解决方案是为每个元数据字段手动处理Map,然后在创建新页面时手动填充每个映射(使用键映射 -> 设置页面)。然后对于我的应用程序的每个单独案例,我查询一个或多个 Map,交叉(或合并)结果,然后处理。这是乏味且容易出错的。

这就是为什么我想设计一个通用文档存储,有点像 Data.IxSet 提供的。请注意,我的问题是关于正确键入这样的商店,而不是关于使用 IxSet。

如果所有字段都有相同类型的键K(如Text),则可以键入这样的索引:Map Field (Map K (Set Page)),其中Field是字段的类型,例如

data Field = Tag | Author | ...

不幸的是,事实并非如此,字段键可以是 Text 或 Date 或 Int 或 Bool 等。它们都满足 Map 约束 Ord,所以这部分没有问题。

您将如何设计此数据类型并正确键入它,以便可以使用以下接口。我在不知道如何处理这些类型的地方打上了问号。这通常与正确处理可能是异构类型的集合有关。

data Field model key = Field { fieldId :: FieldId
                             , extractKeys :: model -> [key] }

-- initIndex create an index for values 'model' with descriptions for fields and keys extractors. 
initIndex :: [Field model ?] -> Index ? model

-- adding a document to the index
addToIndex :: Index ? model -> model -> Index ? model

-- a request combines filters with boolean logic
data Request field ? = And [Request field ?] | Or [Request field ?] | Term (Filter field ?)
-- a filter is a constraint on a field keys. 'All' will intersect, 'Any' will unite ...
data Filter field k = All field [k] | Any field [k] | GreaterThan field k | LowerThan field k

queryIndex :: Index ? model -> Request field ? -> Set model

我希望这足够清楚,如果没有,我会根据需要进行更新。谢谢

【问题讨论】:

    标签: haskell


    【解决方案1】:

    一种方法是将所有索引存储在一个 Map 中。

    首先,为您的不同键定义联合数据类型:

    data Key =
       KeyTag Text,
       KeyAuthor Text,
       KeyScore Int
       -- etc.
       deriving (Eq, Ord)
    

    然后编写一个返回页面所有键的函数。

    pageKeys :: Page -> [Key]
    

    现在您可以编写一个函数,将页面添加到单个索引的所有键中,并且您可以使用适当的构造函数检索单个作者或标签的文档集。如果您使用Map.split 来匹配分数范围,那么这也将以相同的方式工作。

    编辑:更多关于Map.split

    来自Data.Mapsplit :: Ord k => k -> Map k a -> (Map k a, Map k a)

    第一个结果是key k的所有值。因此,您可以像这样获取所有带有 k1 和 k2 之间键的页面(假设 import qualified Data.Map as M):

    subRange :: (Ord k) => (k, k) -> M.Map k a -> M.Map k a
    subRange (k1, k2) = fst . M.split k2 . snd . M.split k1
    

    请注意,这将排除等于 k1 和 k2 的键:包含函数将需要使用 M.splitLookup 并且更复杂。

    如果您的页面有这个大统一索引,那么您可以轻松获得这样的日期子范围:

    dateSubRange :: (Date, Date) -> Map Key (Set Page) -> Map Key (Set Page)
    dateSubRange (d1, d2) = subRange (KeyDate d1, KeyDate d2)
    

    诀窍在于 Key 的 Ord 实例具有完全相同的顺序 作为它的组件,所以compare d1 d2 == compare (KeyDate d1) (KeyDate d2)。如果您要按键顺序打印大索引,您将首先获得所有标签,然后是所有作者,然后是所有分数等。

    单个大索引的效率几乎不低于几个部分索引。请记住,地图的查找是O(log n),因此将地图的大小乘以常数c 只会为O(log c) 的查找增加一个常数时间。除非您试图从代码中消耗所有可能的微秒,否则不值得担心。

    (事实上,当您查找多个不同的键时,让 CPU 缓存索引的顶层甚至可能使其比多个索引更快。但这很复杂,并且取决于很多事情)。

    【讨论】:

    • 嗨,保罗,谢谢你的提议。目前,我真的在寻找一种解决方案来为每个字段保留一个索引(又名 Map),因为它会提供更好的整体性能,并且在许多情况下都很有用,例如,应用于日期字段的 GreaterThan 过滤器.您能否详细说明使用Map.split 匹配分数范围?这可能是“GreaterThan / LowerThan”过滤器的解决方案吗?
    • 太好了,谢谢。我接受这个解决方案,并将在我的项目kiwi 上探索这条路径。最好的问候,
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-02-16
    • 2018-06-25
    • 1970-01-01
    • 2018-10-24
    • 2014-09-09
    • 2022-11-18
    • 1970-01-01
    相关资源
    最近更新 更多