HitItemDTO.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. <?php
  2. namespace App\DTO\Search;
  3. class HitItemDTO
  4. {
  5. public function __construct(
  6. public string $id,
  7. public string $resId,
  8. public float $score,
  9. public string $content,
  10. public string $title,
  11. public string $path,
  12. public array $category,
  13. public array $tags,
  14. public string $highlight,
  15. public string $updated,
  16. public string $type,
  17. public string $language,
  18. public ?string $display = null,
  19. ) {}
  20. /**
  21. * 从 OpenSearch hit 数组构建 DTO
  22. *
  23. * title 和 content 的拼接策略:
  24. * 因为一条记录通常只有一种语言被赋值(pali 或 zh),
  25. * 所以直接将 text.* 下所有非空值拼接输出,无需判断语言类型。
  26. *
  27. * title: 取 title.text.pali + title.text.zh(跳过空值)
  28. * content: 取 content.text.pali + content.text.zh(跳过空值)
  29. * display: 取 content.display,列表页查询时因被 sourceExcludes 排除
  30. * 通常为 null;详情页通过 get() 获取完整文档时才有值
  31. *
  32. * @param array $data OpenSearch 单条 hit 原始数据
  33. * @return static
  34. */
  35. public static function fromArray(array $data): self
  36. {
  37. $source = $data['_source'];
  38. // ---------- highlight ----------
  39. $highlightArray = [];
  40. if (!empty($data['highlight']) && is_array($data['highlight'])) {
  41. foreach ($data['highlight'] as $fragments) {
  42. $highlightArray = array_merge($highlightArray, $fragments);
  43. }
  44. }
  45. // ---------- title ----------
  46. // 取 title.text 下所有非空语言值拼接
  47. $titleText = $source['title']['text'] ?? [];
  48. $title = implode('', array_filter(
  49. is_array($titleText) ? array_values($titleText) : [],
  50. fn($v) => is_string($v) && $v !== ''
  51. ));
  52. // ---------- content ----------
  53. // 取 content.text 下所有非空语言值拼接
  54. $contentText = $source['content']['text'] ?? [];
  55. $content = implode('', array_filter(
  56. is_array($contentText) ? array_values($contentText) : [],
  57. fn($v) => is_string($v) && $v !== ''
  58. ));
  59. // ---------- display ----------
  60. // 列表页查询时被 sourceExcludes 排除,值为 null
  61. // 详情页通过 get() 取完整文档时才有值
  62. $display = $source['content']['display'] ?? null;
  63. // ---------- category ----------
  64. $rawCategory = $source['category'] ?? [];
  65. $category = is_array($rawCategory) ? $rawCategory : [$rawCategory];
  66. return new self(
  67. id: $source['id'],
  68. score: $data['_score'] ?? 0,
  69. title: $title,
  70. content: $content,
  71. path: $source['path'] ?? '',
  72. category: $category,
  73. tags: $source['tags'],
  74. highlight: implode('', $highlightArray),
  75. updated: $source['updated_at'],
  76. type: $source['resource_type'],
  77. language: $source['language'],
  78. resId: $source['resource_id'],
  79. display: $display,
  80. );
  81. }
  82. /**
  83. * 将 DTO 转为数组(用于 API 响应输出)
  84. *
  85. * display 字段仅在有值时输出,避免列表页响应体携带 null 键。
  86. *
  87. * @return array
  88. */
  89. public function toArray(): array
  90. {
  91. $data = [
  92. 'id' => $this->id,
  93. 'res_id' => $this->resId,
  94. 'score' => $this->score,
  95. 'title' => $this->title,
  96. 'content' => $this->content,
  97. 'path' => $this->path,
  98. 'category' => $this->category,
  99. 'tags' => $this->tags,
  100. 'highlight' => $this->highlight,
  101. 'updated' => $this->updated,
  102. 'type' => $this->type,
  103. 'language' => $this->language,
  104. 'para_id' => $this->getParaId(),
  105. 'para_link' => $this->getParaLink(),
  106. ];
  107. if ($this->display !== null) {
  108. $data['display'] = $this->display;
  109. }
  110. return $data;
  111. }
  112. /**
  113. * 提取 para 引用 ID
  114. *
  115. * 匹配文档 ID 格式 "pali_para_{bookId}_{paraId}",
  116. * 返回 "{bookId}-{paraId}" 格式字符串。
  117. *
  118. * @return string|null
  119. */
  120. public function getParaId(): ?string
  121. {
  122. if (preg_match('/pali_para_(\d+)_(\d+)/', $this->id, $matches)) {
  123. return "{$matches[1]}-{$matches[2]}";
  124. }
  125. return null;
  126. }
  127. /**
  128. * 生成 para wiki 模板引用字符串
  129. *
  130. * @return string|null 例如:{{para|id=1-23|title=1-23|style=reference}}
  131. */
  132. public function getParaLink(): ?string
  133. {
  134. $id = $this->getParaId();
  135. if (!$id) return null;
  136. return "{{para|id={$id}|title={$id}|style=reference}}";
  137. }
  138. }