FreeMarker 模板语言 (FTL):拆分多值 XML 字段元数据以对各自的值进行分组

FreeMarker Template Language (FTL): Split Multivalue XML Field Metadata to Group Respective Values

我有一个人员目录的 XML 元数据,其中一个人可以有多个头衔、部门和 phone 号码等与其姓名相关联的内容。

具有多个值的元数据分别存在于一个字段中(例如字段:标题、部门、phone)。

FTL 似乎 return 记录数据的顺序正确,即

name (returns: name)   
title (returns: title0, title1, title2)
department (returns: department0, department1, department2) 
and phone (returns: phone0, phone1, phone2)

我想打印这样的数据:

name
title0
department0
phone0

title1
department1
phone1

title2
department2
phone2

是否有一些 FTL 工具可以将具有多个值的字段放在一个数组或其他东西中,从而允许控制在何处打印(和分组)它们各自的值(如上所述)?

具有多个值的字段(即标题、部门、phone)将具有正常值,例如; phone0、phone1 和 phone2 - 每个都是不同的 phone 数字。

我发现了这个:

https://freemarker.apache.org/docs/ref_builtins.html

sequences 的内置函数似乎有点相关,

但我不确定它如何或是否能达到目的?


[更新 1]

我尝试了来自@ddekany 的宏和代码,只更改了

 entry.<xxx> 

字段以匹配我们的数据,在本例中,

 <@listGroups peopleDirectory "name"; personName, personEntries>
   Name: ${personName}
   <#list personEntries as entry>
     - Title: ${entry.personTitle1}
     - Department: ${entry.personDepartment1}
     - Phone: ${entry.personPhone1}
   </#list>
 </@listGroups>

where each of these

entry.<xxx> 

fields might hold 0-x values, either pipe or comma delimited, but in this case, contain 4 values for each

${entry.personTitle1}, ${entry.personDepartment1}, and ${entry.personPhone1}

The 1 suffix is a remnant from when our data model had a separate field (i.e., personTitle1, personTitle2, personTitle3, etc.) for each possible multi value.

There are currently only 2 records in the set.

但我收到以下错误:

