Elastic Search 和 Y10k(超过 4 位数的年份)

Posted

技术标签:

【中文标题】Elastic Search 和 Y10k(超过 4 位数的年份)【英文标题】:Elastic Search and Y10k (years with more than 4 digits) 【发布时间】:2020-06-23 18:23:37 【问题描述】:

我发现这个问题与 Elastic Search 查询有关,但由于 ES date format documentation 链接到 API documentation for the java.time.format.DateTimeFormatter 类,因此问题并不是真正的 ES 特定的。

简短摘要:我们遇到了超过 9999 年的日期问题,更准确地说,是超过 4 位数的年份。

存储在 ES 中的文档有一个日期字段,它在索引描述符中定义为格式“日期”,对应于使用 DateTimeFormatter 的模式语言的“yyyy-MM-dd”。我们正在获取用户输入,使用 org.apache.commons.validator.DateValidator.isValid 验证输入,也使用模式“yyyy-MM-dd”,如果有效,我们使用用户输入创建一个 ES 查询。如果用户输入类似 20202-12-03 的内容,则会失败并出现异常。搜索词可能不是故意的,但预期的行为是找不到任何东西,也不是软件咳出异常。

问题是 org.apache.commons.validator.DateValidator 在内部使用较旧的 SimpleDateFormat 类来验证输入是否符合模式,并且 SimpleDateFormat 解释的“yyyy”的含义类似于:至少使用4 位数,但如果需要允许更多位数。因此,使用模式“yyyy-MM-dd”创建 SimpleDateFormat 将解析像“20202-07-14”这样的输入,并类似地格式化年份超过 9999 的 Date 对象。

新的 DateTimeFormatter 类更加严格,意味着“yyyy”正好四位数。它将无法解析像“20202-07-14”这样的输入字符串,也无法格式化超过 9999 年的 Temporal 对象。值得注意的是,DateTimeFormatter 本身能够处理可变长度字段。例如,常量 DateTimeFormatter.ISO_LOCAL_DATE 不等同于“yyyy-MM-dd”,但符合 ISO8601,允许超过四位数字的年份,但至少使用四位数字。此常量是使用 DateTimeFormatterBuilder 以编程方式创建的,而不是使用模式字符串。

ES 不能配置为使用 DateTimeFormatter 中定义的常量,如 ISO_LOCAL_DATE,而只能使用模式字符串。 ES 也知道预定义模式的列表,有时文档中也会提到 ISO 标准,但它们似乎是错误的,并忽略了有效的 ISO 日期字符串可以包含五位数的年份。

我可以为 ES 配置多个允许的日期模式列表,例如“yyyy-MM-dd||yyyyy-MM-dd”。这将允许四位数和五位数的年份,但不允许六位数的年份。我可以通过添加另一个允许的模式来支持六位数的年份:“yyyy-MM-dd||yyyyy-MM-dd||yyyyyy-MM-dd”,但是它会在七位数的年份中失败,依此类推。

我是否在监督某些事情,或者真的不可能将 ES(或使用模式字符串的 DateTimeFormatter 实例)配置为具有 ISO 标准所使用的至少四位数(但可能更多)的年份字段?

【问题讨论】:

我不确定我是否理解,也许我不需要。输入 5 位数的年份是错误的。我认为这样报告没有错。即使你坚持接受 5 位数的年份,也没有人会输入 6 位数,所以无论是否报告为错误,谁在乎? 5 位数年份不是错误。你为什么这么认为? 搜索词可能不是故意的……这就是我所说的错误。您是否明确要求能够在 9999 年之后进行搜索,如果是,为什么? @OleV.V.搜索姓氏“oqgfqhf”也可能不是故意的,但仍然不是错误。预期的行为是找不到记录。这同样适用于日期字段。搜索一个有效但未知的值应该不会返回任何结果,但不会是错误。我们没有明确要求能够搜索 oqgfqhf 作为姓氏,也没有超出预期范围的日期,也没有明确提到 2020-06-28 作为我们应该支持的搜索词。 您的代码。你的决定。既然你不能使用我的这个想法,请把它扔在你的肩膀上,我没有问题。 【参考方案1】:

编辑

ISO 8601