When calling macro "listGroups", required parameter "items" (parameter #1) was specified, but had null/missing value. ---- Tip: If the parameter value expression on the caller side is known to be legally null/missing, you may want to specify a default value for it with the "!" operator, like paramValue!defaultValue. ---- ---- FTL stack trace ("~" means nesting-related): - Failed at: #macro listGroups items groupByName [in template "conf/people/default/simple.ftl" in macro "listGroups" at line 533, column 1] - Reached through: @listGroups peopleDirectory, "name"; ... [in template "conf/people/default/simple.ftl" at line 552, column 1] ~ Reached through: #nested [in template "web/templates/modernui/search_classic.ftl" in macro "Results" at line 244, column 13] ~ Reached through: @s.Results [in template "conf/people/default/simple.ftl" at line 523, column 9] ~ Reached through: #nested [in template "web/templates/modernui/search_classic.ftl" in macro "AfterSearchOnly" at line 94, column 9] ~ Reached through: @s.AfterSearchOnly [in template "conf/people/default/simple.ftl" at line 521, column 1] ~ Reached through: #nested [in template "web/templates/modernui/search_classic.ftl" in macro "AfterSearchOnly" at line 94, column 9] ~ Reached through: @s.AfterSearchOnly [in template "conf/people/default/simple.ftl" at line 90, column 3] ----

我需要指定 paramValue!defaultValue 吗?还是宏有问题?

We'd tried using a data model where any field that might contain multiple values had a separate field, and we could then group the multi value entries in their respective orders.

However, our app has a search narrowing feature we want to use called facets, which seems to only allow for grouping/searching on a single meta field (for each facet). In this case, since we'd changed our model to put multiple values (such as title) into multiple fields (i.e., title1, title2, title3, etc.), and since it seems facets can only be used to group/search on one meta field, they therefore wouldn't match correctly those with multiple titles; which is why we went back to overloading a single field with multiple values.

Is there an XML model that would allow for our app's facets, but not require a less than ideal solution?


[更新 2]

我尝试用 :

替换 peopleDirectory 变量
 <@listGroups s.result.metaData "name"; personName, personEntries>
   Name: ${personName}
   <#list personEntries as entry>
     - Title: ${entry.personTitle1}
     - Department: ${entry.personDepartment1}
     - Phone: ${entry.personPhone1}
   </#list>
 </@listGroups>

但收到以下错误:

For "?sort_by" left-hand operand: Expected a sequence, but this has evaluated to an extended_hash (wrapper: f.t.SimpleHash): ==> items [in template "conf/people/default/simple.ftl" at line 539, column 10] ---- FTL stack trace ("~" means nesting-related): - Failed at: #list items?sort_by(groupByName) as item [in template "conf/people/default/simple.ftl" in macro "listGroups" at line 539, column 3] - Reached through: @listGroups s.result.metaData, "name"... [in template "conf/people/default/simple.ftl" at line 555, column 2] ~ Reached through: #nested [in template "web/templates/modernui/search_classic.ftl" in macro "Results" at line 244, column 13] ~ Reached through: @s.Results [in template "conf/people/default/simple.ftl" at line 523, column 9] ~ Reached through: #nested [in template "web/templates/modernui/search_classic.ftl" in macro "AfterSearchOnly" at line 94, column 9] ~ Reached through: @s.AfterSearchOnly [in template "conf/people/default/simple.ftl" at line 521, column 1] ~ Reached through: #nested [in template "web/templates/modernui/search_classic.ftl" in macro "AfterSearchOnly" at line 94, column 9] ~ Reached through: @s.AfterSearchOnly [in template "conf/people/default/simple.ftl" at line 90, column 3] ----
     

我也试过了:

<@listGroups s.result.listMetadata "name"; personName, personEntries>
  Name: ${personName}
  <#list personEntries as entry>
    - Title: ${entry.personTitle1}
    - Department: ${entry.personDepartment1}
    - Phone: ${entry.personPhone1}
  </#list>
</@listGroups>

但收到:

For "?sort_by" left-hand operand: Expected a sequence, but this has evaluated to an extended_hash (com.google.common.collect.Multimaps$CustomListMultimap wrapped into com.appsearch.publicui.search.web.views.freemarker.AppsearchObjectWrapper$ListMultimapAdapter): ==> items [in template "conf/people/default/simple.ftl" at line 537, column 10] ---- FTL stack trace ("~" means nesting-related): - Failed at: #list items?sort_by(groupByName) as item [in template "conf/people/default/simple.ftl" in macro "listGroups" at line 537, column 3] - Reached through: @listGroups s.result.listMetadata, "n... [in template "conf/people/default/simple.ftl" at line 553, column 1] ~ Reached through: #nested [in template "web/templates/modernui/search_classic.ftl" in macro "Results" at line 244, column 13] ~ Reached through: @s.Results [in template "conf/people/default/simple.ftl" at line 529, column 9] ~ Reached through: #nested [in template "web/templates/modernui/search_classic.ftl" in macro "AfterSearchOnly" at line 94, column 9] ~ Reached through: @s.AfterSearchOnly [in template "conf/people/default/simple.ftl" at line 527, column 1] ~ Reached through: #nested [in template "web/templates/modernui/search_classic.ftl" in macro "AfterSearchOnly" at line 94, column 9] ~ Reached through: @s.AfterSearchOnly [in template "conf/people/default/simple.ftl" at line 90, column 3] ----

我们目前打印结果的方式如下所示:

<@s.AfterSearchOnly>
    <#if response.resultPacket.resultsSummary.totalMatching != 0>
        <@s.Results>

<#if s.result.metaData["personName"]??><strong>
    ${s.result.metaData["personName"]!} </strong> <br/>
<#else>
</#if>  

<#if s.result.metaData["personTitle1"]??>
    ${s.result.metaData["personTitle1"]!} <br/>
<#else>
</#if>   

<#if s.result.metaData["personDepartment1"]??>
    ${s.result.metaData["personDepartment1"]!} <br/>
<#else>
</#if>

<#if s.result.metaData["personPhone1"]??>
Tel: ${s.result.metaData["personPhone1"]!} <br/>
<#else>
</#if> 

        </@s.Results>
    </#if>   
</@s.AfterSearchOnly>

打印结果如下:

Jane Doe
Chair|Professor|Co-Site Director, World Universtiy Florence|Academic Director
History Department|World Universtiy - Florence|Global Programs|Academic Directors
Tel: +1 000 111 2222|+1 333 444 5555|+1 666 777 8888|+1 999 101 1111

John Doe
Professor with Chair|Professor with Chair|Co-Site Director, World Universtiy|Academic Director
History Department|World Universtiy - Florence|Global Programs
Tel: +1 121 131 1414|+1 151 161 1717|+1 181 191 2020|+1 212 222 2323
  

[更新 3]

我们控制 data-model、数据和数据条目,并在某一时刻将数据结构化为每个值都有一个字段,即 title1、title2、title3,在此case,可以根据需要打印结果,像这样:

Jane Doe (personName)
Chair (personTitle1)
History Department (personDepartment1)
Tel: +1 000 111 2222 (personPhone1)

Professor (personTitle2)
World Universtiy - Florence (personDepartment2)
Tel: +1 333 444 5555 (personPhone2)

Co-Site Director, World Universtiy Florence (personTitle3)
Global Programs (personDepartment3)
Tel: +1 666 777 8888 (personPhone3)

Academic Director (personTitle4)
Academic Directors (personDepartment4)
Tel: +1 999 101 1111 (personPhone4)


John Doe (personName)
Professor with Chair (personTitle1)
History Department (personDepartment1)
Tel: +1 121 131 1414 (personPhone1)

Professor with Chair (personTitle2)
World Universtiy - Florence (personDepartment2)
+1 151 161 1717 (personPhone2)

Co-Site Director, World Universtiy (personTitle3)
Global Programs (personDepartment3)
+1 181 191 2020 (personPhone3)

Academic Director (personTitle4)
<!-- data missing -->  (personDepartment4) - don't know if this is a problem (could it jumble the results)
+1 212 222 2323 (personPhone4)

但是搜索分面不起作用,因为它们似乎只能在单个字段上进行搜索。如果我们想查找所有 title1 = "Academic Director" 的记录,我们将看不到那些 title2、title3 等具有 "Academic Director" 值的记录。可能值得要求供应商增强分面,以便他们能够在多个字段上进行搜索。

我想您可能是在建议更改 s.Results 工作方式的功能?我相信这是由应用程序提供的。我们需要向供应商请求增强功能。或者,您是否建议对数据的结构做一些不同的事情?如果是这样,这与为每个值设置一个字段(如上所述)有什么不同吗?

另一种可能性...供应商提供挂钩脚本:

https://docs.funnelback.com/develop/programming-options/hook-scripts.html

The most commonly used hook scripts are the pre and post process hooks.

  • Pre-process: (hook_pre_process.groovy) This runs after initial question object population, but before any of the input processing occurs. Manipulation of the query and addition or modification of most question attributes can be made at this point.

    Example uses: modify the user’s query terms; convert a postcode to a geo-coordinate and add geospatial constraints

  • Extra searches: (hook_extra_searches.groovy) This runs after the extra search question is populated but before any extra search runs allowing modification of the extra search’s question.

    Example uses: add additional constraints (such as scoping) to the extra search.

  • Pre-datafetch: (hook_pre_datafetch.groovy) This runs after all of the input processing is complete, but just before the query is submitted. This hook can be used to manipulate any additional data model elements that are populated by the input processing. This is most commonly used for modifying faceted navigation.

    Example uses: Update metadata, gscope or facet constraints.

  • Post-datafetch: (hook_post_datafetch.groovy) This runs immediately after the response object is populated based on the raw XML return, but before other response elements are built. This is most commonly used to modify underlying data before the faceted navigation is built.

    Example uses: Rename or sort faceted navigation categories, modify live URLs

  • Post-process: (hook_post_process.groovy) This is used to modify the final data model prior to rendering of the search results.

    Example uses: clean titles; load additional custom data into the data model for display purposes.

An addition hook script is available for working with cached documents.

  • pre-cache: (hook_pre_cache.groovy) This is used to modify the cached document prior to display.

目的是模板不会进行此类分组,这是创建数据模型的任何人的责任。所以至少从 2.3.30 开始,没有内置功能可以执行此操作(但我认为必须添加它,因为它会不断出现)。

现在,如果我们必须纯粹在模板中解决这个问题,您可以这样做(尽管在模板中解决这样的事情是一个非常不正当的想法):

<#--
  Splits a list to groups, and calls the nested content for each group.
  Do NOT use this if the size of a group is above a few dozens, as it will become slow.
-->
<#macro listGroups items groupByName>
  <#local group = []>
  <#list items?sort_by(groupByName) as item>
    <#local groupByValue = item[groupByName]!>
    <#if item?is_first || groupByValue != lastGroupByValue>
      <#if group?size != 0>
        <#nested lastGroupByValue group>
      </#if>
      <#local group = []>
      <#local lastGroupByValue = groupByValue>
    </#if>
    <#local group += [item]>
    <#if item?is_last>
      <#nested groupByValue group>
    </#if>
  </#list>
</#macro>

所以这只是一个宏,要真正列出分组的内容,请执行以下操作:

<@listGroups peopleDirectory "name"; personName, personEntries>
  Name: ${personName}
  <#list personEntries as entry>
    - Title: ${entry.title}
    - Department: ${entry.department}
    - Phone: ${entry.phone}
  </#list>
</@listGroups>

假设 tittle> 1 且 title(n)=phone(n)=department(n)。

在数据模型中,所有元数据值都分配给一个用竖线分隔的字段 |。您可以拆分第一个元数据,即:personTitle1 使用 FTL built-in ?split('|') 并将所有分配的标题列为单个值。

现在您已经将第一个字段分解为单个值,您可以使用相同的方法将剩余的字段拆分,另外,您可以显示我们的 personTitle1 的相应值调用索引,以便字段匹配。查看以下代码片段以更好地理解我的意思:

<#list s.result.metaData["personTitle"]?split("|") as person>
    ${s.result.metaData["personTitle"]!?split('|')[person_index]!}
    ${s.result.metaData["personDepartment"]!?split('|')[person_index]!}
    Tel: ${s.result.metaData["personPhone"]!?split('|')[person_index]!}
</#list>

希望这是有道理的。