由于您的要求是符合 ISO 8601,我们先来看看 ISO 8601 是怎么说的(引自底部链接):

为了表示 0000 之前或 9999 之后的年份,标准还 允许扩大年度代表,但只能通过事先 发送者和接收者之间的协议。扩大的一年 表示 [±YYYYY] 必须有一个商定的额外年份数 超出最小四位数的数字,并且必须以 + 为前缀 或 - 用符号代替更常见的 AD/BC(或 CE/BCE)符号; …

所以20202-12-03 在 ISO 8601 中不是有效日期。如果您明确告知用户您接受最多 6 位数的年份,则 +20202-12-03-20202-12-03 是有效的,并且仅与 @ 987654327@ 或- 签名。

接受超过 4 位数字

格式模式uuuu-MM-dd 根据 ISO 8601 格式化和解析日期,也包括四位数以上的年份。例如:

    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd");
    LocalDate date = LocalDate.parse("+20202-12-03", dateFormatter);
    System.out.println("Parsed: " + date);
    System.out.println("Formatted back: " + date.format(dateFormatter));

输出:

Parsed: +20202-12-03
Formatted back: +20202-12-03

对于前缀减号而不是加号,它的工作原理非常相似。

接受超过 4 位不带符号的数字

    yyyy-MM-dd||yyyyy-MM-dd||yyyyyy-MM-dd||yyyyyyy-MM-dd||yyyyyyyy-MM-dd||yyyyyyyyy-MM-dd

正如我所说,这不符合 ISO 8601。我也同意你的观点,即它不好。显然它会在 10 位或更多位上失败,但无论如何都会因为不同的原因而失败:java.time 在 -999 999 999 到 +999 999 999 之间处理年份。所以尝试yyyyyyyyyy-MM-dd(10 位年份)会会给你带来严重的麻烦,除非在用户输入带有前导零的年份的极端情况下。

对不起,这是最好的。 DateTimeFormatter 格式模式不支持您要求的所有内容。没有(单一)模式可以为您提供 0000 到 9999 范围内的四位数年份以及之后的年份更多位数。

DateTimeFormatter 的文档说明了格式化和解析年份:

年份:字母数决定了使用填充的最小字段宽度。如果字母数是两个,那么 使用减少的两位数形式。对于打印,这将输出 最右边两位数。对于解析,这将使用基础进行解析 2000 的值,导致 2000 到 2099 范围内的年份 包括的。如果字母数少于四个(但不是两个), 那么这个符号只在负年份输出 SignStyle.NORMAL。否则,如果焊盘宽度为 超出,根据SignStyle.EXCEEDS_PAD

因此,无论您选择哪种模式字母,您都将无法解析没有符号的数字较多的年份,数字较少的年份将被格式化为带有前导零的这么多数字.

原答案

您可能可以摆脱u-MM-dd 的模式。示范:

    String formatPattern = "u-MM-dd";
    
    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(formatPattern);
    
    LocalDate normalDate = LocalDate.parse("2020-07-14", dateFormatter);
    String formattedAgain = normalDate.format(dateFormatter);
    System.out.format("LocalDate: %s. String: %s.%n", normalDate, formattedAgain);
    
    LocalDate largeDate = LocalDate.parse("20202-07-14", dateFormatter);
    String largeFormattedAgain = largeDate.format(dateFormatter);
    System.out.format("LocalDate: %s. String: %s.%n", largeDate, largeFormattedAgain);

输出:

LocalDate: 2020-07-14. String: 2020-07-14.
LocalDate: +20202-07-14. String: 20202-07-14.

反直觉但非常实用的一个格式字母并不意味着 1 位数字,而是 尽可能多的数字。因此,上述情况的另一面是 1000 年之前的年份将被格式化为少于 4 位数字。正如您所说,这不符合 ISO 8601。

关于模式字母yu 年份的区别,请参见底部的链接。

您也可以考虑使用一个M 和/或一个d 来接受2020-007-014,但同样,对于小于10 的数字,这将导致格式化为1 位数字,例如2020-7-14,这可能不是t 你想要什么,又不同意 ISO。

链接

Years section ***文章:ISO 8601 Documentation of DateTimeFormatter uuuu versus yyyy in DateTimeFormatter formatting pattern codes in Java?

【讨论】:

我们希望遵循 ISO 标准,因此不能接受单个模式字母(无论是 u 还是 y)。【参考方案2】:

也许这会起作用:

[uuuu][uuuuu][...]-MM-dd

方括号之间的格式说明符是可选部分。括号内的格式说明符可以重复以允许接受多个选项。

此模式将允许四位或五位数字的年份,但拒绝所有其他情况。

Here is this pattern in action。请注意,此模式对于将字符串解析LocalDate 很有用。但是,要将LocalDate 实例格式化 成字符串,模式应该是uuuu-MM-dd。那是因为两个可选的年份部分会导致年份数字被打印两次。

重复所有可能的年份数字计数,是您可以得到的最接近的方法,以使其按您期望的方式工作。

DateTimeFormatter 当前实现的问题在于,当您指定4 个或更多 uys 时,解析器会尝试准确地消费 年位数。但是,如果 小于 4,那么解析器将尝试消耗尽可能多的数据。我不知道这种行为是否是故意的。

因此,可以使用格式化程序构建器来实现预期的行为,但不能使用模式字符串。正如 JodaStephen once pointed out,“模式是可能的格式化程序的子集”


也许# 这些字符保留供将来使用,在这方面会有用。

【讨论】:

我已经提到了在问题中配置多个模式的可能性(ES 语法不同)以及为什么这不是解决方案。您在此处建议的配置将允许 4 或 5 位数的年份,而不是所需的“4 或更多”。 @jarnbjo 好吧,从 Elastic Search 的角度来看,这是一个单一的模式字符串。我已经更新了答案。【参考方案3】:

更新

您可以使用DateTimeFormatterBuilder#appendValueReduced 将一年中的位数限制在4-9 位数的范围内。

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;

public class Main 
    public static void main(String[] args) 
        DateTimeFormatter formatter = new DateTimeFormatterBuilder()
                .appendValueReduced(ChronoField.YEAR, 4, 9, 1000)
                .appendPattern("-MM-dd")
                .toFormatter();

        String[] dateStrArr =  "2017-10-20", "20171-10-20", "201712-10-20", "2017123-10-20" ;
        for (String dateStr : dateStrArr) 
            System.out.println(LocalDate.parse(dateStr, formatter));
        
    

输出:

2017-10-20
+20171-10-20
+201712-10-20
+2017123-10-20

原答案

您可以使用模式[uuuu][u]-MM-dd,其中[uuuu] 符合4 位数年份,[u] 可以满足年份允许的任意位数的要求。

演示:

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class Main 
    public static void main(String[] args) 
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[uuuu][u]-MM-dd");
        String[] dateStrArr =  "2017-10-20", "20171-10-20", "201712-10-20", "2017123-10-20" ;
        for (String dateStr : dateStrArr) 
            System.out.println(LocalDate.parse(dateStr, formatter));
        
    

输出:

2017-10-20
+20171-10-20
+201712-10-20
+2017123-10-20

【讨论】:

虽然它适用于解析,但您的格式化程序格式与 20172017-10-20、+2017120171-10-20、+201712201712-10-20 和 +20171232017123-10-20 的日期相同,相反符合要求。 [uuuu][u] 没有多大意义,因为模式 uuuu 已经被模式 u 覆盖了。出于解析目的,“[uuuu][u]-MM-dd”和“u-MM-dd”是等价的,正如我已经在对 Ole 答案的评论中所写的那样,简单地使用 u 是不可接受的,因为它允许使用数年少于 4 位数,这是不想要的。 @jarnbjo - 我发布了一个更新,它将限制4-9 范围内的数字。如果仍然不符合要求,请告诉我。 我已经在问题中写道,我可以通过使用 DateTimeFormatterBuilder 创建 DateFormatBuilder 来做我想做的事情,但是不能使用 DateTimeFormatterBuilder 来配置 Elastic Search。这也没有回答我的问题。

以上是关于Elastic Search 和 Y10k(超过 4 位数的年份)的主要内容,如果未能解决你的问题,请参考以下文章

Elastic Search优化

Elastic Search和Kibana入门

一. Windows安装Elastic Search和Head插件

Elastic Search 基本操作

Elastic Search 分词器的介绍和使用

docker安装elastic search